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<string>
archived?: boolean
associations?: Array<string>
}
export type GetObjectInput = HubSpotAuthInput & {
objectType: string
id: string
idProperty?: string
properties?: Array<string>
archived?: boolean
associations?: Array<string>
}
export type SearchObjectsInput = HubSpotAuthInput & {
objectType: string
query?: string
filterGroups?: Array<HubSpotFilterGroup>
sorts?: Array<string>
properties?: Array<string>
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<HubSpotCrmRecord>
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<ListObjectsResult> {
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<unknown>({
...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<HubSpotCrmRecord> {
const spec = resolveObjectSpec(input.objectType)
const id = requireString(input.id, 'id')
const properties = optionalStringArray(input.properties, 'properties') ?? spec.defaultProperties
const result = await hubspotRequest<unknown>({
...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<ListObjectsResult> {
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<unknown>({
...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<HubSpotCrmRecord | DryRunResult<{ method: string; path: string; body: JsonRecord }>> {
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<unknown>({
...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<HubSpotCrmRecord | DryRunResult<{ method: string; path: string; body: JsonRecord }>> {
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<unknown>({
...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<ListObjectsInput> & Record<string, unknown> = {},
) {
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,
})
}