Google Docs Integration

Integrating Google Docs with ShipKit for document management

Google Docs Integration

ShipKit integrates with Google Docs API to enable document creation, editing, and management. This guide covers authentication, API usage, and document handling.

Setup

Installation

pnpm add googleapis @google-cloud/local-auth

Configuration

// src/lib/google/config.ts
import { env } from '@/env';

export const GOOGLE_CONFIG = {
  clientId: env.GOOGLE_CLIENT_ID,
  clientSecret: env.GOOGLE_CLIENT_SECRET,
  redirectUri: `${env.NEXT_PUBLIC_APP_URL}/api/auth/callback/google`,
  scopes: [
    'https://www.googleapis.com/auth/documents',
    'https://www.googleapis.com/auth/drive.file',
  ],
} as const;

export const CREDENTIALS_PATH = '.credentials/google-credentials.json';
export const TOKEN_PATH = '.credentials/google-token.json';

Authentication

OAuth2 Setup

// src/lib/google/auth.ts
import { google } from 'googleapis';
import { authenticate } from '@google-cloud/local-auth';
import { GOOGLE_CONFIG, CREDENTIALS_PATH, TOKEN_PATH } from './config';
import { promises as fs } from 'fs';

export async function getAuthClient() {
  let client = await loadSavedCredentialsIfExist();
  if (client) {
    return client;
  }

  client = await authenticate({
    scopes: GOOGLE_CONFIG.scopes,
    keyfilePath: CREDENTIALS_PATH,
  });

  if (client.credentials) {
    await saveCredentials(client);
  }

  return client;
}

async function loadSavedCredentialsIfExist() {
  try {
    const content = await fs.readFile(TOKEN_PATH);
    const credentials = JSON.parse(content.toString());
    return google.auth.fromJSON(credentials);
  } catch (err) {
    return null;
  }
}

async function saveCredentials(client: any) {
  const content = await fs.readFile(CREDENTIALS_PATH);
  const keys = JSON.parse(content.toString());
  const key = keys.installed || keys.web;
  const payload = JSON.stringify({
    type: 'authorized_user',
    client_id: key.client_id,
    client_secret: key.client_secret,
    refresh_token: client.credentials.refresh_token,
  });
  await fs.writeFile(TOKEN_PATH, payload);
}

Service Integration

// src/lib/google/docs.ts
import { google } from 'googleapis';
import { getAuthClient } from './auth';

export class GoogleDocsService {
  private static async getClient() {
    const auth = await getAuthClient();
    return google.docs({ version: 'v1', auth });
  }

  static async createDocument(title: string) {
    const docs = await this.getClient();
    const document = await docs.documents.create({
      requestBody: {
        title,
      },
    });
    return document.data;
  }

  static async getDocument(documentId: string) {
    const docs = await this.getClient();
    const document = await docs.documents.get({
      documentId,
    });
    return document.data;
  }
}

Document Management

Create Documents

// src/app/api/documents/route.ts
import { NextResponse } from 'next/server';
import { GoogleDocsService } from '@/lib/google/docs';

export async function POST(req: Request) {
  try {
    const { title, content } = await req.json();
    const document = await GoogleDocsService.createDocument(title);

    if (content) {
      await GoogleDocsService.updateDocument(document.documentId, content);
    }

    return NextResponse.json(document);
  } catch (error) {
    console.error('Document creation error:', error);
    return NextResponse.json(
      { error: 'Failed to create document' },
      { status: 500 }
    );
  }
}

Document Updates

// src/lib/google/docs.ts
export class GoogleDocsService {
  // ... previous methods

  static async updateDocument(
    documentId: string,
    content: DocumentContent
  ) {
    const docs = await this.getClient();

    const requests = this.buildUpdateRequests(content);

    await docs.documents.batchUpdate({
      documentId,
      requestBody: {
        requests,
      },
    });
  }

  private static buildUpdateRequests(content: DocumentContent) {
    return [
      {
        insertText: {
          location: {
            index: 1,
          },
          text: content.text,
        },
      },
      // Add more formatting requests as needed
    ];
  }
}

Document Sharing

// src/lib/google/drive.ts
import { google } from 'googleapis';
import { getAuthClient } from './auth';

