@memberjunction/server 2.109.0 → 2.110.1
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.
- package/dist/agents/skip-agent.d.ts +1 -0
- package/dist/agents/skip-agent.d.ts.map +1 -1
- package/dist/agents/skip-agent.js +65 -7
- package/dist/agents/skip-agent.js.map +1 -1
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +29 -14
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/generated/generated.d.ts +34 -13
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +194 -86
- package/dist/generated/generated.js.map +1 -1
- package/dist/resolvers/CreateQueryResolver.d.ts +1 -0
- package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/CreateQueryResolver.js +73 -11
- package/dist/resolvers/CreateQueryResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +6 -2
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +234 -8
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/TaskResolver.d.ts +1 -1
- package/dist/resolvers/TaskResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskResolver.js +4 -3
- package/dist/resolvers/TaskResolver.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts +3 -1
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +77 -2
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/package.json +35 -35
- package/src/agents/skip-agent.ts +93 -9
- package/src/agents/skip-sdk.ts +45 -16
- package/src/generated/generated.ts +135 -69
- package/src/resolvers/CreateQueryResolver.ts +125 -28
- package/src/resolvers/RunAIAgentResolver.ts +397 -9
- package/src/resolvers/TaskResolver.ts +3 -2
- package/src/services/TaskOrchestrator.ts +118 -3
|
@@ -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,
|
|
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
|
|
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
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
806
|
+
// If query fails, return null
|
|
710
807
|
return null;
|
|
711
808
|
}
|
|
712
809
|
}
|