History

Fix Google Discovery execution base URL

Rhys Sullivan committed 7 months ago

Commit c128df31 parent 4758a443 files · +209 −6Browse files

Unified · Split

3 files
  1. .changeset/rude-shrimps-teach.md +5 −0
  2. packages/sources/google-discovery/src/adapter.test.ts +202 −0
  3. packages/sources/google-discovery/src/adapter.ts +2 −6
.changeset/rude-shrimps-teach.md+5−0
@@ -0,0 +1,5 @@
1---
2"executor": patch
3---
4
5Fix Google Discovery tool execution for sources stored with discovery document endpoints
packages/sources/google-discovery/src/adapter.test.ts+202−0
@@ -0,0 +1,202 @@
1import { createServer } from "node:http";
2
3import { describe, expect, it } from "@effect/vitest";
4import {
5 EXECUTABLE_BINDING_VERSION,
6 type Source,
7} from "@executor/source-core";
8import * as Effect from "effect/Effect";
9
10import { googleDiscoverySourceAdapter } from "./adapter";
11import { googleDiscoveryProviderDataFromDefinition } from "./tools";
12
13const withJsonServer = async <T>(handler: (input: {
14 baseUrl: string;
15 requests: Array<{
16 method: string;
17 url: string;
18 headers: Record<string, string | string[] | undefined>;
19 body: string;
20 }>;
21}) => Promise<T>): Promise<T> => {
22 const requests: Array<{
23 method: string;
24 url: string;
25 headers: Record<string, string | string[] | undefined>;
26 body: string;
27 }> = [];
28 const server = createServer(async (request, response) => {
29 const chunks: Buffer[] = [];
30 for await (const chunk of request) {
31 chunks.push(Buffer.from(chunk));
32 }
33 requests.push({
34 method: request.method ?? "GET",
35 url: request.url ?? "/",
36 headers: request.headers,
37 body: Buffer.concat(chunks).toString("utf8"),
38 });
39
40 response.statusCode = 200;
41 response.setHeader("content-type", "application/json");
42 response.end(
43 JSON.stringify({
44 ok: true,
45 url: request.url ?? "/",
46 }),
47 );
48 });
49
50 await new Promise<void>((resolve, reject) => {
51 server.listen(0, "127.0.0.1", (error?: Error) => {
52 if (error) {
53 reject(error);
54 return;
55 }
56 resolve();
57 });
58 });
59
60 try {
61 const address = server.address();
62 if (!address || typeof address === "string") {
63 throw new Error("Failed to resolve local adapter test server address");
64 }
65 return await handler({
66 baseUrl: `http://127.0.0.1:${address.port}`,
67 requests,
68 });
69 } finally {
70 await new Promise<void>((resolve, reject) => {
71 server.close((error) => {
72 if (error) {
73 reject(error);
74 return;
75 }
76 resolve();
77 });
78 });
79 }
80};
81
82const makeGoogleDiscoverySource = (discoveryUrl: string): Source => ({
83 id: "src_google_gmail",
84 workspaceId: "ws_test",
85 name: "Gmail",
86 kind: "google_discovery",
87 endpoint: discoveryUrl,
88 status: "connected",
89 enabled: true,
90 namespace: "google.gmail",
91 bindingVersion: 1,
92 binding: {
93 service: "gmail",
94 version: "v1",
95 discoveryUrl,
96 defaultHeaders: null,
97 scopes: [],
98 },
99 importAuthPolicy: "reuse_runtime",
100 importAuth: { kind: "none" },
101 auth: { kind: "none" },
102 sourceHash: null,
103 lastError: null,
104 createdAt: 1,
105 updatedAt: 1,
106});
107
108describe("google discovery source adapter", () => {
109 it("invokes requests against the discovery document runtime root instead of the stored discovery endpoint", () =>
110 Effect.tryPromise({
111 try: async () => {
112 await withJsonServer(async ({ baseUrl, requests }) => {
113 const discoveryUrl = `${baseUrl}/$discovery/rest?version=v1`;
114 const source = makeGoogleDiscoverySource(discoveryUrl);
115 const providerData = googleDiscoveryProviderDataFromDefinition({
116 service: "gmail",
117 version: "v1",
118 rootUrl: `${baseUrl}/`,
119 servicePath: "gmail/v1/",
120 definition: {
121 toolId: "users.messages.list",
122 rawToolId: "users.messages.list",
123 methodId: "gmail.users.messages.list",
124 name: "List Messages",
125 description: null,
126 group: "users.messages",
127 leaf: "list",
128 method: "get",
129 path: "users/{userId}/messages",
130 flatPath: null,
131 parameters: [
132 {
133 name: "userId",
134 location: "path",
135 required: true,
136 repeated: false,
137 description: null,
138 type: "string",
139 },
140 {
141 name: "labelIds",
142 location: "query",
143 required: false,
144 repeated: true,
145 description: null,
146 type: "string",
147 },
148 ],
149 requestSchemaId: null,
150 responseSchemaId: null,
151 scopes: [],
152 supportsMediaUpload: false,
153 supportsMediaDownload: false,
154 },
155 });
156
157 const result = await Effect.runPromise(
158 googleDiscoverySourceAdapter.invoke({
159 source,
160 capability: {} as any,
161 executable: {
162 id: "exe_google_gmail_list",
163 bindingVersion: EXECUTABLE_BINDING_VERSION,
164 binding: providerData,
165 } as any,
166 descriptor: {} as any,
167 catalog: {} as any,
168 args: {
169 userId: "me",
170 labelIds: ["INBOX", "UNREAD"],
171 },
172 auth: {
173 placements: [],
174 headers: {
175 authorization: "Bearer live-test-token",
176 },
177 queryParams: {},
178 cookies: {},
179 bodyValues: {},
180 expiresAt: null,
181 refreshAfter: null,
182 },
183 }),
184 );
185
186 expect(result.status).toBe(200);
187 expect(result.error).toBeNull();
188 expect(result.data).toEqual({
189 ok: true,
190 url: "/gmail/v1/users/me/messages?labelIds=INBOX&labelIds=UNREAD",
191 });
192 expect(requests).toHaveLength(1);
193 expect(requests[0]?.url).toBe(
194 "/gmail/v1/users/me/messages?labelIds=INBOX&labelIds=UNREAD",
195 );
196 expect(requests[0]?.headers.authorization).toBe("Bearer live-test-token");
197 });
198 },
199 catch: (cause) =>
200 cause instanceof Error ? cause : new Error(String(cause)),
201 }));
202});
packages/sources/google-discovery/src/adapter.ts+2−6
@@ -266,14 +266,11 @@ const replacePathParameters = (input: { · 265 unmodified lines
266 return encodeURIComponent(values[0]!);266 return encodeURIComponent(values[0]!);
267 });267 });
268 268
269// `source.endpoint` stores the discovery document URL, not the runtime API
270// root. Always derive invocation targets from the discovery document metadata.
269const resolveGoogleDiscoveryBaseUrl = (input: {271const resolveGoogleDiscoveryBaseUrl = (input: {
270 providerData: GoogleDiscoveryToolProviderData;272 providerData: GoogleDiscoveryToolProviderData;
271 baseUrl?: string;
272}): string => {273}): string => {
273 if (input.baseUrl) {
274 return new URL(input.baseUrl).toString();
275 }
276
277 return new URL(274 return new URL(
278 input.providerData.invocation.servicePath || "",275 input.providerData.invocation.servicePath || "",
279 input.providerData.invocation.rootUrl,276 input.providerData.invocation.rootUrl,
@@ -618,7 +615,6 @@ export const googleDiscoverySourceAdapter = { · 338 unmodified lines
618 resolvedPath.replace(/^\//, ""),615 resolvedPath.replace(/^\//, ""),
619 resolveGoogleDiscoveryBaseUrl({616 resolveGoogleDiscoveryBaseUrl({
620 providerData,617 providerData,
621 baseUrl: input.source.endpoint,
622 }),618 }),
623 );619 );
624 const headers: Record<string, string> = {620 const headers: Record<string, string> = {