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
Authentication
Securely store credentials
Implement token refresh
Handle auth errors
Performance
Cache document metadata
Batch API requests
Implement rate limiting
Error Handling
Handle API quotas
Retry failed requests
Provide user feedback
Security
Validate permissions
Sanitize content
Audit document access
Next Steps