@memberjunction/server 5.36.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/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +42 -28
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/package.json +70 -70
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +269 -0
- package/src/agents/skip-agent.ts +15 -2
- package/src/agents/skip-sdk.ts +148 -1
- package/src/resolvers/AdhocQueryResolver.ts +42 -28
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
|
*/
|
|
@@ -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';
|
|
@@ -19,10 +20,10 @@ class AdhocQueryInput {
|
|
|
19
20
|
@Field(() => Int, { nullable: true, description: 'Query timeout in seconds. Defaults to 30.' })
|
|
20
21
|
TimeoutSeconds?: number;
|
|
21
22
|
|
|
22
|
-
@Field(() => Int, { nullable: true, description: 'Maximum number of rows to return
|
|
23
|
+
@Field(() => Int, { nullable: true, description: 'Maximum number of rows to return; applied at the database via the render pipeline.' })
|
|
23
24
|
MaxRows?: number;
|
|
24
25
|
|
|
25
|
-
@Field(() => Int, { nullable: true, description: 'Zero-based offset for pagination.
|
|
26
|
+
@Field(() => Int, { nullable: true, description: 'Zero-based offset for pagination. When > 0, the row cap switches to OFFSET/FETCH pagination.' })
|
|
26
27
|
StartRow?: number;
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -62,25 +63,45 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
62
63
|
return this.buildErrorResult('No read-only data source available for ad-hoc query execution');
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
// 3.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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).
|
|
70
79
|
const startRow = input.StartRow ?? 0;
|
|
71
80
|
const maxRows = input.MaxRows;
|
|
72
|
-
const
|
|
81
|
+
const usePaging =
|
|
73
82
|
maxRows != null &&
|
|
74
83
|
Number.isInteger(maxRows) &&
|
|
75
84
|
maxRows > 0 &&
|
|
76
85
|
Number.isInteger(startRow) &&
|
|
77
|
-
startRow
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|
|
82
103
|
|
|
83
|
-
//
|
|
104
|
+
// 5. Execute with timeout
|
|
84
105
|
const timeoutMs = (input.TimeoutSeconds ?? 30) * 1000;
|
|
85
106
|
const request = new sql.Request(readOnlyDS);
|
|
86
107
|
|
|
@@ -92,23 +113,16 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
92
113
|
]);
|
|
93
114
|
const executionTimeMs = Date.now() - startTime;
|
|
94
115
|
|
|
95
|
-
// 5. Apply in-memory pagination. With the wrap applied this is a no-op for
|
|
96
|
-
// first-page reads; for StartRow > 0 (or CTE-headed SQL where the wrap was
|
|
97
|
-
// skipped) it carves out the requested page.
|
|
98
|
-
const fullRecordset = result.recordset ?? [];
|
|
99
|
-
const totalRowCount = fullRecordset.length;
|
|
100
|
-
let paginated = fullRecordset;
|
|
101
|
-
if (startRow > 0) paginated = paginated.slice(startRow);
|
|
102
|
-
if (maxRows != null && maxRows > 0) paginated = paginated.slice(0, maxRows);
|
|
103
|
-
|
|
104
116
|
// 6. Return as RunQueryResultType
|
|
117
|
+
const recordset = result.recordset ?? [];
|
|
118
|
+
|
|
105
119
|
return {
|
|
106
120
|
QueryID: '',
|
|
107
121
|
QueryName: 'Ad-Hoc Query',
|
|
108
122
|
Success: true,
|
|
109
|
-
Results: JSON.stringify(
|
|
110
|
-
RowCount:
|
|
111
|
-
TotalRowCount:
|
|
123
|
+
Results: JSON.stringify(recordset),
|
|
124
|
+
RowCount: recordset.length,
|
|
125
|
+
TotalRowCount: recordset.length,
|
|
112
126
|
PageNumber: maxRows != null && maxRows > 0 ? Math.floor(startRow / maxRows) + 1 : undefined,
|
|
113
127
|
PageSize: maxRows ?? undefined,
|
|
114
128
|
ExecutionTime: executionTimeMs,
|