client.ts•14.5 kB
import {
  AppendBlockChildrenArgs,
  RetrieveBlockArgs,
  RetrieveBlockChildrenArgs,
  DeleteBlockArgs,
  UpdateBlockArgs,
  RetrievePageArgs,
  UpdatePagePropertiesArgs,
  ListAllUsersArgs,
  RetrieveUserArgs,
  CreateDatabaseArgs,
  QueryDatabaseArgs,
  RetrieveDatabaseArgs,
  UpdateDatabaseArgs,
  CreateDatabaseItemArgs,
  CreateCommentArgs,
  RetrieveCommentsArgs,
  SearchArgs,
  CreateWorkflowArgs,
  ImportFromCSVArgs,
  ExportToCSVArgs,
  CreateRecurringTaskArgs,
} from './types';
import { ApiClient } from './utils/api-client';
import { logger } from './utils/logger';
export class NotionClientWrapper {
  private apiClient: ApiClient;
  private customFeatures: Map<string, boolean> = new Map();
  constructor(apiToken: string) {
    // Initialize API client with provided Notion API token
    const baseUrl = 'https://api.notion.com/v1';
    const headers = {
      Authorization: `Bearer ${apiToken}`,
      'Content-Type': 'application/json',
      'Notion-Version': '2022-06-28',
    };
    this.apiClient = new ApiClient(baseUrl, headers);
    // Set which custom features are available
    this.customFeatures.set('workflows', false);
    this.customFeatures.set('csv_import', true);
    this.customFeatures.set('csv_export', true);
    this.customFeatures.set('recurring_tasks', true);
    logger.info('NotionClientWrapper initialized');
  }
  /**
   * Check if a custom feature is available
   */
  public isFeatureAvailable(featureName: string): boolean {
    return this.customFeatures.get(featureName) || false;
  }
  // Blocks
  async appendBlockChildren({
    block_id,
    children,
    after,
  }: AppendBlockChildrenArgs): Promise<any> {
    const body: any = { children };
    if (after) body.after = after;
    const response = await this.apiClient.request({
      method: 'PATCH',
      path: `/blocks/${block_id}/children`,
      body,
    });
    return response.data;
  }
  async retrieveBlock({ block_id }: RetrieveBlockArgs): Promise<any> {
    const response = await this.apiClient.request({
      method: 'GET',
      path: `/blocks/${block_id}`,
    });
    return response.data;
  }
  async retrieveBlockChildren({
    block_id,
    start_cursor,
    page_size,
  }: RetrieveBlockChildrenArgs): Promise<any> {
    const query: Record<string, string> = {};
    if (start_cursor) query.start_cursor = start_cursor;
    if (page_size) query.page_size = page_size.toString();
    const response = await this.apiClient.request({
      method: 'GET',
      path: `/blocks/${block_id}/children`,
      query,
    });
    return response.data;
  }
  async updateBlock({ block_id, properties }: UpdateBlockArgs): Promise<any> {
    const response = await this.apiClient.request({
      method: 'PATCH',
      path: `/blocks/${block_id}`,
      body: properties,
    });
    return response.data;
  }
  async deleteBlock({ block_id }: DeleteBlockArgs): Promise<any> {
    const response = await this.apiClient.request({
      method: 'DELETE',
      path: `/blocks/${block_id}`,
    });
    return response.data;
  }
  // Pages
  async retrievePage({ page_id }: RetrievePageArgs): Promise<any> {
    const response = await this.apiClient.request({
      method: 'GET',
      path: `/pages/${page_id}`,
    });
    return response.data;
  }
  async updatePageProperties({
    page_id,
    properties,
  }: UpdatePagePropertiesArgs): Promise<any> {
    const response = await this.apiClient.request({
      method: 'PATCH',
      path: `/pages/${page_id}`,
      body: { properties },
    });
    return response.data;
  }
  // Users
  async listAllUsers({
    start_cursor,
    page_size,
  }: ListAllUsersArgs = {}): Promise<any> {
    const query: Record<string, string> = {};
    if (start_cursor) query.start_cursor = start_cursor;
    if (page_size) query.page_size = page_size.toString();
    const response = await this.apiClient.request({
      method: 'GET',
      path: '/users',
      query,
    });
    return response.data;
  }
  async retrieveUser({ user_id }: RetrieveUserArgs): Promise<any> {
    const response = await this.apiClient.request({
      method: 'GET',
      path: `/users/${user_id}`,
    });
    return response.data;
  }
  async retrieveBotUser(): Promise<any> {
    const response = await this.apiClient.request({
      method: 'GET',
      path: '/users/me',
    });
    return response.data;
  }
  // Databases
  async createDatabase({
    parent,
    title,
    properties,
  }: CreateDatabaseArgs): Promise<any> {
    const response = await this.apiClient.request({
      method: 'POST',
      path: '/databases',
      body: { parent, title, properties },
    });
    return response.data;
  }
  async queryDatabase({
    database_id,
    filter,
    sorts,
    start_cursor,
    page_size,
  }: QueryDatabaseArgs): Promise<any> {
    const body: any = {};
    if (filter) body.filter = filter;
    if (sorts) body.sorts = sorts;
    if (start_cursor) body.start_cursor = start_cursor;
    if (page_size) body.page_size = page_size;
    const response = await this.apiClient.request({
      method: 'POST',
      path: `/databases/${database_id}/query`,
      body,
    });
    return response.data;
  }
  async retrieveDatabase({ database_id }: RetrieveDatabaseArgs): Promise<any> {
    const response = await this.apiClient.request({
      method: 'GET',
      path: `/databases/${database_id}`,
    });
    return response.data;
  }
  async updateDatabase({
    database_id,
    title,
    description,
    properties,
  }: UpdateDatabaseArgs): Promise<any> {
    const body: any = {};
    if (title) body.title = title;
    if (description) body.description = description;
    if (properties) body.properties = properties;
    const response = await this.apiClient.request({
      method: 'PATCH',
      path: `/databases/${database_id}`,
      body,
    });
    return response.data;
  }
  async createDatabaseItem({
    database_id,
    properties,
  }: CreateDatabaseItemArgs): Promise<any> {
    const response = await this.apiClient.request({
      method: 'POST',
      path: '/pages',
      body: {
        parent: { database_id },
        properties,
      },
    });
    return response.data;
  }
  // Comments
  async createComment({
    parent,
    discussion_id,
    rich_text,
  }: CreateCommentArgs): Promise<any> {
    const body: any = { rich_text };
    if (parent) body.parent = parent;
    if (discussion_id) body.discussion_id = discussion_id;
    const response = await this.apiClient.request({
      method: 'POST',
      path: '/comments',
      body,
    });
    return response.data;
  }
  async retrieveComments({
    block_id,
    start_cursor,
    page_size,
  }: RetrieveCommentsArgs): Promise<any> {
    const query: Record<string, string> = {
      block_id,
    };
    if (start_cursor) query.start_cursor = start_cursor;
    if (page_size) query.page_size = page_size.toString();
    const response = await this.apiClient.request({
      method: 'GET',
      path: '/comments',
      query,
    });
    return response.data;
  }
  // Search
  async search({
    query,
    filter,
    sort,
    start_cursor,
    page_size,
  }: SearchArgs = {}): Promise<any> {
    const body: any = {};
    if (query) body.query = query;
    if (filter) body.filter = filter;
    if (sort) body.sort = sort;
    if (start_cursor) body.start_cursor = start_cursor;
    if (page_size) body.page_size = page_size;
    const response = await this.apiClient.request({
      method: 'POST',
      path: '/search',
      body,
    });
    return response.data;
  }
  // Custom functionality
  async createWorkflow({
    title,
    trigger,
    actions,
  }: CreateWorkflowArgs): Promise<any> {
    if (!this.isFeatureAvailable('workflows')) {
      logger.warn('Workflow feature is not available');
      return {
        object: 'error',
        status: 400,
        code: 'feature_not_available',
        message: 'Workflow creation is not available with current API version.',
      };
    }
    // This is a custom extension - Notion does not have a native workflow API
    // This would need to be implemented using Notion's API in a custom way
    // or integrate with a third-party automation service
    logger.info(`Creating workflow: ${title} with ${actions.length} actions`);
    return {
      success: true,
      message: 'Workflow created (simulated)',
      workflow_id: `wf_${Date.now()}`,
      title,
      trigger,
      actions,
    };
  }
  async importFromCSV({
    database_id,
    csv_content,
    column_mappings,
  }: ImportFromCSVArgs): Promise<any> {
    if (!this.isFeatureAvailable('csv_import')) {
      logger.warn('CSV import feature is not available');
      return {
        object: 'error',
        status: 400,
        code: 'feature_not_available',
        message: 'CSV import is not available with current API version.',
      };
    }
    logger.info(`Importing CSV data to database: ${database_id}`);
    try {
      // Simple CSV parsing (in production, use a proper CSV parser)
      const rows = csv_content.split('\n').map((row) => row.split(','));
      const headers = rows[0];
      const dataRows = rows.slice(1);
      logger.info(
        `Found ${dataRows.length} rows to import with headers: ${headers.join(', ')}`,
      );
      // In a real implementation, we would:
      // 1. Use a proper CSV parser (like PapaParse)
      // 2. Map CSV columns to database properties
      // 3. Create database items in batches
      // For now, we'll create a simulated response
      return {
        object: 'import',
        status: 'success',
        results: {
          total_rows: dataRows.length,
          imported_rows: dataRows.length,
          errors: [],
        },
      };
    } catch (error) {
      logger.error('Error parsing CSV:', { error: (error as Error).message });
      return {
        object: 'error',
        status: 400,
        code: 'csv_parse_error',
        message: `CSV import failed: ${(error as Error).message}`,
      };
    }
  }
  async exportToCSV({ database_id, filter }: ExportToCSVArgs): Promise<any> {
    if (!this.isFeatureAvailable('csv_export')) {
      logger.warn('CSV export feature is not available');
      return {
        object: 'error',
        status: 400,
        code: 'feature_not_available',
        message: 'CSV export is not available with current API version.',
      };
    }
    try {
      logger.info(`Exporting database to CSV: ${database_id}`);
      // First, query the database
      const queryResult = await this.queryDatabase({
        database_id,
        filter,
        page_size: 100, // Max page size
      });
      // Build a sample CSV header
      const properties =
        queryResult.results?.length > 0
          ? Object.keys(queryResult.results[0].properties || {})
          : [];
      // In a real implementation, we would:
      // 1. Handle pagination to get all results
      // 2. Extract properties from all results
      // 3. Convert to proper CSV format
      // For now, we'll create a simulated response
      return {
        object: 'export',
        status: 'success',
        results: {
          total_rows: queryResult.results?.length || 0,
          format: 'csv',
          properties: properties,
          // In a real implementation, we'd include the actual CSV content:
          // csv_content: "header1,header2\nvalue1,value2\n..."
        },
      };
    } catch (error) {
      logger.error('Error exporting to CSV:', {
        error: (error as Error).message,
      });
      return {
        object: 'error',
        status: 400,
        code: 'export_error',
        message: `CSV export failed: ${(error as Error).message}`,
      };
    }
  }
  async createRecurringTask({
    database_id,
    task_properties,
    recurrence_pattern,
  }: CreateRecurringTaskArgs): Promise<any> {
    if (!this.isFeatureAvailable('recurring_tasks')) {
      logger.warn('Recurring tasks feature is not available');
      return {
        object: 'error',
        status: 400,
        code: 'feature_not_available',
        message: 'Recurring tasks are not available with current API version.',
      };
    }
    logger.info(
      `Creating recurring task in database: ${database_id} with frequency: ${recurrence_pattern.frequency}`,
    );
    try {
      // In a real implementation, we would:
      // 1. Validate the database schema includes necessary properties
      // 2. Add recurrence metadata to the task properties
      // 3. Create task and potentially setup scheduled creation for future instances
      // Create initial task with recurrence metadata
      const taskResult = await this.createDatabaseItem({
        database_id,
        properties: {
          ...task_properties,
          // Add recurrence metadata to the task properties
          Recurrence: {
            rich_text: [
              {
                type: 'text',
                text: {
                  content: `${recurrence_pattern.frequency} (every ${recurrence_pattern.interval})`,
                },
              },
            ],
          },
        },
      });
      return {
        object: 'recurring_task',
        id: taskResult.id,
        status: 'success',
        recurrence_pattern,
        next_occurrence: this.calculateNextOccurrence(recurrence_pattern),
      };
    } catch (error) {
      logger.error('Error creating recurring task:', {
        error: (error as Error).message,
      });
      return {
        object: 'error',
        status: 400,
        code: 'task_creation_error',
        message: `Recurring task creation failed: ${(error as Error).message}`,
      };
    }
  }
  /**
   * Helper function to calculate the next occurrence based on recurrence pattern
   */
  private calculateNextOccurrence(
    pattern: CreateRecurringTaskArgs['recurrence_pattern'],
  ): string {
    const now = new Date();
    const nextDate = new Date(now);
    switch (pattern.frequency) {
    case 'daily':
      nextDate.setDate(now.getDate() + pattern.interval);
      break;
    case 'weekly':
      nextDate.setDate(now.getDate() + pattern.interval * 7);
      break;
    case 'monthly':
      nextDate.setMonth(now.getMonth() + pattern.interval);
      break;
    case 'yearly':
      nextDate.setFullYear(now.getFullYear() + pattern.interval);
      break;
    }
    return nextDate.toISOString().split('T')[0];
  }
}