export class GoogleDriveService {
  private static async getClient() {
    const auth = await getAuthClient();
    return google.drive({ version: 'v3', auth });
  }

  static async shareDocument(
    documentId: string,
    email: string,
    role: 'reader' | 'writer' | 'commenter'
  ) {
    const drive = await this.getClient();

    await drive.permissions.create({
      fileId: documentId,
      requestBody: {
        type: 'user',
        role,
        emailAddress: email,
      },
      sendNotificationEmail: true,
    });
  }
}

Real-Time Collaboration

Document Editor Component

// src/components/document-editor.tsx
'use client';

import { useEffect, useState } from 'react';
import { GoogleDocsService } from '@/lib/google/docs';

interface DocumentEditorProps {
  documentId: string;
  onChange?: (content: string) => void;
}

export const DocumentEditor = ({
  documentId,
  onChange,
}: DocumentEditorProps) => {
  const [content, setContent] = useState('');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadDocument() {
      try {
        const doc = await GoogleDocsService.getDocument(documentId);
        setContent(extractContent(doc));
        setLoading(false);
      } catch (error) {
        console.error('Failed to load document:', error);
      }
    }

    loadDocument();
  }, [documentId]);

  const handleChange = async (newContent: string) => {
    setContent(newContent);
    onChange?.(newContent);

    try {
      await GoogleDocsService.updateDocument(documentId, {
        text: newContent,
      });
    } catch (error) {
      console.error('Failed to update document:', error);
    }
  };

  if (loading) {
    return <div>Loading document...</div>;
  }

  return (
    <div className="w-full min-h-[500px] border rounded-lg p-4">
      <textarea
        value={content}
        onChange={(e) => handleChange(e.target.value)}
        className="w-full h-full resize-none focus:outline-none"
      />
    </div>
  );
};

Document Templates

Template Management

// src/lib/google/templates.ts
import { GoogleDocsService } from './docs';

export class TemplateService {
  static async createFromTemplate(
    templateId: string,
    title: string,
    variables: Record<string, string>
  ) {
    // Copy template
    const document = await GoogleDocsService.copyDocument(
      templateId,
      title
    );

    // Replace variables
    const requests = Object.entries(variables).map(([key, value]) => ({
      replaceAllText: {
        containsText: {
          text: `{{${key}}}`,
          matchCase: true,
        },
        replaceText: value,
      },
    }));

    await GoogleDocsService.batchUpdate(document.documentId, requests);
    return document;
  }
}

Document Search

Search Implementation

// src/lib/google/search.ts
import { google } from 'googleapis';
import { getAuthClient } from './auth';

export class DocumentSearchService {
  static async searchDocuments(query: string) {
    const drive = await this.getDriveClient();

    const response = await drive.files.list({
      q: `fullText contains '${query}' and mimeType = 'application/vnd.google-apps.document'`,
      fields: 'files(id, name, createdTime, modifiedTime)',
      orderBy: 'modifiedTime desc',
    });

    return response.data.files;
  }

  private static async getDriveClient() {
    const auth = await getAuthClient();
    return google.drive({ version: 'v3', auth });
  }
}

Export & Import

Document Export

// src/lib/google/export.ts
import { GoogleDocsService } from './docs';

export class DocumentExportService {
  static async exportToPDF(documentId: string) {
    const drive = await this.getDriveClient();

    const response = await drive.files.export({
      fileId: documentId,
      mimeType: 'application/pdf',
    }, {
      responseType: 'stream',
    });

    return response.data;
  }

  static async exportToWord(documentId: string) {
    const drive = await this.getDriveClient();

    const response = await drive.files.export({
      fileId: documentId,
      mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    }, {
      responseType: 'stream',
    });

    return response.data;
  }
}

Best Practices

  1. Authentication

    • Securely store credentials
    • Implement token refresh
    • Handle auth errors
  2. Performance

    • Cache document metadata
    • Batch API requests
    • Implement rate limiting
  3. Error Handling

    • Handle API quotas
    • Retry failed requests
    • Provide user feedback
  4. Security

    • Validate permissions
    • Sanitize content
    • Audit document access

Next Steps