@memberjunction/server 2.95.0 → 2.96.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 (92) hide show
  1. package/dist/context.d.ts.map +1 -1
  2. package/dist/context.js +4 -0
  3. package/dist/context.js.map +1 -1
  4. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  5. package/dist/resolvers/ActionResolver.js +5 -4
  6. package/dist/resolvers/ActionResolver.js.map +1 -1
  7. package/dist/resolvers/AskSkipResolver.d.ts +4 -4
  8. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  9. package/dist/resolvers/AskSkipResolver.js +9 -9
  10. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  11. package/dist/resolvers/CreateQueryResolver.d.ts +2 -2
  12. package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
  13. package/dist/resolvers/CreateQueryResolver.js +25 -17
  14. package/dist/resolvers/CreateQueryResolver.js.map +1 -1
  15. package/dist/resolvers/DatasetResolver.d.ts +2 -2
  16. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  17. package/dist/resolvers/DatasetResolver.js +6 -5
  18. package/dist/resolvers/DatasetResolver.js.map +1 -1
  19. package/dist/resolvers/EntityRecordNameResolver.d.ts +4 -4
  20. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  21. package/dist/resolvers/EntityRecordNameResolver.js +6 -5
  22. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  23. package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
  24. package/dist/resolvers/FileCategoryResolver.js +10 -11
  25. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  26. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  27. package/dist/resolvers/FileResolver.js +5 -6
  28. package/dist/resolvers/FileResolver.js.map +1 -1
  29. package/dist/resolvers/GetDataContextDataResolver.js +2 -2
  30. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  31. package/dist/resolvers/GetDataResolver.js +3 -3
  32. package/dist/resolvers/GetDataResolver.js.map +1 -1
  33. package/dist/resolvers/MergeRecordsResolver.d.ts +3 -3
  34. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  35. package/dist/resolvers/MergeRecordsResolver.js +8 -7
  36. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  37. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +1 -1
  38. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
  39. package/dist/resolvers/PotentialDuplicateRecordResolver.js +4 -3
  40. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  41. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  42. package/dist/resolvers/QueryResolver.js +19 -14
  43. package/dist/resolvers/QueryResolver.js.map +1 -1
  44. package/dist/resolvers/ReportResolver.d.ts +2 -2
  45. package/dist/resolvers/ReportResolver.d.ts.map +1 -1
  46. package/dist/resolvers/ReportResolver.js +8 -6
  47. package/dist/resolvers/ReportResolver.js.map +1 -1
  48. package/dist/resolvers/RunAIAgentResolver.d.ts +3 -7
  49. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  50. package/dist/resolvers/RunAIAgentResolver.js +8 -5
  51. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  52. package/dist/resolvers/RunAIPromptResolver.d.ts +3 -7
  53. package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
  54. package/dist/resolvers/RunAIPromptResolver.js +10 -8
  55. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  56. package/dist/resolvers/RunTemplateResolver.d.ts +2 -4
  57. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  58. package/dist/resolvers/RunTemplateResolver.js +5 -4
  59. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  60. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  61. package/dist/resolvers/SqlLoggingConfigResolver.js +7 -7
  62. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  63. package/dist/resolvers/UserFavoriteResolver.d.ts +3 -4
  64. package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
  65. package/dist/resolvers/UserFavoriteResolver.js +10 -68
  66. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  67. package/dist/resolvers/UserViewResolver.d.ts +1 -1
  68. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  69. package/dist/resolvers/UserViewResolver.js +3 -4
  70. package/dist/resolvers/UserViewResolver.js.map +1 -1
  71. package/package.json +39 -39
  72. package/src/context.ts +5 -0
  73. package/src/resolvers/ActionResolver.ts +5 -4
  74. package/src/resolvers/AskSkipResolver.ts +13 -13
  75. package/src/resolvers/CreateQueryResolver.ts +28 -17
  76. package/src/resolvers/DatasetResolver.ts +5 -4
  77. package/src/resolvers/EntityRecordNameResolver.ts +8 -6
  78. package/src/resolvers/FileCategoryResolver.ts +9 -10
  79. package/src/resolvers/FileResolver.ts +6 -7
  80. package/src/resolvers/GetDataContextDataResolver.ts +2 -2
  81. package/src/resolvers/GetDataResolver.ts +2 -2
  82. package/src/resolvers/InfoResolver.ts +1 -1
  83. package/src/resolvers/MergeRecordsResolver.ts +7 -6
  84. package/src/resolvers/PotentialDuplicateRecordResolver.ts +3 -2
  85. package/src/resolvers/QueryResolver.ts +22 -15
  86. package/src/resolvers/ReportResolver.ts +9 -6
  87. package/src/resolvers/RunAIAgentResolver.ts +12 -4
  88. package/src/resolvers/RunAIPromptResolver.ts +12 -8
  89. package/src/resolvers/RunTemplateResolver.ts +5 -5
  90. package/src/resolvers/SqlLoggingConfigResolver.ts +7 -6
  91. package/src/resolvers/UserFavoriteResolver.ts +8 -67
  92. package/src/resolvers/UserViewResolver.ts +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/server",
