Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/hubspot

src/models.ts

167 lines · 5.7 KB · TypeScript
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]
}