@memberjunction/server 5.30.1 → 5.32.0

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 (104) hide show
  1. package/dist/agents/skip-sdk.d.ts +17 -1
  2. package/dist/agents/skip-sdk.d.ts.map +1 -1
  3. package/dist/agents/skip-sdk.js +18 -5
  4. package/dist/agents/skip-sdk.js.map +1 -1
  5. package/dist/auth/exampleNewUserSubClass.js +1 -1
  6. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  7. package/dist/auth/index.js +2 -2
  8. package/dist/auth/index.js.map +1 -1
  9. package/dist/auth/newUsers.js +2 -2
  10. package/dist/auth/newUsers.js.map +1 -1
  11. package/dist/context.js +3 -3
  12. package/dist/context.js.map +1 -1
  13. package/dist/generated/generated.d.ts +217 -4
  14. package/dist/generated/generated.d.ts.map +1 -1
  15. package/dist/generated/generated.js +1251 -24
  16. package/dist/generated/generated.js.map +1 -1
  17. package/dist/generic/ResolverBase.d.ts +5 -5
  18. package/dist/generic/ResolverBase.d.ts.map +1 -1
  19. package/dist/generic/ResolverBase.js +21 -18
  20. package/dist/generic/ResolverBase.js.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +9 -8
  24. package/dist/index.js.map +1 -1
  25. package/dist/multiTenancy/index.js +1 -1
  26. package/dist/multiTenancy/index.js.map +1 -1
  27. package/dist/resolvers/APIKeyResolver.d.ts.map +1 -1
  28. package/dist/resolvers/APIKeyResolver.js +5 -3
  29. package/dist/resolvers/APIKeyResolver.js.map +1 -1
  30. package/dist/resolvers/AutotagPipelineResolver.d.ts +3 -3
  31. package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
  32. package/dist/resolvers/AutotagPipelineResolver.js +18 -12
  33. package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
  34. package/dist/resolvers/ComponentRegistryResolver.d.ts +1 -1
  35. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  36. package/dist/resolvers/ComponentRegistryResolver.js +6 -4
  37. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  38. package/dist/resolvers/FileResolver.js +2 -2
  39. package/dist/resolvers/FileResolver.js.map +1 -1
  40. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  41. package/dist/resolvers/GetDataContextDataResolver.js +1 -2
  42. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  43. package/dist/resolvers/ISAEntityResolver.d.ts.map +1 -1
  44. package/dist/resolvers/ISAEntityResolver.js +2 -5
  45. package/dist/resolvers/ISAEntityResolver.js.map +1 -1
  46. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  47. package/dist/resolvers/IntegrationDiscoveryResolver.js +75 -66
  48. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  49. package/dist/resolvers/SyncDataResolver.d.ts +4 -4
  50. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  51. package/dist/resolvers/SyncDataResolver.js +9 -8
  52. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  53. package/dist/resolvers/SyncRolesUsersResolver.d.ts +6 -6
  54. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  55. package/dist/resolvers/SyncRolesUsersResolver.js +22 -18
  56. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  57. package/dist/resolvers/TagGovernanceResolver.d.ts +43 -0
  58. package/dist/resolvers/TagGovernanceResolver.d.ts.map +1 -0
  59. package/dist/resolvers/TagGovernanceResolver.js +245 -0
  60. package/dist/resolvers/TagGovernanceResolver.js.map +1 -0
  61. package/dist/resolvers/TaskResolver.d.ts +1 -1
  62. package/dist/resolvers/TaskResolver.d.ts.map +1 -1
  63. package/dist/resolvers/TaskResolver.js +4 -2
  64. package/dist/resolvers/TaskResolver.js.map +1 -1
  65. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  66. package/dist/resolvers/TransactionGroupResolver.js +2 -1
  67. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  68. package/dist/rest/EntityCRUDHandler.js +4 -4
  69. package/dist/rest/EntityCRUDHandler.js.map +1 -1
  70. package/dist/rest/RESTEndpointHandler.js +9 -9
  71. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  72. package/dist/rest/ViewOperationsHandler.js +4 -4
  73. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  74. package/dist/services/TaskOrchestrator.d.ts +4 -2
  75. package/dist/services/TaskOrchestrator.d.ts.map +1 -1
  76. package/dist/services/TaskOrchestrator.js +16 -12
  77. package/dist/services/TaskOrchestrator.js.map +1 -1
  78. package/package.json +68 -66
  79. package/src/__tests__/TagGovernanceResolver.test.ts +255 -0
  80. package/src/agents/skip-sdk.ts +30 -7
  81. package/src/auth/exampleNewUserSubClass.ts +1 -1
  82. package/src/auth/index.ts +2 -2
  83. package/src/auth/newUsers.ts +2 -2
  84. package/src/context.ts +3 -3
  85. package/src/generated/generated.ts +861 -21
  86. package/src/generic/ResolverBase.ts +28 -21
  87. package/src/index.ts +9 -9
  88. package/src/multiTenancy/index.ts +1 -1
  89. package/src/resolvers/APIKeyResolver.ts +7 -4
  90. package/src/resolvers/AutotagPipelineResolver.ts +20 -11
  91. package/src/resolvers/ComponentRegistryResolver.ts +8 -5
  92. package/src/resolvers/FileResolver.ts +2 -2
  93. package/src/resolvers/GetDataContextDataResolver.ts +1 -2
  94. package/src/resolvers/ISAEntityResolver.ts +3 -5
  95. package/src/resolvers/IntegrationDiscoveryResolver.ts +83 -66
  96. package/src/resolvers/SyncDataResolver.ts +12 -11
  97. package/src/resolvers/SyncRolesUsersResolver.ts +23 -19
  98. package/src/resolvers/TagGovernanceResolver.ts +189 -0
  99. package/src/resolvers/TaskResolver.ts +5 -3
  100. package/src/resolvers/TransactionGroupResolver.ts +3 -2
  101. package/src/rest/EntityCRUDHandler.ts +4 -4
  102. package/src/rest/RESTEndpointHandler.ts +9 -9
  103. package/src/rest/ViewOperationsHandler.ts +4 -4
  104. package/src/services/TaskOrchestrator.ts +18 -13
