import type { Cover, DateValue, Icon, NoticonColor, PeopleValue, PlaceValue, RelationReference, TextValue, } from "./types.js"; function validateDateString(value: string): void { if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(new Date(value).getTime())) { throw new Error(`Invalid date: ${value}. Expected YYYY-MM-DD.`); } } function dateToken(value: DateValue): TextValue { return [["‣", [["d", value]]]]; } function parseISODateTime(value: string): { date: string; time: string } { if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value) || Number.isNaN(new Date(value).getTime())) { throw new Error(`Invalid ISO 8601 datetime: ${value}`); } return { date: value.slice(0, 10), time: value.slice(11, 16) }; } /** Build property values returned by a sync handler. */ export const Builder = { richText: (content: string): TextValue => [[content]], text: (content: string): TextValue => [[content]], title: (content: string): TextValue => [[content]], url: (url: string): TextValue => [[url]], email: (email: string): TextValue => [[email]], phoneNumber: (phone: string): TextValue => [[phone]], checkbox: (checked: boolean): TextValue => (checked ? [["Yes"]] : [["No"]]), file: (url: string, name?: string): TextValue => [[name ?? url, [["a", url]]]], number: (value: number): TextValue => (Number.isNaN(value) ? [] : [[String(value)]]), date(value: string): TextValue { validateDateString(value); return dateToken({ type: "date", start_date: value }); }, dateTime(value: string, timeZone?: string): TextValue { const parsed = parseISODateTime(value); return dateToken({ type: "datetime", start_date: parsed.date, start_time: parsed.time, ...(timeZone === undefined ? {} : { time_zone: timeZone }), }); }, dateRange(startDate: string, endDate: string): TextValue { validateDateString(startDate); validateDateString(endDate); return dateToken({ type: "daterange", start_date: startDate, end_date: endDate }); }, dateTimeRange(startValue: string, endValue: string, timeZone?: string): TextValue { const start = parseISODateTime(startValue); const end = parseISODateTime(endValue); return dateToken({ type: "datetimerange", start_date: start.date, start_time: start.time, end_date: end.date, end_time: end.time, ...(timeZone === undefined ? {} : { time_zone: timeZone }), }); }, link: (text: string, url: string): TextValue => [[text, [["a", url]]]], select: (value: string): TextValue => [[value]], multiSelect: (...values: string[]): TextValue => values.length === 0 ? [] : [[values.join(",")]], status: (value: string): TextValue => [[value]], people: (...emails: string[]): PeopleValue => emails.map((email) => ({ email })), place(value: PlaceValue): PlaceValue { if (!Number.isFinite(value.lat) || !Number.isFinite(value.lon)) { throw new Error("Place value must have numeric lat and lon coordinates"); } return value; }, relation: (primaryKey: string): RelationReference => ({ type: "primaryKey", value: primaryKey, }), emojiIcon: (value: string): Icon => ({ type: "emoji", value }), notionIcon: (icon: string, color: NoticonColor = "gray"): Icon => ({ type: "notion", icon, color, }), imageIcon: (url: string): Icon => ({ type: "image", url }), imageCover(url: string, position = 0.5): Cover { if (!Number.isFinite(position) || position < 0 || position > 1) { throw new Error("Cover position must be between 0 and 1"); } return { type: "image", url, position }; }, };