import type { ManifestDataSource } from "@notionhq/custom-blocks"; import type { DataSourceHandle, SingleSourceDatabaseHandle } from "./notion-as-code/database.js"; import { getCurrentMetadataCapabilityKey } from "./notion-as-code/recorder.js"; import { toCustomBlockDefaultBinding, toCustomBlockDataSource } from "./translate.js"; export type CustomBlockDefaultBinding = { dataSourceResourceId: string; propertyResourceIdsByKey: Record; }; export type CustomBlockHandle = { readonly key?: string; readonly config: { readonly defaultBindings?: Readonly>; }; }; type CustomBlockDataSource = ManifestDataSource | DataSourceHandle | SingleSourceDatabaseHandle; export type CustomBlockOptions = { path: string; name?: string; description?: string; slashCommand?: string; dataSources?: Record; } & ( | { static: true; command?: never; output?: never } | { static?: false; command?: string; output?: string } ); /** Declare a browser project. Its filename under src/customBlocks supplies its key. */ export function customBlock(options: CustomBlockOptions) { if (typeof options.path !== "string" || options.path.trim() === "") { throw new Error("customBlock requires a non-empty path relative to the app root"); } const slashCommand = options.slashCommand?.replace(/^\//, ""); if (slashCommand !== undefined && !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(slashCommand)) { throw new Error( "Custom block slashCommand must be 1–64 lowercase letters, numbers, hyphens or underscores, starting with a letter or number", ); } const { dataSources, defaultBindings } = lowerDataSources(options.dataSources ?? {}); const key = getCurrentMetadataCapabilityKey(); return { _tag: "custom_block" as const, ...(key === undefined ? {} : { key }), config: { source: options.static ? { type: "static" as const, path: options.path } : { type: "project" as const, path: options.path, command: options.command ?? "npx vite build", output: options.output ?? "dist", }, manifest: { version: 1 as const, ...(options.name === undefined ? {} : { name: options.name }), ...(options.description === undefined ? {} : { description: options.description }), ...(slashCommand === undefined ? {} : { slashCommand }), dataSources, }, ...(Object.keys(defaultBindings).length === 0 ? {} : { defaultBindings }), }, }; } function lowerDataSources(dataSources: Record) { const manifestDataSources: Record = {}; const defaultBindings: Record = {}; for (const [key, dataSource] of Object.entries(dataSources)) { const handle = isSingleSourceDatabaseHandle(dataSource) ? dataSource.dataSource : dataSource; if (isDataSourceHandle(handle)) { manifestDataSources[key] = toCustomBlockDataSource(handle); defaultBindings[key] = toCustomBlockDefaultBinding(handle); } else { manifestDataSources[key] = handle; } } return { dataSources: manifestDataSources, defaultBindings }; } function isSingleSourceDatabaseHandle( value: CustomBlockDataSource, ): value is SingleSourceDatabaseHandle { return "dataSource" in value; } function isDataSourceHandle( value: ManifestDataSource | DataSourceHandle, ): value is DataSourceHandle { return "resourceId" in value && "schema" in value && "database" in value; }