UNPKG

@notionhq/client

Version:

A simple and easy to use client for the Notion API

425 lines 15.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.iteratePaginatedAPI = iteratePaginatedAPI; exports.collectPaginatedAPI = collectPaginatedAPI; exports.iterateDataSourceTemplates = iterateDataSourceTemplates; exports.collectDataSourceTemplates = collectDataSourceTemplates; exports.iterateAllDataSourceRows = iterateAllDataSourceRows; exports.collectAllDataSourceRows = collectAllDataSourceRows; exports.isFullBlock = isFullBlock; exports.isFullPage = isFullPage; exports.isFullDataSource = isFullDataSource; exports.isFullDatabase = isFullDatabase; exports.isFullPageOrDataSource = isFullPageOrDataSource; exports.isFullUser = isFullUser; exports.isFullComment = isFullComment; exports.isFullView = isFullView; exports.isTextRichTextItemResponse = isTextRichTextItemResponse; exports.isEquationRichTextItemResponse = isEquationRichTextItemResponse; exports.isMentionRichTextItemResponse = isMentionRichTextItemResponse; exports.extractNotionId = extractNotionId; exports.extractDatabaseId = extractDatabaseId; exports.extractPageId = extractPageId; exports.extractBlockId = extractBlockId; /** * Returns an async iterator over the results of any paginated Notion API. * * Example (given a notion Client called `notion`): * * ``` * for await (const block of iteratePaginatedAPI(notion.blocks.children.list, { * block_id: parentBlockId, * })) { * // Do something with block. * } * ``` * * @param listFn A bound function on the Notion client that represents a conforming paginated * API. Example: `notion.blocks.children.list`. * @param firstPageArgs Arguments that should be passed to the API on the first and subsequent * calls to the API. Any necessary `next_cursor` will be automatically populated by * this function. Example: `{ block_id: "<my block id>" }` */ async function* iteratePaginatedAPI(listFn, firstPageArgs) { let nextCursor = firstPageArgs.start_cursor; do { const response = await listFn({ ...firstPageArgs, start_cursor: nextCursor, }); yield* response.results; nextCursor = response.next_cursor; } while (nextCursor); } /** * Collect all of the results of paginating an API into an in-memory array. * * Example (given a notion Client called `notion`): * * ``` * const blocks = await collectPaginatedAPI(notion.blocks.children.list, { * block_id: parentBlockId, * }) * // Do something with blocks. * ``` * * @param listFn A bound function on the Notion client that represents a conforming paginated * API. Example: `notion.blocks.children.list`. * @param firstPageArgs Arguments that should be passed to the API on the first and subsequent * calls to the API. Any necessary `next_cursor` will be automatically populated by * this function. Example: `{ block_id: "<my block id>" }` */ async function collectPaginatedAPI(listFn, firstPageArgs) { const results = []; for await (const item of iteratePaginatedAPI(listFn, firstPageArgs)) { results.push(item); } return results; } /** * Returns an async iterator over data source templates. * * Example (given a notion Client called `notion`): * * ``` * for await (const template of iterateDataSourceTemplates(notion, { * data_source_id: dataSourceId, * })) { * console.log(template.name, template.is_default) * } * ``` * * @param client A Notion client instance. * @param args Arguments including the data_source_id and optional start_cursor. */ async function* iterateDataSourceTemplates(client, args) { let nextCursor = args.start_cursor; do { const response = await client.dataSources.listTemplates({ ...args, start_cursor: nextCursor, }); yield* response.templates; nextCursor = response.next_cursor; } while (nextCursor); } /** * Collect all data source templates into an in-memory array. * * Example (given a notion Client called `notion`): * * ``` * const templates = await collectDataSourceTemplates(notion, { * data_source_id: dataSourceId, * }) * // Do something with templates. * ``` * * @param client A Notion client instance. * @param args Arguments including the data_source_id and optional start_cursor. */ async function collectDataSourceTemplates(client, args) { const results = []; for await (const template of iterateDataSourceTemplates(client, args)) { results.push(template); } return results; } /** * Iterate over every row of a data source, including rows past the per-query * result limit that `dataSources.query` enforces on large data sources. * * A single query (one filter and sort) returns at most a fixed number of rows * (10,000 by default). Once that limit is reached, `has_more` becomes `false` * and the response carries `request_status.type === "incomplete"`. Plain * pagination such as {@link iteratePaginatedAPI} stops there and silently * misses the rest of the data source. * * This helper works around the limit by partitioning the data source into * created_time windows. It sorts by created_time ascending; whenever a window * reaches the limit, it starts a fresh query from the last row's created_time. * Each fresh query has a different filter, so it gets its own result budget. * Rows that share a boundary timestamp are de-duplicated by id, so every row is * yielded exactly once. * * created_time is used because it never changes. last_edited_time would shift * rows between windows as they are edited, causing gaps or duplicates. * * Throws if a single created_time value holds more rows than the limit, since * the window cannot be narrowed by time alone. Add a filter in that case so * each window stays under the limit. * * Example (given a notion Client called `notion`): * * ``` * for await (const row of iterateAllDataSourceRows(notion, { * data_source_id: dataSourceId, * })) { * // Do something with row. * } * ``` * * @param client A Notion client instance. * @param args Query arguments. `start_cursor` and `sorts` are managed by the * helper; `filter` is combined with the created_time window bound. */ async function* iterateAllDataSourceRows(client, args) { var _a, _b; const seenRowIds = new Set(); let windowStart = undefined; for (;;) { let limitReached = false; let lastCreatedTime = undefined; let cursor = undefined; do { const response = await client.dataSources.query({ ...args, sorts: [{ timestamp: "created_time", direction: "ascending" }], filter: createdTimeLowerBound(args.filter, windowStart), start_cursor: cursor, }); for (const row of response.results) { // Wiki data sources can return child data-source rows alongside pages. // Both carry created_time and either can be the window's boundary row, // so advance on either; reading only pages would stall a window made up // of data-source rows and throw spuriously. if (isFullPageOrDataSource(row)) { lastCreatedTime = row.created_time; } if (!seenRowIds.has(row.id)) { seenRowIds.add(row.id); yield row; } } if (((_a = response.request_status) === null || _a === void 0 ? void 0 : _a.type) === "incomplete") { limitReached = true; } cursor = (_b = response.next_cursor) !== null && _b !== void 0 ? _b : undefined; } while (cursor); if (!limitReached) { return; } if (lastCreatedTime === undefined || lastCreatedTime === windowStart) { throw new Error("iterateAllDataSourceRows cannot make progress: the per-query result " + "limit was reached but the created_time window could not advance " + `past ${String(lastCreatedTime)}. More rows share this timestamp ` + "than the limit allows. Add a filter to narrow the query."); } windowStart = lastCreatedTime; } } /** * Collect every row of a data source into an in-memory array, including rows * past the per-query result limit. See {@link iterateAllDataSourceRows} for how * the limit is handled. * * Before using this, check that the full data source fits in memory. For very * large data sources, prefer {@link iterateAllDataSourceRows} and process rows * as they stream. * * Example (given a notion Client called `notion`): * * ``` * const rows = await collectAllDataSourceRows(notion, { * data_source_id: dataSourceId, * }) * // Do something with rows. * ``` * * @param client A Notion client instance. * @param args Query arguments. See {@link iterateAllDataSourceRows}. */ async function collectAllDataSourceRows(client, args) { const rows = []; for await (const row of iterateAllDataSourceRows(client, args)) { rows.push(row); } return rows; } /** * Combine a caller-provided filter with the created_time lower bound used to * advance the partition window. Returns the filter unchanged for the first * window, which has no bound yet. */ function createdTimeLowerBound(filter, windowStart) { if (windowStart === undefined) { return filter; } const bound = { timestamp: "created_time", created_time: { on_or_after: windowStart }, }; if (filter === undefined) { return bound; } if ("and" in filter) { return { and: [...filter.and, bound] }; } return { and: [filter, bound] }; } /** * @returns `true` if `response` is a full `BlockObjectResponse`. */ function isFullBlock(response) { return response.object === "block" && "type" in response; } /** * @returns `true` if `response` is a full `PageObjectResponse`. */ function isFullPage(response) { return response.object === "page" && "url" in response; } /** * @returns `true` if `response` is a full `DataSourceObjectResponse`. */ function isFullDataSource(response) { return response.object === "data_source"; } /** * @returns `true` if `response` is a full `DatabaseObjectResponse`. */ function isFullDatabase(response) { return response.object === "database"; } /** * @returns `true` if `response` is a full `DataSourceObjectResponse` or a full * `PageObjectResponse`. * * Can be used on the results of the list response from `queryDataSource` or * `search` APIs. */ function isFullPageOrDataSource(response) { if (response.object === "data_source") { return isFullDataSource(response); } else { return isFullPage(response); } } /** * @returns `true` if `response` is a full `UserObjectResponse`. */ function isFullUser(response) { return "type" in response; } /** * @returns `true` if `response` is a full `CommentObjectResponse`. */ function isFullComment(response) { return "created_by" in response; } /** * @returns `true` if `response` is a full `DataSourceViewObjectResponse`. */ function isFullView(response) { return "type" in response; } /** * @returns `true` if `richText` is a `TextRichTextItemResponse`. */ function isTextRichTextItemResponse(richText) { return richText.type === "text"; } /** * @returns `true` if `richText` is an `EquationRichTextItemResponse`. */ function isEquationRichTextItemResponse(richText) { return richText.type === "equation"; } /** * @returns `true` if `richText` is an `MentionRichTextItemResponse`. */ function isMentionRichTextItemResponse(richText) { return richText.type === "mention"; } /** * Extracts a Notion ID from a Notion URL or returns the input if it's already a valid ID. * * Prioritizes path IDs over query parameters to avoid extracting view IDs instead of database IDs. * * @param urlOrId A Notion URL or ID string * @returns The extracted UUID in standard format (with hyphens) or null if invalid * * @example * ```typescript * // Database URL with view ID - extracts database ID, not view ID * extractNotionId('https://notion.so/workspace/DB-abc123def456789012345678901234ab?v=viewid123') * // Returns: 'abc123de-f456-7890-1234-5678901234ab' (database ID) * * // Already formatted UUID * extractNotionId('12345678-1234-1234-1234-123456789abc') * // Returns: '12345678-1234-1234-1234-123456789abc' * ``` */ function extractNotionId(urlOrId) { if (!urlOrId || typeof urlOrId !== "string") { return null; } const trimmed = urlOrId.trim(); // Check if it's already a properly formatted UUID const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (uuidRegex.test(trimmed)) { return trimmed.toLowerCase(); } // Check if it's a compact UUID (32 chars, no hyphens) const compactUuidRegex = /^[0-9a-f]{32}$/i; if (compactUuidRegex.test(trimmed)) { return formatUuid(trimmed); } // Extract from URL - prioritize path over query parameters // This prevents extracting view IDs when database IDs are in the path const pathMatch = trimmed.match(/\/[^/?#]*-([0-9a-f]{32})(?:[/?#]|$)/i); if (pathMatch && pathMatch[1]) { return formatUuid(pathMatch[1]); } // Fallback to query parameters if no path ID found const queryMatch = trimmed.match(/[?&](?:p|page_id|database_id)=([0-9a-f]{32})/i); if (queryMatch && queryMatch[1]) { return formatUuid(queryMatch[1]); } // Last resort: any 32-char hex string in the URL const anyMatch = trimmed.match(/([0-9a-f]{32})/i); if (anyMatch && anyMatch[1]) { return formatUuid(anyMatch[1]); } return null; } /** * Formats a 32-character hex string into a standard UUID format. * @param compactId 32-character hex string without hyphens * @returns UUID with hyphens in standard format */ function formatUuid(compactId) { const clean = compactId.toLowerCase(); return `${clean.slice(0, 8)}-${clean.slice(8, 12)}-${clean.slice(12, 16)}-${clean.slice(16, 20)}-${clean.slice(20, 32)}`; } /** * Extracts a database ID from a Notion database URL. * Convenience wrapper around `extractNotionId`. */ function extractDatabaseId(databaseUrl) { return extractNotionId(databaseUrl); } /** * Extracts a page ID from a Notion page URL. * Convenience wrapper around `extractNotionId`. */ function extractPageId(pageUrl) { return extractNotionId(pageUrl); } /** * Extracts a block ID from a Notion URL with a block fragment. * Looks for #block-<id> or #<id> patterns. */ function extractBlockId(urlWithBlock) { if (!urlWithBlock || typeof urlWithBlock !== "string") { return null; } // Look for block fragment in URL (#block-32chars or just #32chars) const blockMatch = urlWithBlock.match(/#(?:block-)?([0-9a-f]{32})/i); if (blockMatch && blockMatch[1]) { return formatUuid(blockMatch[1]); } return null; } //# sourceMappingURL=helpers.js.map