notion-ts-client
Version:
Generates an easy to use and fully typed API to access and modify the data in Notion Databases
1,357 lines (1,293 loc) • 49.4 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/index.ts
var import_commander = require("commander");
var import_dotenv = __toESM(require("dotenv"), 1);
// package.json
var version = "0.2.18";
// src/cli/log.ts
var import_chalk = __toESM(require("chalk"), 1);
function log(message, ...params) {
console.log(message, ...params);
}
function logSubtle(message, ...params) {
console.log(import_chalk.default.gray(message), ...params);
}
function logWarn(message, ...params) {
console.log(import_chalk.default.magenta(message), ...params);
}
function logSuccess(message, ...params) {
console.log(import_chalk.default.green(message), ...params);
}
function logError(message, ...params) {
console.error(import_chalk.default.red(message), ...params);
}
// src/parsers/custom-config.ts
function createCustomConfigFromNotionDatabases(res, databases) {
const notionDatabases = res.results.filter((r) => r.object === "database");
const dbCustomConfig = Object.entries(databases).reduce((dbCustomConfig2, [dbId, dbConfig]) => {
const { properties } = dbConfig;
const notionDbConfig = notionDatabases.find((db) => db.id === dbId);
const customProperties = Object.entries(properties).reduce((propConfig, [propId, prop]) => {
const notionPropConfig = Object.values(notionDbConfig.properties).find((p) => p.id === propId);
if (!notionPropConfig) {
logWarn(`Property ${prop._name} is missing in Notion database ${dbConfig._name}`);
return propConfig;
}
const addProp = (type, options2, groups) => {
propConfig[propId] = {
type,
name: prop._name,
varName: prop.varName,
options: options2,
groups
};
};
if (notionPropConfig.type === "select") {
addProp(
"select",
notionPropConfig.select.options.map((option) => ({ name: option.name, color: option.color }))
);
}
if (notionPropConfig.type === "multi_select") {
addProp(
"multi_select",
notionPropConfig.multi_select.options.map((option) => ({ name: option.name, color: option.color }))
);
}
if (notionPropConfig.type === "status") {
addProp(
"status",
notionPropConfig.status.options.map((option) => ({ name: option.name, color: option.color })),
notionPropConfig.status.groups.map((group) => ({ name: group.name, optionIds: group.option_ids }))
);
}
return propConfig;
}, {});
dbCustomConfig2[dbId] = customProperties;
return dbCustomConfig2;
}, {});
return dbCustomConfig;
}
// src/parsers/db-config.ts
var import_prompts = require("@inquirer/prompts");
// src/parsers/normalize.ts
var changeCase = __toESM(require("change-case-all"), 1);
var import_slugify = __toESM(require("slugify"), 1);
function normalizeProperty(property, caseType = "camelCase") {
const propertySlug = (0, import_slugify.default)(property, {
strict: true,
trim: true
});
const casedSlug = changeCase[caseType](propertySlug);
if (!(casedSlug == null ? void 0 : casedSlug.length)) {
console.error(`Could not normalize property: ${property}. Got empty string!`);
}
return casedSlug != null ? casedSlug : "";
}
function makeTypeName(varName) {
const typeName = changeCase.pascalCase(varName);
if (!(typeName == null ? void 0 : typeName.length)) {
console.error(`Could make type name from property: ${varName}. Got empty string!`);
}
return typeName;
}
function makeConstVarName(varName) {
const constVarName = changeCase.constantCase(varName);
if (!(constVarName == null ? void 0 : constVarName.length)) {
console.error(`Could make const var name from property: ${varName}. Got empty string!`);
}
return constVarName;
}
function capitalizeVarName(varName) {
const capVarName = varName.charAt(0).toUpperCase() + varName.slice(1);
if (!(capVarName == null ? void 0 : capVarName.length)) {
console.error(`Could make capitalized var name from property: ${varName}. Got empty string!`);
}
return capVarName;
}
// src/parsers/db-config.ts
var DEFAULT_READONLY_PROPERTIES = ["id", "created_time", "last_edited_time", "last_edited_by", "created_by"];
function createConfigFromNotionDatabases(res, config) {
log("Parsing Notion databases into config...");
const databases = res.results.filter((r) => r.object === "database");
const dbConfigObject = databases.reduce((dbConfig, db) => {
var _a, _b;
const { id, title, properties } = db;
const dbName = (_a = title[0]) == null ? void 0 : _a.plain_text;
if (!dbName) {
logWarn(`Could not parse database name for database with id: ${id}`);
return dbConfig;
}
if ((_b = config.ignore) == null ? void 0 : _b.some((ignoredDb) => ignoredDb.id === id)) {
log(`Ignoring database: ${dbName} (${id})`);
return dbConfig;
}
const normalizedDbName = normalizeProperty(dbName);
if (!(normalizedDbName == null ? void 0 : normalizedDbName.length)) {
logError(
`Could not normalize database name for database ${dbName} (id: ${id}).
Please edit the DB name in Notion to make sure it contains at least one english character.`
);
throw new Error(`Could not normalize database name for database ${dbName} (id: ${id})`);
}
try {
dbConfig[id] = {
_name: dbName,
varName: normalizeProperty(dbName),
pathName: normalizeProperty(dbName, "kebabCase"),
properties: remapToConfigProperties(properties)
};
return dbConfig;
} catch (err) {
logError(`Error parsing database ${dbName} with id: ${id}`, err);
throw err;
}
}, {});
return dbConfigObject;
}
function remapToConfigProperties(properties) {
const remappedProperties = Object.values(properties).reduce((acc, property) => {
const { id, name, type } = property;
const varName = normalizeProperty(name);
if (!(varName == null ? void 0 : varName.length)) {
logError(
`Property: ${name} (id: ${id}, type: ${type}) is normalized into an empty string!
Please edit the config file manually to fix the "varName" for this property. Or edit the property name in Notion and make sure it contains at least one english character, then run the command again.`
);
}
return __spreadProps(__spreadValues({}, acc), {
[id]: {
_name: name.replace(/\\/g, "\\\\"),
_type: type,
varName,
readOnly: DEFAULT_READONLY_PROPERTIES.includes(type)
}
});
}, {});
return remappedProperties;
}
function moveDefaultReadOnlyPropertiesToTheEnd(dbConfigs) {
for (const dbConfig of Object.values(dbConfigs)) {
const readOnlyProps = Object.entries(dbConfig.properties).filter(
([, propConfig]) => DEFAULT_READONLY_PROPERTIES.includes(propConfig._type)
);
for (const [propId, propConfig] of readOnlyProps) {
delete dbConfig.properties[propId];
dbConfig.properties[propId] = propConfig;
}
}
}
function confirmNewDatabases(originalConfig, newConfig) {
return __async(this, null, function* () {
var _a, _b;
const resultConfig = {
ignore: JSON.parse(JSON.stringify((_a = originalConfig.ignore) != null ? _a : [])),
databases: {}
};
for (const [dbId, newDbConfig] of Object.entries(newConfig.databases)) {
if (originalConfig == null ? void 0 : originalConfig.databases[dbId]) {
resultConfig.databases[dbId] = newDbConfig;
} else {
if ((_b = originalConfig == null ? void 0 : originalConfig.ignore) == null ? void 0 : _b.some((ignoredDb) => ignoredDb.id === dbId)) {
continue;
}
logWarn(`New database found: ${newDbConfig._name} (${dbId})`);
const isAdd = yield (0, import_prompts.confirm)({
message: `Add the new database "${newDbConfig._name}" (${dbId}) to the config?
`,
transformer: (value) => value ? "Yes" : "No, add to ignore list",
default: true
});
if (isAdd) {
resultConfig.databases[dbId] = newDbConfig;
} else {
if (!resultConfig.ignore) {
resultConfig.ignore = [];
}
resultConfig.ignore.push({
name: newDbConfig._name,
id: dbId
});
}
}
}
return resultConfig;
});
}
// src/parsers/merge-configs.ts
function mergeDatabaseConfigs(originalDbConfigs, updatedDbConfigs) {
return __async(this, null, function* () {
var _a, _b;
const mergedDbConfigs = JSON.parse(JSON.stringify(originalDbConfigs));
const changes = {};
for (const [dbId, updatedDbConfig] of Object.entries(updatedDbConfigs)) {
const dbChange = (value) => {
var _a2, _b2, _c;
return changes[dbId] = {
name: updatedDbConfig._name,
varName: (_a2 = originalDbConfig == null ? void 0 : originalDbConfig.varName) != null ? _a2 : "",
change: value,
properties: (_c = (_b2 = changes[dbId]) == null ? void 0 : _b2.properties) != null ? _c : {}
};
};
const originalDbConfig = originalDbConfigs[dbId];
if (originalDbConfig) {
if (updatedDbConfig._name !== originalDbConfig._name) {
dbChange({ type: "renamed", oldName: originalDbConfig._name, newName: updatedDbConfig._name });
const conf = mergedDbConfigs[dbId];
if (conf) {
conf._name = updatedDbConfig._name;
}
}
for (const [propId, updatedPropConfig] of Object.entries(updatedDbConfig.properties)) {
const originalPropConfig = originalDbConfig.properties[propId];
const propChange = (change) => {
var _a2;
const dbChanges = changes[dbId];
if (dbChanges) {
dbChanges.properties[propId] = { name: updatedPropConfig._name, varName: updatedPropConfig.varName, change };
} else {
changes[dbId] = {
name: updatedDbConfig._name,
varName: (_a2 = originalPropConfig == null ? void 0 : originalPropConfig.varName) != null ? _a2 : "",
change: void 0,
properties: {
[propId]: { name: updatedPropConfig._name, varName: updatedPropConfig.varName, change }
}
};
}
};
if (originalPropConfig) {
if (updatedPropConfig._name !== originalPropConfig._name) {
propChange({ type: "renamed", oldName: originalPropConfig._name, newName: updatedPropConfig._name });
}
if (updatedPropConfig._type !== originalPropConfig._type) {
propChange({ type: "retyped", oldType: originalPropConfig._type, newType: updatedPropConfig._type });
}
const conf = (_a = mergedDbConfigs[dbId]) == null ? void 0 : _a.properties[propId];
if (conf) {
conf._name = updatedPropConfig._name;
conf._type = updatedPropConfig._type;
}
} else {
propChange({ type: "added" });
const conf = mergedDbConfigs[dbId];
if (conf) {
conf.properties[propId] = updatedPropConfig;
}
}
}
} else {
dbChange({ type: "added" });
mergedDbConfigs[dbId] = updatedDbConfig;
}
}
for (const [dbId, originalDbConfig] of Object.entries(originalDbConfigs)) {
const dbChange = (change) => {
var _a2, _b2;
return changes[dbId] = {
name: originalDbConfig._name,
varName: originalDbConfig.varName,
change,
properties: (_b2 = (_a2 = changes[dbId]) == null ? void 0 : _a2.properties) != null ? _b2 : {}
};
};
const updatedDbConfig = updatedDbConfigs[dbId];
if (updatedDbConfig) {
for (const [propId, originalPropConfig] of Object.entries(originalDbConfig.properties)) {
const propChange = (change) => {
const dbChanges = changes[dbId];
if (dbChanges) {
dbChanges.properties[propId] = {
name: originalPropConfig._name,
varName: originalPropConfig.varName,
change
};
} else {
changes[dbId] = {
name: originalDbConfig._name,
varName: originalDbConfig.varName,
change: void 0,
properties: {
[propId]: { name: originalPropConfig._name, varName: originalPropConfig.varName, change }
}
};
}
};
if (updatedDbConfig.properties[propId] === void 0) {
propChange({ type: "removed" });
(_b = mergedDbConfigs[dbId]) == null ? true : delete _b.properties[propId];
}
}
} else {
dbChange({ type: "removed" });
delete mergedDbConfigs[dbId];
}
}
return {
mergedDbConfigs,
changes
};
});
}
// src/cli/generate-clients.ts
var import_chalk2 = __toESM(require("chalk"), 1);
var import_fs = __toESM(require("fs"), 1);
var import_path = __toESM(require("path"), 1);
// src/output/copy-core-files.ts
var fs = __toESM(require("fs"), 1);
var path = __toESM(require("path"), 1);
function copyCoreFiles(opts) {
if (!fs.existsSync(opts.fromPath)) {
throw new Error(`Path "${opts.fromPath}" does not exist`);
}
fs.mkdirSync(opts.toPath, { recursive: true });
const srcFromPath = path.join(opts.fromPath, "src");
const typesFromPath = path.join(opts.fromPath, "types");
const srcToPath = path.join(opts.toPath, "src");
const typesToPath = path.join(opts.toPath, "types");
if (!fs.existsSync(srcToPath)) {
fs.mkdirSync(srcToPath);
fs.copyFileSync(path.join(srcFromPath, "generic-db.ts"), path.join(srcToPath, "generic-db.ts"));
fs.copyFileSync(path.join(srcFromPath, "notion-urls.ts"), path.join(srcToPath, "notion-urls.ts"));
fs.copyFileSync(path.join(srcFromPath, "p-throttle.ts"), path.join(srcToPath, "p-throttle.ts"));
fs.copyFileSync(path.join(srcFromPath, "redis-lock.ts"), path.join(srcToPath, "redis-lock.ts"));
}
if (!fs.existsSync(typesToPath)) {
fs.mkdirSync(typesToPath);
fs.copyFileSync(path.join(typesFromPath, "helper.types.ts"), path.join(typesToPath, "helper.types.ts"));
fs.copyFileSync(path.join(typesFromPath, "notion-api.types.ts"), path.join(typesToPath, "notion-api.types.ts"));
}
}
// src/output/file-utils.ts
var fs2 = __toESM(require("fs"), 1);
function saveContentToFile(content, path3, fileName) {
fs2.mkdirSync(path3, { recursive: true });
fs2.writeFileSync(`${path3}/${fileName}`, content);
}
// src/output/generate/constants-file.ts
function createConstantsFile(opts) {
const { dbPath, fileName, dbVarName, propsConfig, customPropsConfig } = opts;
const dbConstVarName = makeConstVarName(dbVarName);
const dbTypeName = makeTypeName(dbVarName);
const propsWithValues = getPropsWithValues(customPropsConfig);
let content = `export const ${dbConstVarName}_PROP_VALUES = ${propsWithValues}`;
const propsToIds = getPropsToIds(propsConfig);
const idsToProps = getIdsToProps(propsConfig);
const propsToTypes = getPropsToTypes(propsConfig);
content += `
export const ${dbConstVarName}_PROPS_TO_IDS = ${JSON.stringify(propsToIds, null, 2)} as const`;
content += `
export const ${dbConstVarName}_IDS_TO_PROPS = ${JSON.stringify(idsToProps, null, 2)} as const`;
content += `
export const ${dbConstVarName}_PROPS_TO_TYPES = ${JSON.stringify(propsToTypes, null, 2)} as const`;
content += `
export type ${dbTypeName}DTOProperties = keyof typeof ${dbConstVarName}_PROPS_TO_IDS
`;
saveContentToFile(content, dbPath, fileName);
}
function getPropsWithValues(customPropsConfig) {
if (!customPropsConfig) {
throw new Error("customPropsConfig is required");
}
let content = "{";
for (const propConfig of Object.values(customPropsConfig)) {
const arrayContent = JSON.stringify(
propConfig.options.map((o) => o.name),
null,
2
);
content += `
"${propConfig.varName}": ${arrayContent} as const,`;
}
return content + "\n}\n";
}
function getIdsToProps(propsConfig) {
return Object.entries(propsConfig).reduce(
(acc, [propId, propConfig]) => {
acc[propId] = propConfig.varName;
return acc;
},
{}
);
}
function getPropsToIds(propsConfig) {
return Object.entries(propsConfig).reduce(
(acc, [propId, propConfig]) => {
acc[propConfig.varName] = propId;
return acc;
},
{}
);
}
function getPropsToTypes(propsConfig) {
return Object.values(propsConfig).reduce(
(acc, propConfig) => {
acc[propConfig.varName] = propConfig._type;
return acc;
},
{}
);
}
// src/output/core/src/notion-urls.ts
var normId = (id) => id.replace(/-/g, "");
// src/output/generate/db-file.ts
function createDBFile(opts) {
const { dbPath, fileName, dbTypeName, dbId } = opts;
const constVarName = makeConstVarName(dbTypeName);
const content = `import { ${dbTypeName}Response, ${dbTypeName}Query, ${dbTypeName}QueryResponse } from './types'
import { ${dbTypeName}PatchDTO } from './patch.dto'
import { GenericDatabaseClass, DatabaseOptions } from '../../core/src/generic-db'
import { ${constVarName}_PROPS_TO_TYPES, ${constVarName}_PROPS_TO_IDS, ${dbTypeName}DTOProperties } from './constants'
export class ${dbTypeName}Database extends GenericDatabaseClass<
${dbTypeName}Response,
${dbTypeName}PatchDTO,
${dbTypeName}Query,
${dbTypeName}QueryResponse,
${dbTypeName}DTOProperties
> {
protected notionDatabaseId: string
constructor(options: DatabaseOptions) {
super(options)
this.notionDatabaseId = '${normId(dbId)}'
}
protected queryRemapFilter(filter?: Record<string, unknown>) {
if (!filter) {
return undefined
}
const notionFilter = {} as Record<string, unknown>
Object.entries(filter).forEach(([key, value]) => {
if (key === 'and' || key === 'or') {
if (Array.isArray(value)) {
notionFilter[key] = value.map((v) => this.queryRemapFilter(v))
} else {
throw new Error(\`${dbTypeName}: Invalid filter value for \${key}: \${value}\`)
}
} else {
if (!(key in ${constVarName}_PROPS_TO_TYPES)) {
throw new Error(\`${dbTypeName}: Invalid filter key: \${key}\`)
}
const propType = ${constVarName}_PROPS_TO_TYPES[key as keyof typeof ${constVarName}_PROPS_TO_TYPES];
const propId = ${constVarName}_PROPS_TO_IDS[key as keyof typeof ${constVarName}_PROPS_TO_IDS];
notionFilter['property'] = propId
notionFilter[propType] = value
}
})
return notionFilter
}
protected queryRemapSorts(sorts?: Record<string, string>[]) {
return sorts?.map((sort) => {
if ('property' in sort) {
return {
property: ${constVarName}_PROPS_TO_IDS[sort.property as keyof typeof ${constVarName}_PROPS_TO_IDS],
direction: sort.direction,
}
}
return sort
})
}
protected queryRemapFilterProperties(filterProps?: string[]) {
return filterProps?.map((p) => ${constVarName}_PROPS_TO_IDS[p as keyof typeof ${constVarName}_PROPS_TO_IDS])
}
}
`;
saveContentToFile(content, dbPath, fileName);
}
// src/output/generate/index-file.ts
function createIndexFile(opts) {
const content = `export * from './constants'
export * from './db'
export * from './patch.dto'
export * from './response.dto'
export * from './types'
`;
saveContentToFile(content, opts.dbPath, opts.fileName);
}
// src/output/generate/patch-dto-file.ts
var TYPE_INDENT = " ";
var PATCH_IGNORE_PROPS = ["created_by", "created_time", "last_edited_by", "last_edited_time"];
function createPatchDTOFile(opts) {
const imports = getDTOFileImports(opts.dbTypeName, opts.propsConfig);
const type = getDTOFileType(opts.dbTypeName, opts.propsConfig);
const code = getDTOFileCode(opts.propsConfig);
const content = `${imports}
type TypeFromRecord<Obj, Type> = Obj extends Record<string, infer T> ? Extract<T, Type> : never
export type ${opts.dbTypeName}PropertiesPatch = {
${type}}
export class ${opts.dbTypeName}PatchDTO {
__data: UpdatePageBodyParameters
constructor(opts: {
properties?: ${opts.dbTypeName}PropertiesPatch
coverUrl?: string
icon?: UpdatePageBodyParameters['icon']
archived?: UpdatePageBodyParameters['archived']
}) {
const { properties: props, coverUrl, icon, archived } = opts
this.__data = {}
this.__data.properties = {}
this.__data.cover = coverUrl ? { type: 'external', external: { url: coverUrl } } : undefined
this.__data.icon = icon
this.__data.archived = archived
${code} }
}
`;
saveContentToFile(content, opts.dbPath, opts.fileName);
}
function getDTOFileImports(dbTypeName, propsConfig) {
const imports = Object.values(propsConfig).map((prop) => getImportType(prop._type)).filter((i) => i !== void 0);
const uniqueImports = Array.from(new Set(imports)).sort();
return `import { ${dbTypeName}Response } from "./types"
import { UpdatePageBodyParameters,
` + uniqueImports.join(",\n") + `
} from '../../core/types/notion-api.types'`;
}
function getImportType(type) {
switch (type) {
case "title":
case "rich_text":
return "RichTextItemRequest";
}
}
function getDTOFileType(dbTypeName, dbPropsConfig) {
const content = Object.values(dbPropsConfig).reduce((acc, propConfig) => {
let typeValue;
if (PATCH_IGNORE_PROPS.includes(propConfig._type) || propConfig.readOnly || ["button", "rollup"].includes(propConfig._type)) {
return acc;
}
if (propConfig._type === "multi_select") {
typeValue = `${dbTypeName}Response['properties']['${propConfig._name}']['multi_select'][number]['name'][]`;
} else if (propConfig._type === "select") {
typeValue = `${dbTypeName}Response['properties']['${propConfig._name}']['select']['name']`;
} else if (["rich_text", "title"].includes(propConfig._type)) {
typeValue = `string | { text: string; url?: string; annotations?: RichTextItemRequest['annotations'] } | RichTextItemRequest[]`;
} else {
typeValue = `TypeFromRecord<UpdatePageBodyParameters['properties'], { type?: '${propConfig._type}' }>['${propConfig._type}']`;
}
acc += `${TYPE_INDENT}${propConfig.varName}?: ${typeValue}
`;
return acc;
}, "");
return content;
}
function getDTOFileCode(dbPropsConfig) {
const content = Object.entries(dbPropsConfig).reduce((acc, [propId, propConfig]) => {
let objValue;
if (PATCH_IGNORE_PROPS.includes(propConfig._type) || propConfig.readOnly || ["button", "rollup"].includes(propConfig._type)) {
return acc;
}
if (propConfig._type === "multi_select") {
objValue = `
type: 'multi_select',
multi_select: props.${propConfig.varName}?.map((item) => ({ name: item })),`;
} else if (propConfig._type === "select") {
objValue = `
type: 'select',
select: { name: props.${propConfig.varName} },`;
} else if (["rich_text", "title"].includes(propConfig._type)) {
const propsVar = `props.${propConfig.varName}`;
objValue = `
type: '${propConfig._type}',
${propConfig._type}: typeof ${propsVar} === 'string'
? [{ type: 'text', text: { content: ${propsVar} } }]
: Array.isArray(${propsVar})
? ${propsVar}
: ${propsVar} === null
? []
: [
{
type: 'text',
text: {
content: ${propsVar}.text,
link: ${propsVar}?.url ? { url: ${propsVar}.url } : undefined
},
annotations: ${propsVar}.annotations
},
]`;
} else {
objValue = `
type: '${propConfig._type}',
${propConfig._type}: props.${propConfig.varName},`;
}
acc += `
if (props?.${propConfig.varName} !== undefined) {
this.__data.properties['${propId}'] = {${objValue}
}
}
`;
return acc;
}, "");
return content;
}
// src/output/generate/response-dto-file.ts
function createResponseDTOFile(opts) {
const { dbPath, fileName, dbTypeName, propsConfig } = opts;
const imports = getDTOFileImports2(dbTypeName);
const constructorCode = getDTOConstructorFileCode(propsConfig);
const code = getDTOFileCode2(propsConfig);
const content = `${imports}
export class ${dbTypeName}ResponseDTO {
__data: ${dbTypeName}Response
id: ${dbTypeName}Response['id']
title: ${dbTypeName}Response['title']
description: ${dbTypeName}Response['description']
parent: ${dbTypeName}Response['parent']
createdBy: ${dbTypeName}Response['created_by']
lastEditedBy: ${dbTypeName}Response['last_edited_by']
createdTime: ${dbTypeName}Response['created_time']
lastEditedTime: ${dbTypeName}Response['last_edited_time']
isInline: ${dbTypeName}Response['is_inline']
archived: ${dbTypeName}Response['archived']
url: ${dbTypeName}Response['url']
publicUrl: ${dbTypeName}Response['public_url']
properties: ${dbTypeName}PropertiesResponseDTO
constructor(res: ${dbTypeName}Response) {
this.__data = res
this.id = res.id
this.title = res.title
this.description = res.description
this.parent = res.parent
this.createdBy = res.created_by
this.lastEditedBy = res.last_edited_by
this.createdTime = res.created_time
this.lastEditedTime = res.last_edited_time
this.isInline = res.is_inline
this.archived = res.archived
this.url = res.url
this.publicUrl = res.public_url
this.properties = new ${dbTypeName}PropertiesResponseDTO(res.properties)
}
get cover() {
return {
type: this.__data.cover?.type,
url: this.__data.cover?.type === 'external' ? this.__data.cover?.external?.url : this.__data.cover?.file?.url,
}
}
get icon() {
return {
type: this.__data.icon?.type,
url:
this.__data.icon?.type === 'external'
? this.__data.icon?.external?.url
: this.__data.icon?.type === 'file'
? this.__data.icon?.file?.url
: undefined,
emoji: this.__data.icon?.type === 'emoji' ? this.__data.icon?.emoji : undefined,
}
}
}
export class ${dbTypeName}PropertiesResponseDTO {
__props: ${dbTypeName}Response['properties']
__data
constructor(props: ${dbTypeName}Response['properties']) {
this.__props = props
${constructorCode}
}
${code}
}
`;
saveContentToFile(content, dbPath, fileName);
}
function getDTOFileImports2(dbTypeName) {
return `import { ${dbTypeName}Response } from "./types"`;
}
function getDTOConstructorFileCode(dbPropsConfig) {
const content = Object.values(dbPropsConfig).reduce((acc, propConfig) => {
if (propConfig._type === "button") {
return acc;
}
return acc + ` ${propConfig.varName}: this.__props['${propConfig._name}'],
`;
}, "");
return ` this.__data = {
${content} }`;
}
function getDTOFileCode2(dbPropsConfig) {
const content = Object.values(dbPropsConfig).reduce((acc, propConfig) => {
if (propConfig._type === "button") {
return acc;
}
if (propConfig._type === "rich_text" || propConfig._type === "title") {
acc += `
get ${propConfig.varName}() {
return {
text: this.__props['${propConfig._name}']?.${propConfig._type} ? this.__props['${propConfig._name}'].${propConfig._type}.reduce((acc, item) => acc + item.plain_text, '') : undefined,
links: this.__props['${propConfig._name}']?.${propConfig._type} ? this.__props['${propConfig._name}'].${propConfig._type}.filter((item) => item.href?.length).map((item) => item.href) : [],
${propConfig._type}: this.__props['${propConfig._name}']?.${propConfig._type},
}
}`;
} else if (propConfig._type === "multi_select") {
acc += `
get ${propConfig.varName}() {
return {
values: this.__props['${propConfig._name}']?.${propConfig._type} ? this.__props['${propConfig._name}'].${propConfig._type}.map((item) => item.name) : [],
${propConfig._type}: this.__props['${propConfig._name}']?.${propConfig._type},
}
}`;
} else if (propConfig._type === "files") {
acc += `
get ${propConfig.varName}() {
return {
urls: this.__props['${propConfig._name}'].files.map((item) =>
item.type === 'external' ? item.external.url : item.type === 'file' ? item.file.url : undefined
),
}
}
`;
} else if (propConfig._type === "relation") {
acc += `
get ${propConfig.varName}Ids() {
return (this.__props['${propConfig._name}']?.relation as unknown as Array<{ id: string }>).map((item) => item.id)
}
`;
} else {
acc += `
get ${propConfig.varName}() {
return this.__props['${propConfig._name}']?.${propConfig._type}
}`;
}
return acc;
}, "");
return content;
}
// src/output/generate/query-types.ts
function getQueryTypes(dbTypeName, propsConfig) {
const constVarName = makeConstVarName(dbTypeName);
const customFilterTypes = getCustomFilterTypes(dbTypeName, propsConfig);
const customPropFilterType = getDBCustomFilterType(dbTypeName, propsConfig);
return `${customFilterTypes}
${customPropFilterType}
export type ${dbTypeName}Query = Omit<QueryDatabaseBodyParameters, 'filter' | 'sorts'> & {
sorts?: Array<
| {
property: keyof typeof ${constVarName}_PROPS_TO_IDS
direction: 'ascending' | 'descending'
}
| {
timestamp: 'created_time' | 'last_edited_time'
direction: 'ascending' | 'descending'
}
>
filter?:
| {
or: Array<
| ${dbTypeName}PropertyFilter
| TimestampCreatedTimeFilter
| TimestampLastEditedTimeFilter
| {
// or: ${dbTypeName}Query['filter']
or: Array<${dbTypeName}PropertyFilter>
}
| {
// and: ${dbTypeName}Query['filter']
and: Array<${dbTypeName}PropertyFilter>
}
>
}
| {
and: Array<
| ${dbTypeName}PropertyFilter
| TimestampCreatedTimeFilter
| TimestampLastEditedTimeFilter
| {
// or: ${dbTypeName}Query['filter']
or: Array<${dbTypeName}PropertyFilter>
}
| {
// and: ${dbTypeName}Query['filter']
and: Array<${dbTypeName}PropertyFilter>
}
>
}
| ${dbTypeName}PropertyFilter
| TimestampCreatedTimeFilter
| TimestampLastEditedTimeFilter
}
export type ${dbTypeName}QueryFilter = ${dbTypeName}Query['filter']
export type ${dbTypeName}QueryResponse = {
results: ${dbTypeName}Response[]
next_cursor: string | null
has_more: boolean
}
`;
}
function getDBCustomFilterType(dbTypeName, propsConfig) {
const unionTypes = Object.values(propsConfig).map((prop) => {
if (prop._type === "button") {
return;
}
const typePrefix = `${dbTypeName}${makeTypeName(prop.varName)}`;
return `{ ${prop.varName}: ${typePrefix}PropertyFilter }`;
}).filter((t) => t !== void 0);
return `export type ${dbTypeName}PropertyFilter = ${unionTypes.join(" | ")}`;
}
function getCustomFilterTypes(dbTypeName, propsConfig) {
return Object.values(propsConfig).map((prop) => {
const typePrefix = `${dbTypeName}${makeTypeName(prop.varName.replace(/_/g, " "))}`;
switch (prop._type) {
case "status":
case "select":
let exportStr;
if (prop._name === "Created by") {
exportStr = `export type ${typePrefix}PropertyType = NonNullable<${dbTypeName}Response['properties']['${prop._name}']['${prop._type}']>['name']`;
} else {
exportStr = `export type ${typePrefix}PropertyType = ${dbTypeName}Response['properties']['${prop._name}']['${prop._type}']['name']`;
}
return `
${exportStr}
type ${typePrefix}PropertyFilter =
| {
equals: ${typePrefix}PropertyType
}
| {
does_not_equal: ${typePrefix}PropertyType
}
| ExistencePropertyFilter
`;
case "multi_select":
return `
export type ${typePrefix}PropertyType = ${dbTypeName}Response['properties']['${prop._name}']['multi_select'][number]['name']
type ${typePrefix}PropertyFilter =
| {
contains: ${typePrefix}PropertyType
}
| {
does_not_contain: ${typePrefix}PropertyType
}
| ExistencePropertyFilter
`;
case "title":
case "rich_text":
case "url":
case "email":
case "phone_number":
return `type ${typePrefix}PropertyFilter = TextPropertyFilter`;
case "created_by":
case "last_edited_by":
return `type ${typePrefix}PropertyFilter = PeoplePropertyFilter`;
case "created_time":
case "last_edited_time":
return `type ${typePrefix}PropertyFilter = DatePropertyFilter`;
case "files":
return `type ${typePrefix}PropertyFilter = ExistencePropertyFilter`;
case "unique_id":
return `type ${typePrefix}PropertyFilter = NumberPropertyFilter`;
case "button":
return;
default:
return `type ${typePrefix}PropertyFilter = ${capitalizeVarName(prop._type)}PropertyFilter`;
}
}).filter((t) => t !== void 0).join("\n");
}
function getQueryFilterTypeImports(propsConfig) {
const imports = Object.values(propsConfig).map((prop) => getQueryImportType(prop._type)).filter((i) => i !== void 0);
const uniqueImports = Array.from(new Set(imports)).sort();
return `ExistencePropertyFilter,
QueryDatabaseBodyParameters,
TimestampCreatedTimeFilter,
TimestampLastEditedTimeFilter,
` + uniqueImports.join(",\n");
}
function getQueryImportType(type) {
switch (type) {
case "select":
case "multi_select":
case "button":
case "files":
return;
case "title":
case "rich_text":
case "url":
case "email":
case "phone_number":
return `TextPropertyFilter`;
case "created_by":
case "last_edited_by":
return `PeoplePropertyFilter`;
case "created_time":
case "last_edited_time":
return `DatePropertyFilter`;
case "unique_id":
return `NumberPropertyFilter`;
case "status":
return;
default:
return `${capitalizeVarName(type)}PropertyFilter`;
}
}
// src/output/generate/types-file.ts
var PROPERTIES_INDENT = " ";
function createTypesFile(opts) {
const { dbPath, fileName, dbTypeName, propsConfig, customPropsConfig } = opts;
const imports = getTypesFileImports(dbTypeName, propsConfig);
const properties = getTypesFileProperties(propsConfig, customPropsConfig);
const queryTypes = getQueryTypes(dbTypeName, propsConfig);
const content = `import { WithOptional, Join, PathsToStringProps } from '../../core/types/helper.types'
${imports}
export interface ${dbTypeName}Response extends WithOptional<Omit<DatabaseObjectResponse, 'properties'>, 'title'| 'description'| 'is_inline'| 'url'| 'public_url'> {
properties: {
${properties}
}
}
export type ${dbTypeName}ResponseProperties = keyof ${dbTypeName}Response['properties']
export type ${dbTypeName}Path = Join<PathsToStringProps<${dbTypeName}Response>>
${queryTypes}
`;
saveContentToFile(content, dbPath, fileName);
}
function getTypesFileImports(dbTypeName, propsConfig) {
const constVarName = makeConstVarName(dbTypeName);
const imports = Object.values(propsConfig).map((prop) => getImportType2(prop._type, prop._name)).filter((p) => p !== void 0);
const uniqueImports = Array.from(new Set(imports)).sort();
return `import {
DatabaseObjectResponse,
StringRequest,
` + uniqueImports.join(",\n") + ",\n" + getQueryFilterTypeImports(propsConfig) + `
} from '../../core/types/notion-api.types'
import { ${constVarName}_PROPS_TO_IDS } from './constants'`;
}
function getImportType2(type, name) {
switch (type) {
case "number":
return "NumberPropertyItemObjectResponse";
case "url":
return "UrlPropertyItemObjectResponse";
case "select":
return "SelectPropertyItemObjectResponse";
case "multi_select":
return "MultiSelectPropertyItemObjectResponse";
case "status":
return "StatusPropertyItemObjectResponse";
case "date":
return "DatePropertyItemObjectResponse";
case "email":
return "EmailPropertyItemObjectResponse";
case "phone_number":
return "PhoneNumberPropertyItemObjectResponse";
case "checkbox":
return "CheckboxPropertyItemObjectResponse";
case "files":
return "FilesPropertyItemObjectResponse";
case "created_by":
return "CreatedByPropertyItemObjectResponse";
case "created_time":
return "CreatedTimePropertyItemObjectResponse";
case "last_edited_by":
return "LastEditedByPropertyItemObjectResponse";
case "last_edited_time":
return "LastEditedTimePropertyItemObjectResponse";
case "formula":
return "FormulaPropertyItemObjectResponse";
case "unique_id":
return "UniqueIdPropertyItemObjectResponse";
case "verification":
return "VerificationPropertyItemObjectResponse";
case "title":
return "TitlePropertyItemObjectResponse";
case "rich_text":
return "RichTextPropertyItemObjectResponse";
case "people":
return "PeoplePropertyItemObjectResponse";
case "relation":
return "RelationPropertyItemObjectResponse";
case "rollup":
return "RollupPropertyItemObjectResponse";
case "button":
if (name) {
logWarn(`Button property is not supported. Ignoring property: "${name}"`);
}
return;
default:
if (name) {
logError(`Error: Unknown/unsupported property type: "${type}". Ignoring property: "${name}"`);
}
return;
}
}
function getTypesFileProperties(propsConfig, customPropsConfig) {
if (!customPropsConfig) {
throw new Error("customPropsConfig is required");
}
const properties = Object.entries(propsConfig).map(([propId, propConfig]) => {
const propType = getPropertyType(propConfig._type, customPropsConfig[propId]);
if (propType) {
return `${PROPERTIES_INDENT}"${propConfig._name}": ${propType}`;
}
}).filter((p) => p !== void 0).join(",\n");
return properties;
}
function getPropertyType(type, propConfig) {
const makeTypesUnion = (propConfig2) => {
var _a, _b;
return (_b = (_a = propConfig2 == null ? void 0 : propConfig2.options) == null ? void 0 : _a.map(({ name, color }) => `{ id: StringRequest, name: '${name.replace(/'/g, "\\'")}', color: '${color}' }`)) == null ? void 0 : _b.join(" | ");
};
const typesUnion = makeTypesUnion(propConfig);
switch (type) {
case "select":
if (!(typesUnion == null ? void 0 : typesUnion.length)) {
return "SelectPropertyItemObjectResponse";
} else {
return "Omit<SelectPropertyItemObjectResponse, 'select'> & { select: " + typesUnion + "}";
}
case "multi_select":
if (!(typesUnion == null ? void 0 : typesUnion.length)) {
return "MultiSelectPropertyItemObjectResponse";
} else {
return "Omit<MultiSelectPropertyItemObjectResponse, 'multi_select'> & { multi_select: [" + typesUnion + "]}";
}
case "status":
if (!(typesUnion == null ? void 0 : typesUnion.length)) {
return "StatusPropertyItemObjectResponse";
} else {
return "Omit<StatusPropertyItemObjectResponse, 'status'> & { status: " + typesUnion + "}";
}
default:
return getImportType2(type);
}
}
// src/cli/generate-clients.ts
function generateClients(sdkPath, notionResJSON, userConfigData) {
if (import_fs.default.existsSync(sdkPath)) {
import_fs.default.rmSync(sdkPath, { recursive: true });
}
const dbCustomConfig = createCustomConfigFromNotionDatabases(notionResJSON, userConfigData.databases);
Object.entries(userConfigData.databases).map(([dbId, dbConfig]) => {
const dbPath = import_path.default.join(sdkPath, "dbs", dbConfig.pathName);
const dbTypeName = makeTypeName(dbConfig.varName);
const originDir = buildOriginDir(process.argv[1]);
log(`Generating SDK for database: ${dbConfig._name} in ${import_chalk2.default.yellow(dbPath)}`);
createTypesFile({
dbPath,
fileName: "types.ts",
dbTypeName,
propsConfig: dbConfig.properties,
customPropsConfig: dbCustomConfig[dbId]
});
createConstantsFile({
dbPath,
fileName: "constants.ts",
dbVarName: dbConfig.varName,
propsConfig: dbConfig.properties,
customPropsConfig: dbCustomConfig[dbId]
});
createResponseDTOFile({
fileName: "response.dto.ts",
dbPath,
dbTypeName,
propsConfig: dbConfig.properties
});
createPatchDTOFile({
fileName: "patch.dto.ts",
dbPath,
dbTypeName,
propsConfig: dbConfig.properties
});
createDBFile({
fileName: "db.ts",
dbPath,
dbTypeName,
dbId
});
createIndexFile({
dbPath,
fileName: "index.ts"
});
copyCoreFiles({
fromPath: import_path.default.join(originDir, "output", "core"),
toPath: `${sdkPath}/core`
});
});
logSuccess(`
Notion Typescript clients have been generated in ${import_chalk2.default.yellow(sdkPath)}`);
}
function buildOriginDir(appPath) {
if (!appPath) {
throw new Error("appPath is required");
}
const runDir = import_path.default.parse(appPath).dir;
const parts = runDir.split(import_path.default.sep);
const last = parts[parts.length - 1];
const beforeLast = parts[parts.length - 2];
if (process.env.NOTION_TS_CLIENT_DEBUG) {
return import_path.default.join(runDir, "../src");
}
if (last === ".bin" && beforeLast === "node_modules") {
return import_path.default.join(runDir, "..", "notion-ts-client", "src");
}
if (beforeLast === "notion-ts-client") {
return import_path.default.join(runDir, "..", "src");
}
return import_path.default.join(runDir, "..", "notion-ts-client", "src");
}
// src/cli/notion-api.ts
var import_client = require("@notionhq/client");
function fetchNotionDatabases(secret) {
return __async(this, null, function* () {
const notion = new import_client.Client({ auth: secret });
log("Fetching databases from Notion...");
try {
return yield notion.search({
filter: {
value: "database",
property: "object"
}
});
} catch (error) {
logError("Notion search API failed", error);
if ((error == null ? void 0 : error.code) === 401) {
logError("Make sure your Notion API secret is correct.");
}
process.exit(1);
}
});
}
// src/cli/update-config.ts
var import_chalk3 = __toESM(require("chalk"), 1);
var import_fs2 = __toESM(require("fs"), 1);
var isEqual = (a, b) => JSON.stringify(a == null ? void 0 : a.sort()) === JSON.stringify(b == null ? void 0 : b.sort());
var defaultConfig = { ignore: [], databases: {} };
function updateConfigFile(configFile, dbConfigData) {
return __async(this, null, function* () {
var _a, _b;
if (!import_fs2.default.existsSync(configFile)) {
logError(`File ${import_chalk3.default.yellow(configFile)} does not exists, will not update.`);
process.exit(1);
}
const userConfig = readUserConfig(configFile);
const newConfig = yield confirmNewDatabases(userConfig, { databases: dbConfigData });
const { mergedDbConfigs, changes } = yield mergeDatabaseConfigs(userConfig.databases, newConfig.databases);
const numChanges = Object.keys(changes).length;
moveDefaultReadOnlyPropertiesToTheEnd(mergedDbConfigs);
const resultConfig = { ignore: newConfig.ignore, databases: mergedDbConfigs };
if (numChanges !== 0 || !isEqual(
(_a = resultConfig.ignore) == null ? void 0 : _a.map((i) => i.id),
(_b = userConfig.ignore) == null ? void 0 : _b.map((i) => i.id)
)) {
import_fs2.default.writeFileSync(configFile, JSON.stringify(resultConfig, null, 2));
logSuccess("Updated config file.");
if (numChanges !== 0) {
log("Changes:", JSON.stringify(changes, null, 2));
}
} else {
log("No changes detected. Not updating the config file.");
}
return resultConfig;
});
}
function readUserConfig(configPath) {
let userConfig;
if (!import_fs2.default.existsSync(configPath)) {
logWarn(`File ${import_chalk3.default.yellow(configPath)} does not exists. Creating it...`);
import_fs2.default.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
return defaultConfig;
}
try {
userConfig = import_fs2.default.readFileSync(configPath, "utf8");
} catch (err) {
logError(`Failed reading ${import_chalk3.default.yellow(configPath)}.`, err);
process.exit(1);
}
try {
const userConfigData = JSON.parse(userConfig);
if (!userConfigData.databases) {
logWarn(`File ${import_chalk3.default.yellow(configPath)} is invalid, will overwrite...`);
return defaultConfig;
}
return userConfigData;
} catch (err) {
logError(`Error parsing ${import_chalk3.default.yellow(configPath)}.`, err);
process.exit(1);
}
}
// src/cli/generate.ts
function generateTypescriptClients(options2) {
return __async(this, null, function* () {
logSubtle("Config file");
const userConfigData = readUserConfig(options2.config);
const notionResJSON = yield fetchNotionDatabases(options2.secret);
const dbConfigData = createConfigFromNotionDatabases(notionResJSON, userConfigData);
const mergedConfig = yield updateConfigFile(options2.config, dbConfigData);
logSubtle("\nSDK");
generateClients(options2.sdk, notionResJSON, mergedConfig);
});
}
// src/cli/init-config.ts
var import_chalk4 = __toESM(require("chalk"), 1);
// src/index.ts
import_dotenv.default.config({
path: [".env", ".env.local", ".env.dev", ".env.prod"]
});
import_commander.program.name("notion-ts-client").description(
"Notion Typescript CLI: Generate an easy to use and fully typed client API to access and modify the data in your Notion Databases."
).version(version);
var options = {
secret: new import_commander.Option(
"--secret <secret>",
"Notion API secret with read access to your databases"
).env("NOTION_TS_CLIENT_NOTION_SECRET").makeOptionMandatory(),
config: new import_commander.Option("--config <config>", "Path to config file").default("./notion-sdk.json").env("NOTION_TS_CLIENT_CONFIG_PATH"),
sdk: new import_commander.Option("--sdk <sdk>", "Path to folder where the generated SDK will be saved").default("./notion-sdk").env("NOTION_TS_CLIENT_SDK_PATH")
};
import_commander.program.command("generate").description(
"Generate Typescript clients for all configured databases, also updates the config file if needed"
).action(generateTypescriptClients).addOption(options.secret).addOption(options.config).addOption(options.sdk);
import_commander.program.parse();