@@ -1,10 +1,11 @@
1
1
  import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
- import { DatabaseProviderBase, EntityDeleteOptions, EntitySaveOptions, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
3
+ import { DatabaseProviderBase, EntityDeleteOptions, EntitySaveOptions, IMetadataProvider, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
4
  import { UUIDsEqual } from '@memberjunction/global';
5
5
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
6
6
  import { MJRoleEntity, MJUserEntity, MJUserRoleEntity } from '@memberjunction/core-entities';
7
7
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
8
+ import { GetReadWriteProvider } from '../util.js';
8
9
 
9
10
  @ObjectType()
10
11
  export class SyncRolesAndUsersResultType {
@@ -92,16 +93,17 @@ export class SyncRolesAndUsersResolver {
92
93
  // attempted this with a TransactionGroup but ran into nesting issues —
93
94
  // direct DB transactions per the plan doc avoid that.
94
95
  try {
95
- const provider = Metadata.Provider as DatabaseProviderBase;
96
+ const md = GetReadWriteProvider(context.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
97
+ const provider = md as unknown as DatabaseProviderBase;
96
98
  await provider.BeginTransaction();
97
99
  try {
98
- const roleResult = await this.DoSyncRoles(data.Roles, context.userPayload.userRecord, context.userPayload);
100
+ const roleResult = await this.DoSyncRoles(data.Roles, context.userPayload.userRecord, context.userPayload, md);
99
101
  if (!roleResult.Success) {
100
102
  await provider.RollbackTransaction();
101
103
  return roleResult;
102
104
  }
103
105
 
104
- const usersResult = await this.DoSyncUsers(data.Users, context.userPayload.userRecord, context.userPayload);
106
+ const usersResult = await this.DoSyncUsers(data.Users, context.userPayload.userRecord, context.userPayload, md);
105
107
  if (!usersResult.Success) {
106
108
  await provider.RollbackTransaction();
107
109
  return usersResult;
@@ -136,10 +138,11 @@ export class SyncRolesAndUsersResolver {
136
138
  // Wrap delete + add + update of roles in one DB transaction so any failure
137
139
  // rolls back the whole batch. Keeps the sync idempotent across retries.
138
140
  try {
139
- const provider = Metadata.Provider as DatabaseProviderBase;
141
+ const md = GetReadWriteProvider(context.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
142
+ const provider = md as unknown as DatabaseProviderBase;
140
143
  await provider.BeginTransaction();
141
144
  try {
142
- const result = await this.DoSyncRoles(roles, context.userPayload.userRecord, context.userPayload);
145
+ const result = await this.DoSyncRoles(roles, context.userPayload.userRecord, context.userPayload, md);
143
146
  if (result.Success) {
144
147
  await provider.CommitTransaction();
145
148
  } else {
@@ -161,7 +164,7 @@ export class SyncRolesAndUsersResolver {
161
164
  * Throws on any Save/Delete failure so the outer transaction rolls back. A returned
162
165
  * `{ Success: true }` means every operation succeeded.
163
166
  */
164
- protected async DoSyncRoles(roles: RoleInputType[], user: UserInfo, userPayload: UserPayload): Promise<SyncRolesAndUsersResultType> {
167
+ protected async DoSyncRoles(roles: RoleInputType[], user: UserInfo, userPayload: UserPayload, provider?: IMetadataProvider): Promise<SyncRolesAndUsersResultType> {
165
168
  const rv = new RunView();
166
169
  const result = await rv.RunView<MJRoleEntity>({
167
170
  EntityName: "MJ: Roles",
@@ -174,7 +177,7 @@ export class SyncRolesAndUsersResolver {
174
177
 
175
178
  const currentRoles = result.Results;
176
179
  await this.DeleteRemovedRoles(currentRoles, roles, user, userPayload);
177
- await this.AddNewRoles(currentRoles, roles, user, userPayload);
180
+ await this.AddNewRoles(currentRoles, roles, user, userPayload, provider);
178
181
  await this.UpdateExistingRoles(currentRoles, roles, userPayload);
179
182
  return { Success: true };
180
183
  }
@@ -192,9 +195,9 @@ export class SyncRolesAndUsersResolver {
192
195
  }
193
196
  }
194
197
 
195
- protected async AddNewRoles(currentRoles: MJRoleEntity[], futureRoles: RoleInputType[], user: UserInfo, userPayload: UserPayload): Promise<void> {
198
+ protected async AddNewRoles(currentRoles: MJRoleEntity[], futureRoles: RoleInputType[], user: UserInfo, userPayload: UserPayload, provider?: IMetadataProvider): Promise<void> {
196
199
  // go through the future roles and add any that are not in the current roles
197
- const md = new Metadata();
200
+ const md = provider ?? new Metadata();
198
201
 
199
202
  for (const add of futureRoles) {
200
203
  if (!currentRoles.find(r => r.Name.trim().toLowerCase() === add.Name.trim().toLowerCase())) {
@@ -262,10 +265,11 @@ export class SyncRolesAndUsersResolver {
262
265
  ) : Promise<SyncRolesAndUsersResultType> {
263
266
  // Wrap delete + add + update + role-sync of users in one DB transaction.
264
267
  try {
265
- const provider = Metadata.Provider as DatabaseProviderBase;
268
+ const md = GetReadWriteProvider(context.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
269
+ const provider = md as unknown as DatabaseProviderBase;
266
270
  await provider.BeginTransaction();
267
271
  try {
268
- const result = await this.DoSyncUsers(users, context.userPayload.userRecord, context.userPayload);
272
+ const result = await this.DoSyncUsers(users, context.userPayload.userRecord, context.userPayload, md);
269
273
  if (result.Success) {
270
274
  await provider.CommitTransaction();
271
275
  } else {
@@ -286,7 +290,7 @@ export class SyncRolesAndUsersResolver {
286
290
  * Transaction-free core of SyncUsers — expected to be invoked inside an outer transaction.
287
291
  * Throws on any Save/Delete failure so the outer transaction rolls back.
288
292
  */
289
- protected async DoSyncUsers(users: UserInputType[], user: UserInfo, userPayload: UserPayload): Promise<SyncRolesAndUsersResultType> {
293
+ protected async DoSyncUsers(users: UserInputType[], user: UserInfo, userPayload: UserPayload, provider?: IMetadataProvider): Promise<SyncRolesAndUsersResultType> {
290
294
  const rv = new RunView();
291
295
  const result = await rv.RunView<MJUserEntity>({
292
296
  EntityName: "MJ: Users",
@@ -299,9 +303,9 @@ export class SyncRolesAndUsersResolver {
299
303
 
300
304
  const currentUsers = result.Results;
301
305
  await this.DeleteRemovedUsers(currentUsers, users, user, userPayload);
302
- await this.AddNewUsers(currentUsers, users, userPayload);
306
+ await this.AddNewUsers(currentUsers, users, userPayload, provider);
303
307
  await this.UpdateExistingUsers(currentUsers, users, userPayload);
304
- await this.SyncUserRoles(users, user, userPayload);
308
+ await this.SyncUserRoles(users, user, userPayload, provider);
305
309
  return { Success: true };
306
310
  }
307
311
 
@@ -322,9 +326,9 @@ export class SyncRolesAndUsersResolver {
322
326
  }
323
327
  }
324
328
 
325
- protected async AddNewUsers(currentUsers: MJUserEntity[], futureUsers: UserInputType[], userPayload: UserPayload): Promise<void> {
329
+ protected async AddNewUsers(currentUsers: MJUserEntity[], futureUsers: UserInputType[], userPayload: UserPayload, provider?: IMetadataProvider): Promise<void> {
326
330
  // add users that are not in the current users
327
- const md = new Metadata();
331
+ const md = provider ?? new Metadata();
328
332
 
329
333
  for (const add of futureUsers) {
330
334
  const match = currentUsers.find(currentUser => currentUser.Email?.trim().toLowerCase() === add.Email?.trim().toLowerCase());
@@ -389,10 +393,10 @@ export class SyncRolesAndUsersResolver {
389
393
  }
390
394
  }
391
395
 
392
- protected async SyncUserRoles(users: UserInputType[], u: UserInfo, userPayload: UserPayload): Promise<void> {
396
+ protected async SyncUserRoles(users: UserInputType[], u: UserInfo, userPayload: UserPayload, provider?: IMetadataProvider): Promise<void> {
393
397
  // for each user in the users array, make sure there is a User Role that matches. First, get a list of all DATABASE user and roels so we have that for fast lookup in memory
394
398
  const rv = new RunView();
395
- const md = new Metadata();
399
+ const md = provider ?? new Metadata();
396
400
 
397
401
  const p1 = rv.RunView<MJUserEntity>({
398
402
  EntityName: "MJ: Users",
@@ -0,0 +1,189 @@
1
+ import { Resolver, Mutation, Ctx, Arg, ObjectType, Field, Int } from 'type-graphql';
2
+ import { AppContext } from '../types.js';
3
+ import { LogError, LogStatus } from '@memberjunction/core';
4
+ import { ResolverBase } from '../generic/ResolverBase.js';
5
+ import { TagGovernanceEngine, TagHealthJob, DEFAULT_TAG_HEALTH_THRESHOLDS, TagEngine } from '@memberjunction/tag-engine';
6
+
7
+ @ObjectType()
8
+ export class PromoteSuggestionResult {
9
+ @Field()
10
+ Success: boolean;
11
+
12
+ @Field({ nullable: true })
13
+ ResolvedTagID?: string;
14
+
15
+ @Field({ nullable: true })
16
+ ResolvedTagName?: string;
17
+
18
+ @Field({ nullable: true })
19
+ ErrorMessage?: string;
20
+ }
21
+
22
+ @ObjectType()
23
+ export class RejectSuggestionResult {
24
+ @Field()
25
+ Success: boolean;
26
+
27
+ @Field({ nullable: true })
28
+ ErrorMessage?: string;
29
+ }
30
+
31
+ @ObjectType()
32
+ export class RebuildTagEmbeddingsResult {
33
+ @Field()
34
+ Success: boolean;
35
+
36
+ @Field(() => Int)
37
+ Refreshed: number;
38
+
39
+ @Field(() => Int)
40
+ Total: number;
41
+
42
+ @Field({ nullable: true })
43
+ ErrorMessage?: string;
44
+ }
45
+
46
+ @ObjectType()
47
+ export class RunTagHealthResult {
48
+ @Field()
49
+ Success: boolean;
50
+
51
+ @Field(() => Int)
52
+ MergeCount: number;
53
+
54
+ @Field(() => Int)
55
+ LowUsageCount: number;
56
+
57
+ @Field(() => Int)
58
+ WideNodeCount: number;
59
+
60
+ @Field(() => Int)
61
+ DurationMs: number;
62
+
63
+ @Field({ nullable: true })
64
+ ErrorMessage?: string;
65
+ }
66
+
67
+ /**
68
+ * GraphQL surface for the tag governance lifecycle that the Suggestion Inbox
69
+ * UI invokes. Wraps `TagGovernanceEngine` (PromoteSuggestion / RejectSuggestion),
70
+ * `TagEngine.RebuildTagEmbeddings`, and `TagHealthJob.Run` so the client gets a
71
+ * single transactional call per disposition instead of multiple BaseEntity saves.
72
+ *
73
+ * Multi-provider safety: each mutation reads the per-request user from the
74
+ * context and lets the engines use their default provider; if a non-default
75
+ * provider is in play it should be passed via `TagGovernanceEngine.Provider`.
76
+ */
77
+ @Resolver()
78
+ export class TagGovernanceResolver extends ResolverBase {
79
+ @Mutation(() => PromoteSuggestionResult)
80
+ async PromoteTagSuggestion(
81
+ @Arg('suggestionID') suggestionID: string,
82
+ @Arg('strategy') strategy: 'create-new' | 'merge-into-existing',
83
+ @Arg('targetTagID', { nullable: true }) targetTagID: string | undefined,
84
+ @Ctx() { userPayload }: AppContext = {} as AppContext
85
+ ): Promise<PromoteSuggestionResult> {
86
+ try {
87
+ const currentUser = this.GetUserFromPayload(userPayload);
88
+ if (!currentUser) {
89
+ return { Success: false, ErrorMessage: 'Unable to determine current user' };
90
+ }
91
+
92
+ if (strategy !== 'create-new' && strategy !== 'merge-into-existing') {
93
+ return { Success: false, ErrorMessage: `Unknown strategy "${strategy}"` };
94
+ }
95
+ if (strategy === 'merge-into-existing' && !targetTagID) {
96
+ return { Success: false, ErrorMessage: 'targetTagID is required when strategy is "merge-into-existing".' };
97
+ }
98
+
99
+ const resolved = strategy === 'create-new'
100
+ ? await TagGovernanceEngine.Instance.PromoteSuggestion(suggestionID, { kind: 'create-new' }, currentUser)
101
+ : await TagGovernanceEngine.Instance.PromoteSuggestion(suggestionID, { kind: 'merge-into-existing', targetTagID: targetTagID! }, currentUser);
102
+
103
+ return {
104
+ Success: true,
105
+ ResolvedTagID: resolved.ID,
106
+ ResolvedTagName: resolved.Name,
107
+ };
108
+ } catch (error) {
109
+ const msg = error instanceof Error ? error.message : String(error);
110
+ LogError(`PromoteTagSuggestion failed for ${suggestionID}: ${msg}`);
111
+ return { Success: false, ErrorMessage: msg };
112
+ }
113
+ }
114
+
115
+ @Mutation(() => RejectSuggestionResult)
116
+ async RejectTagSuggestion(
117
+ @Arg('suggestionID') suggestionID: string,
118
+ @Arg('reviewerNotes', { nullable: true }) reviewerNotes: string | undefined,
119
+ @Ctx() { userPayload }: AppContext = {} as AppContext
120
+ ): Promise<RejectSuggestionResult> {
121
+ try {
122
+ const currentUser = this.GetUserFromPayload(userPayload);
123
+ if (!currentUser) {
124
+ return { Success: false, ErrorMessage: 'Unable to determine current user' };
125
+ }
126
+ await TagGovernanceEngine.Instance.RejectSuggestion(suggestionID, reviewerNotes ?? null, currentUser);
127
+ return { Success: true };
128
+ } catch (error) {
129
+ const msg = error instanceof Error ? error.message : String(error);
130
+ LogError(`RejectTagSuggestion failed for ${suggestionID}: ${msg}`);
131
+ return { Success: false, ErrorMessage: msg };
132
+ }
133
+ }
134
+
135
+ @Mutation(() => RebuildTagEmbeddingsResult)
136
+ async RebuildTagEmbeddings(
137
+ @Ctx() { userPayload }: AppContext = {} as AppContext
138
+ ): Promise<RebuildTagEmbeddingsResult> {
139
+ try {
140
+ const currentUser = this.GetUserFromPayload(userPayload);
141
+ if (!currentUser) {
142
+ return { Success: false, Refreshed: 0, Total: 0, ErrorMessage: 'Unable to determine current user' };
143
+ }
144
+ const result = await TagEngine.Instance.RebuildTagEmbeddings(currentUser);
145
+ LogStatus(`RebuildTagEmbeddings: refreshed ${result.refreshed}/${result.total} tags.`);
146
+ return { Success: true, Refreshed: result.refreshed, Total: result.total };
147
+ } catch (error) {
148
+ const msg = error instanceof Error ? error.message : String(error);
149
+ LogError(`RebuildTagEmbeddings failed: ${msg}`);
150
+ return { Success: false, Refreshed: 0, Total: 0, ErrorMessage: msg };
151
+ }
152
+ }
153
+
154
+ @Mutation(() => RunTagHealthResult)
155
+ async RunTagHealth(
156
+ @Arg('minCoOccurrence', () => Int, { nullable: true }) minCoOccurrence: number | undefined,
157
+ @Arg('minNameSimilarity', { nullable: true }) minNameSimilarity: number | undefined,
158
+ @Arg('minEmbeddingSimilarity', { nullable: true }) minEmbeddingSimilarity: number | undefined,
159
+ @Arg('maxUsage', () => Int, { nullable: true }) maxUsage: number | undefined,
160
+ @Arg('maxImplicitChildren', () => Int, { nullable: true }) maxImplicitChildren: number | undefined,
161
+ @Ctx() { userPayload }: AppContext = {} as AppContext
162
+ ): Promise<RunTagHealthResult> {
163
+ try {
164
+ const currentUser = this.GetUserFromPayload(userPayload);
165
+ if (!currentUser) {
166
+ return { Success: false, MergeCount: 0, LowUsageCount: 0, WideNodeCount: 0, DurationMs: 0, ErrorMessage: 'Unable to determine current user' };
167
+ }
168
+ const summary = await TagHealthJob.Instance.Run({
169
+ ...DEFAULT_TAG_HEALTH_THRESHOLDS,
170
+ ...(minCoOccurrence != null ? { minCoOccurrence } : {}),
171
+ ...(minNameSimilarity != null ? { minNameSimilarity } : {}),
172
+ ...(minEmbeddingSimilarity != null ? { minEmbeddingSimilarity } : {}),
173
+ ...(maxUsage != null ? { maxUsage } : {}),
174
+ ...(maxImplicitChildren != null ? { maxImplicitChildren } : {}),
175
+ }, currentUser);
176
+ return {
177
+ Success: true,
178
+ MergeCount: summary.mergeCount,
179
+ LowUsageCount: summary.lowUsageCount,
180
+ WideNodeCount: summary.wideNodeCount,
181
+ DurationMs: summary.durationMs,
182
+ };
183
+ } catch (error) {
184
+ const msg = error instanceof Error ? error.message : String(error);
185
+ LogError(`RunTagHealth failed: ${msg}`);
186
+ return { Success: false, MergeCount: 0, LowUsageCount: 0, WideNodeCount: 0, DurationMs: 0, ErrorMessage: msg };
187
+ }
188
+ }
189
+ }
@@ -1,8 +1,9 @@
1
1
  import { Resolver, Mutation, Arg, Ctx, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
- import { LogError, LogStatus } from '@memberjunction/core';
3
+ import { IMetadataProvider, LogError, LogStatus } from '@memberjunction/core';
4
4
  import { ResolverBase } from '../generic/ResolverBase.js';
5
5
  import { TaskOrchestrator, TaskGraphResponse, TaskExecutionResult } from '../services/TaskOrchestrator.js';
6
+ import { GetReadWriteProvider } from '../util.js';
6
7
 
7
8
  @ObjectType()
8
9
  export class TaskExecutionResultType {
@@ -53,7 +54,7 @@ export class TaskOrchestrationResolver extends ResolverBase {
53
54
  @Arg('environmentId') environmentId: string,
54
55
  @Arg('sessionId') sessionId: string,
55
56
  @PubSub() pubSub: PubSubEngine,
56
- @Ctx() { userPayload }: AppContext,
57
+ @Ctx() { userPayload, providers }: AppContext,
57
58
  @Arg('createNotifications', { nullable: true }) createNotifications?: boolean
58
59
  ): Promise<ExecuteTaskGraphResult> {
59
60
  // Check API key scope authorization for task execution
@@ -82,7 +83,8 @@ export class TaskOrchestrationResolver extends ResolverBase {
82
83
  }
83
84
 
84
85
  // Create task orchestrator with PubSub for progress updates
85
- const orchestrator = new TaskOrchestrator(currentUser, pubSub, sessionId, userPayload, createNotifications || false, conversationDetailId);
86
+ const provider = GetReadWriteProvider(providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
87
+ const orchestrator = new TaskOrchestrator(currentUser, pubSub, sessionId, userPayload, createNotifications || false, conversationDetailId, provider);
86
88
 
87
89
  // Create parent task and child tasks with dependencies
88
90
  const { parentTaskId, taskIdMap } = await orchestrator.createTasksFromGraph(
@@ -1,7 +1,8 @@
1
1
  import { Arg, Ctx, Field, InputType, Int, Mutation, ObjectType, registerEnumType } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
- import { CompositeKey, KeyValuePair, LogError, Metadata, TransactionVariable, BaseEntity, EntityDeleteOptions, EntitySaveOptions } from '@memberjunction/core';
3
+ import { CompositeKey, IMetadataProvider, KeyValuePair, LogError, Metadata, TransactionVariable, BaseEntity, EntityDeleteOptions, EntitySaveOptions } from '@memberjunction/core';
4
4
  import { SafeJSONParse } from '@memberjunction/global';
5
+ import { GetReadWriteProvider } from '../util.js';
5
6
 
6
7
  export enum TransactionVariableType {
7
8
  Define = "Define",
@@ -84,7 +85,7 @@ export class TransactionResolver {
84
85
  ) {
85
86
  try {
86
87
  // we have received the transaction group information via the network, now we need to reconstruct our TransactionGroup object and run it
87
- const md = new Metadata();
88
+ const md = (GetReadWriteProvider(context.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider) ?? new Metadata();
88
89
  const tg = await md.CreateTransactionGroup();
89
90
  const entityObjects: BaseEntity[] = [];
90
91
  const objectValues: any[] = [];
@@ -14,7 +14,7 @@ export class EntityCRUDHandler {
14
14
  static async createEntity(entityName: string, data: any, user: UserInfo): Promise<{ success: boolean, entity?: any, error?: string, details?: any, validationErrors?: any[] }> {
15
15
  try {
16
16
  // Get entity object
17
- const md = new Metadata();
17
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
18
18
  const entity = await md.GetEntityObject(entityName, user);
19
19
 
20
20
  // Check permissions
@@ -84,7 +84,7 @@ export class EntityCRUDHandler {
84
84
  static async getEntity(entityName: string, id: string | number, relatedEntities: string[] = null, user: UserInfo): Promise<{ success: boolean, entity?: any, error?: string }> {
85
85
  try {
86
86
  // Get entity object
87
- const md = new Metadata();
87
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
88
88
  const entity = await md.GetEntityObject(entityName, user);
89
89
 
90
90
  // Check permissions
@@ -123,7 +123,7 @@ export class EntityCRUDHandler {
123
123
  static async updateEntity(entityName: string, id: string | number, data: any, user: UserInfo): Promise<{ success: boolean, entity?: any, error?: string, details?: any, validationErrors?: any[] }> {
124
124
  try {
125
125
  // Get entity object
126
- const md = new Metadata();
126
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
127
127
  const entity = await md.GetEntityObject(entityName, user);
128
128
 
129
129
  // Check permissions
@@ -210,7 +210,7 @@ export class EntityCRUDHandler {
210
210
  static async deleteEntity(entityName: string, id: string | number, options: EntityDeleteOptions, user: UserInfo): Promise<{ success: boolean, error?: string, details?: any }> {
211
211
  try {
212
212
  // Get entity object
213
- const md = new Metadata();
213
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
214
214
  const entity = await md.GetEntityObject(entityName, user);
215
215
 
216
216
  // Check permissions
@@ -74,7 +74,7 @@ export class RESTEndpointHandler {
74
74
  */
75
75
  private isEntityAllowed(entityName: string): boolean {
76
76
  const name = entityName.toLowerCase();
77
- const md = new Metadata();
77
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
78
78
  const entity = md.Entities.find(e => e.Name.toLowerCase() === name);
79
79
 
80
80
  // If entity not found in metadata, don't allow it
@@ -417,7 +417,7 @@ export class RESTEndpointHandler {
417
417
  const user = req['mjUser'];
418
418
 
419
419
  // Get the entity object
420
- const md = new Metadata();
420
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
421
421
  const entity = await md.GetEntityObject(entityName, user);
422
422
 
423
423
  // Create a composite key
@@ -445,7 +445,7 @@ export class RESTEndpointHandler {
445
445
  const user = req['mjUser'];
446
446
 
447
447
  // Get the entity object
448
- const md = new Metadata();
448
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
449
449
  const entity = await md.GetEntityObject(entityName, user);
450
450
 
451
451
  // Create a composite key
@@ -473,7 +473,7 @@ export class RESTEndpointHandler {
473
473
  const user = req['mjUser'];
474
474
 
475
475
  // Get the entity object
476
- const md = new Metadata();
476
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
477
477
  const entity = await md.GetEntityObject(entityName, user);
478
478
 
479
479
  // Create a composite key
@@ -589,7 +589,7 @@ export class RESTEndpointHandler {
589
589
  const user = req['mjUser'];
590
590
 
591
591
  // Filter entities based on user permissions and REST API configuration
592
- const md = new Metadata();
592
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
593
593
  const entities = md.Entities.filter(e => {
594
594
  // First check if entity is allowed based on configuration
595
595
  if (!this.isEntityAllowed(e.Name)) {
@@ -629,7 +629,7 @@ export class RESTEndpointHandler {
629
629
 
630
630
  const user = req['mjUser'];
631
631
 
632
- const md = new Metadata();
632
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
633
633
  const entity = md.Entities.find(e => e.Name === entityName);
634
634
  if (!entity) {
635
635
  res.status(404).json({ error: `Entity '${entityName}' not found` });
@@ -695,7 +695,7 @@ export class RESTEndpointHandler {
695
695
  const user = req['mjUser'];
696
696
 
697
697
  // Get the entity object
698
- const md = new Metadata();
698
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
699
699
  const entity = await md.GetEntityObject(entityName, user);
700
700
 
701
701
  // Create a composite key
@@ -723,7 +723,7 @@ export class RESTEndpointHandler {
723
723
  const user = req['mjUser'];
724
724
 
725
725
  // Get the entity object
726
- const md = new Metadata();
726
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
727
727
  const entity = await md.GetEntityObject(entityName, user);
728
728
 
729
729
  // Create a composite key
@@ -751,7 +751,7 @@ export class RESTEndpointHandler {
751
751
  const user = req['mjUser'];
752
752
 
753
753
  // Get the entity object
754
- const md = new Metadata();
754
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
755
755
  const entity = await md.GetEntityObject(entityName, user);
756
756
 
757
757
  // Create a composite key
@@ -14,7 +14,7 @@ export class ViewOperationsHandler {
14
14
  static async runView(params: RunViewParams, user: UserInfo): Promise<{ success: boolean, result?: RunViewResult, error?: string }> {
15
15
  try {
16
16
  // Validate entity exists
17
- const md = new Metadata();
17
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
18
18
  const entity = md.Entities.find(e => e.Name === params.EntityName);
19
19
  if (!entity) {
20
20
  return {
@@ -52,7 +52,7 @@ export class ViewOperationsHandler {
52
52
  static async runViews(paramsArray: RunViewParams[], user: UserInfo): Promise<{ success: boolean, results?: RunViewResult[], error?: string }> {
53
53
  try {
54
54
  // Validate and sanitize each set of parameters
55
- const md = new Metadata();
55
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
56
56
  for (const params of paramsArray) {
57
57
  // Validate entity exists
58
58
  const entity = md.Entities.find(e => e.Name === params.EntityName);
@@ -93,7 +93,7 @@ export class ViewOperationsHandler {
93
93
  static async listEntities(params: RunViewParams, user: UserInfo): Promise<RunViewResult> {
94
94
  try {
95
95
  // Check entity exists and user has permission
96
- const md = new Metadata();
96
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
97
97
  const entity = md.Entities.find(e => e.Name === params.EntityName);
98
98
  if (!entity) {
99
99
  throw new Error(`Entity '${params.EntityName}' not found`);
@@ -122,7 +122,7 @@ export class ViewOperationsHandler {
122
122
  static async getEntityViews(entityName: string, user: UserInfo): Promise<{ success: boolean, views?: any[], error?: string }> {
123
123
  try {
124
124
  // Validate entity exists
125
- const md = new Metadata();
125
+ const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
126
126
  const entity = md.Entities.find(e => e.Name === entityName);
127
127
  if (!entity) {
128
128
  return {
@@ -1,4 +1,4 @@
1
- import { DatabaseProviderBase, Metadata, RunView, UserInfo, LogError, LogStatus } from '@memberjunction/core';
1
+ import { DatabaseProviderBase, IMetadataProvider, Metadata, RunView, UserInfo, LogError, LogStatus } from '@memberjunction/core';
2
2
  import { MJTaskEntity, MJTaskDependencyEntity, MJTaskTypeEntity, MJConversationDetailEntity, MJArtifactEntity, MJArtifactVersionEntity, MJConversationDetailArtifactEntity, MJUserNotificationEntity } from '@memberjunction/core-entities';
3
3
  import { AgentRunner } from '@memberjunction/ai-agents';
4
4
  import { ChatMessageRole } from '@memberjunction/ai';
@@ -53,9 +53,14 @@ export class TaskOrchestrator {
53
53
  private sessionId?: string,
54
54
  private userPayload?: UserPayload,
55
55
  private createNotifications: boolean = false,
56
- private conversationDetailId?: string
56
+ private conversationDetailId?: string,
57
+ private provider?: IMetadataProvider
57
58
  ) {}
58
59
 
60
+ private getMetadata(): IMetadataProvider {
61
+ return this.provider ?? (new Metadata() as unknown as IMetadataProvider);
62
+ }
63
+
59
64
  /**
60
65
  * Initialize the orchestrator by finding/creating the AI Agent Task type
61
66
  */
@@ -77,7 +82,7 @@ export class TaskOrchestrator {
77
82
  }
78
83
 
79
84
  // Create the task type if it doesn't exist
80
- const md = new Metadata();
85
+ const md = this.getMetadata();
81
86
  const taskType = await md.GetEntityObject<MJTaskTypeEntity>('MJ: Task Types', this.contextUser);
82
87
  taskType.Name = 'AI Agent Execution';
83
88
  taskType.Description = 'Task executed by an AI agent as part of conversation workflow';
@@ -104,7 +109,7 @@ export class TaskOrchestrator {
104
109
  environmentId: string
105
110
  ): Promise<{ parentTaskId: string; taskIdMap: Map<string, string> }> {
106
111
  const taskTypeId = await this.ensureTaskType();
107
- const md = new Metadata();
112
+ const md = this.getMetadata();
108
113
  const tempIdToRealId = new Map<string, string>();
109
114
 
110
115
  // Build the parent task, deduplicate the incoming task defs, and resolve agents
@@ -142,7 +147,7 @@ export class TaskOrchestrator {
142
147
  }
143
148
 
144
149
  // Persist parent + children + dependency graph in one transaction
145
- const provider = Metadata.Provider as DatabaseProviderBase;
150
+ const provider = (this.provider ?? Metadata.Provider) as DatabaseProviderBase;
146
151
  await provider.BeginTransaction();
147
152
  try {
148
153
  if (!await parentTask.Save()) {
@@ -302,7 +307,7 @@ export class TaskOrchestrator {
302
307
  let hasMore = true;
303
308
 
304
309
  // Get parent task for progress updates
305
- const md = new Metadata();
310
+ const md = this.getMetadata();
306
311
  const parentTask = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
307
312
  await parentTask.Load(parentTaskId);
308
313
 
@@ -381,7 +386,7 @@ export class TaskOrchestrator {
381
386
  * Update parent task progress based on child task completion
382
387
  */
383
388
  private async updateParentTaskProgress(parentTaskId: string): Promise<void> {
384
- const md = new Metadata();
389
+ const md = this.getMetadata();
385
390
  const parentTask = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
386
391
  const loaded = await parentTask.Load(parentTaskId);
387
392
  if (!loaded) return;
@@ -414,7 +419,7 @@ export class TaskOrchestrator {
414
419
  * Mark parent task as complete when all children are done
415
420
  */
416
421
  private async completeParentTask(parentTaskId: string): Promise<void> {
417
- const md = new Metadata();
422
+ const md = this.getMetadata();
418
423
  const parentTask = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
419
424
  const loaded = await parentTask.Load(parentTaskId);
420
425
  if (!loaded) return;
@@ -464,7 +469,7 @@ export class TaskOrchestrator {
464
469
  * Load a task by ID
465
470
  */
466
471
  private async loadTask(taskId: string): Promise<MJTaskEntity | null> {
467
- const md = new Metadata();
472
+ const md = this.getMetadata();
468
473
  const task = await md.GetEntityObject<MJTaskEntity>('MJ: Tasks', this.contextUser);
469
474
  const loaded = await task.Load(taskId);
470
475
  return loaded ? task : null;
@@ -483,7 +488,7 @@ export class TaskOrchestrator {
483
488
  await task.Save();
484
489
 
485
490
  // Load the agent entity
486
- const md = new Metadata();
491
+ const md = this.getMetadata();
487
492
  const agentEntity = await md.GetEntityObject<MJAIAgentEntityExtended>('MJ: AI Agents', this.contextUser);
488
493
  const loaded = await agentEntity.Load(task.AgentID!);
489
494
  if (!loaded) {
@@ -707,8 +712,8 @@ export class TaskOrchestrator {
707
712
  agent: MJAIAgentEntityExtended,
708
713
  taskName: string
709
714
  ): Promise<void> {
710
- const md = new Metadata();
711
- const provider = Metadata.Provider as DatabaseProviderBase;
715
+ const md = this.getMetadata();
716
+ const provider = (this.provider ?? Metadata.Provider) as DatabaseProviderBase;
712
717
 
713
718
  await provider.BeginTransaction();
714
719
  try {
@@ -795,7 +800,7 @@ export class TaskOrchestrator {
795
800
  return;
796
801
  }
797
802
 
798
- const md = new Metadata();
803
+ const md = this.getMetadata();
799
804
 
800
805
  // Load conversation detail to get conversation ID
801
806
  const detail = await md.GetEntityObject<MJConversationDetailEntity>(