import * as v from "valibot" import type { CustomBlockErrorInfo } from "../errors" import type { NotionPagePropertyInputMap, NotionPagePropertyWriteMap, } from "../pages/page.js" import { notionPagePropertyValueSchema } from "../pages/page.js" import type { NotionDataSource } from "./dataSource.js" // The schema accepts any string code. The type lists known codes for // autocomplete, with an open string fallback for newer senders. export const customBlockPropertyErrorCodeSchema = v.string() export type CustomBlockPropertyErrorCode = | "unmapped_property" | "property_id_mismatch" | "duplicate_property" | "invalid_property_value" | (string & {}) export type CustomBlockPropertyErrorInfo = CustomBlockErrorInfo export type PropertyWriteMapResolutionResult = | { status: "success"; properties: NotionPagePropertyWriteMap } | { status: "error"; error: CustomBlockPropertyErrorInfo } function makePropertyError( code: CustomBlockPropertyErrorCode, message: string, ): CustomBlockPropertyErrorInfo { return { code, message, isRetryable: false } } /** * Resolves a public SDK property write map into the ID-keyed bridge shape. * * Identifiers in `properties` may be raw property IDs or data-source property keys; the latter * are looked up in the `dataSource`'s `propertyIdsByKey`. Each value is re-parsed through * `notionPagePropertyValueSchema` with its `id` rewritten to the resolved property ID. */ export function resolvePropertyWriteMapForDataSource(args: { dataSource: NotionDataSource | undefined properties: NotionPagePropertyInputMap operationName: string }): PropertyWriteMapResolutionResult { const { dataSource, properties, operationName } = args const resolvedProperties: NotionPagePropertyWriteMap = {} for (const [identifier, value] of Object.entries(properties)) { const propertyIdResult = resolvePropertyIdentifierForDataSource({ dataSource, identifier, operationName, }) if (propertyIdResult.status === "error") { return propertyIdResult } const propertyId = propertyIdResult.propertyId if ( value.id !== undefined && value.id !== identifier && value.id !== propertyId ) { return { status: "error", error: makePropertyError( "property_id_mismatch", `Property ${identifier} resolved to ${propertyId} but value id was ${value.id}.`, ), } } if (resolvedProperties[propertyId] !== undefined) { return { status: "error", error: makePropertyError( "duplicate_property", `Cannot set property ${propertyId} more than once.`, ), } } const parsedValue = v.safeParse(notionPagePropertyValueSchema, { ...value, id: propertyId, }) if (!parsedValue.success) { return { status: "error", error: makePropertyError( "invalid_property_value", `Invalid value for property ${identifier}.`, ), } } resolvedProperties[propertyId] = parsedValue.output } return { status: "success", properties: resolvedProperties } } /** * Treats matching data-source keys as aliases for property IDs; all other identifiers are raw IDs. */ export function resolvePropertyIdentifierForDataSource(args: { dataSource: NotionDataSource | undefined identifier: string operationName: string }): | { status: "success"; propertyId: string } | { status: "error"; error: CustomBlockPropertyErrorInfo } { const { dataSource, identifier, operationName } = args if (dataSource !== undefined && identifier in dataSource.propertyIdsByKey) { const propertyId = dataSource.propertyIdsByKey[identifier] if (propertyId === undefined) { return { status: "error", error: makePropertyError( "unmapped_property", `${operationName} cannot resolve property key "${identifier}" because it is not bound to a Notion property.`, ), } } return { status: "success", propertyId } } return { status: "success", propertyId: identifier } }