UNPKG

@mikemajara/notion-cms

Version:

A TypeScript library for using Notion as a headless CMS

511 lines (492 loc) 19.9 kB
import * as _notionhq_client_build_src_api_endpoints from '@notionhq/client/build/src/api-endpoints'; import { BlockObjectResponse, QueryDatabaseParameters, PageObjectResponse, PropertyItemObjectResponse, RichTextItemResponse } from '@notionhq/client/build/src/api-endpoints'; import { Client } from '@notionhq/client'; type DatabaseRecordType = "simple" | "advanced" | "raw"; interface DatabaseRecord { id?: string; [key: string]: any; } /** * Configuration options for NotionCMS file management */ /** * File management strategies */ type FileStrategy = "direct" | "local" | "remote"; /** * Configuration options for debug logging */ interface DebugConfig { /** * Enable/disable all logging * @default false */ enabled?: boolean; /** * Log level - controls what gets logged * @default "info" */ level?: "error" | "warn" | "info" | "debug"; } /** * Unified storage configuration */ interface StorageConfig { /** * Storage path - used for: * - Local strategy: Local directory path (e.g., "./public/notion-files") * - Remote strategy: S3 key prefix (e.g., "uploads/notion/") */ path?: string; /** * S3-compatible endpoint (required for remote strategy) */ endpoint: string; /** * S3 bucket name (required for remote strategy) */ bucket?: string; /** * S3 access key (required for remote strategy) */ accessKey: string; /** * S3 secret key (required for remote strategy) */ secretKey: string; /** * S3 region (optional) */ region?: string; } interface NotionCMSConfig { /** * File handling configuration */ files?: { /** * File handling strategy * - "direct": Link directly to Notion files (default) * - "local": Download and cache files locally * - "remote": Store files in S3-compatible storage */ strategy?: FileStrategy; /** * Storage configuration (used for local and remote strategies) */ storage?: StorageConfig; }; debug?: DebugConfig; } interface FileInfo { name: string; url: string; type?: "external" | "file"; expiry_time?: string; } declare class FileManager { private strategy; constructor(config: NotionCMSConfig); isCacheEnabled(): boolean; processFileUrl(url: string, fileName: string): Promise<string>; processFileInfoArray(files: FileInfo[]): Promise<FileInfo[]>; extractFileUrl(file: any): string; createFileInfo(file: any): FileInfo; } type SortDirection = "ascending" | "descending"; type LogicalOperator = "and" | "or"; type NotionFieldType = "title" | "rich_text" | "number" | "select" | "multi_select" | "date" | "people" | "files" | "checkbox" | "url" | "email" | "phone_number" | "formula" | "relation" | "rollup" | "created_time" | "created_by" | "last_edited_time" | "last_edited_by" | "status" | "unique_id" | "verification" | "unknown"; type OperatorMap = { title: "equals" | "does_not_equal" | "contains" | "does_not_contain" | "starts_with" | "ends_with" | "is_empty" | "is_not_empty"; rich_text: "equals" | "does_not_equal" | "contains" | "does_not_contain" | "starts_with" | "ends_with" | "is_empty" | "is_not_empty"; url: "equals" | "does_not_equal" | "contains" | "does_not_contain" | "starts_with" | "ends_with" | "is_empty" | "is_not_empty"; email: "equals" | "does_not_equal" | "contains" | "does_not_contain" | "starts_with" | "ends_with" | "is_empty" | "is_not_empty"; phone_number: "equals" | "does_not_equal" | "contains" | "does_not_contain" | "starts_with" | "ends_with" | "is_empty" | "is_not_empty"; number: "equals" | "does_not_equal" | "greater_than" | "less_than" | "greater_than_or_equal_to" | "less_than_or_equal_to" | "is_empty" | "is_not_empty"; select: "equals" | "does_not_equal" | "is_empty" | "is_not_empty"; multi_select: "contains" | "does_not_contain" | "is_empty" | "is_not_empty"; status: "equals" | "does_not_equal" | "is_empty" | "is_not_empty"; date: "equals" | "before" | "after" | "on_or_before" | "on_or_after" | "is_empty" | "is_not_empty"; created_time: "equals" | "before" | "after" | "on_or_before" | "on_or_after"; last_edited_time: "equals" | "before" | "after" | "on_or_before" | "on_or_after"; checkbox: "equals"; people: "contains" | "does_not_contain" | "is_empty" | "is_not_empty"; relation: "contains" | "does_not_contain" | "is_empty" | "is_not_empty"; created_by: "contains" | "does_not_contain"; last_edited_by: "contains" | "does_not_contain"; files: "is_empty" | "is_not_empty"; formula: "equals" | "does_not_equal" | "contains" | "does_not_contain" | "greater_than" | "less_than" | "greater_than_or_equal_to" | "less_than_or_equal_to" | "is_empty" | "is_not_empty"; rollup: "equals" | "does_not_equal" | "contains" | "does_not_contain" | "greater_than" | "less_than" | "greater_than_or_equal_to" | "less_than_or_equal_to" | "is_empty" | "is_not_empty"; unique_id: "equals" | "does_not_equal" | "greater_than" | "less_than" | "greater_than_or_equal_to" | "less_than_or_equal_to"; verification: "equals" | "before" | "after" | "on_or_before" | "on_or_after"; unknown: "equals" | "does_not_equal" | "is_empty" | "is_not_empty"; }; declare const OPERATOR_MAP: Record<keyof OperatorMap, readonly string[]>; interface DatabaseFieldMetadata { [fieldName: string]: { type: Exclude<NotionFieldType, "select" | "multi_select">; } | { type: "select"; options: readonly string[]; } | { type: "multi_select"; options: readonly string[]; }; } type FieldTypeFor<K extends keyof M, M extends DatabaseFieldMetadata> = M[K] extends { type: infer T; } ? T : never; type OperatorsFor<K extends keyof M, M extends DatabaseFieldMetadata> = FieldTypeFor<K, M> extends keyof OperatorMap ? OperatorMap[FieldTypeFor<K, M>] : never; type SelectOptionsFor<K extends keyof M, M extends DatabaseFieldMetadata> = M[K] extends { type: "select"; options: readonly (infer U)[]; } ? U : M[K] extends { type: "multi_select"; options: readonly (infer U)[]; } ? U : never; type ValueTypeMap = { title: string; rich_text: string; number: number; select: string; multi_select: string[]; date: Date | string; people: string[]; files: Array<{ name: string; url: string; }>; checkbox: boolean; url: string; email: string; phone_number: string; formula: any; relation: string[]; rollup: any; created_time: Date | string; created_by: string; last_edited_time: Date | string; last_edited_by: string; status: string; unique_id: number; unknown: any; }; type ValueTypeFor<K extends keyof M, M extends DatabaseFieldMetadata, O extends OperatorsFor<K, M> = OperatorsFor<K, M>> = O extends "is_empty" | "is_not_empty" ? any : FieldTypeFor<K, M> extends "select" ? SelectOptionsFor<K, M> : FieldTypeFor<K, M> extends "multi_select" ? SelectOptionsFor<K, M> : FieldTypeFor<K, M> extends "people" | "relation" ? O extends "contains" | "does_not_contain" ? string : ValueTypeMap[FieldTypeFor<K, M>] : FieldTypeFor<K, M> extends keyof ValueTypeMap ? ValueTypeMap[FieldTypeFor<K, M>] : any; interface TypeSafeFilterCondition<K extends keyof M, M extends DatabaseFieldMetadata> { property: K; operator: OperatorsFor<K, M>; value: ValueTypeFor<K, M>; propertyType: FieldTypeFor<K, M>; } interface FilterCondition { property: string; operator: string; value: any; propertyType?: string; } interface QueryResult<T> { results: T[]; hasMore: boolean; nextCursor: string | null; } declare class QueryBuilder<T, M extends DatabaseFieldMetadata = {}> implements PromiseLike<T[] | T | null> { private client; private databaseId; private fieldTypes; private filterConditions; private logicalOperator; private nestedFilters; private sortOptions; private pageLimit; private startCursor?; private singleMode; private fileManager?; private recordType; constructor(client: Client, databaseId: string, fieldTypes?: M, fileManager?: FileManager, recordType?: DatabaseRecordType); filter<K extends keyof M & string, O extends OperatorsFor<K, M>>(property: K, operator: O, value: ValueTypeFor<K, M, O>): QueryBuilder<T, M>; private mapFieldTypeToNotionProperty; private prepareFilterValue; sort(property: keyof M & string, direction?: SortDirection): QueryBuilder<T, M>; limit(limit: number): QueryBuilder<T, M>; startAfter(cursor: string): QueryBuilder<T, M>; single(): QueryBuilder<T, M>; maybeSingle(): QueryBuilder<T, M>; then<TResult1 = T[] | T | null, TResult2 = never>(onfulfilled?: ((value: T[] | T | null) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): PromiseLike<TResult1 | TResult2>; private execute; paginate(pageSize?: number): Promise<QueryResult<T>>; all(): Promise<T[]>; private buildFilter; private mapToNotionOperator; getFieldTypeForFilter(property: keyof M & string): NotionFieldType | undefined; isValidOperatorForField<K extends keyof M>(property: K, operator: string): operator is OperatorsFor<K, M>; private isValidSortField; } type ContentBlockAdvanced = { id: string; type: "paragraph" | "quote"; text: string; text_md: string; children?: ContentBlockAdvanced[]; } | { id: string; type: "heading_1" | "heading_2" | "heading_3"; text: string; text_md: string; } | { id: string; type: "bulleted_list_item" | "numbered_list_item" | "toggle"; text: string; text_md: string; children?: ContentBlockAdvanced[]; } | { id: string; type: "to_do"; checked: boolean; text: string; text_md: string; children?: ContentBlockAdvanced[]; } | { id: string; type: "code"; language: string; text: string; text_md: string; } | { id: string; type: "bookmark" | "embed" | "link_preview"; url: string; caption_text?: string; caption_md?: string; } | { id: string; type: "image" | "video" | "audio" | "file" | "pdf"; url: string; caption_text?: string; caption_md?: string; expiry_time?: string; } | { id: string; type: "equation"; expression: string; } | { id: string; type: "divider"; } | { id: string; type: "table"; hasColumnHeader: boolean; hasRowHeader: boolean; rows: ContentTableRowAdvanced[]; } | { id: string; type: "table_row"; cells: { text: string; text_md: string; }[]; } | { id: string; type: "columns"; children: { type: "column"; children: ContentBlockAdvanced[]; }[]; } | { id: string; type: "column"; children: ContentBlockAdvanced[]; } | { id: string; type: "synced_block"; originalBlockId?: string; children?: ContentBlockAdvanced[]; } | { id: string; type: "child_page"; title?: string; pageId: string; } | { id: string; type: "child_database"; title?: string; databaseId: string; } | { id: string; type: "breadcrumb"; } | { id: string; type: "table_of_contents"; } | { id: string; type: "template"; children?: ContentBlockAdvanced[]; }; type ContentTableRowAdvanced = Extract<ContentBlockAdvanced, { type: "table_row"; }>; type ContentBlockRaw = BlockObjectResponse & { children?: ContentBlockRaw[]; }; interface QueryOptions { filter?: QueryDatabaseParameters["filter"]; sorts?: QueryDatabaseParameters["sorts"]; pageSize?: number; startCursor?: string; } interface RecordOptions { recordType?: DatabaseRecordType; } interface RecordGetOptions { } declare class DatabaseService { private client; private fileManager; constructor(client: Client, fileManager: FileManager); query<T, M extends DatabaseFieldMetadata = {}>(databaseId: string, fieldMetadata?: M, options?: RecordOptions): QueryBuilder<T, M>; getDatabase(databaseId: string, options?: QueryOptions): Promise<{ results: PageObjectResponse[]; nextCursor: string | null; hasMore: boolean; }>; getRecord(pageId: string): Promise<PageObjectResponse>; getRecordRaw(pageId: string, _options?: RecordGetOptions): Promise<PageObjectResponse>; getAllDatabaseRecords(databaseId: string, options?: Omit<QueryOptions, "startCursor" | "pageSize">): Promise<PageObjectResponse[]>; private enrichRecordFiles; private processPageCover; private processPageIcon; private processPropertyFile; private safeProcessUrl; } interface DatabaseRegistry { } type ContentOptions = { recursive?: boolean; mediaUrlResolver?: (block: ContentBlockRaw, field: any) => Promise<string>; }; declare class NotionCMS { private client; private config; private fileManager; private pageContentService; private databaseService; databases: Record<string, { id: string; fields: DatabaseFieldMetadata; }>; constructor(token: string, config?: NotionCMSConfig); query<K extends keyof DatabaseRegistry>(databaseKey: K): QueryBuilder<DatabaseRegistry[K]["record"], DatabaseRegistry[K]["fields"]>; query<K extends keyof DatabaseRegistry, V extends DatabaseRecordType>(databaseKey: K, options: { recordType: V; }): QueryBuilder<V extends "simple" ? DatabaseRegistry[K]["record"] : V extends "advanced" ? DatabaseRegistry[K]["recordAdvanced"] : DatabaseRegistry[K]["recordRaw"], DatabaseRegistry[K]["fields"]>; private _query; getRecordRaw(pageId: string, options?: RecordGetOptions): Promise<_notionhq_client_build_src_api_endpoints.PageObjectResponse>; getPageContentRaw(pageId: string, options?: ContentOptions): Promise<ContentBlockRaw[]>; } declare function registerDatabase(key: string, config: { id: string; fields: DatabaseFieldMetadata; }): void; interface RawMarkdownOptions { listIndent?: string; debug?: boolean; alternateOrderedListStyles?: boolean; } declare function blocksToMarkdown(rawBlocks?: ContentBlockRaw[], opts?: RawMarkdownOptions): string; interface RawHtmlOptions { classPrefix?: string; } declare function blocksToHtml(rawBlocks?: ContentBlockRaw[], opts?: RawHtmlOptions): string; interface RawAdvancedOptions { mediaUrlResolver?: (block: ContentBlockRaw, field: any) => string | Promise<string>; } interface SimpleBlock { id: string; type: string; content: any; children?: SimpleBlock[]; hasChildren: boolean; } interface TableBlockContent { tableWidth: number; hasColumnHeader: boolean; hasRowHeader: boolean; } interface TableRowCell { plainText: string; richText: any[]; } interface TableRowBlockContent { cells: TableRowCell[]; } interface SimpleTableBlock extends SimpleBlock { type: "table"; content: TableBlockContent; children: SimpleTableRowBlock[]; } interface SimpleTableRowBlock extends SimpleBlock { type: "table_row"; content: TableRowBlockContent; } type NotionPropertyType = "title" | "rich_text" | "number" | "select" | "multi_select" | "date" | "people" | "files" | "checkbox" | "url" | "email" | "phone_number" | "formula" | "relation" | "rollup" | "created_time" | "created_by" | "last_edited_time" | "last_edited_by" | "status" | "unique_id"; type NotionProperty = PropertyItemObjectResponse; declare class PageContentService { private client; private fileManager; constructor(client: Client, fileManager: FileManager); getPageContentRaw(pageId: string, recursive?: boolean): Promise<ContentBlockRaw[]>; private getBlocks; private getBlocksRaw; convertBlocksToSimple(blocks: ContentBlockRaw[]): Promise<SimpleBlock[]>; private enrichBlockFiles; } declare function richTextToPlain(rich?: RichTextItemResponse[]): string; declare function richTextToMarkdown(rich?: RichTextItemResponse[]): string; declare function richTextToHtml(rich?: RichTextItemResponse[], classPrefix?: string): string; /** * Block traversal utilities for processing Notion content blocks * * This module provides utilities for traversing and grouping Notion blocks, * particularly for handling list items and nested block structures. * * Key functionality: * - Groups consecutive list items of the same type into logical groups * - Provides depth-aware traversal of nested block structures * - Handles different list types (bulleted and numbered lists) * - Enables efficient processing of hierarchical content structures * * Used by content converters (markdown, HTML) to properly format * lists and maintain correct nesting relationships. */ type ListGroupType = "bulleted_list_item" | "numbered_list_item"; interface ContentListGroup { kind: "list_group"; listType: ListGroupType; items: ContentBlockRaw[]; } type ContentGroupedNode = ContentBlockRaw | ContentListGroup; declare function groupConsecutiveListItems(siblings: ContentBlockRaw[]): ContentGroupedNode[]; type ContentRawNode = { block: ContentBlockRaw; depth: number; }; declare function mapRawBlocksWithDepth(blocks: ContentBlockRaw[], depth?: number): ContentRawNode[]; declare function walkRawBlocks(blocks: ContentBlockRaw[], visitor: (block: ContentBlockRaw, depth: number) => void, depth?: number): void; type ConvertRecordOptions = { fileManager?: FileManager; }; declare function convertRecord(page: PageObjectResponse, recordType: DatabaseRecordType, options?: ConvertRecordOptions): Promise<PageObjectResponse | Record<string, any>>; declare function convertRecordToSimple(page: PageObjectResponse, fileManager?: FileManager): Promise<Record<string, any>>; declare function convertRecordToAdvanced(page: PageObjectResponse, fileManager?: FileManager): Promise<Record<string, any>>; declare function convertRecords(pages: PageObjectResponse[], recordType: DatabaseRecordType, options?: ConvertRecordOptions): Promise<(PageObjectResponse | Record<string, any>)[]>; type ConvertBlockOptions = { fileManager?: FileManager; }; type ConvertBlocksOptions = ConvertBlockOptions; declare function convertBlockToSimple(block: ContentBlockRaw, options?: ConvertBlockOptions): Promise<SimpleBlock>; declare function convertBlocksToSimple(blocks?: ContentBlockRaw[], options?: ConvertBlocksOptions): Promise<SimpleBlock[]>; interface ConvertBlocksToAdvancedOptions { mediaUrlResolver?: RawAdvancedOptions["mediaUrlResolver"]; fileManager?: FileManager; } declare function convertBlocksToAdvanced(blocks?: ContentBlockRaw[], options?: ConvertBlocksToAdvancedOptions): Promise<ContentBlockAdvanced[]>; export { type ContentBlockAdvanced, type ContentBlockRaw, type ContentTableRowAdvanced, type DatabaseFieldMetadata, type DatabaseRecord, type DatabaseRecordType, type DatabaseRegistry, DatabaseService, type FieldTypeFor, type FilterCondition, type LogicalOperator, NotionCMS, type NotionCMSConfig, type NotionFieldType, type NotionProperty, type NotionPropertyType, OPERATOR_MAP, type OperatorMap, type OperatorsFor, PageContentService, QueryBuilder, type QueryResult, type RecordGetOptions, type SelectOptionsFor, type SimpleBlock, type SimpleTableBlock, type SimpleTableRowBlock, type SortDirection, type TableBlockContent, type TableRowBlockContent, type TableRowCell, type TypeSafeFilterCondition, type ValueTypeFor, type ValueTypeMap, blocksToHtml, blocksToMarkdown, convertBlockToSimple, convertBlocksToAdvanced, convertBlocksToSimple, convertRecord, convertRecordToAdvanced, convertRecordToSimple, convertRecords, groupConsecutiveListItems, mapRawBlocksWithDepth, registerDatabase, richTextToHtml, richTextToMarkdown, richTextToPlain, walkRawBlocks };