@memberjunction/server 2.108.0 → 2.110.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 (39) hide show
  1. package/dist/agents/skip-agent.d.ts +1 -0
  2. package/dist/agents/skip-agent.d.ts.map +1 -1
  3. package/dist/agents/skip-agent.js +71 -20
  4. package/dist/agents/skip-agent.js.map +1 -1
  5. package/dist/agents/skip-sdk.d.ts.map +1 -1
  6. package/dist/agents/skip-sdk.js +29 -14
  7. package/dist/agents/skip-sdk.js.map +1 -1
  8. package/dist/generated/generated.d.ts +186 -15
  9. package/dist/generated/generated.d.ts.map +1 -1
  10. package/dist/generated/generated.js +1118 -95
  11. package/dist/generated/generated.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/resolvers/CreateQueryResolver.d.ts +1 -0
  16. package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
  17. package/dist/resolvers/CreateQueryResolver.js +73 -11
  18. package/dist/resolvers/CreateQueryResolver.js.map +1 -1
  19. package/dist/resolvers/RunAIAgentResolver.d.ts +6 -2
  20. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  21. package/dist/resolvers/RunAIAgentResolver.js +234 -8
  22. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  23. package/dist/resolvers/TaskResolver.d.ts +1 -1
  24. package/dist/resolvers/TaskResolver.d.ts.map +1 -1
  25. package/dist/resolvers/TaskResolver.js +4 -3
  26. package/dist/resolvers/TaskResolver.js.map +1 -1
  27. package/dist/services/TaskOrchestrator.d.ts +3 -1
  28. package/dist/services/TaskOrchestrator.d.ts.map +1 -1
  29. package/dist/services/TaskOrchestrator.js +77 -2
  30. package/dist/services/TaskOrchestrator.js.map +1 -1
  31. package/package.json +35 -34
  32. package/src/agents/skip-agent.ts +130 -56
  33. package/src/agents/skip-sdk.ts +45 -16
  34. package/src/generated/generated.ts +720 -77
  35. package/src/index.ts +4 -0
  36. package/src/resolvers/CreateQueryResolver.ts +125 -28
  37. package/src/resolvers/RunAIAgentResolver.ts +397 -9
  38. package/src/resolvers/TaskResolver.ts +3 -2
  39. package/src/services/TaskOrchestrator.ts +118 -3
package/src/index.ts CHANGED
@@ -40,6 +40,10 @@ LoadCoreEntitiesServerSubClasses(); // prevent tree shaking for this dynamic mod
40
40
  import { LoadAgentManagementActions } from '@memberjunction/ai-agent-manager-actions';
41
41
  LoadAgentManagementActions();
42
42
 
43
+ // Load agent manager core classes (registers custom agent classes like AgentBuilderAgent, AgentArchitectAgent)
44
+ import { LoadAgentManagerCore } from '@memberjunction/ai-agent-manager';
45
+ LoadAgentManagerCore();
46
+
43
47
  import { LoadSchedulingEngine } from '@memberjunction/scheduling-engine';
44
48
  LoadSchedulingEngine(); // This also loads drivers
45
49
 
@@ -1,6 +1,6 @@
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, DatabaseProviderBase } from '@memberjunction/core';
3
+ import { LogError, RunView, UserInfo, CompositeKey, DatabaseProviderBase, LogStatus } from '@memberjunction/core';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
5
  import { QueryCategoryEntity, QueryPermissionEntity } from '@memberjunction/core-entities';
6
6
  import { MJQueryResolver } from '../generated/generated.js';
