Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { objectSchema } from '../functions/schema-functions'
import { HSPropType, HSPropFieldType, HSPropTypeFieldType } from '../types'
import { Payload } from '../generated-types'

function makePayload(properties: Record<string, unknown>): Payload {
return {
object_details: {
object_type: 'contact',
id_field_name: 'email',
id_field_value: 'test@test.com'
},
properties,
association_sync_mode: 'upsert',
enable_batching: true,
batch_size: 100
} as unknown as Payload
}

describe('objectSchema / format()', () => {
describe('type detection', () => {
it('classifies a date-only string as date:date', () => {
const schema = objectSchema([makePayload({ churn_date: '2024-01-08' })], 'contact')
const prop = schema.properties.find((p) => p.name === 'churn_date')!
expect(prop.type).toBe(HSPropType.Date)
expect(prop.fieldType).toBe(HSPropFieldType.Date)
expect(prop.typeFieldType).toBe(HSPropTypeFieldType.DateDate)
})

it('classifies a full ISO datetime string as datetime:date', () => {
const schema = objectSchema([makePayload({ appt_start: '2024-01-08T13:52:50.212Z' })], 'contact')
const prop = schema.properties.find((p) => p.name === 'appt_start')!
expect(prop.type).toBe(HSPropType.DateTime)
expect(prop.fieldType).toBe(HSPropFieldType.Date)
expect(prop.typeFieldType).toBe(HSPropTypeFieldType.DateTimeDate)
})

it('classifies a datetime string with no timezone as datetime:date', () => {
const schema = objectSchema([makePayload({ appt_start: '2024-01-08T00:00:00' })], 'contact')
const prop = schema.properties.find((p) => p.name === 'appt_start')!
expect(prop.type).toBe(HSPropType.DateTime)
expect(prop.fieldType).toBe(HSPropFieldType.Date)
expect(prop.typeFieldType).toBe(HSPropTypeFieldType.DateTimeDate)
})

it('classifies a numeric epoch string as string:text', () => {
const schema = objectSchema([makePayload({ appt_start: '1780382814' })], 'contact')
const prop = schema.properties.find((p) => p.name === 'appt_start')!
expect(prop.type).toBe(HSPropType.String)
expect(prop.fieldType).toBe(HSPropFieldType.Text)
expect(prop.typeFieldType).toBe(HSPropTypeFieldType.StringText)
})

it('classifies a number as number:number', () => {
const schema = objectSchema([makePayload({ count: 42 })], 'contact')
const prop = schema.properties.find((p) => p.name === 'count')!
expect(prop.type).toBe(HSPropType.Number)
expect(prop.fieldType).toBe(HSPropFieldType.Number)
expect(prop.typeFieldType).toBe(HSPropTypeFieldType.NumberNumber)
})

it('classifies a boolean as enumeration:booleancheckbox', () => {
const schema = objectSchema([makePayload({ active: true })], 'contact')
const prop = schema.properties.find((p) => p.name === 'active')!
expect(prop.type).toBe(HSPropType.Enumeration)
expect(prop.fieldType).toBe(HSPropFieldType.BooleanCheckbox)
expect(prop.typeFieldType).toBe(HSPropTypeFieldType.EnumerationBooleanCheckbox)
})

it('classifies a plain string as string:text', () => {
const schema = objectSchema([makePayload({ name: 'hello' })], 'contact')
const prop = schema.properties.find((p) => p.name === 'name')!
expect(prop.type).toBe(HSPropType.String)
expect(prop.fieldType).toBe(HSPropFieldType.Text)
expect(prop.typeFieldType).toBe(HSPropTypeFieldType.StringText)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,141 @@ beforeEach((done) => {

describe('Hubspot.upsertObject', () => {
describe('where syncMode = upsert', () => {
describe('Hubspot datetime field receives an epoch string value', () => {
it('should allow a plain string value (e.g. Unix epoch) to match a HubSpot datetime field', async () => {
const epochPayload = {
event: 'Test Custom Object Event',
type: 'track',
userId: 'user_id_1',
properties: {
email: 'test@test.com',
regular: {
datetime_prop: '1780382814'
},
sensitive: {}
}
} as Partial<SegmentEvent>

const epochMapping = {
__segment_internal_sync_mode: 'upsert',
object_details: {
object_type: 'contact',
id_field_name: 'email',
id_field_value: { '@path': '$.properties.email' },
property_group: 'contactinformation'
},
properties: { '@path': '$.properties.regular' },
sensitive_properties: { '@path': '$.properties.sensitive' },
association_sync_mode: 'upsert',
associations: [],
enable_batching: true,
batch_size: 100
}

nock(HUBSPOT_BASE_URL)
.get('/crm/v3/properties/contact')
.reply(200, {
results: [
{
name: 'datetime_prop',
type: 'datetime',
fieldType: 'date',
hasUniqueValue: false
}
]
})

nock(HUBSPOT_BASE_URL).get('/crm/v3/properties/contact?dataSensitivity=sensitive').reply(200, { results: [] })

nock(HUBSPOT_BASE_URL)
.post('/crm/v3/objects/contact/batch/upsert')
.reply(200, {
status: 'COMPLETE',
results: [{ id: '1', properties: { email: 'test@test.com', datetime_prop: '1780382814' } }]
})

nock(HUBSPOT_BASE_URL).post('/crm/v3/objects/contact/batch/read').reply(200, { results: [] })

const event = createTestEvent(epochPayload)

await expect(
testDestination.testAction('upsertObject', {
event,
settings,
useDefaultMappings: true,
mapping: epochMapping
})
).resolves.not.toThrow()
})

it('should allow an ISO datetime string to match a cached string:text schema (reverse coercion)', async () => {
const isoPayload = {
event: 'Test Custom Object Event',
type: 'track',
userId: 'user_id_1',
properties: {
email: 'test@test.com',
regular: {
datetime_prop: '2024-01-08T13:52:50.212Z'
},
sensitive: {}
}
} as Partial<SegmentEvent>

const isoMapping = {
__segment_internal_sync_mode: 'upsert',
object_details: {
object_type: 'contact',
id_field_name: 'email',
id_field_value: { '@path': '$.properties.email' },
property_group: 'contactinformation'
},
properties: { '@path': '$.properties.regular' },
sensitive_properties: { '@path': '$.properties.sensitive' },
association_sync_mode: 'upsert',
associations: [],
enable_batching: true,
batch_size: 100
}

// HubSpot schema returns string:text (simulating a cache populated by a prior epoch string event)
nock(HUBSPOT_BASE_URL)
.get('/crm/v3/properties/contact')
.reply(200, {
results: [
{
name: 'datetime_prop',
type: 'string',
fieldType: 'text',
hasUniqueValue: false
}
]
})

nock(HUBSPOT_BASE_URL).get('/crm/v3/properties/contact?dataSensitivity=sensitive').reply(200, { results: [] })

nock(HUBSPOT_BASE_URL)
.post('/crm/v3/objects/contact/batch/upsert')
.reply(200, {
status: 'COMPLETE',
results: [{ id: '1', properties: { email: 'test@test.com', datetime_prop: '2024-01-08T13:52:50.212Z' } }]
})

nock(HUBSPOT_BASE_URL).post('/crm/v3/objects/contact/batch/read').reply(200, { results: [] })

const event = createTestEvent(isoPayload)

await expect(
testDestination.testAction('upsertObject', {
event,
settings,
useDefaultMappings: true,
mapping: isoMapping
})
).resolves.not.toThrow()
})
})

describe('Hubspot object schema has a mis-matched property', () => {
it('should throw an error explaining the property type mismatch.', async () => {
const event = createTestEvent(payload)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,21 @@ function compareProps(prop1: Prop, prop2: Prop): boolean {
// string:text is OK to match to textarea:string
return true
}
if (
(prop1.type === HSPropType.String &&
prop1.fieldType === HSPropFieldType.Text &&
prop2.type === HSPropType.DateTime &&
prop2.fieldType === HSPropFieldType.Date) ||
(prop1.type === HSPropType.DateTime &&
prop1.fieldType === HSPropFieldType.Date &&
prop2.type === HSPropType.String &&
prop2.fieldType === HSPropFieldType.Text)
) {
// string values (e.g. Unix epoch strings) are valid for HubSpot datetime fields.
// coercion is symmetric to handle cache hits where the cached schema was inferred
// from a different string encoding (e.g. epoch string cached, ISO string arrives later)
return true
}
throw new IntegrationError(
`Payload property with name ${prop1.name} has a different type to the property in HubSpot. Expected: type = ${prop1.type} fieldType = ${prop1.fieldType}. Received: type = ${prop2.type} fieldType = ${prop2.fieldType}`,
'HUBSPOT_PROPERTY_TYPE_MISMATCH',
Expand Down