Skip to content

Commit e1c04cd

Browse files
committed
fix: add google discovery media upload operations
1 parent 76fcb1c commit e1c04cd

2 files changed

Lines changed: 443 additions & 1 deletion

File tree

packages/plugins/google/src/sdk/discovery.test.ts

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,195 @@ it.effect("supplies documented scopes when Picker Discovery omits auth metadata"
395395
}),
396396
);
397397

398+
it.effect(
399+
"generates a separate media-upload operation for Google Discovery methods with supportsMediaUpload",
400+
() =>
401+
Effect.gen(function* () {
402+
const result = yield* convertGoogleDiscoveryToOpenApi({
403+
discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
404+
// @effect-diagnostics-next-line preferSchemaOverJson:off
405+
documentText: JSON.stringify({
406+
name: "drive",
407+
version: "v3",
408+
title: "Drive API",
409+
rootUrl: "https://www.googleapis.com/",
410+
servicePath: "drive/v3/",
411+
resources: {
412+
files: {
413+
methods: {
414+
create: {
415+
id: "drive.files.create",
416+
httpMethod: "POST",
417+
path: "files",
418+
request: { $ref: "File" },
419+
response: { $ref: "File" },
420+
supportsMediaUpload: true,
421+
mediaUpload: {
422+
accept: ["*/*"],
423+
maxSize: "5497558138880",
424+
protocols: {
425+
simple: {
426+
multipart: true,
427+
path: "/upload/drive/v3/files",
428+
},
429+
resumable: {
430+
multipart: true,
431+
path: "/resumable/upload/drive/v3/files",
432+
},
433+
},
434+
},
435+
scopes: ["https://www.googleapis.com/auth/drive.file"],
436+
parameters: {
437+
enforceSingleParent: {
438+
location: "query",
439+
type: "boolean",
440+
},
441+
},
442+
},
443+
update: {
444+
id: "drive.files.update",
445+
httpMethod: "PATCH",
446+
path: "files/{fileId}",
447+
request: { $ref: "File" },
448+
response: { $ref: "File" },
449+
supportsMediaUpload: true,
450+
mediaUpload: {
451+
accept: ["*/*"],
452+
protocols: {
453+
simple: {
454+
multipart: true,
455+
path: "/upload/drive/v3/files/{fileId}",
456+
},
457+
},
458+
},
459+
scopes: ["https://www.googleapis.com/auth/drive.file"],
460+
parameters: {
461+
fileId: { location: "path", required: true, type: "string" },
462+
},
463+
},
464+
export: {
465+
id: "drive.files.export",
466+
httpMethod: "GET",
467+
path: "files/{fileId}/export",
468+
supportsMediaDownload: true,
469+
useMediaDownloadService: true,
470+
parameters: {
471+
fileId: { location: "path", required: true, type: "string" },
472+
mimeType: { location: "query", required: true, type: "string" },
473+
},
474+
},
475+
},
476+
},
477+
},
478+
schemas: {
479+
File: {
480+
id: "File",
481+
type: "object",
482+
properties: {
483+
name: { type: "string" },
484+
},
485+
},
486+
},
487+
}),
488+
});
489+
490+
const spec = decodeConvertedSpec(result.specText);
491+
// The metadata-only operation should keep the original JSON behavior.
492+
const createMetadata = spec.paths["/files"]?.post;
493+
expect(createMetadata).toMatchObject({
494+
operationId: "files.create",
495+
"x-executor-toolPath": "files.create",
496+
requestBody: {
497+
content: {
498+
"application/json": {
499+
schema: { $ref: "#/components/schemas/File" },
500+
},
501+
},
502+
},
503+
});
504+
505+
// A separate media-upload operation should be emitted with the simple upload path.
506+
const createMedia = spec.paths["/upload/drive/v3/files"]?.post;
507+
expect(createMedia).toMatchObject({
508+
operationId: "files.createMedia",
509+
"x-executor-toolPath": "files.createMedia",
510+
"x-executor-pathTemplate": "/upload/drive/v3/files",
511+
});
512+
expect(createMedia?.parameters).toContainEqual(
513+
expect.objectContaining({
514+
name: "uploadType",
515+
in: "query",
516+
required: true,
517+
schema: {
518+
type: "string",
519+
description: "The upload type for the media upload.",
520+
enum: ["media"],
521+
default: "media",
522+
},
523+
}),
524+
);
525+
expect(createMedia?.requestBody).toMatchObject({
526+
content: {
527+
"application/octet-stream": {
528+
schema: { type: "string", format: "binary" },
529+
},
530+
},
531+
});
532+
533+
// The updateMedia operation should preserve path parameters from the original method.
534+
const updateMedia = spec.paths["/upload/drive/v3/files/{fileId}"]?.patch;
535+
expect(updateMedia).toMatchObject({
536+
operationId: "files.updateMedia",
537+
"x-executor-toolPath": "files.updateMedia",
538+
"x-executor-pathTemplate": "/upload/drive/v3/files/{fileId}",
539+
});
540+
expect(updateMedia?.parameters).toContainEqual(
541+
expect.objectContaining({
542+
name: "fileId",
543+
in: "path",
544+
required: true,
545+
}),
546+
);
547+
expect(updateMedia?.parameters).toContainEqual(
548+
expect.objectContaining({
549+
name: "uploadType",
550+
in: "query",
551+
required: true,
552+
schema: {
553+
type: "string",
554+
description: "The upload type for the media upload.",
555+
enum: ["media"],
556+
default: "media",
557+
},
558+
}),
559+
);
560+
561+
// Extraction should produce a usable input schema that includes bodyBase64.
562+
const parsed = yield* parse(result.specText);
563+
const extracted = yield* extract(parsed);
564+
const createMediaOp = extracted.operations.find(
565+
(candidate) => candidate.operationId === "files.createMedia",
566+
);
567+
expect(createMediaOp?.operationId).toBe("files.createMedia");
568+
const inputSchema = createMediaOp
569+
? (Option.getOrUndefined(createMediaOp.inputSchema) as
570+
| {
571+
properties?: Record<string, unknown>;
572+
required?: string[];
573+
}
574+
| undefined)
575+
: undefined;
576+
expect(inputSchema).toBeDefined();
577+
expect(inputSchema?.properties?.bodyBase64).toMatchObject({
578+
type: "string",
579+
contentEncoding: "base64",
580+
contentMediaType: "application/octet-stream",
581+
});
582+
expect(inputSchema?.required).toContain("bodyBase64");
583+
expect(inputSchema?.properties?.body).toBeUndefined();
584+
}),
585+
);
586+
398587
it.effect("bundles Google Discovery documents into one Google OpenAPI source", () =>
399588
Effect.gen(function* () {
400589
const result = yield* convertGoogleDiscoveryBundleToOpenApi({
@@ -631,6 +820,91 @@ const decodeConvertedSpecSecurity = Schema.decodeUnknownSync(
631820
Schema.fromJsonString(ConvertedSpecSecurity),
632821
);
633822

823+
it.effect("generates media-upload operations for bundled Google Discovery documents", () =>
824+
Effect.gen(function* () {
825+
const result = yield* convertGoogleDiscoveryBundleToOpenApi({
826+
documents: [
827+
{
828+
discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
829+
// @effect-diagnostics-next-line preferSchemaOverJson:off
830+
documentText: JSON.stringify({
831+
name: "drive",
832+
version: "v3",
833+
title: "Drive API",
834+
rootUrl: "https://www.googleapis.com/",
835+
servicePath: "drive/v3/",
836+
auth: {
837+
oauth2: {
838+
scopes: {
839+
"https://www.googleapis.com/auth/drive.file": {
840+
description: "Drive file access",
841+
},
842+
},
843+
},
844+
},
845+
resources: {
846+
files: {
847+
methods: {
848+
create: {
849+
id: "drive.files.create",
850+
httpMethod: "POST",
851+
path: "files",
852+
request: { $ref: "File" },
853+
response: { $ref: "File" },
854+
supportsMediaUpload: true,
855+
mediaUpload: {
856+
accept: ["*/*"],
857+
protocols: {
858+
simple: {
859+
multipart: true,
860+
path: "/upload/drive/v3/files",
861+
},
862+
},
863+
},
864+
scopes: ["https://www.googleapis.com/auth/drive.file"],
865+
parameters: {},
866+
},
867+
},
868+
},
869+
},
870+
schemas: {
871+
File: {
872+
id: "File",
873+
type: "object",
874+
properties: {
875+
name: { type: "string" },
876+
},
877+
},
878+
},
879+
}),
880+
},
881+
],
882+
});
883+
884+
const spec = decodeConvertedSpec(result.specText);
885+
const createMedia = spec.paths["/upload/drive/v3/files"]?.post;
886+
expect(createMedia).toMatchObject({
887+
operationId: "drive.files.createMedia",
888+
"x-executor-toolPath": "drive.files.createMedia",
889+
"x-executor-pathTemplate": "/upload/drive/v3/files",
890+
});
891+
expect(createMedia?.parameters).toContainEqual(
892+
expect.objectContaining({
893+
name: "uploadType",
894+
in: "query",
895+
required: true,
896+
}),
897+
);
898+
expect(createMedia?.requestBody).toMatchObject({
899+
content: {
900+
"application/octet-stream": {
901+
schema: { type: "string", format: "binary" },
902+
},
903+
},
904+
});
905+
}),
906+
);
907+
634908
it.effect("compacts and filters the merged bundle scope set into a clean consent set", () =>
635909
Effect.gen(function* () {
636910
const result = yield* convertGoogleDiscoveryBundleToOpenApi({

0 commit comments

Comments
 (0)