/**
* Document Service
*
* Implements FeiShu Document API operations.
*/
import { DocumentClient } from '@/client/documents/document-client.js';
import type { CreateBlockRequest } from '@/client/documents/types/index.js';
import type { ApiClientConfig } from '@/client/types.js';
import { FeiShuApiError } from '../error.js';
import type {
CreatedBlocksBO,
CreatedDocumentBO,
DocumentContentBO,
DocumentInfoBO,
} from './types/index.js';
/**
* Document service for FeiShu
*/
export class DocumentService {
private client: DocumentClient;
/**
* Create document service
*
* @param config - API client configuration
*/
constructor(config: ApiClientConfig) {
this.client = new DocumentClient(config);
}
/**
* Get document raw content
*
* @param documentId - Document ID
* @returns Document content as string
* @throws FeiShuApiError if API request fails
*/
async getDocumentContent(documentId: string): Promise<string> {
try {
const response = await this.client.getDocumentContent(documentId);
if (response.code !== 0) {
throw new FeiShuApiError(
`Failed to get document content: ${response.msg}`,
response.code,
);
}
if (!response.data?.content) {
throw new FeiShuApiError('Document returned empty content');
}
return response.data.content;
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError(
`Error accessing document: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Get document metadata
*
* @param documentId - Document ID
* @returns Document metadata
* @throws FeiShuApiError if API request fails
*/
async getDocumentInfo(documentId: string): Promise<DocumentInfoBO> {
try {
const response = await this.client.getDocumentInfo(documentId);
if (response.code !== 0) {
throw new FeiShuApiError(
`Failed to get document info: ${response.msg}`,
response.code,
);
}
if (!response.data?.document) {
throw new FeiShuApiError('Document info not found');
}
const doc = response.data.document;
return {
id: doc.document_id || '',
revisionId: doc.revision_id || 0,
title: doc.title || '',
};
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError(
`Error accessing document info: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Create a new document
*
* @param title - Document title
* @param folderToken - Folder token to create document in (optional)
* @returns Created document info
* @throws FeiShuApiError if API request fails
*/
async createDocument(
title?: string,
folderToken?: string,
): Promise<CreatedDocumentBO> {
try {
const response = await this.client.createDocument({ title, folderToken });
if (response.code !== 0) {
throw new FeiShuApiError(
`Failed to create document: ${response.msg}`,
response.code,
);
}
if (!response.data?.document) {
throw new FeiShuApiError('Failed to create document: no document returned');
}
const doc = response.data.document;
return {
documentId: doc.document_id,
revisionId: doc.revision_id,
title: doc.title,
};
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError(
`Error creating document: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Create blocks in a document
*
* @param documentId - Document ID
* @param blockId - Parent block ID (use document_id for root)
* @param children - Array of blocks to create
* @param index - Index to insert blocks at (-1 for end)
* @returns Created blocks info
* @throws FeiShuApiError if API request fails
*/
async createBlocks(
documentId: string,
blockId: string,
children: CreateBlockRequest[],
index = -1,
): Promise<CreatedBlocksBO> {
try {
const response = await this.client.createBlocks(
documentId,
blockId,
children,
index,
);
if (response.code !== 0) {
throw new FeiShuApiError(
`Failed to create blocks: ${response.msg}`,
response.code,
);
}
const blocks = response.data?.children || [];
return {
blocks: blocks.map((block) => ({
blockId: block.block_id || '',
blockType: block.block_type,
})),
};
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError(
`Error creating blocks: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Helper: Create text block request
*/
static createTextBlock(content: string): CreateBlockRequest {
return {
block_type: 2,
text: {
elements: [{ text_run: { content } }],
},
};
}
/**
* Helper: Create heading block request
*/
static createHeadingBlock(
content: string,
level: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 = 1,
): CreateBlockRequest {
const blockTypeMap: Record<number, number> = {
1: 3, 2: 4, 3: 5, 4: 6, 5: 7, 6: 8, 7: 9, 8: 10, 9: 11,
};
const blockType = blockTypeMap[level] as CreateBlockRequest['block_type'];
const headingKey = `heading${level}` as keyof CreateBlockRequest;
return {
block_type: blockType,
[headingKey]: {
elements: [{ text_run: { content } }],
},
} as CreateBlockRequest;
}
/**
* Helper: Create bullet list block request
*/
static createBulletBlock(content: string): CreateBlockRequest {
return {
block_type: 12,
bullet: {
elements: [{ text_run: { content } }],
},
};
}
/**
* Helper: Create ordered list block request
*/
static createOrderedBlock(content: string): CreateBlockRequest {
return {
block_type: 13,
ordered: {
elements: [{ text_run: { content } }],
},
};
}
/**
* Helper: Create divider block request
*/
static createDividerBlock(): CreateBlockRequest {
return {
block_type: 22,
divider: {},
};
}
/**
* Helper: Create code block request
*/
static createCodeBlock(content: string, language = 1): CreateBlockRequest {
return {
block_type: 14,
code: {
elements: [{ text_run: { content } }],
style: { language },
},
};
}
/**
* Helper: Create quote block request
*/
static createQuoteBlock(content: string): CreateBlockRequest {
return {
block_type: 15,
quote: {
elements: [{ text_run: { content } }],
},
};
}
/**
* Helper: Create todo block request
*/
static createTodoBlock(content: string, done = false): CreateBlockRequest {
return {
block_type: 17,
todo: {
elements: [{ text_run: { content } }],
style: { done },
},
};
}
/**
* Set document public sharing settings
*
* @param documentToken - Token of the document
* @param linkShareEntity - Link sharing permission level
* @returns Response
*/
async setPublicSharing(
documentToken: string,
linkShareEntity: 'tenant_readable' | 'tenant_editable' | 'anyone_readable' | 'anyone_editable' | 'closed' = 'anyone_editable',
): Promise<{ success: boolean }> {
try {
const response = await this.client.setPublicSharing(documentToken, {
link_share_entity: linkShareEntity,
external_access_entity: 'open',
security_entity: 'anyone_can_edit',
share_entity: 'anyone',
invite_external: true,
});
if (response.code !== 0) {
throw new FeiShuApiError(
`Failed to set sharing: ${response.msg}`,
response.code,
);
}
return { success: true };
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError(
`Error setting sharing: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Create password for document sharing
*
* @param documentToken - Token of the document
* @returns Response with password
*/
async createSharePassword(documentToken: string): Promise<{ password: string }> {
try {
const response = await this.client.createSharePassword(documentToken);
if (response.code !== 0) {
throw new FeiShuApiError(
`Failed to create password: ${response.msg}`,
response.code,
);
}
return { password: response.data?.password || '' };
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError(
`Error creating password: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Add collaborator to document
*
* @param documentToken - Token of the document
* @param memberId - User ID or open_id of the collaborator
* @param memberType - Type of member
* @param perm - Permission level
* @returns Response
*/
async addCollaborator(
documentToken: string,
memberId: string,
memberType: 'user' | 'chat' | 'department' | 'group' = 'user',
perm: 'view' | 'edit' | 'full_access' = 'edit',
): Promise<{ success: boolean }> {
try {
const response = await this.client.addCollaborator(
documentToken,
memberId,
memberType,
perm,
);
if (response.code !== 0) {
throw new FeiShuApiError(
`Failed to add collaborator: ${response.msg}`,
response.code,
);
}
return { success: true };
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError(
`Error adding collaborator: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}