@memberjunction/server 5.35.0 → 5.37.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.
- package/dist/agents/skip-agent.d.ts.map +1 -1
- package/dist/agents/skip-agent.js +14 -2
- package/dist/agents/skip-agent.js.map +1 -1
- package/dist/agents/skip-sdk.d.ts +10 -0
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +139 -1
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/generated/generated.d.ts +29 -5
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +137 -15
- package/dist/generated/generated.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +2 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +57 -9
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/ListOperationsResolver.d.ts +175 -0
- package/dist/resolvers/ListOperationsResolver.d.ts.map +1 -0
- package/dist/resolvers/ListOperationsResolver.js +930 -0
- package/dist/resolvers/ListOperationsResolver.js.map +1 -0
- package/package.json +70 -68
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +269 -0
- package/src/__tests__/ListOperationsResolver.test.ts +182 -0
- package/src/agents/skip-agent.ts +15 -2
- package/src/agents/skip-sdk.ts +148 -1
- package/src/generated/generated.ts +101 -15
- package/src/resolvers/AdhocQueryResolver.ts +57 -10
- package/src/resolvers/ListOperationsResolver.ts +607 -0
package/src/agents/skip-agent.ts
CHANGED
|
@@ -358,13 +358,25 @@ export class SkipProxyAgent extends BaseAgent {
|
|
|
358
358
|
const skipMessage = response.messages?.filter(msg => msg.role === 'system').pop();
|
|
359
359
|
const analysisText = response.analysis?.trim();
|
|
360
360
|
|
|
361
|
+
// Skip-Brain attaches actionableCommands as an extension field on the
|
|
362
|
+
// analysis_complete response (e.g. `client:capture-data-snapshot` when
|
|
363
|
+
// the Analysis Agent needs the user's current view). Forward to the
|
|
364
|
+
// next step so AgentRunner persists them to ConversationDetail.ActionableCommands
|
|
365
|
+
// — which the chat UI's actionable-commands.component reads to render buttons.
|
|
366
|
+
const responseExt = response as unknown as { actionableCommands?: unknown[] };
|
|
367
|
+
const actionableCommands = Array.isArray(responseExt.actionableCommands) && responseExt.actionableCommands.length > 0
|
|
368
|
+
? (responseExt.actionableCommands as BaseAgentNextStep<ComponentSpec>['actionableCommands'])
|
|
369
|
+
: undefined;
|
|
370
|
+
LogStatus(`[SkipProxyAgent DEBUG] handleAnalysisComplete actionableCommands from response: ${JSON.stringify(responseExt.actionableCommands)}`);
|
|
371
|
+
|
|
361
372
|
if (!hasComponentOptions) {
|
|
362
373
|
if (analysisText || skipMessage?.content) {
|
|
363
374
|
return {
|
|
364
375
|
terminate: true,
|
|
365
376
|
step: 'Success',
|
|
366
377
|
message: analysisText || skipMessage!.content,
|
|
367
|
-
newPayload: undefined
|
|
378
|
+
newPayload: undefined,
|
|
379
|
+
actionableCommands
|
|
368
380
|
};
|
|
369
381
|
}
|
|
370
382
|
|
|
@@ -387,7 +399,8 @@ export class SkipProxyAgent extends BaseAgent {
|
|
|
387
399
|
terminate: true,
|
|
388
400
|
step: 'Success',
|
|
389
401
|
message: skipMessage?.content || response.title || 'Analysis complete',
|
|
390
|
-
newPayload: componentSpec
|
|
402
|
+
newPayload: componentSpec,
|
|
403
|
+
actionableCommands
|
|
391
404
|
};
|
|
392
405
|
}
|
|
393
406
|
|
package/src/agents/skip-sdk.ts
CHANGED
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
SkipAPIArtifactType
|
|
25
25
|
} from '@memberjunction/skip-types';
|
|
26
26
|
import { DataContext } from '@memberjunction/data-context';
|
|
27
|
-
import { IMetadataProvider, UserInfo, LogStatus, LogError, Metadata, RunQuery, EntityInfo, EntityFieldInfo, EntityFieldValueInfo, DatabaseProviderBase } from '@memberjunction/core';
|
|
27
|
+
import { IMetadataProvider, UserInfo, LogStatus, LogError, Metadata, RunQuery, RunView, EntityInfo, EntityFieldInfo, EntityFieldValueInfo, DatabaseProviderBase } from '@memberjunction/core';
|
|
28
|
+
import { MJConversationDetailEntity } from '@memberjunction/core-entities';
|
|
28
29
|
import { request as httpRequest } from 'http';
|
|
29
30
|
import { request as httpsRequest } from 'https';
|
|
30
31
|
import { gzip as gzipCompress, createGunzip } from 'zlib';
|
|
@@ -682,6 +683,16 @@ export class SkipSDK {
|
|
|
682
683
|
versions: entry.versions
|
|
683
684
|
}));
|
|
684
685
|
|
|
686
|
+
// Also include INPUT artifacts (e.g., user-captured Data Snapshots
|
|
687
|
+
// attached to messages via the Analyze button or
|
|
688
|
+
// client:capture-data-snapshot actionable command). The query above
|
|
689
|
+
// only returns Direction='Output' artifacts produced BY Skip — but
|
|
690
|
+
// Skip also needs to see artifacts the user gave it as input.
|
|
691
|
+
const inputArtifacts = await this.buildInputArtifacts(contextUser, conversationId, artifactMap);
|
|
692
|
+
if (inputArtifacts.length > 0) {
|
|
693
|
+
artifacts.push(...inputArtifacts);
|
|
694
|
+
}
|
|
695
|
+
|
|
685
696
|
return artifacts;
|
|
686
697
|
} catch (error) {
|
|
687
698
|
LogError(`Failed to build artifacts for conversation ${conversationId}: ${error}`);
|
|
@@ -689,6 +700,142 @@ export class SkipSDK {
|
|
|
689
700
|
}
|
|
690
701
|
}
|
|
691
702
|
|
|
703
|
+
/**
|
|
704
|
+
* Fetch INPUT artifacts attached to user messages in this conversation
|
|
705
|
+
* (Direction='Input' on ConversationDetailArtifact). Skip should see these
|
|
706
|
+
* so it can use captured Data Snapshots, user-uploaded files, etc., in
|
|
707
|
+
* its analysis.
|
|
708
|
+
*
|
|
709
|
+
* Deduplicates against `alreadyLoaded` (the Output artifacts) so the same
|
|
710
|
+
* artifact ID doesn't appear twice if it was both produced and re-attached.
|
|
711
|
+
*/
|
|
712
|
+
private async buildInputArtifacts(
|
|
713
|
+
contextUser: UserInfo,
|
|
714
|
+
conversationId: string,
|
|
715
|
+
alreadyLoaded: Map<string, { artifact: any; artifactType: SkipAPIArtifactType; versions: SkipAPIArtifactVersion[] }>
|
|
716
|
+
): Promise<SkipAPIArtifact[]> {
|
|
717
|
+
try {
|
|
718
|
+
const rv = new RunView();
|
|
719
|
+
// Pull conversation detail IDs in this conversation
|
|
720
|
+
const detailsResult = await rv.RunView<MJConversationDetailEntity>({
|
|
721
|
+
EntityName: 'MJ: Conversation Details',
|
|
722
|
+
ExtraFilter: `ConversationID='${conversationId}'`,
|
|
723
|
+
Fields: ['ID'],
|
|
724
|
+
ResultType: 'simple',
|
|
725
|
+
}, contextUser);
|
|
726
|
+
const detailIds = detailsResult.Success && detailsResult.Results
|
|
727
|
+
? (detailsResult.Results as { ID: string }[]).map(r => r.ID)
|
|
728
|
+
: [];
|
|
729
|
+
if (detailIds.length === 0) return [];
|
|
730
|
+
|
|
731
|
+
// Junction rows where Direction='Input' for those details
|
|
732
|
+
const junctionResult = await rv.RunView({
|
|
733
|
+
EntityName: 'MJ: Conversation Detail Artifacts',
|
|
734
|
+
ExtraFilter: `ConversationDetailID IN ('${detailIds.join("','")}') AND Direction='Input'`,
|
|
735
|
+
Fields: ['ArtifactVersionID', 'ConversationDetailID'],
|
|
736
|
+
ResultType: 'simple',
|
|
737
|
+
}, contextUser);
|
|
738
|
+
const junctions = junctionResult.Success && junctionResult.Results
|
|
739
|
+
? (junctionResult.Results as { ArtifactVersionID: string; ConversationDetailID: string }[])
|
|
740
|
+
: [];
|
|
741
|
+
if (junctions.length === 0) return [];
|
|
742
|
+
|
|
743
|
+
// Load each ArtifactVersion + its parent Artifact + ArtifactType
|
|
744
|
+
const versionIds = [...new Set(junctions.map(j => j.ArtifactVersionID))];
|
|
745
|
+
const versionsResult = await rv.RunView({
|
|
746
|
+
EntityName: 'MJ: Artifact Versions',
|
|
747
|
+
ExtraFilter: `ID IN ('${versionIds.join("','")}')`,
|
|
748
|
+
ResultType: 'simple',
|
|
749
|
+
}, contextUser);
|
|
750
|
+
const versions = versionsResult.Success && versionsResult.Results
|
|
751
|
+
? (versionsResult.Results as Record<string, any>[])
|
|
752
|
+
: [];
|
|
753
|
+
if (versions.length === 0) return [];
|
|
754
|
+
|
|
755
|
+
const artifactIds = [...new Set(versions.map(v => v.ArtifactID as string))];
|
|
756
|
+
const artifactsResult = await rv.RunView({
|
|
757
|
+
EntityName: 'MJ: Artifacts',
|
|
758
|
+
ExtraFilter: `ID IN ('${artifactIds.join("','")}')`,
|
|
759
|
+
ResultType: 'simple',
|
|
760
|
+
}, contextUser);
|
|
761
|
+
const artifactRows = artifactsResult.Success && artifactsResult.Results
|
|
762
|
+
? (artifactsResult.Results as Record<string, any>[])
|
|
763
|
+
: [];
|
|
764
|
+
|
|
765
|
+
const typeIds = [...new Set(artifactRows.map(a => a.TypeID as string))];
|
|
766
|
+
const typesResult = await rv.RunView({
|
|
767
|
+
EntityName: 'MJ: Artifact Types',
|
|
768
|
+
ExtraFilter: `ID IN ('${typeIds.join("','")}')`,
|
|
769
|
+
ResultType: 'simple',
|
|
770
|
+
}, contextUser);
|
|
771
|
+
const typeRows = typesResult.Success && typesResult.Results
|
|
772
|
+
? (typesResult.Results as Record<string, any>[])
|
|
773
|
+
: [];
|
|
774
|
+
const typeMap = new Map<string, Record<string, any>>(typeRows.map(t => [t.ID as string, t]));
|
|
775
|
+
|
|
776
|
+
// Build a junction lookup: artifactVersionId -> conversationDetailId
|
|
777
|
+
const versionToDetail = new Map(junctions.map(j => [j.ArtifactVersionID, j.ConversationDetailID]));
|
|
778
|
+
|
|
779
|
+
// Build SkipAPIArtifact entries
|
|
780
|
+
const inputArtifactMap = new Map<string, { artifact: any; artifactType: SkipAPIArtifactType; versions: SkipAPIArtifactVersion[] }>();
|
|
781
|
+
for (const v of versions) {
|
|
782
|
+
const aRow = artifactRows.find(a => UUIDsEqual(a.ID, v.ArtifactID));
|
|
783
|
+
if (!aRow) continue;
|
|
784
|
+
if (alreadyLoaded.has(aRow.ID as string)) continue; // dedup
|
|
785
|
+
|
|
786
|
+
const typeRow = typeMap.get(aRow.TypeID as string);
|
|
787
|
+
if (!typeRow) continue;
|
|
788
|
+
|
|
789
|
+
let entry = inputArtifactMap.get(aRow.ID as string);
|
|
790
|
+
if (!entry) {
|
|
791
|
+
entry = {
|
|
792
|
+
artifact: {
|
|
793
|
+
id: aRow.ID,
|
|
794
|
+
conversationId,
|
|
795
|
+
name: aRow.Name,
|
|
796
|
+
description: aRow.Description || '',
|
|
797
|
+
sharingScope: 'None',
|
|
798
|
+
comments: aRow.Comments || '',
|
|
799
|
+
createdAt: new Date(aRow.__mj_CreatedAt),
|
|
800
|
+
updatedAt: new Date(aRow.__mj_UpdatedAt),
|
|
801
|
+
},
|
|
802
|
+
artifactType: {
|
|
803
|
+
id: typeRow.ID,
|
|
804
|
+
name: typeRow.Name,
|
|
805
|
+
description: typeRow.Description,
|
|
806
|
+
contentType: typeRow.ContentType,
|
|
807
|
+
enabled: true,
|
|
808
|
+
createdAt: new Date(typeRow.__mj_CreatedAt),
|
|
809
|
+
updatedAt: new Date(typeRow.__mj_UpdatedAt),
|
|
810
|
+
},
|
|
811
|
+
versions: [],
|
|
812
|
+
};
|
|
813
|
+
inputArtifactMap.set(aRow.ID as string, entry);
|
|
814
|
+
}
|
|
815
|
+
entry.versions.push({
|
|
816
|
+
id: v.ID,
|
|
817
|
+
artifactId: v.ArtifactID,
|
|
818
|
+
conversationDetailID: versionToDetail.get(v.ID) ?? '',
|
|
819
|
+
version: v.VersionNumber,
|
|
820
|
+
configuration: v.Configuration || '',
|
|
821
|
+
content: v.Content || '',
|
|
822
|
+
comments: v.Comments || '',
|
|
823
|
+
createdAt: new Date(v.__mj_CreatedAt),
|
|
824
|
+
updatedAt: new Date(v.__mj_UpdatedAt),
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return Array.from(inputArtifactMap.values()).map(entry => ({
|
|
829
|
+
...entry.artifact,
|
|
830
|
+
artifactType: entry.artifactType,
|
|
831
|
+
versions: entry.versions,
|
|
832
|
+
}));
|
|
833
|
+
} catch (error) {
|
|
834
|
+
LogError(`Failed to load input artifacts for conversation ${conversationId}: ${error}`);
|
|
835
|
+
return [];
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
692
839
|
/**
|
|
693
840
|
* Build API keys for AI services
|
|
694
841
|
*/
|
|
@@ -21885,9 +21885,8 @@ export class MJArtifactUse_ {
|
|
|
21885
21885
|
@Field()
|
|
21886
21886
|
_mj__UpdatedAt: Date;
|
|
21887
21887
|
|
|
21888
|
-
@Field(
|
|
21889
|
-
|
|
21890
|
-
ArtifactVersion?: string;
|
|
21888
|
+
@Field(() => Int)
|
|
21889
|
+
ArtifactVersion: number;
|
|
21891
21890
|
|
|
21892
21891
|
@Field()
|
|
21893
21892
|
@MaxLength(100)
|
|
@@ -22067,9 +22066,8 @@ export class MJArtifactVersionAttribute_ {
|
|
|
22067
22066
|
@Field()
|
|
22068
22067
|
_mj__UpdatedAt: Date;
|
|
22069
22068
|
|
|
22070
|
-
@Field(
|
|
22071
|
-
|
|
22072
|
-
ArtifactVersion?: string;
|
|
22069
|
+
@Field(() => Int)
|
|
22070
|
+
ArtifactVersion: number;
|
|
22073
22071
|
|
|
22074
22072
|
}
|
|
22075
22073
|
|
|
@@ -23718,9 +23716,8 @@ export class MJCollectionArtifact_ {
|
|
|
23718
23716
|
@MaxLength(255)
|
|
23719
23717
|
Collection: string;
|
|
23720
23718
|
|
|
23721
|
-
@Field(
|
|
23722
|
-
|
|
23723
|
-
ArtifactVersion?: string;
|
|
23719
|
+
@Field(() => Int)
|
|
23720
|
+
ArtifactVersion: number;
|
|
23724
23721
|
|
|
23725
23722
|
}
|
|
23726
23723
|
|
|
@@ -32690,9 +32687,8 @@ export class MJConversationDetailArtifact_ {
|
|
|
32690
32687
|
@Field()
|
|
32691
32688
|
ConversationDetail: string;
|
|
32692
32689
|
|
|
32693
|
-
@Field(
|
|
32694
|
-
|
|
32695
|
-
ArtifactVersion?: string;
|
|
32690
|
+
@Field(() => Int)
|
|
32691
|
+
ArtifactVersion: number;
|
|
32696
32692
|
|
|
32697
32693
|
}
|
|
32698
32694
|
|
|
@@ -32902,9 +32898,8 @@ export class MJConversationDetailAttachment_ {
|
|
|
32902
32898
|
@MaxLength(500)
|
|
32903
32899
|
File?: string;
|
|
32904
32900
|
|
|
32905
|
-
@Field({nullable: true})
|
|
32906
|
-
|
|
32907
|
-
ArtifactVersion?: string;
|
|
32901
|
+
@Field(() => Int, {nullable: true})
|
|
32902
|
+
ArtifactVersion?: number;
|
|
32908
32903
|
|
|
32909
32904
|
}
|
|
32910
32905
|
|
|
@@ -51945,6 +51940,27 @@ export class MJList_ {
|
|
|
51945
51940
|
@Field()
|
|
51946
51941
|
_mj__UpdatedAt: Date;
|
|
51947
51942
|
|
|
51943
|
+
@Field({nullable: true, description: `Optional ID of the User View this list was materialized from. NULL for hand-built lists. When set, the list can be refreshed against this view via ListOperations.RefreshFromSource.`})
|
|
51944
|
+
@MaxLength(36)
|
|
51945
|
+
SourceViewID?: string;
|
|
51946
|
+
|
|
51947
|
+
@Field({nullable: true, description: `JSON snapshot of the source filter at materialization time. When UseSnapshot=1, refreshes re-apply this snapshot rather than re-reading the live source view. Null when no snapshot was captured.`})
|
|
51948
|
+
SourceFilterSnapshot?: string;
|
|
51949
|
+
|
|
51950
|
+
@Field({nullable: true, description: `Timestamp (UTC) of the most recent successful RefreshFromSource. Null when the list has never been refreshed.`})
|
|
51951
|
+
LastRefreshedAt?: Date;
|
|
51952
|
+
|
|
51953
|
+
@Field({nullable: true, description: `User who triggered the most recent successful RefreshFromSource. Null when the list has never been refreshed.`})
|
|
51954
|
+
@MaxLength(36)
|
|
51955
|
+
LastRefreshedByUserID?: string;
|
|
51956
|
+
|
|
51957
|
+
@Field({description: `Default refresh mode for this list. Additive only adds new members; Sync reconciles in both directions (may remove members no longer in the source — requires explicit drop-confirmation).`})
|
|
51958
|
+
@MaxLength(20)
|
|
51959
|
+
RefreshMode: string;
|
|
51960
|
+
|
|
51961
|
+
@Field(() => Boolean, {description: `When 1, RefreshFromSource uses SourceFilterSnapshot as the source. When 0 (default), it re-reads the live SourceView.`})
|
|
51962
|
+
UseSnapshot: boolean;
|
|
51963
|
+
|
|
51948
51964
|
@Field()
|
|
51949
51965
|
@MaxLength(255)
|
|
51950
51966
|
Entity: string;
|
|
@@ -51961,6 +51977,14 @@ export class MJList_ {
|
|
|
51961
51977
|
@MaxLength(255)
|
|
51962
51978
|
CompanyIntegration?: string;
|
|
51963
51979
|
|
|
51980
|
+
@Field({nullable: true})
|
|
51981
|
+
@MaxLength(100)
|
|
51982
|
+
SourceView?: string;
|
|
51983
|
+
|
|
51984
|
+
@Field({nullable: true})
|
|
51985
|
+
@MaxLength(100)
|
|
51986
|
+
LastRefreshedByUser?: string;
|
|
51987
|
+
|
|
51964
51988
|
@Field(() => [MJDuplicateRun_])
|
|
51965
51989
|
MJDuplicateRuns_SourceListIDArray: MJDuplicateRun_[]; // Link to MJDuplicateRuns
|
|
51966
51990
|
|
|
@@ -52004,6 +52028,24 @@ export class CreateMJListInput {
|
|
|
52004
52028
|
@Field({ nullable: true })
|
|
52005
52029
|
CompanyIntegrationID: string | null;
|
|
52006
52030
|
|
|
52031
|
+
@Field({ nullable: true })
|
|
52032
|
+
SourceViewID: string | null;
|
|
52033
|
+
|
|
52034
|
+
@Field({ nullable: true })
|
|
52035
|
+
SourceFilterSnapshot: string | null;
|
|
52036
|
+
|
|
52037
|
+
@Field({ nullable: true })
|
|
52038
|
+
LastRefreshedAt: Date | null;
|
|
52039
|
+
|
|
52040
|
+
@Field({ nullable: true })
|
|
52041
|
+
LastRefreshedByUserID: string | null;
|
|
52042
|
+
|
|
52043
|
+
@Field({ nullable: true })
|
|
52044
|
+
RefreshMode?: string;
|
|
52045
|
+
|
|
52046
|
+
@Field(() => Boolean, { nullable: true })
|
|
52047
|
+
UseSnapshot?: boolean;
|
|
52048
|
+
|
|
52007
52049
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
52008
52050
|
RestoreContext___?: RestoreContextInput;
|
|
52009
52051
|
}
|
|
@@ -52038,6 +52080,24 @@ export class UpdateMJListInput {
|
|
|
52038
52080
|
@Field({ nullable: true })
|
|
52039
52081
|
CompanyIntegrationID?: string | null;
|
|
52040
52082
|
|
|
52083
|
+
@Field({ nullable: true })
|
|
52084
|
+
SourceViewID?: string | null;
|
|
52085
|
+
|
|
52086
|
+
@Field({ nullable: true })
|
|
52087
|
+
SourceFilterSnapshot?: string | null;
|
|
52088
|
+
|
|
52089
|
+
@Field({ nullable: true })
|
|
52090
|
+
LastRefreshedAt?: Date | null;
|
|
52091
|
+
|
|
52092
|
+
@Field({ nullable: true })
|
|
52093
|
+
LastRefreshedByUserID?: string | null;
|
|
52094
|
+
|
|
52095
|
+
@Field({ nullable: true })
|
|
52096
|
+
RefreshMode?: string;
|
|
52097
|
+
|
|
52098
|
+
@Field(() => Boolean, { nullable: true })
|
|
52099
|
+
UseSnapshot?: boolean;
|
|
52100
|
+
|
|
52041
52101
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
52042
52102
|
OldValues___?: KeyValuePairInput[];
|
|
52043
52103
|
|
|
@@ -76926,6 +76986,9 @@ export class MJUserView_ {
|
|
|
76926
76986
|
@Field(() => [MJUserViewRun_])
|
|
76927
76987
|
MJUserViewRuns_UserViewIDArray: MJUserViewRun_[]; // Link to MJUserViewRuns
|
|
76928
76988
|
|
|
76989
|
+
@Field(() => [MJList_])
|
|
76990
|
+
MJLists_SourceViewIDArray: MJList_[]; // Link to MJLists
|
|
76991
|
+
|
|
76929
76992
|
}
|
|
76930
76993
|
|
|
76931
76994
|
//****************************************************************************
|
|
@@ -77173,6 +77236,16 @@ export class MJUserViewResolverBase extends ResolverBase {
|
|
|
77173
77236
|
return result;
|
|
77174
77237
|
}
|
|
77175
77238
|
|
|
77239
|
+
@FieldResolver(() => [MJList_])
|
|
77240
|
+
async MJLists_SourceViewIDArray(@Root() mjuserview_: MJUserView_, @Ctx() { userPayload, providers }: AppContext, @PubSub() pubSub: PubSubEngine) {
|
|
77241
|
+
this.CheckUserReadPermissions('MJ: Lists', userPayload);
|
|
77242
|
+
const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
|
|
77243
|
+
const sSQL = `SELECT * FROM ${provider.QuoteSchemaAndView(Metadata.Provider.ConfigData.MJCoreSchemaName, 'vwLists')} WHERE ${provider.QuoteIdentifier('SourceViewID')}='${mjuserview_.ID}' ` + this.getRowLevelSecurityWhereClause(provider, 'MJ: Lists', userPayload, EntityPermissionType.Read, 'AND');
|
|
77244
|
+
const rows = await provider.ExecuteSQL(sSQL, undefined, undefined, this.GetUserFromPayload(userPayload));
|
|
77245
|
+
const result = await this.ArrayMapFieldNamesToCodeNames('MJ: Lists', rows, this.GetUserFromPayload(userPayload));
|
|
77246
|
+
return result;
|
|
77247
|
+
}
|
|
77248
|
+
|
|
77176
77249
|
@Mutation(() => MJUserView_)
|
|
77177
77250
|
async CreateMJUserView(
|
|
77178
77251
|
@Arg('input', () => CreateMJUserViewInput) input: CreateMJUserViewInput,
|
|
@@ -77579,6 +77652,9 @@ export class MJUser_ {
|
|
|
77579
77652
|
@Field(() => [MJAIAgent_])
|
|
77580
77653
|
MJAIAgents_OwnerUserIDArray: MJAIAgent_[]; // Link to MJAIAgents
|
|
77581
77654
|
|
|
77655
|
+
@Field(() => [MJList_])
|
|
77656
|
+
MJLists_LastRefreshedByUserIDArray: MJList_[]; // Link to MJLists
|
|
77657
|
+
|
|
77582
77658
|
}
|
|
77583
77659
|
|
|
77584
77660
|
//****************************************************************************
|
|
@@ -78714,6 +78790,16 @@ export class MJUserResolverBase extends ResolverBase {
|
|
|
78714
78790
|
return result;
|
|
78715
78791
|
}
|
|
78716
78792
|
|
|
78793
|
+
@FieldResolver(() => [MJList_])
|
|
78794
|
+
async MJLists_LastRefreshedByUserIDArray(@Root() mjuser_: MJUser_, @Ctx() { userPayload, providers }: AppContext, @PubSub() pubSub: PubSubEngine) {
|
|
78795
|
+
this.CheckUserReadPermissions('MJ: Lists', userPayload);
|
|
78796
|
+
const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
|
|
78797
|
+
const sSQL = `SELECT * FROM ${provider.QuoteSchemaAndView(Metadata.Provider.ConfigData.MJCoreSchemaName, 'vwLists')} WHERE ${provider.QuoteIdentifier('LastRefreshedByUserID')}='${mjuser_.ID}' ` + this.getRowLevelSecurityWhereClause(provider, 'MJ: Lists', userPayload, EntityPermissionType.Read, 'AND');
|
|
78798
|
+
const rows = await provider.ExecuteSQL(sSQL, undefined, undefined, this.GetUserFromPayload(userPayload));
|
|
78799
|
+
const result = await this.ArrayMapFieldNamesToCodeNames('MJ: Lists', rows, this.GetUserFromPayload(userPayload));
|
|
78800
|
+
return result;
|
|
78801
|
+
}
|
|
78802
|
+
|
|
78717
78803
|
@Mutation(() => MJUser_)
|
|
78718
78804
|
async CreateMJUser(
|
|
78719
78805
|
@Arg('input', () => CreateMJUserInput) input: CreateMJUserInput,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Arg, Ctx, Query, Resolver, Field, Int, InputType } from 'type-graphql';
|
|
2
|
-
import { LogError } from '@memberjunction/core';
|
|
2
|
+
import { DatabasePlatform, LogError } from '@memberjunction/core';
|
|
3
3
|
import { SQLExpressionValidator } from '@memberjunction/global';
|
|
4
|
+
import { RenderPipeline } from '@memberjunction/generic-database-provider';
|
|
4
5
|
import { AppContext } from '../types.js';
|
|
5
|
-
import { GetReadOnlyDataSource } from '../util.js';
|
|
6
|
+
import { GetReadOnlyDataSource, GetReadOnlyProvider } from '../util.js';
|
|
6
7
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
7
8
|
import { RunQueryResultType } from './QueryResolver.js';
|
|
8
9
|
import sql from 'mssql';
|
|
@@ -18,6 +19,12 @@ class AdhocQueryInput {
|
|
|
18
19
|
|
|
19
20
|
@Field(() => Int, { nullable: true, description: 'Query timeout in seconds. Defaults to 30.' })
|
|
20
21
|
TimeoutSeconds?: number;
|
|
22
|
+
|
|
23
|
+
@Field(() => Int, { nullable: true, description: 'Maximum number of rows to return; applied at the database via the render pipeline.' })
|
|
24
|
+
MaxRows?: number;
|
|
25
|
+
|
|
26
|
+
@Field(() => Int, { nullable: true, description: 'Zero-based offset for pagination. When > 0, the row cap switches to OFFSET/FETCH pagination.' })
|
|
27
|
+
StartRow?: number;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/**
|
|
@@ -56,28 +63,68 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
56
63
|
return this.buildErrorResult('No read-only data source available for ad-hoc query execution');
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
// 3.
|
|
66
|
+
// 3. Resolve platform from the read-only provider for the render pipeline.
|
|
67
|
+
let platform: DatabasePlatform = 'sqlserver';
|
|
68
|
+
try {
|
|
69
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: false });
|
|
70
|
+
if (provider?.PlatformKey) platform = provider.PlatformKey;
|
|
71
|
+
} catch {
|
|
72
|
+
// Provider not configured — keep the default platform.
|
|
73
|
+
}
|
|
74
|
+
const contextUser = context.userPayload?.userRecord;
|
|
75
|
+
|
|
76
|
+
// 4. Route the SQL through RenderPipeline so composition tokens
|
|
77
|
+
// resolve, comments and templates are processed, and the row cap
|
|
78
|
+
// is applied at the database (via TOP / LIMIT / OFFSET-FETCH).
|
|
79
|
+
const startRow = input.StartRow ?? 0;
|
|
80
|
+
const maxRows = input.MaxRows;
|
|
81
|
+
const usePaging =
|
|
82
|
+
maxRows != null &&
|
|
83
|
+
Number.isInteger(maxRows) &&
|
|
84
|
+
maxRows > 0 &&
|
|
85
|
+
Number.isInteger(startRow) &&
|
|
86
|
+
startRow > 0;
|
|
87
|
+
let executableSql: string;
|
|
88
|
+
try {
|
|
89
|
+
const rendered = RenderPipeline.Run(input.SQL, {
|
|
90
|
+
Platform: platform,
|
|
91
|
+
ContextUser: contextUser,
|
|
92
|
+
...(usePaging
|
|
93
|
+
? { Paging: { StartRow: startRow, MaxRows: maxRows! } }
|
|
94
|
+
: maxRows != null && maxRows > 0
|
|
95
|
+
? { MaxRows: maxRows }
|
|
96
|
+
: {}),
|
|
97
|
+
});
|
|
98
|
+
executableSql = rendered.FinalSQL;
|
|
99
|
+
} catch (renderErr) {
|
|
100
|
+
const renderMsg = renderErr instanceof Error ? renderErr.message : String(renderErr);
|
|
101
|
+
return this.buildErrorResult(`Ad-hoc query rendering failed: ${renderMsg}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5. Execute with timeout
|
|
60
105
|
const timeoutMs = (input.TimeoutSeconds ?? 30) * 1000;
|
|
61
106
|
const request = new sql.Request(readOnlyDS);
|
|
62
107
|
|
|
63
108
|
const result = await Promise.race([
|
|
64
|
-
request.query(
|
|
109
|
+
request.query(executableSql),
|
|
65
110
|
new Promise<never>((_, reject) =>
|
|
66
111
|
setTimeout(() => reject(new Error('Query timeout exceeded')), timeoutMs)
|
|
67
112
|
)
|
|
68
113
|
]);
|
|
69
114
|
const executionTimeMs = Date.now() - startTime;
|
|
70
115
|
|
|
71
|
-
//
|
|
116
|
+
// 6. Return as RunQueryResultType
|
|
117
|
+
const recordset = result.recordset ?? [];
|
|
118
|
+
|
|
72
119
|
return {
|
|
73
120
|
QueryID: '',
|
|
74
121
|
QueryName: 'Ad-Hoc Query',
|
|
75
122
|
Success: true,
|
|
76
|
-
Results: JSON.stringify(
|
|
77
|
-
RowCount:
|
|
78
|
-
TotalRowCount:
|
|
79
|
-
PageNumber: undefined,
|
|
80
|
-
PageSize: undefined,
|
|
123
|
+
Results: JSON.stringify(recordset),
|
|
124
|
+
RowCount: recordset.length,
|
|
125
|
+
TotalRowCount: recordset.length,
|
|
126
|
+
PageNumber: maxRows != null && maxRows > 0 ? Math.floor(startRow / maxRows) + 1 : undefined,
|
|
127
|
+
PageSize: maxRows ?? undefined,
|
|
81
128
|
ExecutionTime: executionTimeMs,
|
|
82
129
|
ErrorMessage: ''
|
|
83
130
|
};
|