UNPKG

@jahands/notion-client

Version:
569 lines 20 kB
import { name as PACKAGE_NAME, version as PACKAGE_VERSION } from '../package.json'; import { appendBlockChildren, completeFileUpload, createComment, createDatabase, createDataSource, createFileUpload, createPage, deleteBlock, getBlock, getComment, getDatabase, getDataSource, getFileUpload, getPage, getPageProperty, getSelf, getUser, listBlockChildren, listComments, listFileUploads, listUsers, oauthIntrospect, oauthRevoke, oauthToken, queryDataSource, search, sendFileUpload, updateBlock, updateDatabase, updateDataSource, updatePage, } from './api-endpoints'; import { buildRequestError, isHTTPResponseError, isNotionClientError } from './errors.js'; import { LogLevel, logLevelSeverity, makeConsoleLogger } from './logging.js'; import { pick } from './utils.js'; export default class Client { #auth; #logLevel; #logger; #prefixUrl; #timeoutMs; #notionVersion; #userAgent; static defaultNotionVersion = '2025-09-03'; constructor(options) { this.#auth = options?.auth; this.#logLevel = options?.logLevel ?? LogLevel.WARN; this.#logger = options?.logger ?? makeConsoleLogger(PACKAGE_NAME); this.#prefixUrl = `${options?.baseUrl ?? 'https://api.notion.com'}/v1/`; this.#timeoutMs = options?.timeoutMs ?? 60_000; this.#notionVersion = options?.notionVersion ?? Client.defaultNotionVersion; this.#userAgent = `notionhq-client/${PACKAGE_VERSION}`; } /** * Sends a request. */ async request(args) { const { path, method, query, body, formDataParams, auth } = args; this.log(LogLevel.INFO, 'request start', { method, path }); // If the body is empty, don't send the body in the HTTP request const bodyAsJsonString = !body || Object.entries(body).length === 0 ? undefined : JSON.stringify(body); const url = new URL(`${this.#prefixUrl}${path}`); if (query) { for (const [key, value] of Object.entries(query)) { if (value !== undefined) { if (Array.isArray(value)) { for (const val of value) { url.searchParams.append(key, decodeURIComponent(val)); } } else { url.searchParams.append(key, String(value)); } } } } // Allow both client ID / client secret based auth as well as token based auth. let authorizationHeader; if (typeof auth === 'object') { // Client ID and secret based auth is **ONLY** supported when using the // `/oauth/token` endpoint. If this is the case, handle formatting the // authorization header as required by `Basic` auth. const unencodedCredential = `${auth.client_id}:${auth.client_secret}`; const encodedCredential = Buffer.from(unencodedCredential).toString('base64'); authorizationHeader = { authorization: `Basic ${encodedCredential}` }; } else { // Otherwise format authorization header as `Bearer` token auth. authorizationHeader = this.authAsHeaders(auth); } const headers = { // Request-level custom additional headers can be provided, but // don't allow them to override all other headers, e.g. the // standard user agent. ...args.headers, ...authorizationHeader, 'Notion-Version': this.#notionVersion, 'user-agent': this.#userAgent, }; if (bodyAsJsonString !== undefined) { headers['content-type'] = 'application/json'; } let formData; if (formDataParams) { delete headers['content-type']; formData = new FormData(); for (const [key, value] of Object.entries(formDataParams)) { if (typeof value === 'string') { formData.append(key, value); } else if (typeof value === 'object') { formData.append(key, typeof value.data === 'object' ? value.data : new Blob([value.data]), value.filename); } } } try { const response = await fetch(url.toString(), { method: method.toUpperCase(), headers, body: bodyAsJsonString ?? formData, signal: AbortSignal.timeout(this.#timeoutMs), }); const responseText = await response.text(); if (!response.ok) { throw buildRequestError(response, responseText); } const responseJson = JSON.parse(responseText); this.log(LogLevel.INFO, 'request success', { method, path, ...('request_id' in responseJson && typeof responseJson.request_id === 'string' && responseJson.request_id ? { requestId: responseJson.request_id } : {}), }); return responseJson; } catch (error) { if (!isNotionClientError(error)) { throw error; } // Log the error if it's one of our known error types this.log(LogLevel.WARN, 'request fail', { code: error.code, message: error.message, ...('request_id' in error && error.request_id ? { requestId: error.request_id } : {}), }); if (isHTTPResponseError(error)) { // The response body may contain sensitive information so it is logged separately at the DEBUG level this.log(LogLevel.DEBUG, 'failed response body', { body: error.body, }); } throw error; } } /* * Notion API endpoints */ blocks = { /** * Retrieve block */ retrieve: (args) => { return this.request({ path: getBlock.path(args), method: getBlock.method, query: pick(args, getBlock.queryParams), body: pick(args, getBlock.bodyParams), auth: args?.auth, }); }, /** * Update block */ update: (args) => { return this.request({ path: updateBlock.path(args), method: updateBlock.method, query: pick(args, updateBlock.queryParams), body: pick(args, updateBlock.bodyParams), auth: args?.auth, }); }, /** * Delete block */ delete: (args) => { return this.request({ path: deleteBlock.path(args), method: deleteBlock.method, query: pick(args, deleteBlock.queryParams), body: pick(args, deleteBlock.bodyParams), auth: args?.auth, }); }, children: { /** * Append block children */ append: (args) => { return this.request({ path: appendBlockChildren.path(args), method: appendBlockChildren.method, query: pick(args, appendBlockChildren.queryParams), body: pick(args, appendBlockChildren.bodyParams), auth: args?.auth, }); }, /** * Retrieve block children */ list: (args) => { return this.request({ path: listBlockChildren.path(args), method: listBlockChildren.method, query: pick(args, listBlockChildren.queryParams), body: pick(args, listBlockChildren.bodyParams), auth: args?.auth, }); }, }, }; databases = { /** * Retrieve a database */ retrieve: (args) => { return this.request({ path: getDatabase.path(args), method: getDatabase.method, query: pick(args, getDatabase.queryParams), body: pick(args, getDatabase.bodyParams), auth: args?.auth, }); }, /** * Create a database */ create: (args) => { return this.request({ path: createDatabase.path(), method: createDatabase.method, query: pick(args, createDatabase.queryParams), body: pick(args, createDatabase.bodyParams), auth: args?.auth, }); }, /** * Update a database */ update: (args) => { return this.request({ path: updateDatabase.path(args), method: updateDatabase.method, query: pick(args, updateDatabase.queryParams), body: pick(args, updateDatabase.bodyParams), auth: args?.auth, }); }, }; dataSources = { /** * Retrieve a data source */ retrieve: (args) => { return this.request({ path: getDataSource.path(args), method: getDataSource.method, query: pick(args, getDataSource.queryParams), body: pick(args, getDataSource.bodyParams), auth: args?.auth, }); }, /** * Query a data source */ query: (args) => { return this.request({ path: queryDataSource.path(args), method: queryDataSource.method, query: pick(args, queryDataSource.queryParams), body: pick(args, queryDataSource.bodyParams), auth: args?.auth, }); }, /** * Create a data source */ create: (args) => { return this.request({ path: createDataSource.path(), method: createDataSource.method, query: pick(args, createDataSource.queryParams), body: pick(args, createDataSource.bodyParams), auth: args?.auth, }); }, /** * Update a data source */ update: (args) => { return this.request({ path: updateDataSource.path(args), method: updateDataSource.method, query: pick(args, updateDataSource.queryParams), body: pick(args, updateDataSource.bodyParams), auth: args?.auth, }); }, }; pages = { /** * Create a page */ create: (args) => { return this.request({ path: createPage.path(), method: createPage.method, query: pick(args, createPage.queryParams), body: pick(args, createPage.bodyParams), auth: args?.auth, }); }, /** * Retrieve a page */ retrieve: (args) => { return this.request({ path: getPage.path(args), method: getPage.method, query: pick(args, getPage.queryParams), body: pick(args, getPage.bodyParams), auth: args?.auth, }); }, /** * Update page properties */ update: (args) => { return this.request({ path: updatePage.path(args), method: updatePage.method, query: pick(args, updatePage.queryParams), body: pick(args, updatePage.bodyParams), auth: args?.auth, }); }, properties: { /** * Retrieve page property */ retrieve: (args) => { return this.request({ path: getPageProperty.path(args), method: getPageProperty.method, query: pick(args, getPageProperty.queryParams), body: pick(args, getPageProperty.bodyParams), auth: args?.auth, }); }, }, }; users = { /** * Retrieve a user */ retrieve: (args) => { return this.request({ path: getUser.path(args), method: getUser.method, query: pick(args, getUser.queryParams), body: pick(args, getUser.bodyParams), auth: args?.auth, }); }, /** * List all users */ list: (args) => { return this.request({ path: listUsers.path(), method: listUsers.method, query: pick(args, listUsers.queryParams), body: pick(args, listUsers.bodyParams), auth: args?.auth, }); }, /** * Get details about bot */ me: (args) => { return this.request({ path: getSelf.path(), method: getSelf.method, query: pick(args, getSelf.queryParams), body: pick(args, getSelf.bodyParams), auth: args?.auth, }); }, }; comments = { /** * Create a comment */ create: (args) => { return this.request({ path: createComment.path(), method: createComment.method, query: pick(args, createComment.queryParams), body: pick(args, createComment.bodyParams), auth: args?.auth, }); }, /** * List comments */ list: (args) => { return this.request({ path: listComments.path(), method: listComments.method, query: pick(args, listComments.queryParams), body: pick(args, listComments.bodyParams), auth: args?.auth, }); }, /** * Retrieve a comment */ retrieve: (args) => { return this.request({ path: getComment.path(args), method: getComment.method, query: pick(args, getComment.queryParams), body: pick(args, getComment.bodyParams), auth: args?.auth, }); }, }; fileUploads = { /** * Create a file upload */ create: (args) => { return this.request({ path: createFileUpload.path(), method: createFileUpload.method, query: pick(args, createFileUpload.queryParams), body: pick(args, createFileUpload.bodyParams), auth: args?.auth, }); }, /** * Retrieve a file upload */ retrieve: (args) => { return this.request({ path: getFileUpload.path(args), method: getFileUpload.method, query: pick(args, getFileUpload.queryParams), auth: args?.auth, }); }, /** * List file uploads */ list: (args) => { return this.request({ path: listFileUploads.path(), method: listFileUploads.method, query: pick(args, listFileUploads.queryParams), auth: args?.auth, }); }, /** * Send a file upload * * Requires a `file_upload_id`, obtained from the `id` of the Create File * Upload API response. * * The `file` parameter contains the raw file contents or Blob/File object * under `file.data`, and an optional `file.filename` string. * * Supply a stringified `part_number` parameter when using file uploads * in multi-part mode. * * This endpoint sends HTTP multipart/form-data instead of JSON parameters. */ send: (args) => { return this.request({ path: sendFileUpload.path(args), method: sendFileUpload.method, query: pick(args, sendFileUpload.queryParams), formDataParams: pick(args, sendFileUpload.formDataParams), auth: args?.auth, }); }, /** * Complete a file upload */ complete: (args) => { return this.request({ path: completeFileUpload.path(args), method: completeFileUpload.method, query: pick(args, completeFileUpload.queryParams), auth: args?.auth, }); }, }; /** * Search */ search = (args) => { return this.request({ path: search.path(), method: search.method, query: pick(args, search.queryParams), body: pick(args, search.bodyParams), auth: args?.auth, }); }; oauth = { /** * Get token */ token: (args) => { return this.request({ path: oauthToken.path(), method: oauthToken.method, query: pick(args, oauthToken.queryParams), body: pick(args, oauthToken.bodyParams), auth: { client_id: args.client_id, client_secret: args.client_secret, }, }); }, /** * Introspect token */ introspect: (args) => { return this.request({ path: oauthIntrospect.path(), method: oauthIntrospect.method, query: pick(args, oauthIntrospect.queryParams), body: pick(args, oauthIntrospect.bodyParams), auth: { client_id: args.client_id, client_secret: args.client_secret, }, }); }, /** * Revoke token */ revoke: (args) => { return this.request({ path: oauthRevoke.path(), method: oauthRevoke.method, query: pick(args, oauthRevoke.queryParams), body: pick(args, oauthRevoke.bodyParams), auth: { client_id: args.client_id, client_secret: args.client_secret, }, }); }, }; /** * Emits a log message to the console. * * @param level The level for this message * @param args Arguments to send to the console */ log(level, message, extraInfo) { if (logLevelSeverity(level) >= logLevelSeverity(this.#logLevel)) { this.#logger(level, message, extraInfo); } } /** * Transforms an API key or access token into a headers object suitable for an HTTP request. * * This method uses the instance's value as the default when the input is undefined. If neither are defined, it returns * an empty object * * @param auth API key or access token * @returns headers key-value object */ authAsHeaders(auth) { const headers = {}; const authHeaderValue = auth ?? this.#auth; if (authHeaderValue !== undefined) { headers['authorization'] = `Bearer ${authHeaderValue}`; } return headers; } } //# sourceMappingURL=Client.js.map