import { auth, drive, type drive_v3 } from "@googleapis/drive" import { integrationScope as scope } from "../../automation/integrations" import * as z from "zod" const GOOGLE_SECRET_SCHEMA = z.object({ accessToken: z.string().min(1) }) export const GOOGLE_DRIVE_READ_REQUIREMENT = scope.any( "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive.readonly", ) export const GOOGLE_SHARED_DRIVE_READ_REQUIREMENT = scope.any( "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.readonly", ) export const GOOGLE_DRIVE_METADATA_READ_REQUIREMENT = scope.any( "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive.readonly", "https://www.googleapis.com/auth/drive.metadata", "https://www.googleapis.com/auth/drive.metadata.readonly", ) export const GOOGLE_DRIVE_WRITE_REQUIREMENT = scope.any( "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file", ) export const GOOGLE_DRIVE_METADATA_WRITE_REQUIREMENT = scope.any( "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.metadata", "https://www.googleapis.com/auth/drive.file", ) export const GOOGLE_DRIVE_ADMIN_REQUIREMENT = scope.and( "https://www.googleapis.com/auth/drive", ) export const GOOGLE_DRIVE_FILE_SCHEMA = z.object({ createdAt: z.date().optional(), description: z.string().optional(), driveId: z.string().optional(), id: z.string(), mimeType: z.string(), modifiedAt: z.date().optional(), name: z.string(), ownedByMe: z.boolean().optional(), parents: z.string().array(), shared: z.boolean().optional(), size: z.string().optional(), starred: z.boolean(), trashed: z.boolean(), version: z.string().optional(), webContentLink: z.url().optional(), webViewLink: z.url().optional(), }) export const GOOGLE_DRIVE_PERMISSION_SCHEMA = z.object({ deleted: z.boolean(), displayName: z.string().optional(), domain: z.string().optional(), emailAddress: z.email().optional(), expirationTime: z.date().optional(), id: z.string(), pendingOwner: z.boolean(), role: z.enum([ "owner", "organizer", "fileOrganizer", "writer", "commenter", "reader", ]), type: z.enum(["user", "group", "domain", "anyone"]), }) const GOOGLE_DRIVE_USER_SCHEMA = z.object({ displayName: z.string().optional(), emailAddress: z.email().optional(), me: z.boolean(), photoLink: z.url().optional(), }) export const GOOGLE_DRIVE_REPLY_SCHEMA = z.object({ author: GOOGLE_DRIVE_USER_SCHEMA.optional(), content: z.string().optional(), createdAt: z.date().optional(), deleted: z.boolean(), id: z.string(), modifiedAt: z.date().optional(), }) export const GOOGLE_DRIVE_COMMENT_SCHEMA = z.object({ anchor: z.string().optional(), author: GOOGLE_DRIVE_USER_SCHEMA.optional(), content: z.string().optional(), createdAt: z.date().optional(), deleted: z.boolean(), id: z.string(), modifiedAt: z.date().optional(), quotedContent: z.string().optional(), replies: GOOGLE_DRIVE_REPLY_SCHEMA.array(), resolved: z.boolean(), }) export const GOOGLE_SHARED_DRIVE_SCHEMA = z.object({ backgroundImageLink: z.url().optional(), colorRgb: z.string().optional(), createdAt: z.date().optional(), hidden: z.boolean(), id: z.string(), name: z.string(), orgUnitId: z.string().optional(), themeId: z.string().optional(), }) const FILE_FIELDS = "id,name,mimeType,description,starred,trashed,parents,createdTime,modifiedTime,webViewLink,webContentLink,size,version,ownedByMe,shared,driveId" const PERMISSION_FIELDS = "id,type,role,emailAddress,domain,displayName,expirationTime,deleted,pendingOwner" const COMMENT_FIELDS = "id,content,anchor,resolved,deleted,createdTime,modifiedTime,quotedFileContent(value),author(displayName,emailAddress,me,photoLink),replies(id,content,deleted,createdTime,modifiedTime,author(displayName,emailAddress,me,photoLink))" const REPLY_FIELDS = "id,content,deleted,createdTime,modifiedTime,author(displayName,emailAddress,me,photoLink)" const DRIVE_FIELDS = "id,name,colorRgb,themeId,backgroundImageLink,createdTime,hidden,orgUnitId" /** * Creates the official Drive client from a refreshed Google secret. * * @param secret - Refreshed Google account secret. */ export function getGoogleDriveApi(secret: Record) { const { accessToken } = GOOGLE_SECRET_SCHEMA.parse(secret) return drive({ auth: new auth.OAuth2({ credentials: { access_token: accessToken } }), version: "v3", }) } /** @param prefix - Optional provider field-path prefix. */ export function googleDriveFileFields(prefix = "") { return `${prefix}${FILE_FIELDS}` } /** @param prefix - Optional provider field-path prefix. */ export function googleDrivePermissionFields(prefix = "") { return `${prefix}${PERMISSION_FIELDS}` } /** @param prefix - Optional provider field-path prefix. */ export function googleDriveCommentFields(prefix = "") { return `${prefix}${COMMENT_FIELDS}` } /** @param prefix - Optional provider field-path prefix. */ export function googleDriveReplyFields(prefix = "") { return `${prefix}${REPLY_FIELDS}` } /** @param prefix - Optional provider field-path prefix. */ export function googleSharedDriveFields(prefix = "") { return `${prefix}${DRIVE_FIELDS}` } /** * Converts provider file metadata to the stable public shape. * * @param file - Provider file response. * @throws When required file metadata is absent. */ export function normalizeGoogleDriveFile(file: drive_v3.Schema$File) { if (!file.id || !file.name || !file.mimeType) { throw new Error("Google Drive returned incomplete file metadata.") } return { createdAt: parseDate(file.createdTime), description: file.description ?? undefined, driveId: file.driveId ?? undefined, id: file.id, mimeType: file.mimeType, modifiedAt: parseDate(file.modifiedTime), name: file.name, ownedByMe: file.ownedByMe ?? undefined, parents: file.parents ?? [], shared: file.shared ?? undefined, size: file.size ?? undefined, starred: file.starred ?? false, trashed: file.trashed ?? false, version: file.version ?? undefined, webContentLink: file.webContentLink ?? undefined, webViewLink: file.webViewLink ?? undefined, } } /** * Converts a provider permission to the stable public shape. * * @param permission - Provider permission response. * @throws When required permission metadata is absent. */ export function normalizeGoogleDrivePermission( permission: drive_v3.Schema$Permission, ) { if (!permission.id || !permission.type || !permission.role) { throw new Error("Google Drive returned an incomplete permission.") } return GOOGLE_DRIVE_PERMISSION_SCHEMA.parse({ deleted: permission.deleted ?? false, displayName: permission.displayName ?? undefined, domain: permission.domain ?? undefined, emailAddress: permission.emailAddress ?? undefined, expirationTime: parseDate(permission.expirationTime), id: permission.id, pendingOwner: permission.pendingOwner ?? false, role: permission.role, type: permission.type, }) } /** * Converts a provider reply to the stable public shape. * * @param reply - Provider reply response. * @throws When the reply ID is absent. */ export function normalizeGoogleDriveReply(reply: drive_v3.Schema$Reply) { if (!reply.id) throw new Error("Google Drive returned a reply without an ID.") return { author: normalizeGoogleDriveUser(reply.author), content: reply.content ?? undefined, createdAt: parseDate(reply.createdTime), deleted: reply.deleted ?? false, id: reply.id, modifiedAt: parseDate(reply.modifiedTime), } } /** * Converts a provider comment to the stable public shape. * * @param comment - Provider comment response. * @throws When the comment ID is absent. */ export function normalizeGoogleDriveComment(comment: drive_v3.Schema$Comment) { if (!comment.id) { throw new Error("Google Drive returned a comment without an ID.") } return { anchor: comment.anchor ?? undefined, author: normalizeGoogleDriveUser(comment.author), content: comment.content ?? undefined, createdAt: parseDate(comment.createdTime), deleted: comment.deleted ?? false, id: comment.id, modifiedAt: parseDate(comment.modifiedTime), quotedContent: comment.quotedFileContent?.value ?? undefined, replies: (comment.replies ?? []).map(normalizeGoogleDriveReply), resolved: comment.resolved ?? false, } } /** * Converts provider shared-drive metadata to the stable public shape. * * @param value - Provider shared-drive response. * @throws When required shared-drive metadata is absent. */ export function normalizeGoogleSharedDrive(value: drive_v3.Schema$Drive) { if (!value.id || !value.name) { throw new Error("Google Drive returned incomplete shared-drive metadata.") } return { backgroundImageLink: value.backgroundImageLink ?? undefined, colorRgb: value.colorRgb ?? undefined, createdAt: parseDate(value.createdTime), hidden: value.hidden ?? false, id: value.id, name: value.name, orgUnitId: value.orgUnitId ?? undefined, themeId: value.themeId ?? undefined, } } /** * Converts an optional provider user to the embedded public shape. * * @param user - Provider user response. */ function normalizeGoogleDriveUser(user?: drive_v3.Schema$User | null) { if (!user) return undefined return { displayName: user.displayName ?? undefined, emailAddress: user.emailAddress ?? undefined, me: user.me ?? false, photoLink: normalizeGoogleDrivePhotoLink(user.photoLink), } } /** * Converts provider photo links to valid optional absolute URLs. * * @param value - Provider photo link. */ function normalizeGoogleDrivePhotoLink(value?: string | null) { if (!value) return undefined const parsed = z .url() .safeParse(value.startsWith("//") ? `https:${value}` : value) return parsed.success ? parsed.data : undefined } /** * Parses an optional provider timestamp. * * @param value - Provider timestamp. */ function parseDate(value?: string | null) { return value ? new Date(value) : undefined }