@@ -311,11 +311,22 @@ export class MJQueryResolverExtended extends MJQueryResolver {
311
311
  try {
312
312
  // Handle CategoryPath if provided
313
313
  let finalCategoryID = input.CategoryID;
314
- const provider = GetReadWriteProvider(context.providers);
314
+ const provider = GetReadWriteProvider(context.providers);
315
315
  if (input.CategoryPath) {
316
316
  finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, provider, context.userPayload.userRecord);
317
317
  }
318
-
318
+
319
+ // Check for existing query with same name in the same category
320
+ const existingQuery = await this.findExistingQuery(provider, input.Name, finalCategoryID, context.userPayload.userRecord);
321
+
322
+ if (existingQuery) {
323
+ const categoryInfo = input.CategoryPath ? `category path '${input.CategoryPath}'` : `category ID '${finalCategoryID}'`;
324
+ return {
325
+ Success: false,
326
+ ErrorMessage: `Query with name '${input.Name}' already exists in ${categoryInfo}`
327
+ };
328
+ }
329
+
319
330
  // Use QueryEntityExtended which handles AI processing
320
331
  const record = await provider.GetEntityObject<QueryEntityExtended>("Queries", context.userPayload.userRecord);
321
332
 
@@ -331,22 +342,29 @@ export class MJQueryResolverExtended extends MJQueryResolver {
331
342
  CacheTTLMinutes: input.CacheTTLMinutes || null,
332
343
  CacheMaxSize: input.CacheMaxSize || null
333
344
  };
334
- // Remove Permissions from the fields to set since we handle them separately
335
- delete (fieldsToSet as any).Permissions;
336
-
345
+ // Remove non-database fields that we handle separately or are input-only
346
+ delete (fieldsToSet as any).Permissions; // Handled separately via createPermissions
347
+ delete (fieldsToSet as any).CategoryPath; // Input-only field, resolved to CategoryID
348
+
337
349
  record.SetMany(fieldsToSet, true);
338
350
  this.ListenForEntityMessages(record, pubSub, context.userPayload.userRecord);
339
351
 
340
- // Pass the transactionScopeId from the user payload to the save operation
341
- if (await record.Save()) {
342
- // save worked, fire the AfterCreate event and then return all the data
352
+ // Attempt to save the query
353
+ const saveResult = await record.Save();
354
+
355
+ if (saveResult) {
356
+ // Save succeeded - fire the AfterCreate event and return all the data
343
357
  await this.AfterCreate(provider, input); // fire event
344
358
  const queryID = record.ID;
345
-
359
+
346
360
  if (input.Permissions && input.Permissions.length > 0) {
347
361
  await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
348
362
  await record.RefreshRelatedMetadata(true); // force DB update since we just created new permissions
349
- }
363
+ }
364
+
365
+ // Refresh metadata cache to include the newly created query
366
+ // This ensures subsequent operations can find the query without additional DB calls
367
+ await provider.Refresh();
350
368
 
351
369
  return {
352
370
  Success: true,
@@ -361,11 +379,36 @@ export class MJQueryResolverExtended extends MJQueryResolver {
361
379
  }),
362
380
  Permissions: record.QueryPermissions
363
381
  };
364
- }
382
+ }
365
383
  else {
384
+ // Save failed - check if another request created the same query (race condition)
385
+ // Always recheck regardless of error type to handle all duplicate scenarios
386
+ const existingQuery = await this.findExistingQuery(provider, input.Name, finalCategoryID, context.userPayload.userRecord);
387
+
388
+ if (existingQuery) {
389
+ // Found the query that was created by another request
390
+ // Return it as if we created it (it has the same name/category)
391
+ LogStatus(`[CreateQuery] Unique constraint detected for query '${input.Name}'. Using existing query (ID: ${existingQuery.ID}) created by concurrent request.`);
392
+ return {
393
+ Success: true,
394
+ QueryData: JSON.stringify(existingQuery),
395
+ Fields: existingQuery.Fields || [],
396
+ Parameters: existingQuery.Parameters || [],
397
+ Entities: existingQuery.Entities?.map(e => ({
398
+ ID: e.ID,
399
+ QueryID: e.QueryID,
400
+ EntityID: e.EntityID,
401
+ EntityName: e.Entity
402
+ })) || [],
403
+ Permissions: existingQuery.Permissions || []
404
+ };
405
+ }
406
+
407
+ // Genuine failure - couldn't find an existing query with the same name
408
+ const errorMessage = record.LatestResult?.Message || '';
366
409
  return {
367
410
  Success: false,
368
- ErrorMessage: 'Failed to create query using CreateRecord method'
411
+ ErrorMessage: `Failed to create query: ${errorMessage || 'Unknown error'}`
369
412
  };
370
413
  }
371
414
  }
@@ -652,7 +695,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
652
695
  if (!newCategory) {
653
696
  throw new Error(`Failed to create entity object for Query Categories`);
654
697
  }
655
-
698
+
656
699
  newCategory.Name = categoryName;
657
700
  newCategory.ParentID = currentParentID;
658
701
  newCategory.UserID = contextUser.ID;
