/**
* Sheet Service
*
* Business logic for interacting with FeiShu Sheets (Bitable)
*/
import { SheetClient } from '@/client/sheets/sheet-client.js';
import type { ApiClientConfig, PaginationOptions } from '@/client/types.js';
import { FeiShuApiError } from '@/services/error.js';
import { SheetMapper } from './mappers/sheet-mapper.js';
import type {
BatchCreatedRecordsBO,
CreatedRecordBO,
DeletedRecordBO,
RecordInfoBO,
RecordListBO,
SheetMetadataBO,
TableListBO,
UpdatedRecordBO,
ViewInfoBO,
ViewListBO,
} from './types/index.js';
/**
* Service for interacting with FeiShu Sheets
*/
export class SheetService {
private client: SheetClient;
private mapper: SheetMapper;
/**
* Initialize the Sheet service
*
* @param config - API client configuration
*/
constructor(config: ApiClientConfig) {
this.client = new SheetClient(config);
this.mapper = new SheetMapper();
}
/**
* Get metadata for a Bitable (Sheet)
*
* @param appToken - The ID of the Bitable
* @returns Sheet metadata in a standardized format
* @throws FeiShuApiError if the request fails or returns empty data
*/
async getSheetMetadata(appToken: string): Promise<SheetMetadataBO> {
try {
const response = await this.client.getSheetMeta(appToken);
if (response.code !== 0 || !response.data || !response.data.app) {
throw new FeiShuApiError(
`Failed to get sheet metadata: ${response.msg || 'Unknown error'}`,
response.code,
);
}
return this.mapper.toSheetMetadataBO(response.data.app);
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError('Failed to get sheet metadata');
}
}
/**
* Get tables list for a Bitable (Sheet)
*
* @param appToken - The ID of the Bitable
* @param pagination - Pagination options
* @returns List of tables in a standardized format
* @throws FeiShuApiError if the request fails or returns empty data
*/
async getTablesList(
appToken: string,
pagination?: PaginationOptions,
): Promise<TableListBO> {
try {
const response = await this.client.getTablesList(appToken, pagination);
if (response.code !== 0 || !response.data) {
throw new FeiShuApiError(
`Failed to get tables list: ${response.msg || 'Unknown error'}`,
response.code,
);
}
return this.mapper.toTableListBO(response.data);
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError('Failed to get tables list');
}
}
/**
* Get views list for a table
*
* @param appToken - The ID of the Bitable
* @param tableId - The ID of the table
* @param pagination - Pagination options
* @returns List of views in a standardized format
* @throws FeiShuApiError if the request fails or returns empty data
*/
async getViewsList(
appToken: string,
tableId: string,
pagination?: PaginationOptions,
): Promise<ViewListBO> {
try {
const response = await this.client.getViewsList(
appToken,
tableId,
pagination,
);
if (response.code !== 0 || !response.data) {
throw new FeiShuApiError(
`Failed to get views list: ${response.msg || 'Unknown error'}`,
response.code,
);
}
return this.mapper.toViewListBO(response.data);
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError('Failed to get views list');
}
}
/**
* Get details of a single view
*
* @param appToken - The ID of the Bitable
* @param tableId - The ID of the table
* @param viewId - The ID of the view
* @returns View information in a standardized format
* @throws FeiShuApiError if the request fails or returns empty data
*/
async getView(
appToken: string,
tableId: string,
viewId: string,
): Promise<ViewInfoBO> {
try {
const response = await this.client.getView(appToken, tableId, viewId);
if (response.code !== 0 || !response.data || !response.data.view) {
throw new FeiShuApiError(
`Failed to get view details: ${response.msg || 'Unknown error'}`,
response.code,
);
}
return this.mapper.toViewInfoBO(response.data.view);
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError('Failed to get view details');
}
}
/**
* Get records from a table
*
* @param appToken - The ID of the Bitable
* @param tableId - The ID of the table
* @param options - Record list options
* @returns List of records in a standardized format
* @throws FeiShuApiError if the request fails or returns empty data
*/
async getRecordsList(
appToken: string,
tableId: string,
options: {
viewId?: string;
fieldIds?: string[];
filter?: string;
sort?: string;
pageSize?: number;
pageToken?: string;
} = {},
): Promise<RecordListBO> {
try {
const response = await this.client.getRecordsList(
appToken,
tableId,
options,
);
if (response.code !== 0 || !response.data) {
throw new FeiShuApiError(
`Failed to get records list: ${response.msg || 'Unknown error'}`,
response.code,
);
}
return this.mapper.toRecordListBO(response.data);
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError('Failed to get records list');
}
}
/**
* Get a single record from a table
*
* @param appToken - The ID of the Bitable
* @param tableId - The ID of the table
* @param recordId - The ID of the record
* @param options - Record options
* @returns Record data in a standardized format
* @throws FeiShuApiError if the request fails or returns empty data
*/
async getRecord(
appToken: string,
tableId: string,
recordId: string,
options: { fieldIds?: string[] } = {},
): Promise<RecordInfoBO> {
try {
const response = await this.client.getRecord(
appToken,
tableId,
recordId,
options,
);
if (response.code !== 0 || !response.data || !response.data.record) {
throw new FeiShuApiError(
`Failed to get record: ${response.msg || 'Unknown error'}`,
response.code,
);
}
return this.mapper.toRecordInfoBO(response.data.record);
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError('Failed to get record');
}
}
/**
* Create a new record in a table
*
* @param appToken - The ID of the Bitable
* @param tableId - The ID of the table
* @param fields - Record fields to create
* @returns Created record data
* @throws FeiShuApiError if the request fails
*/
async createRecord(
appToken: string,
tableId: string,
fields: Record<string, unknown>,
): Promise<CreatedRecordBO> {
try {
const response = await this.client.createRecord(appToken, tableId, fields);
if (response.code !== 0 || !response.data?.record) {
throw new FeiShuApiError(
`Failed to create record: ${response.msg || 'Unknown error'}`,
response.code,
);
}
return {
id: response.data.record.record_id,
fields: response.data.record.fields,
};
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError('Failed to create record');
}
}
/**
* Update an existing record in a table
*
* @param appToken - The ID of the Bitable
* @param tableId - The ID of the table
* @param recordId - The ID of the record to update
* @param fields - Record fields to update
* @returns Updated record data
* @throws FeiShuApiError if the request fails
*/
async updateRecord(
appToken: string,
tableId: string,
recordId: string,
fields: Record<string, unknown>,
): Promise<UpdatedRecordBO> {
try {
const response = await this.client.updateRecord(
appToken,
tableId,
recordId,
fields,
);
if (response.code !== 0 || !response.data?.record) {
throw new FeiShuApiError(
`Failed to update record: ${response.msg || 'Unknown error'}`,
response.code,
);
}
return {
id: response.data.record.record_id,
fields: response.data.record.fields,
};
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError('Failed to update record');
}
}
/**
* Delete a record from a table
*
* @param appToken - The ID of the Bitable
* @param tableId - The ID of the table
* @param recordId - The ID of the record to delete
* @returns Delete result
* @throws FeiShuApiError if the request fails
*/
async deleteRecord(
appToken: string,
tableId: string,
recordId: string,
): Promise<DeletedRecordBO> {
try {
const response = await this.client.deleteRecord(
appToken,
tableId,
recordId,
);
if (response.code !== 0) {
throw new FeiShuApiError(
`Failed to delete record: ${response.msg || 'Unknown error'}`,
response.code,
);
}
return {
deleted: response.data?.deleted ?? true,
recordId: response.data?.record_id ?? recordId,
};
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError('Failed to delete record');
}
}
/**
* Batch create records in a table
*
* @param appToken - The ID of the Bitable
* @param tableId - The ID of the table
* @param records - Array of records to create
* @returns Created records data
* @throws FeiShuApiError if the request fails
*/
async batchCreateRecords(
appToken: string,
tableId: string,
records: Array<{ fields: Record<string, unknown> }>,
): Promise<BatchCreatedRecordsBO> {
try {
const response = await this.client.batchCreateRecords(
appToken,
tableId,
records,
);
if (response.code !== 0 || !response.data?.records) {
throw new FeiShuApiError(
`Failed to batch create records: ${response.msg || 'Unknown error'}`,
response.code,
);
}
return {
records: response.data.records.map((record) => ({
id: record.record_id,
fields: record.fields,
})),
};
} catch (error) {
if (error instanceof FeiShuApiError) {
throw error;
}
throw new FeiShuApiError('Failed to batch create records');
}
}
}