import { hubspotRequest } from './client.ts' import { mapCrmRecord, objectPath, parseFilterGroups, resolveObjectSpec, asRecordArray, } from './models.ts' import type { DryRunResult, HubSpotAuthInput, HubSpotCrmRecord, HubSpotFilterGroup, HubSpotPageInfo, JsonRecord, } from './types.ts' import { clampInt, compactRecord, optionalBoolean, optionalString, optionalStringArray, requireRecord, requireString, } from './types.ts' export type ListObjectsInput = HubSpotAuthInput & { objectType: string limit?: number after?: string properties?: Array archived?: boolean associations?: Array } export type GetObjectInput = HubSpotAuthInput & { objectType: string id: string idProperty?: string properties?: Array archived?: boolean associations?: Array } export type SearchObjectsInput = HubSpotAuthInput & { objectType: string query?: string filterGroups?: Array sorts?: Array properties?: Array limit?: number after?: string } export type CreateObjectInput = HubSpotAuthInput & { objectType: string properties: JsonRecord confirm?: boolean dryRun?: boolean } export type UpdateObjectInput = HubSpotAuthInput & { objectType: string id: string properties: JsonRecord idProperty?: string confirm?: boolean dryRun?: boolean } export type ListObjectsResult = { objectType: string items: Array pageInfo: HubSpotPageInfo } function requireProperties(value: unknown, label: string): JsonRecord { const record = requireRecord(value, label) const output: JsonRecord = {} for (const [key, item] of Object.entries(record)) { if (item === undefined) continue if (item === null || typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') { output[key] = item continue } throw new Error(`${label}.${key} must be a string, number, boolean, or null.`) } if (Object.keys(output).length === 0) { throw new Error(`${label} must include at least one property.`) } return output } function mutationGuard( action: string, input: { confirm?: boolean; dryRun?: boolean }, ): void { if (input.dryRun) return if (input.confirm !== true) { throw new Error( `${action} requires confirm: true after explicit user approval, or dryRun: true.`, ) } } /** * List HubSpot CRM records for any object type. * Standard types: `contacts`, `companies`, `deals`, `tickets`. * Custom types use the object type id or fully qualified name. * @example * import { listObjects } from 'kody:@kody/hubspot/objects' * const { items } = await listObjects({ objectType: 'contacts', limit: 10 }) */ export async function listObjects(input: ListObjectsInput): Promise { const spec = resolveObjectSpec(input.objectType) const limit = clampInt(input.limit, 1, 100, 10) const properties = optionalStringArray(input.properties, 'properties') ?? spec.defaultProperties const result = await hubspotRequest({ ...input, operation: spec.readOperation, method: 'GET', path: objectPath(spec.objectType), query: compactRecord({ limit, after: optionalString(input.after, 'after'), properties: properties.join(','), archived: optionalBoolean(input.archived, 'archived'), associations: optionalStringArray(input.associations, 'associations')?.join(','), }), }) return { objectType: spec.objectType, items: asRecordArray(result.data).map((item) => mapCrmRecord(item, spec.objectType)), pageInfo: result.pageInfo, } } /** * Get one HubSpot CRM record. Pass `idProperty: 'email'` to look up a contact by email. * @example * import { getObject } from 'kody:@kody/hubspot/objects' * const contact = await getObject({ objectType: 'contacts', id: '123' }) */ export async function getObject(input: GetObjectInput): Promise { const spec = resolveObjectSpec(input.objectType) const id = requireString(input.id, 'id') const properties = optionalStringArray(input.properties, 'properties') ?? spec.defaultProperties const result = await hubspotRequest({ ...input, operation: spec.readOperation, method: 'GET', path: objectPath(spec.objectType, id), query: compactRecord({ idProperty: optionalString(input.idProperty, 'idProperty'), properties: properties.join(','), archived: optionalBoolean(input.archived, 'archived'), associations: optionalStringArray(input.associations, 'associations')?.join(','), }), }) return mapCrmRecord(result.data, spec.objectType) } /** * Search HubSpot CRM records. Reads only. * @example * import { searchObjects } from 'kody:@kody/hubspot/objects' * const { items } = await searchObjects({ * objectType: 'contacts', * query: 'acme', * limit: 10, * }) */ export async function searchObjects(input: SearchObjectsInput): Promise { const spec = resolveObjectSpec(input.objectType) const limit = clampInt(input.limit, 1, 100, 10) const properties = optionalStringArray(input.properties, 'properties') ?? spec.defaultProperties const body = compactRecord({ query: optionalString(input.query, 'query'), filterGroups: parseFilterGroups(input.filterGroups), sorts: optionalStringArray(input.sorts, 'sorts'), properties, limit, after: optionalString(input.after, 'after'), }) const result = await hubspotRequest({ ...input, operation: spec.readOperation, method: 'POST', path: `${objectPath(spec.objectType)}/search`, body, }) return { objectType: spec.objectType, items: asRecordArray(result.data).map((item) => mapCrmRecord(item, spec.objectType)), pageInfo: result.pageInfo, } } /** * Create a standard HubSpot CRM record. Requires `confirm: true`, or use `dryRun: true`. * Generic custom-object writes stay on `./request`. * @example * import { createObject } from 'kody:@kody/hubspot/objects' * const preview = await createObject({ * objectType: 'contacts', * properties: { email: 'pat@example.com' }, * dryRun: true, * }) */ export async function createObject( input: CreateObjectInput, ): Promise> { const spec = resolveObjectSpec(input.objectType) if (spec.kind !== 'standard') { throw new Error( 'createObject only writes the standard contacts, companies, deals, and tickets types. Use ./request with dryRun or confirm for other object types.', ) } const properties = requireProperties(input.properties, 'properties') const path = objectPath(spec.objectType) const body = { properties } if (input.dryRun) { return { dryRun: true, wouldCall: { method: 'POST', path, body } } } mutationGuard(`Creating a HubSpot ${spec.objectType} record`, input) const result = await hubspotRequest({ ...input, operation: spec.writeOperation, method: 'POST', path, body, }) return mapCrmRecord(result.data, spec.objectType) } /** * Update a standard HubSpot CRM record. Requires `confirm: true`, or use `dryRun: true`. * @example * import { updateObject } from 'kody:@kody/hubspot/objects' * const preview = await updateObject({ * objectType: 'deals', * id: '123', * properties: { dealstage: 'closedwon' }, * dryRun: true, * }) */ export async function updateObject( input: UpdateObjectInput, ): Promise> { const spec = resolveObjectSpec(input.objectType) if (spec.kind !== 'standard') { throw new Error( 'updateObject only writes the standard contacts, companies, deals, and tickets types. Use ./request with dryRun or confirm for other object types.', ) } const id = requireString(input.id, 'id') const properties = requireProperties(input.properties, 'properties') const path = objectPath(spec.objectType, id) const body = { properties } if (input.dryRun) { return { dryRun: true, wouldCall: { method: 'PATCH', path, body } } } mutationGuard(`Updating a HubSpot ${spec.objectType} record`, input) const result = await hubspotRequest({ ...input, operation: spec.writeOperation, method: 'PATCH', path, query: compactRecord({ idProperty: optionalString(input.idProperty, 'idProperty'), }), body, }) return mapCrmRecord(result.data, spec.objectType) } export function pickAuth(input: HubSpotAuthInput): HubSpotAuthInput { return { integrationName: input.integrationName, integration: input.integration, account: input.account, secretName: input.secretName, auth: input.auth, } } /** * Generic HubSpot CRM object reads and standard-object writes. * @example * import objects from 'kody:@kody/hubspot/objects' * const listed = await objects({ objectType: 'contacts', limit: 5 }) */ export default async function objectsEntrypoint( params: Partial & Record = {}, ) { const input = requireRecord(params, 'objects') return listObjects({ objectType: requireString(input.objectType, 'objectType'), limit: input.limit as number | undefined, after: optionalString(input.after, 'after'), properties: optionalStringArray(input.properties, 'properties'), archived: optionalBoolean(input.archived, 'archived'), associations: optionalStringArray(input.associations, 'associations'), integrationName: optionalString(input.integrationName, 'integrationName'), integration: optionalString(input.integration, 'integration'), account: optionalString(input.account, 'account'), secretName: optionalString(input.secretName, 'secretName'), auth: input.auth === 'oauth' || input.auth === 'privateApp' ? input.auth : undefined, }) }