3
- "version": "2.95.0",
3
+ "version": "2.96.0",
4
4
  "description": "MemberJunction: This project provides API access via GraphQL to the common data store.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./src/index.ts",
@@ -22,44 +22,44 @@
22
22
  "dependencies": {
23
23
  "@apollo/server": "^4.9.1",
24
24
  "@graphql-tools/utils": "^10.0.1",
25
- "@memberjunction/actions": "2.95.0",
26
- "@memberjunction/ai": "2.95.0",
27
- "@memberjunction/ai-core-plus": "2.95.0",
28
- "@memberjunction/ai-agents": "2.95.0",
29
- "@memberjunction/aiengine": "2.95.0",
30
- "@memberjunction/ai-prompts": "2.95.0",
31
- "@memberjunction/ai-agent-manager-actions": "2.95.0",
32
- "@memberjunction/ai-mistral": "2.95.0",
33
- "@memberjunction/ai-openai": "2.95.0",
34
- "@memberjunction/ai-anthropic": "2.95.0",
35
- "@memberjunction/ai-groq": "2.95.0",
36
- "@memberjunction/ai-cerebras": "2.95.0",
37
- "@memberjunction/ai-lmstudio": "2.95.0",
38
- "@memberjunction/ai-openrouter": "2.95.0",
39
- "@memberjunction/ai-ollama": "2.95.0",
40
- "@memberjunction/ai-vectors-pinecone": "2.95.0",
41
- "@memberjunction/ai-local-embeddings": "2.95.0",
42
- "@memberjunction/core": "2.95.0",
43
- "@memberjunction/core-actions": "2.95.0",
44
- "@memberjunction/actions-apollo": "2.95.0",
45
- "@memberjunction/actions-bizapps-accounting": "2.95.0",
46
- "@memberjunction/actions-bizapps-crm": "2.95.0",
47
- "@memberjunction/actions-bizapps-lms": "2.95.0",
48
- "@memberjunction/actions-bizapps-social": "2.95.0",
49
- "@memberjunction/core-entities": "2.95.0",
50
- "@memberjunction/core-entities-server": "2.95.0",
51
- "@memberjunction/data-context": "2.95.0",
52
- "@memberjunction/data-context-server": "2.95.0",
53
- "@memberjunction/doc-utils": "2.95.0",
54
- "@memberjunction/entity-communications-server": "2.95.0",
55
- "@memberjunction/external-change-detection": "2.95.0",
56
- "@memberjunction/global": "2.95.0",
57
- "@memberjunction/graphql-dataprovider": "2.95.0",
58
- "@memberjunction/queue": "2.95.0",
59
- "@memberjunction/skip-types": "2.95.0",
60
- "@memberjunction/sqlserver-dataprovider": "2.95.0",
61
- "@memberjunction/storage": "2.95.0",
62
- "@memberjunction/templates": "2.95.0",
25
+ "@memberjunction/actions": "2.96.0",
26
+ "@memberjunction/ai": "2.96.0",
27
+ "@memberjunction/ai-core-plus": "2.96.0",
28
+ "@memberjunction/ai-agents": "2.96.0",
29
+ "@memberjunction/aiengine": "2.96.0",
30
+ "@memberjunction/ai-prompts": "2.96.0",
31
+ "@memberjunction/ai-agent-manager-actions": "2.96.0",
32
+ "@memberjunction/ai-mistral": "2.96.0",
33
+ "@memberjunction/ai-openai": "2.96.0",
34
+ "@memberjunction/ai-anthropic": "2.96.0",
35
+ "@memberjunction/ai-groq": "2.96.0",
36
+ "@memberjunction/ai-cerebras": "2.96.0",
37
+ "@memberjunction/ai-lmstudio": "2.96.0",
38
+ "@memberjunction/ai-openrouter": "2.96.0",
39
+ "@memberjunction/ai-ollama": "2.96.0",
40
+ "@memberjunction/ai-vectors-pinecone": "2.96.0",
41
+ "@memberjunction/ai-local-embeddings": "2.96.0",
42
+ "@memberjunction/core": "2.96.0",
43
+ "@memberjunction/core-actions": "2.96.0",
44
+ "@memberjunction/actions-apollo": "2.96.0",
45
+ "@memberjunction/actions-bizapps-accounting": "2.96.0",
46
+ "@memberjunction/actions-bizapps-crm": "2.96.0",
47
+ "@memberjunction/actions-bizapps-lms": "2.96.0",
48
+ "@memberjunction/actions-bizapps-social": "2.96.0",
49
+ "@memberjunction/core-entities": "2.96.0",
50
+ "@memberjunction/core-entities-server": "2.96.0",
51
+ "@memberjunction/data-context": "2.96.0",
52
+ "@memberjunction/data-context-server": "2.96.0",
53
+ "@memberjunction/doc-utils": "2.96.0",
54
+ "@memberjunction/entity-communications-server": "2.96.0",
55
+ "@memberjunction/external-change-detection": "2.96.0",
56
+ "@memberjunction/global": "2.96.0",
57
+ "@memberjunction/graphql-dataprovider": "2.96.0",
58
+ "@memberjunction/queue": "2.96.0",
59
+ "@memberjunction/skip-types": "2.96.0",
60
+ "@memberjunction/sqlserver-dataprovider": "2.96.0",
61
+ "@memberjunction/storage": "2.96.0",
62
+ "@memberjunction/templates": "2.96.0",
63
63
  "@types/compression": "^1.7.5",
64
64
  "@types/cors": "^2.8.13",
65
65
  "@types/jsonwebtoken": "9.0.6",
package/src/context.ts CHANGED
@@ -15,6 +15,7 @@ import e from 'express';
15
15
  import { DatabaseProviderBase } from '@memberjunction/core';
16
16
  import { SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
17
17
  import { AuthProviderFactory } from './auth/AuthProviderFactory.js';
18
+ import { Metadata } from '@memberjunction/core';
18
19
 
19
20
  const verifyAsync = async (issuer: string, token: string): Promise<jwt.JwtPayload> =>
20
21
  new Promise((resolve, reject) => {
@@ -157,6 +158,10 @@ export const contextFunction =
157
158
  apiKey
158
159
  );
159
160
 
161
+ if (Metadata.Provider.Entities.length === 0 ) {
162
+ console.warn('WARNING: No entities found in global/shared metadata, this can often be due to the use of **global** Metadata/RunView/DB Providers in a multi-user environment. Check your code to make sure you are using the providers passed to you in AppContext by MJServer and not calling new Metadata() new RunView() new RunQuery() and similar patterns as those are unstable at times in multi-user server environments!!!');
163
+ }
164
+
160
165
  // now create a new instance of SQLServerDataProvider for each request
161
166
  const config = new SQLServerProviderConfigData(dataSource, mj_core_schema, 0, undefined, undefined, false);
162
167
  const p = new SQLServerDataProvider();
@@ -5,8 +5,9 @@ import { Metadata, UserInfo, BaseEntity, CompositeKey, KeyValuePair, LogError }
5
5
  import { ActionParam, ActionResult } from "@memberjunction/actions-base";
6
6
  import { Field, InputType, ObjectType } from "type-graphql";
7
7
  import { KeyValuePairInput } from "../generic/KeyValuePairInput.js";
8
- import { AppContext } from "../types.js";
8
+ import { AppContext, ProviderInfo } from "../types.js";
9
9
  import { CopyScalarsAndArrays } from "@memberjunction/global";
10
+ import { GetReadOnlyProvider } from "../util.js";
10
11
 
11
12
  /**
12
13
  * Input type for action parameters
@@ -341,7 +342,7 @@ export class ActionResolver {
341
342
 
342
343
  // Add entity object if we have entity information and primary key
343
344
  if ((input.EntityID || input.EntityName) && input.PrimaryKey && input.PrimaryKey.KeyValuePairs.length > 0) {
344
- await this.addEntityObject(params, input, user);
345
+ await this.addEntityObject(ctx.providers, params, input, user);
345
346
  }
346
347
 
347
348
  // Add other parameters
@@ -400,8 +401,8 @@ export class ActionResolver {
400
401
  * @param user The authenticated user
401
402
  * @private
402
403
  */
403
- private async addEntityObject(params: any, input: EntityActionInput, user: UserInfo): Promise<void> {
404
- const md = new Metadata();
404
+ private async addEntityObject(providers: Array<ProviderInfo>, params: any, input: EntityActionInput, user: UserInfo): Promise<void> {
405
+ const md = GetReadOnlyProvider(providers);
405
406
 
406
407
  // Find the entity by ID or name
407
408
  let entity;
@@ -1,5 +1,5 @@
1
1
  import { Arg, Ctx, Field, Mutation, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
2
- import { LogError, LogStatus, Metadata, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo, EntitySaveOptions, EntityDeleteOptions } from '@memberjunction/core';
2
+ import { LogError, LogStatus, Metadata, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo, EntitySaveOptions, EntityDeleteOptions, IMetadataProvider } from '@memberjunction/core';
3
3
  import { AppContext, UserPayload, MJ_SERVER_EVENT_CODE } from '../types.js';
4
4
  import { BehaviorSubject } from 'rxjs';
5
5
  import { take } from 'rxjs/operators';
@@ -59,7 +59,7 @@ import mssql from 'mssql';
59
59
 
60
60
  import { registerEnumType } from 'type-graphql';
61
61
  import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
62
- import { sendPostRequest } from '../util.js';
62
+ import { GetReadWriteProvider, sendPostRequest } from '../util.js';
63
63
  import { GetAIAPIKey } from '@memberjunction/ai';
64
64
  import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
65
65
  import { AIEngine } from '@memberjunction/aiengine';
@@ -479,7 +479,7 @@ export class AskSkipResolver {
479
479
  @Arg('ConversationId', () => String) ConversationId: string,
480
480
  @Arg('EntityName', () => String) EntityName: string,
481
481
  @Arg('CompositeKey', () => CompositeKeyInputType) compositeKey: CompositeKeyInputType,
482
- @Ctx() { dataSource, userPayload }: AppContext,
482
+ @Ctx() { dataSource, userPayload, providers }: AppContext,
483
483
  @PubSub() pubSub: PubSubEngine
484
484
  ) {
485
485
  // In this function we're simply going to call the Skip API and pass along the message from the user
@@ -498,14 +498,14 @@ export class AskSkipResolver {
498
498
  );
499
499
  }
500
500
 
501
- const md = new Metadata();
501
+ const md = GetReadWriteProvider(providers);
502
502
  const { convoEntity, dataContextEntity, convoDetailEntity, dataContext } = await this.HandleSkipChatInitialObjectLoading(
503
503
  dataSource,
504
504
  ConversationId,
505
505
  UserQuestion,
506
506
  user,
507
507
  userPayload,
508
- md,
508
+ md as unknown as Metadata,
509
509
  null
510
510
  );
511
511
 
@@ -558,7 +558,7 @@ export class AskSkipResolver {
558
558
  */
559
559
  @Mutation(() => AskSkipResultType)
560
560
  async ExecuteAskSkipLearningCycle(
561
- @Ctx() { dataSource, userPayload }: AppContext,
561
+ @Ctx() { dataSource, userPayload, providers }: AppContext,
562
562
  @Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean
563
563
  ) {
564
564
  const skipConfigInfo = configInfo.askSkip;
@@ -612,7 +612,7 @@ export class AskSkipResolver {
612
612
  }
613
613
 
614
614
  // Get the Skip agent ID
615
- const md = new Metadata();
615
+ const md = GetReadWriteProvider(providers);
616
616
  const skipAgent = AIEngine.Instance.GetAgentByName('Skip');
617
617
  if (!skipAgent) {
618
618
  throw new Error("Skip agent not found in AIEngine");
@@ -1467,11 +1467,11 @@ cycle.`);
1467
1467
  @Query(() => ReattachConversationResponse)
1468
1468
  async ReattachToProcessingConversation(
1469
1469
  @Arg('ConversationId', () => String) ConversationId: string,
1470
- @Ctx() { userPayload }: AppContext,
1470
+ @Ctx() { userPayload, providers }: AppContext,
1471
1471
  @PubSub() pubSub: PubSubEngine
1472
1472
  ): Promise<ReattachConversationResponse | null> {
1473
1473
  try {
1474
- const md = new Metadata();
1474
+ const md = GetReadWriteProvider(providers);
1475
1475
  const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
1476
1476
  if (!user) {
1477
1477
  LogError(`User ${userPayload.email} not found in UserCache`);
@@ -1581,13 +1581,13 @@ cycle.`);
1581
1581
  async ExecuteAskSkipAnalysisQuery(
1582
1582
  @Arg('UserQuestion', () => String) UserQuestion: string,
1583
1583
  @Arg('ConversationId', () => String) ConversationId: string,
1584
- @Ctx() { dataSource, userPayload }: AppContext,
1584
+ @Ctx() { dataSource, userPayload, providers }: AppContext,
1585
1585
  @PubSub() pubSub: PubSubEngine,
1586
1586
  @Arg('DataContextId', () => String, { nullable: true }) DataContextId?: string,
1587
1587
  @Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean,
1588
1588
  @Arg('StartTime', () => Date, { nullable: true }) StartTime?: Date
1589
1589
  ) {
1590
- const md = new Metadata();
1590
+ const md = GetReadWriteProvider(providers);
1591
1591
  const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
1592
1592
  if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
1593
1593
 
@@ -1600,7 +1600,7 @@ cycle.`);
1600
1600
  UserQuestion,
1601
1601
  user,
1602
1602
  userPayload,
1603
- md,
1603
+ md as unknown as Metadata,
1604
1604
  DataContextId
1605
1605
  );
1606
1606
 
@@ -1625,7 +1625,7 @@ cycle.`);
1625
1625
  ConversationId,
1626
1626
  userPayload,
1627
1627
  pubSub,
1628
- md,
1628
+ md as unknown as Metadata,
1629
1629
  convoEntity,
1630
1630
  convoDetailEntity,
1631
1631
  dataContext,
@@ -1,10 +1,10 @@
1
1
  import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType, Resolver, PubSub, PubSubEngine } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
- import { LogError, Metadata, RunView, UserInfo, CompositeKey } from '@memberjunction/core';
3
+ import { LogError, Metadata, RunView, UserInfo, CompositeKey, DatabaseProviderBase } from '@memberjunction/core';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
5
  import { QueryCategoryEntity, QueryPermissionEntity } from '@memberjunction/core-entities';
6
6
  import { QueryResolver } from '../generated/generated.js';
7
- import { GetReadWriteProvider } from '../util.js';
7
+ import { GetReadOnlyProvider, GetReadWriteProvider } from '../util.js';
8
8
  import { DeleteOptionsInput } from '../generic/DeleteOptionsInput.js';
9
9
  import { QueryEntityExtended } from '@memberjunction/core-entities-server';
10
10
 
@@ -312,8 +312,8 @@ export class QueryResolverExtended extends QueryResolver {
312
312
  // Handle CategoryPath if provided
313
313
  let finalCategoryID = input.CategoryID;
314
314
  if (input.CategoryPath) {
315
- const md = new Metadata();
316
- finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, md, context.userPayload.userRecord);
315
+ const p = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
316
+ finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, p, context.userPayload.userRecord);
317
317
  }
318
318
 
319
319
  // Use QueryEntityExtended which handles AI processing
@@ -345,7 +345,7 @@ export class QueryResolverExtended extends QueryResolver {
345
345
  const queryID = record.ID;
346
346
 
347
347
  if (input.Permissions && input.Permissions.length > 0) {
348
- await this.createPermissions(input.Permissions, queryID, context.userPayload.userRecord);
348
+ await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
349
349
  await record.RefreshRelatedMetadata(true); // force DB update since we just created new permissions
350
350
  }
351
351
 
@@ -374,13 +374,12 @@ export class QueryResolverExtended extends QueryResolver {
374
374
  }
375
375
  }
376
376
 
377
- protected async createPermissions(permissions: QueryPermissionInputType[], queryID: string, contextUser: UserInfo): Promise<QueryPermissionType[]> {
377
+ protected async createPermissions(p: DatabaseProviderBase, permissions: QueryPermissionInputType[], queryID: string, contextUser: UserInfo): Promise<QueryPermissionType[]> {
378
378
  // Create permissions if provided
379
379
  const createdPermissions: QueryPermissionType[] = [];
380
380
  if (permissions && permissions.length > 0) {
381
- const md = new Metadata();
382
381
  for (const perm of permissions) {
383
- const permissionEntity = await md.GetEntityObject<QueryPermissionEntity>('Query Permissions', contextUser);
382
+ const permissionEntity = await p.GetEntityObject<QueryPermissionEntity>('Query Permissions', contextUser);
384
383
  if (permissionEntity) {
385
384
  permissionEntity.QueryID = queryID;
386
385
  permissionEntity.RoleID = perm.RoleID;
@@ -427,8 +426,20 @@ export class QueryResolverExtended extends QueryResolver {
427
426
  // Handle CategoryPath if provided
428
427
  let finalCategoryID = input.CategoryID;
429
428
  if (input.CategoryPath) {
430
- const md = new Metadata();
431
- finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, md, context.userPayload.userRecord);
429
+ finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, provider, context.userPayload.userRecord);
430
+ }
431
+
432
+ // now make sure there is NO existing query by the same name in the specified category
433
+ const existingQueryResult = await provider.RunView({
434
+ EntityName: 'Queries',
435
+ ExtraFilter: `Name='${input.Name}' AND CategoryID='${finalCategoryID}'`
436
+ }, context.userPayload.userRecord);
437
+ if (existingQueryResult.Success && existingQueryResult.Results?.length > 0) {
438
+ // we have a match! Let's return an error
439
+ return {
440
+ Success: false,
441
+ ErrorMessage: `Query with name '${input.Name}' already exists in the specified ${input.CategoryID ? 'category' : 'categoryPath'}`
442
+ };
432
443
  }
433
444
 
434
445
  // Update fields that were provided
@@ -481,7 +492,7 @@ export class QueryResolverExtended extends QueryResolver {
481
492
  }
482
493
 
483
494
  // Create new permissions
484
- await this.createPermissions(input.Permissions, queryID, context.userPayload.userRecord);
495
+ await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
485
496
 
486
497
  // Refresh the metadata to get updated permissions
487
498
  await queryEntity.RefreshRelatedMetadata(true);
@@ -608,7 +619,7 @@ export class QueryResolverExtended extends QueryResolver {
608
619
  * @param contextUser - User context for operations
609
620
  * @returns The ID of the final category in the path
610
621
  */
611
- private async findOrCreateCategoryPath(categoryPath: string, md: Metadata, contextUser: UserInfo): Promise<string> {
622
+ private async findOrCreateCategoryPath(categoryPath: string, p: DatabaseProviderBase, contextUser: UserInfo): Promise<string> {
612
623
  if (!categoryPath || categoryPath.trim() === '') {
613
624
  throw new Error('CategoryPath cannot be empty');
614
625
  }
@@ -625,7 +636,7 @@ export class QueryResolverExtended extends QueryResolver {
625
636
  const categoryName = pathParts[i];
626
637
 
627
638
  // Look for existing category at this level
628
- const existingCategory = await this.findCategoryByNameAndParent(categoryName, currentParentID, contextUser);
639
+ const existingCategory = await this.findCategoryByNameAndParent(p, categoryName, currentParentID, contextUser);
629
640
 
630
641
  if (existingCategory) {
631
642
  currentCategoryID = existingCategory.ID;
@@ -633,7 +644,7 @@ export class QueryResolverExtended extends QueryResolver {
633
644
  } else {
634
645
  try {
635
646
  // Create new category
636
- const newCategory = await md.GetEntityObject<QueryCategoryEntity>("Query Categories", contextUser);
647
+ const newCategory = await p.GetEntityObject<QueryCategoryEntity>("Query Categories", contextUser);
637
648
  if (!newCategory) {
638
649
  throw new Error(`Failed to create entity object for Query Categories`);
639
650
  }
@@ -652,7 +663,7 @@ export class QueryResolverExtended extends QueryResolver {
652
663
  currentParentID = newCategory.ID;
653
664
 
654
665
  // Refresh metadata after each category creation to ensure it's available for subsequent lookups
655
- await md.Refresh();
666
+ await p.Refresh();
656
667
  } catch (error) {
657
668
  throw new Error(`Failed to create category '${categoryName}': ${error instanceof Error ? error.message : String(error)}`);
658
669
  }
@@ -673,9 +684,9 @@ export class QueryResolverExtended extends QueryResolver {
673
684
  * @param contextUser - User context for database operations
674
685
  * @returns The matching category entity or null if not found
675
686
  */
676
- private async findCategoryByNameAndParent(categoryName: string, parentID: string | null, contextUser: UserInfo): Promise<QueryCategoryEntity | null> {
687
+ private async findCategoryByNameAndParent(provider: DatabaseProviderBase, categoryName: string, parentID: string | null, contextUser: UserInfo): Promise<QueryCategoryEntity | null> {
677
688
  try {
678
- const rv = new RunView();
689
+ const rv = provider;
679
690
  const parentFilter = parentID ? `ParentID='${parentID}'` : 'ParentID IS NULL';
680
691
  const nameFilter = `LOWER(Name) = LOWER('${categoryName.replace(/'/g, "''")}')`; // Escape single quotes
681
692
 
@@ -1,6 +1,7 @@
1
1
  import { Arg, Ctx, Field, InputType, Int, ObjectType, Query, Resolver } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
3
  import { LogError, Metadata } from '@memberjunction/core';
4
+ import { GetReadOnlyProvider } from '../util.js';
4
5
 
5
6
  @ObjectType()
6
7
  export class DatasetResultType {
@@ -38,11 +39,11 @@ export class DatasetResolverExtended {
38
39
  @Query(() => DatasetResultType)
39
40
  async GetDatasetByName(
40
41
  @Arg('DatasetName', () => String) DatasetName: string,
41
- @Ctx() {}: AppContext,
42
+ @Ctx() {providers}: AppContext,
42
43
  @Arg('ItemFilters', () => [DatasetItemFilterTypeGQL], { nullable: 'itemsAndList' }) ItemFilters?: DatasetItemFilterTypeGQL[]
43
44
  ) {
44
45
  try {
45
- const md = new Metadata();
46
+ const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
46
47
  const result = await md.GetDatasetByName(DatasetName, ItemFilters);
47
48
  if (result) {
48
49
  return {
@@ -89,11 +90,11 @@ export class DatasetStatusResolver {
89
90
  @Query(() => DatasetStatusResultType)
90
91
  async GetDatasetStatusByName(
91
92
  @Arg('DatasetName', () => String) DatasetName: string,
92
- @Ctx() {}: AppContext,
93
+ @Ctx() {providers}: AppContext,
93
94
  @Arg('ItemFilters', () => [DatasetItemFilterTypeGQL], { nullable: 'itemsAndList' }) ItemFilters?: DatasetItemFilterTypeGQL[]
94
95
  ) {
95
96
  try {
96
- const md = new Metadata();
97
+ const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
97
98
  const result = await md.GetDatasetStatusByName(DatasetName, ItemFilters);
98
99
  if (result) {
99
100
  return {
@@ -1,7 +1,8 @@
1
- import { Metadata, CompositeKey } from '@memberjunction/core';
1
+ import { Metadata, CompositeKey, DatabaseProviderBase } from '@memberjunction/core';
2
2
  import { Arg, Ctx, Field, InputType, ObjectType, Query, Resolver } from 'type-graphql';
3
3
  import { AppContext } from '../types.js';
4
4
  import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
5
+ import { GetReadOnlyProvider } from '../util.js';
5
6
 
6
7
  @InputType()
7
8
  export class EntityRecordNameInput {
@@ -36,26 +37,27 @@ export class EntityRecordNameResolver {
36
37
  async GetEntityRecordName(
37
38
  @Arg('EntityName', () => String) EntityName: string,
38
39
  @Arg('CompositeKey', () => CompositeKeyInputType) primaryKey: CompositeKey,
39
- @Ctx() { userPayload }: AppContext
40
+ @Ctx() { providers, userPayload }: AppContext
40
41
  ): Promise<EntityRecordNameResult> {
41
- const md = new Metadata();
42
+ const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
43
+
42
44
  return await this.InnerGetEntityRecordName(md, EntityName, primaryKey);
43
45
  }
44
46
 
45
47
  @Query(() => [EntityRecordNameResult])
46
48
  async GetEntityRecordNames(
47
49
  @Arg('info', () => [EntityRecordNameInput]) info: EntityRecordNameInput[],
48
- @Ctx() {}: AppContext
50
+ @Ctx() {providers}: AppContext
49
51
  ): Promise<EntityRecordNameResult[]> {
50
52
  const result: EntityRecordNameResult[] = [];
51
- const md = new Metadata();
53
+ const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
52
54
  for (const i of info) {
53
55
  result.push(await this.InnerGetEntityRecordName(md, i.EntityName, i.CompositeKey));
54
56
  }
55
57
  return result;
56
58
  }
57
59
 
58
- async InnerGetEntityRecordName(md: Metadata, EntityName: string, primaryKey: CompositeKeyInputType): Promise<EntityRecordNameResult> {
60
+ async InnerGetEntityRecordName(md: DatabaseProviderBase, EntityName: string, primaryKey: CompositeKeyInputType): Promise<EntityRecordNameResult> {
59
61
  const pk = new CompositeKey(primaryKey.KeyValuePairs);
60
62
  const e = md.Entities.find((e) => e.Name === EntityName);
61
63
  if (e) {
@@ -16,16 +16,15 @@ export class FileResolver extends FileCategoryResolverBase {
16
16
  ) {
17
17
  const key = new CompositeKey();
18
18
  key.LoadFromSingleKeyValuePair('ID', ID);
19
- const provider = GetReadWriteProvider(providers);
19
+ const p = GetReadWriteProvider(providers);
20
20
 
21
- if (!(await this.BeforeDelete(provider, key))) {
21
+ if (!(await this.BeforeDelete(p, key))) {
22
22
  return null;
23
23
  }
24
24
 
25
- const md = new Metadata();
26
25
  const user = this.GetUserFromPayload(userPayload);
27
- const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
28
- const fileCategoryEntity = await md.GetEntityObject<FileCategoryEntity>('File Categories', user);
26
+ const fileEntity = await p.GetEntityObject<FileEntity>('Files', user);
27
+ const fileCategoryEntity = await p.GetEntityObject<FileCategoryEntity>('File Categories', user);
29
28
 
30
29
  fileEntity.CheckPermissions(EntityPermissionType.Update, true);
31
30
  fileCategoryEntity.CheckPermissions(EntityPermissionType.Delete, true);
@@ -34,7 +33,7 @@ export class FileResolver extends FileCategoryResolverBase {
34
33
  const returnValue = fileCategoryEntity.GetAll();
35
34
 
36
35
  // Any files using the deleted category fall back to its parent
37
- await provider.BeginTransaction();
36
+ await p.BeginTransaction();
38
37
  try {
39
38
  // SHOULD USE BaseEntity for each of these records to ensure object model
40
39
  // is used everywhere - new code below. The below is SLOWER than a single
@@ -60,20 +59,20 @@ export class FileResolver extends FileCategoryResolverBase {
60
59
  // iterate through each of the files in filesResult.Results
61
60
  // and update the CategoryID to fileCategoryEntity.ParentID
62
61
  for (const file of filesResult.Results) {
63
- const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
62
+ const fileEntity = await p.GetEntityObject<FileEntity>('Files', user);
64
63
  await fileEntity.Load(file.ID);
65
64
  fileEntity.CategoryID = fileCategoryEntity.ParentID;
66
65
  await fileEntity.Save();
67
66
  }
68
67
  }
69
68
  await fileCategoryEntity.Delete(options);
70
- await provider.CommitTransaction();
69
+ await p.CommitTransaction();
71
70
  } catch (error) {
72
- await provider.RollbackTransaction();
71
+ await p.RollbackTransaction();
73
72
  throw error;
74
73
  }
75
74
 
76
- await this.AfterDelete(provider, key); // fire event
75
+ await this.AfterDelete(p, key); // fire event
77
76
  return returnValue;
78
77
  }
79
78
  }
@@ -51,14 +51,13 @@ export class FileResolver extends FileResolverBase {
51
51
  @Ctx() context: AppContext,
52
52
  @PubSub() pubSub: PubSubEngine
53
53
  ) {
54
- const md = new Metadata();
54
+ // Check to see if there's already an object with that name
55
+ const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true})
55
56
  const user = this.GetUserFromPayload(context.userPayload);
56
- const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
57
- const providerEntity = await md.GetEntityObject<FileStorageProviderEntity>('File Storage Providers', user);
57
+ const fileEntity = await provider.GetEntityObject<FileEntity>('Files', user);
58
+ const providerEntity = await provider.GetEntityObject<FileStorageProviderEntity>('File Storage Providers', user);
58
59
  fileEntity.CheckPermissions(EntityPermissionType.Create, true);
59
60
 
60
- // Check to see if there's already an object with that name
61
- const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true})
62
61
  const [sameName] = await this.findBy(
63
62
  provider,
64
63
  'Files',
@@ -108,7 +107,7 @@ export class FileResolver extends FileResolverBase {
108
107
  @PubSub() pubSub: PubSubEngine
109
108
  ) {
110
109
  // if the name is changing, rename the target object as well
111
- const md = new Metadata();
110
+ const md = GetReadOnlyProvider(context.providers);
112
111
  const user = this.GetUserFromPayload(context.userPayload);
113
112
  const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
114
113
  fileEntity.CheckPermissions(EntityPermissionType.Update, true);
@@ -136,7 +135,7 @@ export class FileResolver extends FileResolverBase {
136
135
  @Ctx() context: AppContext,
137
136
  @PubSub() pubSub: PubSubEngine
138
137
  ) {
139
- const md = new Metadata();
138
+ const md = GetReadOnlyProvider(context.providers);
140
139
  const userInfo = this.GetUserFromPayload(context.userPayload);
141
140
 
142
141
  const fileEntity = await md.GetEntityObject<FileEntity>('Files', userInfo);
@@ -1,7 +1,7 @@
1
1
  import { Arg, Ctx, Field, ObjectType, Query } from "type-graphql";
2
2
  import { AppContext } from "../types.js";
3
3
  import { DataContext } from "@memberjunction/data-context";
4
- import { GetReadOnlyDataSource } from "../util.js";
4
+ import { GetReadOnlyDataSource, GetReadOnlyProvider } from "../util.js";
5
5
  import { Metadata } from "@memberjunction/core";
6
6
  import { DataContextItemEntity } from "@memberjunction/core-entities";
7
7
 
@@ -53,7 +53,7 @@ export class GetDataContextDataResolver {
53
53
  const ds = GetReadOnlyDataSource(appCtx.dataSources, {
54
54
  allowFallbackToReadWrite: true,
55
55
  })
56
- const md = new Metadata();
56
+ const md = GetReadOnlyProvider(appCtx.providers, {allowFallbackToReadWrite: true});
57
57
  const dciData = await md.GetEntityObject<DataContextItemEntity>("Data Context Items", appCtx.userPayload.userRecord);
58
58
  if (await dciData.Load(DataContextItemID)) {
59
59
  const dci = DataContext.CreateDataContextItem(); // use class factory to get whatever lowest level sub-class is registered
@@ -3,7 +3,7 @@ import { AppContext } from '../types.js';
3
3
  import { LogError, LogStatus, Metadata } from '@memberjunction/core';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
5
  import { v4 as uuidv4 } from 'uuid';
6
- import { GetReadOnlyDataSource } from '../util.js';
6
+ import { GetReadOnlyDataSource, GetReadOnlyProvider } from '../util.js';
7
7
  import sql from 'mssql';
8
8
 
9
9
  @InputType()
@@ -169,7 +169,7 @@ export class GetDataResolver {
169
169
  @Ctx() context: AppContext
170
170
  ): Promise<SimpleEntityResultType> {
171
171
  try {
172
- const md = new Metadata();
172
+ const md = GetReadOnlyProvider(context.providers);
173
173
  const result = md.Entities.map((e) => {
174
174
  return {
175
175
  ID: e.ID,
@@ -30,7 +30,7 @@ export class Info {
30
30
  }
31
31
 
32
32
  @Resolver(Info)
33
- export class InfoResolver {
33
+ export class InfoResolver {
34
34
  @Public()
35
35
  @Query(() => Info)
36
36
  Info(@Ctx() context: AppContext): Info {
@@ -3,6 +3,7 @@ import { Arg, Ctx, Field, InputType, Int, Mutation, ObjectType, PubSub, PubSubEn
3
3
  import { AppContext } from '../types.js';
4
4
  import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
5
5
  import { z } from 'zod';
6
+ import { GetReadOnlyProvider, GetReadWriteProvider } from '../util.js';
6
7
 
7
8
  @ObjectType()
8
9
  export class EntityDependencyResult {
@@ -21,11 +22,11 @@ export class EntityDependencyResolver {
21
22
  @Query(() => [EntityDependencyResult])
22
23
  async GetEntityDependencies(
23
24
  @Arg('entityName', () => String) entityName: string,
24
- @Ctx() { dataSource, userPayload }: AppContext,
25
+ @Ctx() { dataSource, userPayload, providers }: AppContext,
25
26
  @PubSub() pubSub: PubSubEngine
26
27
  ) {
27
28
  try {
28
- const md = new Metadata();
29
+ const md = GetReadOnlyProvider(providers);
29
30
  return md.GetEntityDependencies(entityName);
30
31
  } catch (err) {
31
32
  LogError(err);
@@ -57,11 +58,11 @@ export class RecordDependencyResolver {
57
58
  async GetRecordDependencies(
58
59
  @Arg('entityName', () => String) entityName: string,
59
60
  @Arg('CompositeKey', () => CompositeKeyInputType) ckInput: CompositeKeyInputType,
60
- @Ctx() { dataSource, userPayload }: AppContext,
61
+ @Ctx() { dataSource, userPayload, providers }: AppContext,
61
62
  @PubSub() pubSub: PubSubEngine
62
63
  ) {
63
64
  try {
64
- const md = new Metadata();
65
+ const md = GetReadOnlyProvider(providers);
65
66
  const ck = new CompositeKey(ckInput.KeyValuePairs);
66
67
  const result = await md.GetRecordDependencies(entityName, ck);
67
68
 
@@ -165,11 +166,11 @@ export class RecordMergeResolver {
165
166
  @Mutation(() => RecordMergeResult)
166
167
  async MergeRecords(
167
168
  @Arg('request', () => RecordMergeRequest) request: RecordMergeRequest,
168
- @Ctx() { dataSource, userPayload }: AppContext,
169
+ @Ctx() { dataSource, userPayload, providers }: AppContext,
169
170
  @PubSub() pubSub: PubSubEngine
170
171
  ) {
171
172
  try {
172
- const md = new Metadata();
173
+ const md = GetReadWriteProvider(providers);
173
174
  const options = {};
174
175
  const result = await md.MergeRecords(request, userPayload.userRecord, options);
175
176
  return result;