import type {
HubSpotAccountInfo,
HubSpotCrmRecord,
HubSpotFilterGroup,
HubSpotSearchFilter,
JsonRecord,
} from './types.ts'
import { requireRecord, requireString } from './types.ts'
import type { HubSpotOperation } from './setup.ts'
export const STANDARD_OBJECT_TYPES = ['contacts', 'companies', 'deals', 'tickets'] as const
export type StandardObjectType = (typeof STANDARD_OBJECT_TYPES)[number]
export type ObjectSpec = {
objectType: string
kind: 'standard' | 'custom'
readOperation: HubSpotOperation
writeOperation: HubSpotOperation
defaultProperties: Array<string>
}
export const DEFAULT_PROPERTIES: Record<StandardObjectType, Array<string>> = {
contacts: ['email', 'firstname', 'lastname', 'phone', 'company', 'lifecyclestage', 'hs_object_id'],
companies: ['name', 'domain', 'industry', 'city', 'phone', 'lifecyclestage', 'hs_object_id'],
deals: ['dealname', 'amount', 'dealstage', 'pipeline', 'closedate', 'hs_object_id'],
tickets: ['subject', 'content', 'hs_pipeline', 'hs_pipeline_stage', 'hs_ticket_priority', 'hs_object_id'],
}
export function isStandardObjectType(value: string): value is StandardObjectType {
return (STANDARD_OBJECT_TYPES as readonly string[]).includes(value)
}
export function resolveObjectSpec(objectType: string): ObjectSpec {
const slug = requireString(objectType, 'objectType')
if (isStandardObjectType(slug)) {
switch (slug) {
case 'contacts':
return {
objectType: slug,
kind: 'standard',
readOperation: 'contacts.read',
writeOperation: 'contacts.write',
defaultProperties: DEFAULT_PROPERTIES.contacts,
}
case 'companies':
return {
objectType: slug,
kind: 'standard',
readOperation: 'companies.read',
writeOperation: 'companies.write',
defaultProperties: DEFAULT_PROPERTIES.companies,
}
case 'deals':
return {
objectType: slug,
kind: 'standard',
readOperation: 'deals.read',
writeOperation: 'deals.write',
defaultProperties: DEFAULT_PROPERTIES.deals,
}
case 'tickets':
return {
objectType: slug,
kind: 'standard',
readOperation: 'tickets.read',
writeOperation: 'tickets.write',
defaultProperties: DEFAULT_PROPERTIES.tickets,
}
default: {
const exhaustive: never = slug
throw new Error(`Unsupported standard HubSpot object: ${String(exhaustive)}`)
}
}
}
return {
objectType: slug,
kind: 'custom',
readOperation: 'custom.read',
writeOperation: 'unknownMutation',
defaultProperties: ['hs_object_id'],
}
}
export function objectPath(objectType: string, id?: string): string {
const spec = resolveObjectSpec(objectType)
return id ? `/crm/v3/objects/${spec.objectType}/${id}` : `/crm/v3/objects/${spec.objectType}`
}
export function mapProperties(value: unknown): Record<string, string | null> {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
const output: Record<string, string | null> = {}
for (const [key, item] of Object.entries(value as JsonRecord)) {
if (item === null) {
output[key] = null
continue
}
if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
output[key] = String(item)
}
}
return output
}
export function mapCrmRecord(value: unknown, objectType: string): HubSpotCrmRecord {
const record = (value && typeof value === 'object' ? value : {}) as JsonRecord
return {
id: String(record.id || ''),
objectType,
properties: mapProperties(record.properties),
createdAt: typeof record.createdAt === 'string' ? record.createdAt : null,
updatedAt: typeof record.updatedAt === 'string' ? record.updatedAt : null,
archived: typeof record.archived === 'boolean' ? record.archived : null,
}
}
export function mapAccount(value: unknown): HubSpotAccountInfo {
const record = (value && typeof value === 'object' ? value : {}) as JsonRecord
const portalId = record.portalId ?? record.portal_id
return {
hasPortalId: portalId !== undefined && portalId !== null && String(portalId).length > 0,
accountType: typeof record.accountType === 'string' ? record.accountType : null,
timeZone: typeof record.timeZone === 'string' ? record.timeZone : null,
uiDomain: typeof record.uiDomain === 'string' ? record.uiDomain : null,
}
}
export function parseFilter(value: unknown, label: string): HubSpotSearchFilter {
const record = requireRecord(value, label)
const filter: HubSpotSearchFilter = {
propertyName: requireString(record.propertyName, `${label}.propertyName`),
operator: requireString(record.operator, `${label}.operator`),
}
if (record.value !== undefined && record.value !== null) {
filter.value = String(record.value)
}
if (record.highValue !== undefined && record.highValue !== null) {
filter.highValue = String(record.highValue)
}
if (record.values !== undefined) {
if (!Array.isArray(record.values)) {
throw new Error(`${label}.values must be an array.`)
}
filter.values = record.values.map((item, index) => String(item ?? `${label}.values[${index}]`))
}
return filter
}
export function parseFilterGroups(value: unknown): Array<HubSpotFilterGroup> | undefined {
if (value === undefined || value === null) return undefined
if (!Array.isArray(value)) throw new Error('filterGroups must be an array.')
return value.map((group, groupIndex) => {
const record = requireRecord(group, `filterGroups[${groupIndex}]`)
if (!Array.isArray(record.filters)) {
throw new Error(`filterGroups[${groupIndex}].filters must be an array.`)
}
return {
filters: record.filters.map((filter, filterIndex) =>
parseFilter(filter, `filterGroups[${groupIndex}].filters[${filterIndex}]`),
),
}
})
}
export function asRecordArray(value: unknown): Array<unknown> {
if (value === undefined || value === null) return []
return Array.isArray(value) ? value : [value]
}