@@ -660,16 +703,33 @@ export class MJQueryResolverExtended extends MJQueryResolver {
660
703
 
661
704
  const saveResult = await newCategory.Save();
662
705
  if (!saveResult) {
663
- throw new Error(`Failed to create category '${categoryName}': ${newCategory.LatestResult?.Message || 'Unknown error'}`);
706
+ // Save failed - always recheck if another request created the same category
707
+ const recheckExisting = await this.findCategoryByNameAndParent(p, categoryName, currentParentID, contextUser);
708
+ if (recheckExisting) {
709
+ // Another request created it - use that one
710
+ LogStatus(`[CreateQuery] Unique constraint detected for category '${categoryName}'. Using existing category (ID: ${recheckExisting.ID}) created by concurrent request.`);
711
+ currentCategoryID = recheckExisting.ID;
712
+ currentParentID = recheckExisting.ID;
713
+ } else {
714
+ // Genuine failure (not a duplicate)
715
+ const errorMessage = newCategory.LatestResult?.Message || '';
716
+ throw new Error(`Failed to create category '${categoryName}': ${errorMessage || 'Unknown error'}`);
717
+ }
718
+ } else {
719
+ currentCategoryID = newCategory.ID;
720
+ currentParentID = newCategory.ID;
664
721
  }
665
-
666
- currentCategoryID = newCategory.ID;
667
- currentParentID = newCategory.ID;
668
-
669
- // Refresh metadata after each category creation to ensure it's available for subsequent lookups
670
- await p.Refresh();
671
722
  } catch (error) {
672
- throw new Error(`Failed to create category '${categoryName}': ${error instanceof Error ? error.message : String(error)}`);
723
+ // On error, double-check if category exists (race condition handling)
724
+ const recheckExisting = await this.findCategoryByNameAndParent(p, categoryName, currentParentID, contextUser);
725
+ if (recheckExisting) {
726
+ // Category exists, another request created it
727
+ LogStatus(`[CreateQuery] Exception during category creation for '${categoryName}'. Using existing category (ID: ${recheckExisting.ID}) created by concurrent request.`);
728
+ currentCategoryID = recheckExisting.ID;
729
+ currentParentID = recheckExisting.ID;
730
+ } else {
731
+ throw new Error(`Failed to create category '${categoryName}': ${error instanceof Error ? error.message : String(error)}`);
732
+ }
673
733
  }
674
734
  }
675
735
  }
@@ -682,7 +742,44 @@ export class MJQueryResolverExtended extends MJQueryResolver {
682
742
  }
683
743
 
684
744
  /**
685
- * Finds a category by name and parent ID using case-insensitive comparison via RunView.
745
+ * Finds an existing query by name and category ID using RunView.
746
+ * Bypasses metadata cache to ensure we get the latest data from database.
747
+ * @param provider - Database provider
748
+ * @param queryName - Name of the query to find
749
+ * @param categoryID - Category ID (can be null)
750
+ * @param contextUser - User context for database operations
751
+ * @returns The matching query info or null if not found
752
+ */
753
+ private async findExistingQuery(
754
+ provider: DatabaseProviderBase,
755
+ queryName: string,
756
+ categoryID: string | null,
757
+ contextUser: UserInfo
758
+ ): Promise<any | null> {
759
+ try {
760
+ // Query database directly to avoid cache staleness issues
761
+ const categoryFilter = categoryID ? `CategoryID='${categoryID}'` : 'CategoryID IS NULL';
762
+ const nameFilter = `LOWER(Name) = LOWER('${queryName.replace(/'/g, "''")}')`;
763
+
764
+ const result = await provider.RunView({
765
+ EntityName: 'Queries',
766
+ ExtraFilter: `${nameFilter} AND ${categoryFilter}`
767
+ }, contextUser);
768
+
769
+ if (result.Success && result.Results && result.Results.length > 0) {
770
+ return result.Results[0];
771
+ }
772
+
773
+ return null;
774
+ } catch (error) {
775
+ // If query fails, return null (query doesn't exist)
776
+ return null;
777
+ }
778
+ }
779
+
780
+ /**
781
+ * Finds a category by name and parent ID using RunView.
782
+ * Bypasses metadata cache to ensure we get the latest data from database.
686
783
  * @param categoryName - Name of the category to find
687
784
  * @param parentID - Parent category ID (null for root level)
688
785
  * @param contextUser - User context for database operations
@@ -690,11 +787,11 @@ export class MJQueryResolverExtended extends MJQueryResolver {
690
787
  */
691
788
  private async findCategoryByNameAndParent(provider: DatabaseProviderBase, categoryName: string, parentID: string | null, contextUser: UserInfo): Promise<QueryCategoryEntity | null> {
692
789
  try {
693
- const rv = provider;
790
+ // Query database directly to avoid cache staleness issues
694
791
  const parentFilter = parentID ? `ParentID='${parentID}'` : 'ParentID IS NULL';
695
792
  const nameFilter = `LOWER(Name) = LOWER('${categoryName.replace(/'/g, "''")}')`; // Escape single quotes
696
-
697
- const result = await rv.RunView<QueryCategoryEntity>({
793
+
794
+ const result = await provider.RunView<QueryCategoryEntity>({
698
795
  EntityName: 'Query Categories',
699
796
  ExtraFilter: `${nameFilter} AND ${parentFilter}`,
700
797
  ResultType: 'entity_object'
@@ -703,10 +800,10 @@ export class MJQueryResolverExtended extends MJQueryResolver {
703
800
  if (result.Success && result.Results && result.Results.length > 0) {
704
801
  return result.Results[0];
705
802
  }
706
-
803
+
707
804
  return null;
708
805
  } catch (error) {
709
- LogError(error);
806
+ // If query fails, return null
710
807
  return null;
711
808
  }
712
809
  }