@apollo/client-ai-apps 0.5.2 → 0.5.4
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/CHANGELOG.md +39 -0
- package/dist/core/ApolloClient.d.ts +1 -1
- package/dist/core/ApolloClient.d.ts.map +1 -1
- package/dist/core/ApolloClient.js +1 -1
- package/dist/core/ApolloClient.js.map +1 -1
- package/dist/mcp/core/ApolloClient.d.ts +6 -1
- package/dist/mcp/core/ApolloClient.d.ts.map +1 -1
- package/dist/mcp/core/ApolloClient.js +36 -3
- package/dist/mcp/core/ApolloClient.js.map +1 -1
- package/dist/mcp/core/McpAppManager.d.ts +2 -2
- package/dist/mcp/core/McpAppManager.d.ts.map +1 -1
- package/dist/mcp/core/McpAppManager.js +3 -3
- package/dist/mcp/core/McpAppManager.js.map +1 -1
- package/dist/mcp/react/hooks/createHydrationUtils.d.ts +15 -0
- package/dist/mcp/react/hooks/createHydrationUtils.d.ts.map +1 -0
- package/dist/mcp/react/hooks/createHydrationUtils.js +113 -0
- package/dist/mcp/react/hooks/createHydrationUtils.js.map +1 -0
- package/dist/mcp/react/index.d.ts +1 -0
- package/dist/mcp/react/index.d.ts.map +1 -1
- package/dist/mcp/react/index.js +1 -0
- package/dist/mcp/react/index.js.map +1 -1
- package/dist/openai/core/ApolloClient.d.ts +6 -1
- package/dist/openai/core/ApolloClient.d.ts.map +1 -1
- package/dist/openai/core/ApolloClient.js +37 -3
- package/dist/openai/core/ApolloClient.js.map +1 -1
- package/dist/openai/core/McpAppManager.d.ts +2 -2
- package/dist/openai/core/McpAppManager.d.ts.map +1 -1
- package/dist/openai/core/McpAppManager.js +5 -5
- package/dist/openai/core/McpAppManager.js.map +1 -1
- package/dist/openai/react/hooks/createHydrationUtils.d.ts +15 -0
- package/dist/openai/react/hooks/createHydrationUtils.d.ts.map +1 -0
- package/dist/openai/react/hooks/createHydrationUtils.js +113 -0
- package/dist/openai/react/hooks/createHydrationUtils.js.map +1 -0
- package/dist/openai/react/index.d.ts +1 -0
- package/dist/openai/react/index.d.ts.map +1 -1
- package/dist/openai/react/index.js +1 -0
- package/dist/openai/react/index.js.map +1 -1
- package/dist/react/ApolloProvider.js +1 -1
- package/dist/react/ApolloProvider.js.map +1 -1
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mcp.d.ts +1 -1
- package/dist/react/index.mcp.d.ts.map +1 -1
- package/dist/react/index.mcp.js +1 -1
- package/dist/react/index.mcp.js.map +1 -1
- package/dist/react/index.openai.d.ts +1 -1
- package/dist/react/index.openai.d.ts.map +1 -1
- package/dist/react/index.openai.js +1 -1
- package/dist/react/index.openai.js.map +1 -1
- package/dist/react/reactive.d.ts +9 -0
- package/dist/react/reactive.d.ts.map +1 -0
- package/dist/react/reactive.js +11 -0
- package/dist/react/reactive.js.map +1 -0
- package/dist/utilities/getToolNamesFromDocument.d.ts +3 -0
- package/dist/utilities/getToolNamesFromDocument.d.ts.map +1 -0
- package/dist/utilities/getToolNamesFromDocument.js +12 -0
- package/dist/utilities/getToolNamesFromDocument.js.map +1 -0
- package/dist/utilities/getVariableNamesFromDocument.d.ts +3 -0
- package/dist/utilities/getVariableNamesFromDocument.d.ts.map +1 -0
- package/dist/utilities/getVariableNamesFromDocument.js +6 -0
- package/dist/utilities/getVariableNamesFromDocument.js.map +1 -0
- package/dist/utilities/index.d.ts +3 -0
- package/dist/utilities/index.d.ts.map +1 -1
- package/dist/utilities/index.js +3 -0
- package/dist/utilities/index.js.map +1 -1
- package/dist/utilities/warnOnVariableMismatch.d.ts +3 -0
- package/dist/utilities/warnOnVariableMismatch.d.ts.map +1 -0
- package/dist/utilities/warnOnVariableMismatch.js +10 -0
- package/dist/utilities/warnOnVariableMismatch.js.map +1 -0
- package/dist/vite/apolloClientAiApps.d.ts.map +1 -1
- package/dist/vite/apolloClientAiApps.js +35 -31
- package/dist/vite/apolloClientAiApps.js.map +1 -1
- package/package.json +2 -1
- package/src/core/ApolloClient.ts +1 -1
- package/src/mcp/core/ApolloClient.ts +67 -2
- package/src/mcp/core/McpAppManager.ts +3 -3
- package/src/mcp/core/__tests__/ApolloClient.test.ts +109 -6
- package/src/mcp/link/__tests__/ToolCallLink.test.ts +1 -1
- package/src/mcp/react/hooks/__tests__/createHydrationUtils.test.tsx +1228 -0
- package/src/mcp/react/hooks/createHydrationUtils.ts +182 -0
- package/src/mcp/react/index.ts +1 -0
- package/src/openai/core/ApolloClient.ts +68 -2
- package/src/openai/core/McpAppManager.ts +5 -5
- package/src/openai/core/__tests__/ApolloClient.test.ts +113 -7
- package/src/openai/react/hooks/__tests__/createHydrationUtils.test.tsx +1333 -0
- package/src/openai/react/hooks/createHydrationUtils.ts +182 -0
- package/src/openai/react/index.ts +1 -0
- package/src/react/ApolloProvider.tsx +1 -1
- package/src/react/index.mcp.ts +1 -0
- package/src/react/index.openai.ts +1 -0
- package/src/react/index.ts +7 -0
- package/src/react/reactive.ts +19 -0
- package/src/testing/internal/mcp/graphqlToolResult.ts +5 -5
- package/src/utilities/getToolNamesFromDocument.ts +15 -0
- package/src/utilities/getVariableNamesFromDocument.ts +9 -0
- package/src/utilities/index.ts +3 -0
- package/src/utilities/warnOnVariableMismatch.ts +20 -0
- package/src/vite/__tests__/apolloClientAiApps.test.ts +183 -2
- package/src/vite/apolloClientAiApps.ts +46 -40
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useMemo, useLayoutEffect } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
DocumentNode,
|
|
4
|
+
OperationVariables,
|
|
5
|
+
TypedDocumentNode,
|
|
6
|
+
} from "@apollo/client";
|
|
7
|
+
import { useApolloClient } from "./useApolloClient.js";
|
|
8
|
+
import { useToolName } from "./useToolName.js";
|
|
9
|
+
import { isReactive } from "../../../react/reactive.js";
|
|
10
|
+
import type { Reactive } from "../../../react/reactive.js";
|
|
11
|
+
import { equal } from "@wry/equality";
|
|
12
|
+
import { __DEV__ } from "@apollo/client/utilities/environment";
|
|
13
|
+
import {
|
|
14
|
+
getToolNamesFromDocument,
|
|
15
|
+
getVariableNamesFromDocument,
|
|
16
|
+
} from "../../../utilities/index.js";
|
|
17
|
+
|
|
18
|
+
type HydratedVariablesInput<TVariables> = {
|
|
19
|
+
[K in keyof TVariables]: TVariables[K] | Reactive<TVariables[K]>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type StateVariables<TVariables, Input> = {
|
|
23
|
+
[K in keyof TVariables as K extends keyof Input ?
|
|
24
|
+
Input[K] extends Reactive<any> ?
|
|
25
|
+
never
|
|
26
|
+
: K
|
|
27
|
+
: K]: K extends keyof TVariables ? TVariables[K] : never;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type SetVariables<TState> = (
|
|
31
|
+
update: Partial<TState> | ((prev: TState) => Partial<TState>)
|
|
32
|
+
) => void;
|
|
33
|
+
|
|
34
|
+
/** @experimental */
|
|
35
|
+
export function createHydrationUtils<
|
|
36
|
+
TVariables extends OperationVariables = OperationVariables,
|
|
37
|
+
>(document: TypedDocumentNode<any, TVariables> | DocumentNode) {
|
|
38
|
+
const documentToolNames = getToolNamesFromDocument(document);
|
|
39
|
+
const variableNames = getVariableNamesFromDocument(document);
|
|
40
|
+
|
|
41
|
+
function useHydratedVariables<
|
|
42
|
+
TInputVariables extends HydratedVariablesInput<TVariables>,
|
|
43
|
+
>(
|
|
44
|
+
variables: TInputVariables &
|
|
45
|
+
Record<Exclude<keyof TInputVariables, keyof TVariables>, never>
|
|
46
|
+
): [
|
|
47
|
+
variables: TVariables,
|
|
48
|
+
setVariables: SetVariables<StateVariables<TVariables, TInputVariables>>,
|
|
49
|
+
] {
|
|
50
|
+
const client = useApolloClient();
|
|
51
|
+
const toolName = useToolName();
|
|
52
|
+
const [toolInput] = useState(() => client.toolInput);
|
|
53
|
+
|
|
54
|
+
const toolMatches =
|
|
55
|
+
toolInput !== undefined &&
|
|
56
|
+
toolName !== undefined &&
|
|
57
|
+
documentToolNames.has(toolName);
|
|
58
|
+
|
|
59
|
+
const [stateVars, setStateVars] = useState<Record<string, unknown>>(() => {
|
|
60
|
+
const values: Record<string, unknown> = {};
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of Object.entries(
|
|
63
|
+
toolMatches ? toolInput : variables
|
|
64
|
+
)) {
|
|
65
|
+
if (variableNames.has(key) && !isReactive(value)) {
|
|
66
|
+
values[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return values;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const [initialReactiveValues] = useState<Record<string, unknown>>(() => {
|
|
74
|
+
const values: Record<string, unknown> = {};
|
|
75
|
+
|
|
76
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
77
|
+
if (variableNames.has(key) && isReactive(value)) {
|
|
78
|
+
values[key] = value.value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return values;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const [reactiveVars, setReactiveVars] = useState(() => {
|
|
86
|
+
const values: Record<string, unknown> = {};
|
|
87
|
+
|
|
88
|
+
for (const [key, value] of Object.entries(initialReactiveValues)) {
|
|
89
|
+
if (toolMatches && key in toolInput) {
|
|
90
|
+
values[key] = toolInput[key];
|
|
91
|
+
} else if (!toolMatches) {
|
|
92
|
+
values[key] = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return values;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const changedKeysRef = useRef(new Set<string>());
|
|
100
|
+
const nextReactiveVars: Record<string, unknown> = {};
|
|
101
|
+
|
|
102
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
103
|
+
if (!variableNames.has(key) || !isReactive(value)) continue;
|
|
104
|
+
|
|
105
|
+
const hasChanged =
|
|
106
|
+
changedKeysRef.current.has(key) ||
|
|
107
|
+
!equal(value.value, initialReactiveValues[key]);
|
|
108
|
+
|
|
109
|
+
if (toolMatches && !hasChanged) {
|
|
110
|
+
if (key in toolInput) {
|
|
111
|
+
nextReactiveVars[key] = toolInput[key];
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
nextReactiveVars[key] = value.value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!equal(nextReactiveVars, reactiveVars)) {
|
|
119
|
+
setReactiveVars(nextReactiveVars);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Clear the tool input after first mount so that remounting the component
|
|
123
|
+
// uses the user-provided variables rather than the hydrated tool input.
|
|
124
|
+
// This runs once on mount; watchQuery also clears it when useQuery is
|
|
125
|
+
// present, so both paths are idempotent.
|
|
126
|
+
useLayoutEffect(() => {
|
|
127
|
+
if (toolMatches) {
|
|
128
|
+
client.clearToolInput();
|
|
129
|
+
}
|
|
130
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
useLayoutEffect(() => {
|
|
134
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
135
|
+
if (
|
|
136
|
+
variableNames.has(key) &&
|
|
137
|
+
isReactive(value) &&
|
|
138
|
+
!changedKeysRef.current.has(key) &&
|
|
139
|
+
!equal(value.value, initialReactiveValues[key])
|
|
140
|
+
) {
|
|
141
|
+
changedKeysRef.current.add(key);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const resolvedVariables = useMemo(() => {
|
|
147
|
+
return { ...stateVars, ...reactiveVars } as TVariables;
|
|
148
|
+
}, [stateVars, reactiveVars]);
|
|
149
|
+
|
|
150
|
+
const setVariables = useCallback<
|
|
151
|
+
SetVariables<StateVariables<TVariables, TInputVariables>>
|
|
152
|
+
>((update) => {
|
|
153
|
+
setStateVars((prev) => {
|
|
154
|
+
const updates =
|
|
155
|
+
typeof update === "function" ? update(prev as any) : update;
|
|
156
|
+
|
|
157
|
+
const filtered = Object.fromEntries(
|
|
158
|
+
Object.entries(updates).filter(([key]) => {
|
|
159
|
+
if (key in initialReactiveValues) {
|
|
160
|
+
if (__DEV__) {
|
|
161
|
+
console.warn(
|
|
162
|
+
`Attempted to set reactive variable "${key}" via setVariables. ` +
|
|
163
|
+
`Reactive variables are read-only and are ignored. `
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (Object.keys(filtered).length === 0) return prev;
|
|
173
|
+
|
|
174
|
+
return { ...prev, ...filtered };
|
|
175
|
+
});
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
return [resolvedVariables, setVariables];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { useHydratedVariables };
|
|
182
|
+
}
|
|
@@ -2,4 +2,5 @@ export { useApp } from "./hooks/useApp.js";
|
|
|
2
2
|
export { useToolName } from "./hooks/useToolName.js";
|
|
3
3
|
export { useToolMetadata } from "./hooks/useToolMetadata.js";
|
|
4
4
|
export { useToolInput } from "./hooks/useToolInput.js";
|
|
5
|
+
export { createHydrationUtils } from "./hooks/createHydrationUtils.js";
|
|
5
6
|
export { useWidgetState } from "./hooks/useWidgetState.js";
|
package/src/react/index.mcp.ts
CHANGED
package/src/react/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { missingHook } from "./missingHook.js";
|
|
2
2
|
|
|
3
3
|
export { ApolloProvider } from "./ApolloProvider.js";
|
|
4
|
+
export { reactive } from "./reactive.js";
|
|
5
|
+
export type { Reactive } from "./reactive.js";
|
|
4
6
|
|
|
5
7
|
// Use `mcp` related types since these are the most common between the two
|
|
6
8
|
// targets
|
|
@@ -17,3 +19,8 @@ export const useToolMetadata =
|
|
|
17
19
|
|
|
18
20
|
export const useToolName =
|
|
19
21
|
missingHook<typeof import("./index.mcp.js").useToolName>("useToolName");
|
|
22
|
+
|
|
23
|
+
/** @experimental */
|
|
24
|
+
export const createHydrationUtils = missingHook<
|
|
25
|
+
typeof import("./index.mcp.js").createHydrationUtils
|
|
26
|
+
>("createHydrationUtils");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const REACTIVE = Symbol("apollo.reactive");
|
|
2
|
+
|
|
3
|
+
export interface Reactive<T> {
|
|
4
|
+
readonly [REACTIVE]: true;
|
|
5
|
+
readonly value: T;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function reactive<T>(value: T): Reactive<T> {
|
|
9
|
+
return { [REACTIVE]: true, value };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isReactive(value: unknown): value is Reactive<unknown> {
|
|
13
|
+
return (
|
|
14
|
+
typeof value === "object" &&
|
|
15
|
+
value !== null &&
|
|
16
|
+
REACTIVE in value &&
|
|
17
|
+
(value as Record<symbol, unknown>)[REACTIVE] === true
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -2,20 +2,20 @@ import type { McpUiToolResultNotification } from "@modelcontextprotocol/ext-apps
|
|
|
2
2
|
import type { FormattedExecutionResult } from "graphql";
|
|
3
3
|
import type { ApolloMcpServerApps } from "../../../core/types";
|
|
4
4
|
|
|
5
|
-
export function graphqlToolResult(
|
|
5
|
+
export function graphqlToolResult<TData = Record<string, unknown>>(
|
|
6
6
|
options:
|
|
7
|
-
| FormattedExecutionResult
|
|
7
|
+
| FormattedExecutionResult<TData>
|
|
8
8
|
| {
|
|
9
9
|
prefetch?: Record<string, FormattedExecutionResult>;
|
|
10
|
-
result: FormattedExecutionResult
|
|
10
|
+
result: FormattedExecutionResult<TData>;
|
|
11
11
|
}
|
|
12
12
|
): McpUiToolResultNotification["params"] {
|
|
13
13
|
let structuredContent: ApolloMcpServerApps.StructuredContent;
|
|
14
14
|
|
|
15
15
|
if ("data" in options) {
|
|
16
|
-
structuredContent = { result: options };
|
|
16
|
+
structuredContent = { result: options as FormattedExecutionResult };
|
|
17
17
|
} else if ("result" in options) {
|
|
18
|
-
structuredContent = { result: options.result };
|
|
18
|
+
structuredContent = { result: options.result as FormattedExecutionResult };
|
|
19
19
|
|
|
20
20
|
if (options.prefetch) {
|
|
21
21
|
structuredContent.prefetch = options.prefetch;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getOperationDefinition } from "@apollo/client/utilities/internal";
|
|
2
|
+
import { Kind, type DocumentNode } from "graphql";
|
|
3
|
+
|
|
4
|
+
export function getToolNamesFromDocument(document: DocumentNode) {
|
|
5
|
+
const operationDef = getOperationDefinition(document);
|
|
6
|
+
|
|
7
|
+
return new Set(
|
|
8
|
+
operationDef?.directives
|
|
9
|
+
?.filter((d) => d.name.value === "tool")
|
|
10
|
+
.flatMap((d) => {
|
|
11
|
+
const nameArg = d.arguments?.find((arg) => arg.name.value === "name");
|
|
12
|
+
return nameArg?.value.kind === Kind.STRING ? [nameArg.value.value] : [];
|
|
13
|
+
})
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getOperationDefinition } from "@apollo/client/utilities/internal";
|
|
2
|
+
import type { DocumentNode } from "graphql";
|
|
3
|
+
|
|
4
|
+
export function getVariableNamesFromDocument(document: DocumentNode) {
|
|
5
|
+
const operationDef = getOperationDefinition(document);
|
|
6
|
+
return new Set(
|
|
7
|
+
operationDef?.variableDefinitions?.map((v) => v.variable.name.value) ?? []
|
|
8
|
+
);
|
|
9
|
+
}
|
package/src/utilities/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { aiClientSymbol } from "./constants.js";
|
|
2
2
|
export { getVariablesForOperationFromToolInput } from "./getVariablesForOperationFromToolInput.js";
|
|
3
|
+
export { getToolNamesFromDocument } from "./getToolNamesFromDocument.js";
|
|
4
|
+
export { getVariableNamesFromDocument } from "./getVariableNamesFromDocument.js";
|
|
3
5
|
export { cacheAsync } from "./cacheAsync.js";
|
|
4
6
|
export { invariant } from "./invariant.js";
|
|
5
7
|
export { promiseWithResolvers } from "./promiseWithResolvers.js";
|
|
8
|
+
export { warnOnVariableMismatch } from "./warnOnVariableMismatch.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DocumentNode, OperationVariables } from "@apollo/client";
|
|
2
|
+
import { getOperationName } from "@apollo/client/utilities/internal/internal.cjs";
|
|
3
|
+
|
|
4
|
+
export function warnOnVariableMismatch(
|
|
5
|
+
document: DocumentNode,
|
|
6
|
+
toolInput: OperationVariables,
|
|
7
|
+
actualVariables: OperationVariables | undefined
|
|
8
|
+
) {
|
|
9
|
+
const operationName = getOperationName(document, "(anonymous)");
|
|
10
|
+
|
|
11
|
+
console.warn(
|
|
12
|
+
`The operation "${operationName}" has a @tool directive matching the current ` +
|
|
13
|
+
"tool call, but the variables provided to the query don't match the tool " +
|
|
14
|
+
"input. Use the `useHydratedVariables` hook returned from `createHydrationUtils` " +
|
|
15
|
+
"to provide the hydrated variables to the query. " +
|
|
16
|
+
"\n\nExpected variables:\n%o\n\nReceived:\n%o",
|
|
17
|
+
toolInput,
|
|
18
|
+
actualVariables
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { gql, type DocumentNode } from "@apollo/client";
|
|
4
|
-
import { print } from "@apollo/client/utilities";
|
|
4
|
+
import { getMainDefinition, print } from "@apollo/client/utilities";
|
|
5
5
|
import { getOperationName } from "@apollo/client/utilities/internal";
|
|
6
6
|
import { vol } from "memfs";
|
|
7
7
|
import { apolloClientAiApps } from "../apolloClientAiApps.js";
|
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
ManifestWidgetSettings,
|
|
12
12
|
} from "../../types/application-manifest.js";
|
|
13
13
|
import { explorer } from "../utilities/config.js";
|
|
14
|
+
import { invariant } from "@apollo/client/utilities/invariant";
|
|
15
|
+
import { Kind } from "graphql";
|
|
14
16
|
|
|
15
17
|
beforeEach(() => {
|
|
16
18
|
explorer.clearCaches();
|
|
@@ -121,6 +123,173 @@ describe("operations", () => {
|
|
|
121
123
|
`);
|
|
122
124
|
});
|
|
123
125
|
|
|
126
|
+
test("handles operations with fragments in the same file", async () => {
|
|
127
|
+
vol.fromJSON({
|
|
128
|
+
"package.json": mockPackageJson(),
|
|
129
|
+
"src/my-component.tsx": declareOperation(gql`
|
|
130
|
+
query HelloWorldQuery($name: string!)
|
|
131
|
+
@tool(name: "hello-world", description: "This is an awesome tool!") {
|
|
132
|
+
greeting {
|
|
133
|
+
message
|
|
134
|
+
recipient {
|
|
135
|
+
...RecipientFragment
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fragment RecipientFragment on Recipient {
|
|
141
|
+
id
|
|
142
|
+
name
|
|
143
|
+
}
|
|
144
|
+
`),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await using server = await setupServer({
|
|
148
|
+
plugins: [apolloClientAiApps({ targets: ["mcp"] })],
|
|
149
|
+
});
|
|
150
|
+
await server.listen();
|
|
151
|
+
|
|
152
|
+
const manifest = readManifestFile();
|
|
153
|
+
expect(manifest).toMatchInlineSnapshot(`
|
|
154
|
+
{
|
|
155
|
+
"appVersion": "1.0.0",
|
|
156
|
+
"csp": {
|
|
157
|
+
"connectDomains": [],
|
|
158
|
+
"frameDomains": [],
|
|
159
|
+
"redirectDomains": [],
|
|
160
|
+
"resourceDomains": [],
|
|
161
|
+
},
|
|
162
|
+
"format": "apollo-ai-app-manifest",
|
|
163
|
+
"hash": "abc",
|
|
164
|
+
"operations": [
|
|
165
|
+
{
|
|
166
|
+
"body": "query HelloWorldQuery {
|
|
167
|
+
greeting {
|
|
168
|
+
message
|
|
169
|
+
recipient {
|
|
170
|
+
...RecipientFragment
|
|
171
|
+
__typename
|
|
172
|
+
}
|
|
173
|
+
__typename
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fragment RecipientFragment on Recipient {
|
|
178
|
+
id
|
|
179
|
+
name
|
|
180
|
+
__typename
|
|
181
|
+
}",
|
|
182
|
+
"id": "1646a86ae2ff5ad75457161be5cff80f3ba5172da573a0fc796b268870119020",
|
|
183
|
+
"name": "HelloWorldQuery",
|
|
184
|
+
"prefetch": false,
|
|
185
|
+
"tools": [
|
|
186
|
+
{
|
|
187
|
+
"description": "This is an awesome tool!",
|
|
188
|
+
"name": "hello-world",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
"type": "query",
|
|
192
|
+
"variables": {
|
|
193
|
+
"name": "string",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
"resource": "http://localhost:3333",
|
|
198
|
+
"version": "1",
|
|
199
|
+
}
|
|
200
|
+
`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("handles operations with fragments in other files", async () => {
|
|
204
|
+
vol.fromJSON({
|
|
205
|
+
"package.json": mockPackageJson(),
|
|
206
|
+
"src/first-recipient.tsx": declareFragment(gql`
|
|
207
|
+
fragment OtherRecipientFragment on Recipient {
|
|
208
|
+
name
|
|
209
|
+
}
|
|
210
|
+
`),
|
|
211
|
+
"src/my-component.tsx": declareOperation(gql`
|
|
212
|
+
query HelloWorldQuery($name: string!)
|
|
213
|
+
@tool(name: "hello-world", description: "This is an awesome tool!") {
|
|
214
|
+
greeting {
|
|
215
|
+
message
|
|
216
|
+
recipient {
|
|
217
|
+
...RecipientFragment
|
|
218
|
+
...OtherRecipientFragment
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
`),
|
|
223
|
+
"src/my-fragment.tsx": declareFragment(gql`
|
|
224
|
+
fragment RecipientFragment on Recipient {
|
|
225
|
+
id
|
|
226
|
+
name
|
|
227
|
+
}
|
|
228
|
+
`),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await using server = await setupServer({
|
|
232
|
+
plugins: [apolloClientAiApps({ targets: ["mcp"] })],
|
|
233
|
+
});
|
|
234
|
+
await server.listen();
|
|
235
|
+
|
|
236
|
+
const manifest = readManifestFile();
|
|
237
|
+
expect(manifest).toMatchInlineSnapshot(`
|
|
238
|
+
{
|
|
239
|
+
"appVersion": "1.0.0",
|
|
240
|
+
"csp": {
|
|
241
|
+
"connectDomains": [],
|
|
242
|
+
"frameDomains": [],
|
|
243
|
+
"redirectDomains": [],
|
|
244
|
+
"resourceDomains": [],
|
|
245
|
+
},
|
|
246
|
+
"format": "apollo-ai-app-manifest",
|
|
247
|
+
"hash": "abc",
|
|
248
|
+
"operations": [
|
|
249
|
+
{
|
|
250
|
+
"body": "query HelloWorldQuery {
|
|
251
|
+
greeting {
|
|
252
|
+
message
|
|
253
|
+
recipient {
|
|
254
|
+
...RecipientFragment
|
|
255
|
+
...OtherRecipientFragment
|
|
256
|
+
__typename
|
|
257
|
+
}
|
|
258
|
+
__typename
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fragment OtherRecipientFragment on Recipient {
|
|
263
|
+
name
|
|
264
|
+
__typename
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
fragment RecipientFragment on Recipient {
|
|
268
|
+
id
|
|
269
|
+
name
|
|
270
|
+
__typename
|
|
271
|
+
}",
|
|
272
|
+
"id": "c65cb5ec2dc76bbfa992cd2a98c05ab3f909349f3f1478e608a7f16ae29bdd4a",
|
|
273
|
+
"name": "HelloWorldQuery",
|
|
274
|
+
"prefetch": false,
|
|
275
|
+
"tools": [
|
|
276
|
+
{
|
|
277
|
+
"description": "This is an awesome tool!",
|
|
278
|
+
"name": "hello-world",
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
"type": "query",
|
|
282
|
+
"variables": {
|
|
283
|
+
"name": "string",
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
"resource": "http://localhost:3333",
|
|
288
|
+
"version": "1",
|
|
289
|
+
}
|
|
290
|
+
`);
|
|
291
|
+
});
|
|
292
|
+
|
|
124
293
|
test("does not write to dev application manifest file when using a build command", async () => {
|
|
125
294
|
vol.fromJSON({
|
|
126
295
|
"package.json": mockPackageJson(),
|
|
@@ -289,7 +458,7 @@ describe("operations", () => {
|
|
|
289
458
|
});
|
|
290
459
|
await server.listen();
|
|
291
460
|
}).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
292
|
-
`[Error: Found
|
|
461
|
+
`[Error: Found unsupported operation type 'subscription'. Only queries and mutations are supported.]`
|
|
293
462
|
);
|
|
294
463
|
});
|
|
295
464
|
|
|
@@ -1461,6 +1630,18 @@ function declareOperation(operation: DocumentNode) {
|
|
|
1461
1630
|
return `const ${varName} = gql\`\n${print(operation)}\n\``;
|
|
1462
1631
|
}
|
|
1463
1632
|
|
|
1633
|
+
function declareFragment(fragment: DocumentNode) {
|
|
1634
|
+
const definition = getMainDefinition(fragment);
|
|
1635
|
+
invariant(
|
|
1636
|
+
definition.kind === Kind.FRAGMENT_DEFINITION,
|
|
1637
|
+
"declareFragment must receive a fragment definition"
|
|
1638
|
+
);
|
|
1639
|
+
|
|
1640
|
+
const name = definition.name.value;
|
|
1641
|
+
const varName = name.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
|
|
1642
|
+
return `const ${varName} = gql\`\n${print(fragment)}\n\``;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1464
1645
|
function mockPackageJson(config?: Record<string, unknown>) {
|
|
1465
1646
|
return JSON.stringify({ version: "1.0.0", ...config });
|
|
1466
1647
|
}
|