@jahands/notion-client
Version:
A simple and easy to use client for the Notion API
569 lines • 20 kB
JavaScript
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