// TODO: Converge on one schema. Until then, keep this translation layer aligned with Notion-as-Code. import { createDatabase, type Database } from "../database.js"; import type { DatabaseSchema, PropertyConfiguration } from "../schema.js"; import type { SelectOption, StatusGroup } from "../types.js"; import type { DataSourceDefinition, NotionAsCodeProperty, NotionAsCodePropertyConfig, NotionAsCodeSchema, NotionAsCodeSelectOption, NotionAsCodeStatusOption, } from "./intents.js"; type AppsPropertyFor = Property extends { type: "title"; } ? { type: "title" } : Property extends { type: "text" } ? { type: "text" } : Property extends { type: "number" } ? { type: "number" } : Property extends { type: "select" } ? { type: "select"; options: SelectOption[] } : Property extends { type: "multi_select" } ? { type: "multi_select"; options: SelectOption[] } : Property extends { type: "status" } ? { type: "status"; groups: StatusGroup[] } : Property extends { type: "date" } ? { type: "date" } : Property extends { type: "checkbox" } ? { type: "checkbox" } : Property extends { type: "url" } ? { type: "url" } : Property extends { type: "email" } ? { type: "email" } : Property extends { type: "phone_number" } ? { type: "phone_number" } : Property extends { type: "file" } ? { type: "file" } : PropertyConfiguration; /** * Map keyed authoring properties to the Apps schema used by sync handlers. The explicit * string-key mapping avoids a homomorphic mapped type interfering with `NoInfer` key checks. */ export type AppsSchemaForSchema = { [Property in keyof Schema & string]: AppsPropertyFor; }; /** Convert a Notion-as-Code data source into the Apps database used by sync handlers. */ export function createAppsDatabase( databaseKey: string, dataSource: DataSourceDefinition, ): Database { const properties: DatabaseSchema = {}; const propertyResourceIds = new Set(); let titleCount = 0; for (const property of dataSource.properties) { if (property.resourceId.length === 0) { throw new Error( `Data source "${dataSource.resourceId}" has a property without a resourceId`, ); } if (propertyResourceIds.has(property.resourceId)) { throw new Error( `Data source "${dataSource.resourceId}" has duplicate property resourceId "${property.resourceId}"`, ); } if (properties[property.name] !== undefined) { throw new Error( `Data source "${dataSource.resourceId}" has duplicate property name "${property.name}"`, ); } propertyResourceIds.add(property.resourceId); if (property.type === "title") { titleCount += 1; } properties[property.name] = toAppsProperty(dataSource.resourceId, property); } if (titleCount !== 1) { throw new Error( `Data source "${dataSource.resourceId}" must have exactly one title property to bind a sync`, ); } return createDatabase(databaseKey, { initialTitle: dataSource.name, schema: properties, }); } /** Map Notion-as-Code properties to Apps properties; their schemas differ for selects and status groups. */ function toAppsProperty( dataSourceResourceId: string, property: NotionAsCodeProperty, ): PropertyConfiguration { switch (property.type) { case "title": return { type: "title" }; case "text": return { type: "text" }; case "number": return { type: "number" }; case "select": return { type: "select", options: (property.options ?? []).map(toAppsSelectOption) }; case "multi_select": return { type: "multi_select", options: (property.options ?? []).map(toAppsSelectOption), }; case "status": return { type: "status", groups: [ { name: "To-do", options: property.options.todo.map(toAppsStatusOption) }, { name: "In progress", options: property.options.inProgress.map(toAppsStatusOption), }, { name: "Complete", options: property.options.complete.map(toAppsStatusOption), }, ], }; case "date": return { type: "date" }; case "checkbox": return { type: "checkbox" }; case "url": return { type: "url" }; case "email": return { type: "email" }; case "phone_number": return { type: "phone_number" }; case "file": return { type: "file" }; default: throw new Error( `Data source "${dataSourceResourceId}" property "${property.name}" uses unsupported sync type "${property.type}"`, ); } } function toAppsSelectOption(option: NotionAsCodeSelectOption): SelectOption { return option.color === undefined ? { name: option.name } : { name: option.name, color: option.color }; } function toAppsStatusOption(option: NotionAsCodeStatusOption): SelectOption { return option.color === undefined ? { name: option.name } : { name: option.name, color: option.color }; }