@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
@@ -5,8 +5,10 @@ import {
5
5
  CompositeKey,
6
6
  DatabaseProviderBase,
7
7
  EntityFieldTSType,
8
+ EntityInfo,
8
9
  EntityPermissionType,
9
10
  EntitySaveOptions,
11
+ IMetadataProvider,
10
12
  IRunViewProvider,
11
13
  LogDebug,
12
14
  LogError,
@@ -65,7 +67,7 @@ export class ResolverBase {
65
67
  * @param contextUser - Optional user context for decryption (required for encrypted fields)
66
68
  * @returns The processed data object
67
69
  */
68
- protected async MapFieldNamesToCodeNames(entityName: string, dataObject: any, contextUser?: UserInfo): Promise<any> {
70
+ protected async MapFieldNamesToCodeNames(entityName: string, dataObject: any, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<any> {
69
71
  // Return null for empty objects (e.g. when no rows found due to RLS filtering)
70
72
  if (!dataObject || Object.keys(dataObject).length === 0) {
71
73
  return null;
@@ -77,8 +79,8 @@ export class ResolverBase {
77
79
  // with the CodeName, because we can't transfer those via GraphQL as they are not
78
80
  // valid property names in GraphQL
79
81
  {
80
- const md = new Metadata();
81
- const entityInfo = md.Entities.find((e) => e.Name === entityName);
82
+ const md = provider ?? new Metadata();
83
+ const entityInfo = md.EntityByName(entityName);
82
84
  if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
83
85
  // const fields = entityInfo.Fields.filter((f) => f.Name !== f.CodeName || f.Name.startsWith('__mj_'));
84
86
  const mapper = new FieldMapper();
@@ -209,12 +211,13 @@ export class ResolverBase {
209
211
  protected async FilterEncryptedFieldsForAPI(
210
212
  entityName: string,
211
213
  dataObject: Record<string, unknown>,
212
- contextUser: UserInfo
214
+ contextUser: UserInfo,
215
+ provider?: IMetadataProvider
213
216
  ): Promise<Record<string, unknown>> {
214
217
  if (!dataObject) return dataObject;
215
218
 
216
- const md = new Metadata();
217
- const entityInfo = md.Entities.find((e) => e.Name === entityName);
219
+ const md = provider ?? new Metadata();
220
+ const entityInfo = md.EntityByName(entityName);
218
221
  if (!entityInfo) return dataObject;
219
222
 
220
223
  // Find all encrypted fields that need filtering
@@ -269,13 +272,14 @@ export class ResolverBase {
269
272
  protected async ArrayFilterEncryptedFieldsForAPI(
270
273
  entityName: string,
271
274
  dataObjectArray: Record<string, unknown>[],
272
- contextUser: UserInfo
275
+ contextUser: UserInfo,
276
+ provider?: IMetadataProvider
273
277
  ): Promise<Record<string, unknown>[]> {
274
278
  if (!dataObjectArray || dataObjectArray.length === 0) return dataObjectArray;
275
279
 
276
280
  // Check if entity has any encrypted fields first to avoid unnecessary processing
277
- const md = new Metadata();
278
- const entityInfo = md.Entities.find((e) => e.Name === entityName);
281
+ const md = provider ?? new Metadata();
282
+ const entityInfo = md.EntityByName(entityName);
279
283
  if (!entityInfo) return dataObjectArray;
280
284
 
281
285
  const encryptedFields = entityInfo.Fields.filter(f => f.Encrypt && !f.AllowDecryptInAPI);
@@ -283,7 +287,7 @@ export class ResolverBase {
283
287
 
284
288
  // Process each element
285
289
  for (const element of dataObjectArray) {
286
- await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser);
290
+ await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser, provider);
287
291
  }
288
292
 
289
293
  return dataObjectArray;
@@ -562,9 +566,9 @@ export class ResolverBase {
562
566
  }
563
567
  }
564
568
 
565
- protected CheckUserReadPermissions(entityName: string, userPayload: UserPayload | null) {
566
- const md = new Metadata();
567
- const entityInfo = md.Entities.find((e) => e.Name === entityName);
569
+ protected CheckUserReadPermissions(entityName: string, userPayload: UserPayload | null, provider?: IMetadataProvider) {
570
+ const md = provider ?? new Metadata();
571
+ const entityInfo = md.EntityByName(entityName);
568
572
  if (!userPayload) {
569
573
  throw new Error(`userPayload is null`);
570
574
  }
@@ -796,10 +800,10 @@ export class ResolverBase {
796
800
  // Skip processing if no params
797
801
  if (!params.length) return [];
798
802
 
799
- let md: Metadata | null = null;
803
+ let md: IMetadataProvider | null = null;
800
804
  const rv = params[0].provider as any as IRunViewProvider;
801
805
  let runViewParams: RunViewParams[] = [];
802
-
806
+
803
807
  // Fix #1: Get user info only once for all queries
804
808
  let contextUser: UserInfo | null = null;
805
809
  if (params[0]?.userPayload?.email) {
@@ -810,10 +814,12 @@ export class ResolverBase {
810
814
  }
811
815
  contextUser = user;
812
816
  }
813
-
817
+
814
818
  // Create a map of entities to validate only once per entity
815
819
  const validatedEntities = new Set<string>();
816
- md = new Metadata();
820
+ // Use the per-request provider that came in on params instead of `new Metadata()` so
821
+ // multi-tenant servers resolve metadata against the request's own connection.
822
+ md = params[0].provider as unknown as IMetadataProvider;
817
823
 
818
824
  // Transform parameters
819
825
  for (const param of params) {
@@ -821,7 +827,7 @@ export class ResolverBase {
821
827
  // Validate entity only once per entity type
822
828
  const entityName = param.viewInfo.Entity;
823
829
  if (!validatedEntities.has(entityName)) {
824
- const entityInfo = md.Entities.find(e => e.Name === entityName);
830
+ const entityInfo = md.EntityByName(entityName);
825
831
  if (!entityInfo) {
826
832
  throw new Error(`Entity ${entityName} not found in metadata`);
827
833
  }
@@ -998,7 +1004,7 @@ export class ResolverBase {
998
1004
  }
999
1005
 
1000
1006
  public get MJCoreSchema(): string {
1001
- return Metadata.Provider.ConfigData.MJCoreSchemaName;
1007
+ return Metadata.Provider.ConfigData.MJCoreSchemaName; // global-provider-ok: process-wide config (schema name) read once at module level
1002
1008
  }
1003
1009
 
1004
1010
  /**
@@ -1356,9 +1362,10 @@ export class ResolverBase {
1356
1362
  }
1357
1363
  });
1358
1364
 
1359
- // Create ErrorLog record in the database
1365
+ // Create ErrorLog record in the database — use the entity's bound provider so the
1366
+ // ErrorLog write goes to the same connection as the entity that triggered it.
1360
1367
  try {
1361
- const md = new Metadata();
1368
+ const md = entityObject.ProviderToUse as unknown as IMetadataProvider;
1362
1369
  const errorLogEntity = await md.GetEntityObject<MJErrorLogEntity>('MJ: Error Logs', contextUser);
1363
1370
  errorLogEntity.Code = 'ENTITY_SAVE_INCONSISTENCY';
1364
1371
  errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
package/src/index.ts CHANGED
@@ -100,6 +100,7 @@ export * from './resolvers/FetchEntityVectorsResolver.js';
100
100
  export * from './resolvers/PipelineProgressResolver.js';
101
101
  export * from './resolvers/ClientToolRequestResolver.js';
102
102
  export * from './resolvers/AutotagPipelineResolver.js';
103
+ export * from './resolvers/TagGovernanceResolver.js';
103
104
  export * from './resolvers/TaskResolver.js';
104
105
  export * from './generic/KeyValuePairInput.js';
105
106
  export * from './generic/KeyInputOutputTypes.js';
@@ -284,7 +285,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
284
285
  return origExecuteSQLWithPool.call(this, pool, query, parameters, contextUser);
285
286
  };
286
287
 
287
- const md = new Metadata();
288
+ const md = new Metadata(); // global-provider-ok: bootstrap
288
289
  console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
289
290
  } else {
290
291
  // ─── SQL Server Path (existing behavior) ───────────────────────
@@ -326,7 +327,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
326
327
  const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval);
327
328
  await setupSQLServerClient(config);
328
329
  tPhase = lap('Metadata + Provider Setup', tPhase);
329
- const md = new Metadata();
330
+ const md = new Metadata(); // global-provider-ok: bootstrap
330
331
  console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
331
332
 
332
333
  // Set up CodeGen-credentialed provider for RSU DDL operations (CREATE TABLE, CREATE SCHEMA, etc.)
@@ -418,7 +419,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
418
419
  enablePubSub: true,
419
420
  enableLogging: true,
420
421
  });
421
- (Metadata.Provider as GenericDatabaseProvider).SetLocalStorageProvider(redisProvider);
422
+ (Metadata.Provider as GenericDatabaseProvider).SetLocalStorageProvider(redisProvider); // global-provider-ok: bootstrap (Redis cache wiring)
422
423
  await redisProvider.StartListening();
423
424
 
424
425
  // Connect Redis pub/sub events to LocalCacheManager callback dispatch
@@ -449,7 +450,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
449
450
  // LocalCacheManager may have already been initialized (with in-memory provider)
450
451
  // during engine loading. SetStorageProvider migrates cached data to Redis.
451
452
  if (process.env.REDIS_URL) {
452
- await LocalCacheManager.Instance.SetStorageProvider(Metadata.Provider.LocalStorageProvider);
453
+ await LocalCacheManager.Instance.SetStorageProvider(Metadata.Provider.LocalStorageProvider); // global-provider-ok: bootstrap
453
454
  console.log('LocalCacheManager: storage provider swapped to Redis');
454
455
  }
455
456
  // Ensure LocalCacheManager is initialized (no-op if already done during engine loading)
@@ -463,7 +464,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
463
464
  evictionSweepIntervalMs: (cs.evictionSweepIntervalSeconds ?? 300) * 1000,
464
465
  verboseLogging: cs.verboseLogging ?? false,
465
466
  };
466
- await LocalCacheManager.Instance.Initialize(Metadata.Provider.LocalStorageProvider, cacheConfig);
467
+ await LocalCacheManager.Instance.Initialize(Metadata.Provider.LocalStorageProvider, cacheConfig); // global-provider-ok: bootstrap
467
468
  console.log('LocalCacheManager initialized with cache config:', JSON.stringify({
468
469
  maxMemoryMB: cs.maxMemoryMB ?? 150,
469
470
  maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
@@ -943,8 +944,7 @@ async function processRSUPendingWork(): Promise<void> {
943
944
 
944
945
  for (const item of pendingItems) {
945
946
  try {
946
- const md = new Metadata();
947
-
947
+ const md = new Metadata(); // global-provider-ok: server startup recovery — runs once before any per-request context exists
948
948
  // Get system user for server-side operations
949
949
  const systemUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
950
950
  if (!systemUser) {
@@ -952,7 +952,7 @@ async function processRSUPendingWork(): Promise<void> {
952
952
  continue;
953
953
  }
954
954
 
955
- await Metadata.Provider.Refresh();
955
+ await Metadata.Provider.Refresh(); // global-provider-ok: server startup recovery — one-shot global cache refresh
956
956
 
957
957
  // Resolve connector
958
958
  const rv = new RunView();
@@ -1221,7 +1221,7 @@ async function refreshUserCacheFromPG(pgPool: import('pg').Pool, coreSchema: str
1221
1221
  ...user,
1222
1222
  UserRoles: roles.filter((role: Record<string, unknown>) => UUIDsEqual(role.UserID as string, user.ID as string)),
1223
1223
  };
1224
- return new UserInfo(Metadata.Provider, userWithRoles);
1224
+ return new UserInfo(Metadata.Provider, userWithRoles); // global-provider-ok: bootstrap (UserCache initialization)
1225
1225
  });
1226
1226
  // Access the UserCache internals to set users
1227
1227
  const cache = UserCache.Instance;
@@ -67,7 +67,7 @@ export function attachTenantContext(
67
67
  function isEntityScoped(entityName: string, config: MultiTenancyConfig): boolean {
68
68
  // Auto-exclude core MJ entities (entities in the __mj schema)
69
69
  if (config.autoExcludeCoreEntities) {
70
- const md = new Metadata();
70
+ const md = new Metadata(); // global-provider-ok: read-only schema check. Hooks (PreRunViewHook/PreSaveHook) don't carry per-request provider; reading the constant `__mj` schema flag is identical across every provider's catalog.
71
71
  const entity = md.Entities.find(
72
72
  e => e.Name.trim().toLowerCase() === entityName.trim().toLowerCase()
73
73
  );
@@ -1,10 +1,11 @@
1
1
  import { Resolver, Mutation, Arg, Ctx } from "type-graphql";
2
2
  import { Field, InputType, ObjectType } from "type-graphql";
3
- import { LogError, Metadata } from "@memberjunction/core";
3
+ import { IMetadataProvider, LogError, Metadata } from "@memberjunction/core";
4
4
  import { MJAPIKeyScopeEntity } from "@memberjunction/core-entities";
5
5
  import { GetAPIKeyEngine } from "@memberjunction/api-keys";
6
6
  import { AppContext } from "../types.js";
7
7
  import { ResolverBase } from "../generic/ResolverBase.js";
8
+ import { GetReadWriteProvider } from "../util.js";
8
9
 
9
10
  /**
10
11
  * Input type for creating a new API key
@@ -144,7 +145,8 @@ export class APIKeyResolver extends ResolverBase {
144
145
 
145
146
  // Save scope associations if provided
146
147
  if (input.ScopeIDs && input.ScopeIDs.length > 0 && result.APIKeyId) {
147
- await this.saveScopeAssociations(result.APIKeyId, input.ScopeIDs, user);
148
+ const writeProvider = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
149
+ await this.saveScopeAssociations(result.APIKeyId, input.ScopeIDs, user, writeProvider);
148
150
  }
149
151
 
150
152
  return {
@@ -219,9 +221,10 @@ export class APIKeyResolver extends ResolverBase {
219
221
  private async saveScopeAssociations(
220
222
  apiKeyId: string,
221
223
  scopeIds: string[],
222
- user: any
224
+ user: any,
225
+ provider?: IMetadataProvider
223
226
  ): Promise<void> {
224
- const md = new Metadata();
227
+ const md = provider ?? new Metadata();
225
228
 
226
229
  for (const scopeId of scopeIds) {
227
230
  try {
@@ -1,12 +1,13 @@
1
1
  import { Resolver, Mutation, Ctx, Arg, ObjectType, Field } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
- import { LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
3
+ import { IMetadataProvider, LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
4
  import { MJContentProcessRunEntity } from '@memberjunction/core-entities';
5
5
  import { ResolverBase } from '../generic/ResolverBase.js';
6
6
  import { ActionEngineServer } from '@memberjunction/actions';
7
7
  import { PubSubManager } from '../generic/PubSubManager.js';
8
8
  import { PipelineProgressNotification } from './PipelineProgressResolver.js';
9
9
  import { v4 as uuidv4 } from 'uuid';
10
+ import { GetReadWriteProvider } from '../util.js';
10
11
 
11
12
  const PIPELINE_PROGRESS_TOPIC = 'PIPELINE_PROGRESS';
12
13
 
@@ -31,7 +32,7 @@ export class AutotagPipelineResolver extends ResolverBase {
31
32
  async RunAutotagPipeline(
32
33
  @Arg('contentSourceIDs', () => [String], { nullable: true }) contentSourceIDs: string[] | undefined,
33
34
  @Arg('forceReprocess', { nullable: true }) forceReprocess: boolean | undefined,
34
- @Ctx() { userPayload }: AppContext = {} as AppContext
35
+ @Ctx() { userPayload, providers }: AppContext = {} as AppContext
35
36
  ): Promise<AutotagPipelineResult> {
36
37
  try {
37
38
  const currentUser = this.GetUserFromPayload(userPayload);
@@ -42,8 +43,14 @@ export class AutotagPipelineResolver extends ResolverBase {
42
43
  const pipelineRunID = uuidv4();
43
44
  LogStatus(`RunAutotagPipeline: starting pipeline ${pipelineRunID}`);
44
45
 
46
+ // Capture the per-request provider snapshot before returning so the fire-and-forget
47
+ // background job binds to the same connection the caller used. Without this, the
48
+ // background job would silently use the global default — wrong for multi-tenant.
49
+ const provider = (GetReadWriteProvider(providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider)
50
+ ?? (new Metadata() as unknown as IMetadataProvider);
51
+
45
52
  // Fire-and-forget: start the pipeline in the background and return immediately
46
- this.runPipelineInBackground(pipelineRunID, currentUser, contentSourceIDs, forceReprocess);
53
+ this.runPipelineInBackground(pipelineRunID, currentUser, provider, contentSourceIDs, forceReprocess);
47
54
 
48
55
  return {
49
56
  Success: true,
@@ -68,11 +75,12 @@ export class AutotagPipelineResolver extends ResolverBase {
68
75
  private async runPipelineInBackground(
69
76
  pipelineRunID: string,
70
77
  currentUser: UserInfo,
78
+ provider: IMetadataProvider,
71
79
  contentSourceIDs?: string[],
72
80
  forceReprocess?: boolean
73
81
  ): Promise<void> {
74
82
  const startTime = Date.now();
75
- const processRun = await this.createProcessRun(pipelineRunID, currentUser, contentSourceIDs);
83
+ const processRun = await this.createProcessRun(pipelineRunID, currentUser, provider, contentSourceIDs);
76
84
 
77
85
  try {
78
86
  this.publishProgress(pipelineRunID, 'autotag', 0, 0, startTime, 'Initializing pipeline...');
@@ -147,6 +155,7 @@ export class AutotagPipelineResolver extends ResolverBase {
147
155
  private async createProcessRun(
148
156
  pipelineRunID: string,
149
157
  currentUser: UserInfo,
158
+ provider: IMetadataProvider,
150
159
  contentSourceIDs?: string[]
151
160
  ): Promise<MJContentProcessRunEntity | null> {
152
161
  try {
@@ -156,7 +165,7 @@ export class AutotagPipelineResolver extends ResolverBase {
156
165
  sourceID = contentSourceIDs[0];
157
166
  } else {
158
167
  // Load content sources to get any available source ID (SourceID is NOT NULL)
159
- const rv = new RunView();
168
+ const rv = RunView.FromMetadataProvider(provider);
160
169
  const result = await rv.RunView<{ ID: string }>({
161
170
  EntityName: 'MJ: Content Sources',
162
171
  Fields: ['ID'],
@@ -173,7 +182,7 @@ export class AutotagPipelineResolver extends ResolverBase {
173
182
  return null;
174
183
  }
175
184
 
176
- const md = new Metadata();
185
+ const md = provider;
177
186
  const run = await md.GetEntityObject<MJContentProcessRunEntity>('MJ: Content Process Runs', currentUser);
178
187
  run.NewRecord();
179
188
  run.ID = pipelineRunID;
@@ -252,7 +261,7 @@ export class AutotagPipelineResolver extends ResolverBase {
252
261
  @Mutation(() => AutotagPipelineResult)
253
262
  async PauseClassificationPipeline(
254
263
  @Arg('processRunID') processRunID: string,
255
- @Ctx() { userPayload }: AppContext = {} as AppContext
264
+ @Ctx() { userPayload, providers }: AppContext = {} as AppContext
256
265
  ): Promise<AutotagPipelineResult> {
257
266
  try {
258
267
  const currentUser = this.GetUserFromPayload(userPayload);
@@ -260,7 +269,7 @@ export class AutotagPipelineResolver extends ResolverBase {
260
269
  return { Success: false, Status: 'Error', ErrorMessage: 'Unable to determine current user' };
261
270
  }
262
271
 
263
- const md = new Metadata();
272
+ const md = (GetReadWriteProvider(providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider) ?? new Metadata();
264
273
  const run = await md.GetEntityObject<MJContentProcessRunEntity>('MJ: Content Process Runs', currentUser);
265
274
  const loaded = await run.Load(processRunID);
266
275
  if (!loaded) {
@@ -284,7 +293,7 @@ export class AutotagPipelineResolver extends ResolverBase {
284
293
  @Mutation(() => AutotagPipelineResult)
285
294
  async ResumeClassificationPipeline(
286
295
  @Arg('processRunID') processRunID: string,
287
- @Ctx() { userPayload }: AppContext = {} as AppContext
296
+ @Ctx() { userPayload, providers }: AppContext = {} as AppContext
288
297
  ): Promise<AutotagPipelineResult> {
289
298
  try {
290
299
  const currentUser = this.GetUserFromPayload(userPayload);
@@ -292,7 +301,7 @@ export class AutotagPipelineResolver extends ResolverBase {
292
301
  return { Success: false, Status: 'Error', ErrorMessage: 'Unable to determine current user' };
293
302
  }
294
303
 
295
- const md = new Metadata();
304
+ const md = (GetReadWriteProvider(providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider) ?? new Metadata();
296
305
  const run = await md.GetEntityObject<MJContentProcessRunEntity>('MJ: Content Process Runs', currentUser);
297
306
  const loaded = await run.Load(processRunID);
298
307
  if (!loaded) {
@@ -312,7 +321,7 @@ export class AutotagPipelineResolver extends ResolverBase {
312
321
  const pipelineRunID = uuidv4();
313
322
  LogStatus(`ResumeClassificationPipeline: Resuming run ${processRunID} from offset ${run.LastProcessedOffset}`);
314
323
 
315
- this.runPipelineInBackground(pipelineRunID, currentUser, undefined, undefined);
324
+ this.runPipelineInBackground(pipelineRunID, currentUser, md as unknown as IMetadataProvider, undefined, undefined);
316
325
 
317
326
  return { Success: true, Status: 'Resumed', PipelineRunID: pipelineRunID };
318
327
  } catch (error) {
@@ -1,5 +1,5 @@
1
1
  import { Arg, Ctx, Field, InputType, ObjectType, Query, Mutation, Resolver } from 'type-graphql';
2
- import { UserInfo, Metadata, LogError, LogStatus } from '@memberjunction/core';
2
+ import { UserInfo, IMetadataProvider, Metadata, LogError, LogStatus } from '@memberjunction/core';
3
3
  import { UUIDsEqual } from '@memberjunction/global';
4
4
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
5
5
  import { MJComponentEntity, MJComponentRegistryEntity, ComponentMetadataEngine } from '@memberjunction/core-entities';
@@ -16,6 +16,7 @@ import {
16
16
  } from '@memberjunction/component-registry-client-sdk';
17
17
  import { AppContext } from '../types.js';
18
18
  import { configInfo } from '../config.js';
19
+ import { GetReadWriteProvider } from '../util.js';
19
20
 
20
21
  /**
21
22
  * GraphQL types for Component Registry operations
@@ -181,7 +182,7 @@ export class ComponentRegistryExtendedResolver {
181
182
  @Arg('registryName') registryName: string,
182
183
  @Arg('namespace') namespace: string,
183
184
  @Arg('name') name: string,
184
- @Ctx() { userPayload }: AppContext,
185
+ @Ctx() { userPayload, providers }: AppContext,
185
186
  @Arg('version', { nullable: true }) version?: string,
186
187
  @Arg('hash', { nullable: true }) hash?: string
187
188
  ): Promise<ComponentSpecWithHashType> {
@@ -234,7 +235,8 @@ export class ComponentRegistryExtendedResolver {
234
235
 
235
236
  // Optional: Cache in database if configured
236
237
  if (this.shouldCache(registry)) {
237
- await this.cacheComponent(component, registry.ID, user);
238
+ const writeProvider = GetReadWriteProvider(providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
239
+ await this.cacheComponent(component, registry.ID, user, writeProvider);
238
240
  }
239
241
 
240
242
  // Return the ComponentSpec as a JSON string
@@ -505,11 +507,12 @@ export class ComponentRegistryExtendedResolver {
505
507
  private async cacheComponent(
506
508
  component: ComponentSpec,
507
509
  registryId: string,
508
- userInfo: UserInfo
510
+ userInfo: UserInfo,
511
+ provider?: IMetadataProvider
509
512
  ): Promise<void> {
510
513
  try {
511
514
  // Find or create component entity
512
- const md = new Metadata();
515
+ const md = provider ?? new Metadata();
513
516
  const componentEntity = await md.GetEntityObject<MJComponentEntity>('MJ: Components', userInfo);
514
517
 
515
518
  // Check if component already exists
@@ -1,4 +1,4 @@
1
- import { EntityPermissionType, Metadata, FieldValueCollection, EntitySaveOptions } from '@memberjunction/core';
1
+ import { EntityPermissionType, FieldValueCollection, EntitySaveOptions } from '@memberjunction/core';
2
2
  import { NormalizeUUID } from '@memberjunction/global';
3
3
  import { MJFileEntity, MJFileStorageProviderEntity, MJFileStorageAccountEntity } from '@memberjunction/core-entities';
4
4
  import {
@@ -417,7 +417,7 @@ export class FileResolver extends FileResolverBase {
417
417
 
418
418
  @FieldResolver(() => String)
419
419
  async DownloadUrl(@Root() file: MJFile_, @Ctx() context: AppContext) {
420
- const md = new Metadata();
420
+ const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
421
421
  const user = this.GetUserFromPayload(context.userPayload);
422
422
  const fileEntity = await md.GetEntityObject<MJFileEntity>('MJ: Files', user);
423
423
  fileEntity.CheckPermissions(EntityPermissionType.Read, true);
@@ -2,7 +2,6 @@ import { Arg, Ctx, Field, ObjectType, Query, Resolver } from "type-graphql";
2
2
  import { AppContext } from "../types.js";
3
3
  import { DataContext } from "@memberjunction/data-context";
4
4
  import { GetReadOnlyDataSource, GetReadOnlyProvider } from "../util.js";
5
- import { Metadata } from "@memberjunction/core";
6
5
  import { MJDataContextItemEntity } from "@memberjunction/core-entities";
7
6
  import { ResolverBase } from "../generic/ResolverBase.js";
8
7
 
@@ -62,7 +61,7 @@ export class GetDataContextDataResolver extends ResolverBase {
62
61
  const dciData = await md.GetEntityObject<MJDataContextItemEntity>("MJ: Data Context Items", appCtx.userPayload.userRecord);
63
62
  if (await dciData.Load(DataContextItemID)) {
64
63
  const dci = DataContext.CreateDataContextItem(); // use class factory to get whatever lowest level sub-class is registered
65
- await dci.LoadMetadataFromEntityRecord(dciData, Metadata.Provider, appCtx.userPayload.userRecord);
64
+ await dci.LoadMetadataFromEntityRecord(dciData, md, appCtx.userPayload.userRecord);
66
65
  // now the metadata is loaded so we can call the regular load function
67
66
  if (await dci.LoadData(ds)) {
68
67
  return {
@@ -1,4 +1,4 @@
1
- import { EntityInfo, IEntityDataProvider, Metadata, UserInfo } from '@memberjunction/core';
1
+ import { IEntityDataProvider } from '@memberjunction/core';
2
2
  import { Arg, Ctx, Field, ObjectType, Query, Resolver } from 'type-graphql';
3
3
  import { AppContext } from '../types.js';
4
4
  import { GetReadOnlyProvider } from '../util.js';
@@ -63,8 +63,7 @@ export class ISAEntityResolver {
63
63
  ): Promise<ISAChildEntityResult> {
64
64
  try {
65
65
  const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
66
- const md = new Metadata();
67
- const entityInfo = md.Entities.find(e => e.Name === EntityName);
66
+ const entityInfo = provider.Entities.find(e => e.Name === EntityName);
68
67
 
69
68
  if (!entityInfo) {
70
69
  return {
@@ -127,8 +126,7 @@ export class ISAEntityResolver {
127
126
  ): Promise<ISAChildEntitiesResult> {
128
127
  try {
129
128
  const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
130
- const md = new Metadata();
131
- const entityInfo = md.Entities.find(e => e.Name === EntityName);
129
+ const entityInfo = provider.Entities.find(e => e.Name === EntityName);
132
130
 
133
131
  if (!entityInfo) {
134
132
  return {