diff --git a/packages/destination-actions/src/destinations/customerio/__tests__/createUpdateObject.test.ts b/packages/destination-actions/src/destinations/customerio/__tests__/createUpdateObject.test.ts index be10d2dd187..c3628bd4549 100644 --- a/packages/destination-actions/src/destinations/customerio/__tests__/createUpdateObject.test.ts +++ b/packages/destination-actions/src/destinations/customerio/__tests__/createUpdateObject.test.ts @@ -1,10 +1,197 @@ import { createTestEvent } from '@segment/actions-core' +import { MultiStatusResponse } from '@segment/actions-core' import { Settings } from '../generated-types' import dayjs from '../../../lib/dayjs' import { getDefaultMappings, testRunner } from '../test-helper' +import createUpdateObject from '../createUpdateObject' describe('CustomerIO', () => { describe('createUpdateObject', () => { + describe('performBatch', () => { + it('should return a merged MultiStatusResponse with items at original indices', async () => { + const request = jest.fn() + .mockResolvedValueOnce({ status: 200, data: { errors: [] } }) + .mockResolvedValueOnce({ status: 200, data: { errors: [] } }) + + const payloads = [ + { id: 'grp-1', user_id: 'user-1', object_type_id: '1', convert_timestamp: false }, + { id: 'grp-2', anonymous_id: 'anon-1', object_type_id: '1', convert_timestamp: false }, + { id: 'grp-3', user_id: 'user-2', object_type_id: '1', convert_timestamp: false } + ] + + const settings = { siteId: '12345', apiKey: 'abcde', accountRegion: 'US 🇺🇸' } + const response = await createUpdateObject.performBatch!(request as any, { + payload: payloads, + settings, + rawData: [], + rawMapping: {}, + audienceSettings: {}, + features: {}, + statsContext: {} as any, + logger: {} as any, + engageDestinationCache: {} as any, + transactionContext: {} as any, + stateContext: {} as any, + subscriptionMetadata: {} as any, + hookOutputs: {} + } as any) + + expect(response).toBeInstanceOf(MultiStatusResponse) + const multistatus = response as MultiStatusResponse + expect(multistatus.length()).toBe(3) + expect(multistatus.getAllResponses().map((r) => r.value())).toEqual([ + { + status: 200, + body: { + anonymous_id: undefined, + attributes: undefined, + cio_relationships: [{ identifiers: { id: 'user-1' } }], + object_id: 'grp-1', + object_type_id: '1', + person_id: 'user-1' + }, + sent: { + type: 'object', + action: 'identify', + identifiers: { object_id: 'grp-1', object_type_id: '1' }, + attributes: undefined, + cio_relationships: [{ identifiers: { id: 'user-1' } }] + } + }, + { + status: 200, + body: { + anonymous_id: 'anon-1', + attributes: undefined, + cio_relationships: [{ identifiers: { anonymous_id: 'anon-1' } }], + object_id: 'grp-2', + object_type_id: '1', + person_id: undefined + }, + sent: { + type: 'object', + action: 'identify_anonymous', + identifiers: { object_id: 'grp-2', object_type_id: '1' }, + attributes: { anonymous_id: 'anon-1' }, + cio_relationships: [{ identifiers: { anonymous_id: 'anon-1' } }] + } + }, + { + status: 200, + body: { + anonymous_id: undefined, + attributes: undefined, + cio_relationships: [{ identifiers: { id: 'user-2' } }], + object_id: 'grp-3', + object_type_id: '1', + person_id: 'user-2' + }, + sent: { + type: 'object', + action: 'identify', + identifiers: { object_id: 'grp-3', object_type_id: '1' }, + attributes: undefined, + cio_relationships: [{ identifiers: { id: 'user-2' } }] + } + } + ]) + }) + + it('should report per-item errors at the correct original indices', async () => { + const request = jest.fn() + .mockResolvedValueOnce({ + status: 200, + data: { errors: [{ batch_index: 0, reason: 'invalid', message: 'Object ID invalid' }] } + }) + .mockResolvedValueOnce({ status: 200, data: { errors: [] } }) + + const payloads = [ + { id: 'grp-1', user_id: 'user-1', object_type_id: '1', convert_timestamp: false }, + { id: 'grp-2', anonymous_id: 'anon-1', object_type_id: '1', convert_timestamp: false }, + { id: 'grp-3', user_id: 'user-2', object_type_id: '1', convert_timestamp: false } + ] + + const settings = { siteId: '12345', apiKey: 'abcde', accountRegion: 'US 🇺🇸' } + const response = await createUpdateObject.performBatch!(request as any, { + payload: payloads, + settings, + rawData: [], + rawMapping: {}, + audienceSettings: {}, + features: {}, + statsContext: {} as any, + logger: {} as any, + engageDestinationCache: {} as any, + transactionContext: {} as any, + stateContext: {} as any, + subscriptionMetadata: {} as any, + hookOutputs: {} + } as any) + + expect(response).toBeInstanceOf(MultiStatusResponse) + const multistatus = response as MultiStatusResponse + expect(multistatus.length()).toBe(3) + expect(multistatus.getAllResponses().map((r) => r.value())).toEqual([ + { + status: 400, + errormessage: 'Object ID invalid', + errortype: 'BAD_REQUEST', + body: { + anonymous_id: undefined, + attributes: undefined, + cio_relationships: [{ identifiers: { id: 'user-1' } }], + object_id: 'grp-1', + object_type_id: '1', + person_id: 'user-1' + }, + sent: { + type: 'object', + action: 'identify', + identifiers: { object_id: 'grp-1', object_type_id: '1' }, + attributes: undefined, + cio_relationships: [{ identifiers: { id: 'user-1' } }] + } + }, + { + status: 200, + body: { + anonymous_id: 'anon-1', + attributes: undefined, + cio_relationships: [{ identifiers: { anonymous_id: 'anon-1' } }], + object_id: 'grp-2', + object_type_id: '1', + person_id: undefined + }, + sent: { + type: 'object', + action: 'identify_anonymous', + identifiers: { object_id: 'grp-2', object_type_id: '1' }, + attributes: { anonymous_id: 'anon-1' }, + cio_relationships: [{ identifiers: { anonymous_id: 'anon-1' } }] + } + }, + { + status: 200, + body: { + anonymous_id: undefined, + attributes: undefined, + cio_relationships: [{ identifiers: { id: 'user-2' } }], + object_id: 'grp-3', + object_type_id: '1', + person_id: 'user-2' + }, + sent: { + type: 'object', + action: 'identify', + identifiers: { object_id: 'grp-3', object_type_id: '1' }, + attributes: undefined, + cio_relationships: [{ identifiers: { id: 'user-2' } }] + } + } + ]) + }) + }) + testRunner((settings: Settings, action: Function) => { it('should work with default mappings when userId is supplied', async () => { const userId = 'abc123' diff --git a/packages/destination-actions/src/destinations/customerio/__tests__/utils.test.ts b/packages/destination-actions/src/destinations/customerio/__tests__/utils.test.ts index 88aab5d3886..9354705239e 100644 --- a/packages/destination-actions/src/destinations/customerio/__tests__/utils.test.ts +++ b/packages/destination-actions/src/destinations/customerio/__tests__/utils.test.ts @@ -1,4 +1,5 @@ -import { convertValidTimestamp, resolveIdentifiers, isIsoDate } from '../utils' +import { HTTPError, MultiStatusResponse } from '@segment/actions-core' +import { convertValidTimestamp, isIsoDate, parseResponse, resolveIdentifiers, sendBatch } from '../utils' describe('isIsoDate', () => { it('should return true for valid ISO date with fractional seconds from 1-9 digits', () => { @@ -80,8 +81,417 @@ describe('resolveIdentifiers', () => { }) }) +describe('sendBatch', () => { + it('should parse 207 multi-status Track API responses', async () => { + const request = jest.fn().mockResolvedValue({ + status: 207, + data: { + errors: [ + { + batch_index: 1, + reason: 'invalid', + message: 'Attribute value too long' + } + ] + } + }) + + const response = await sendBatch(request, [ + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-1', name: 'First' } + }, + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-2', name: 'Second' } + } + ]) + + expect(response).toBeInstanceOf(MultiStatusResponse) + expect(response!.length()).toBe(2) + expect(response!.getResponseAtIndex(0).value()).toEqual({ + status: 207, + body: { person_id: 'user-1', name: 'First' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-1' }, name: 'First' } + }) + expect(response!.getResponseAtIndex(1).value()).toEqual({ + status: 400, + errormessage: 'Attribute value too long', + errortype: 'BAD_REQUEST', + body: { person_id: 'user-2', name: 'Second' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-2' }, name: 'Second' } + }) + }) + + it('should parse 200 Track API responses that still contain batch errors', async () => { + const request = jest.fn().mockResolvedValue({ + status: 200, + data: { + errors: [ + { + batch_index: 0, + reason: 'required', + field: 'name', + message: 'Name is required' + } + ] + } + }) + + const response = await sendBatch(request, [ + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-1', name: 'First' } + } + ]) + + expect(response).toBeInstanceOf(MultiStatusResponse) + expect(response!.getResponseAtIndex(0).value()).toEqual({ + status: 400, + errormessage: 'Name is required', + errortype: 'BAD_REQUEST', + body: { person_id: 'user-1', name: 'First' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-1' }, name: 'First' } + }) + }) + + it('should return all-success responses when the Track API reports an empty errors array', async () => { + const request = jest.fn().mockResolvedValue({ + status: 200, + data: { + errors: [] + } + }) + + const response = await sendBatch(request, [ + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-1', name: 'First' } + }, + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-2', name: 'Second' } + } + ]) + + expect(response).toBeInstanceOf(MultiStatusResponse) + expect(response!.getAllResponses().map((result) => result.value())).toEqual([ + { + status: 200, + body: { person_id: 'user-1', name: 'First' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-1' }, name: 'First' } + }, + { + status: 200, + body: { person_id: 'user-2', name: 'Second' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-2' }, name: 'Second' } + } + ]) + }) + + it('should convert HTTP errors into per-item MultiStatusResponse entries', async () => { + const error = new HTTPError( + { status: 400, statusText: 'Bad Request' } as any, + { url: 'https://track.customer.io/api/v2/batch' } as any, + {} as any + ) + const request = jest.fn().mockRejectedValue(error) + + const response = await sendBatch(request, [ + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-1', name: 'First' } + }, + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-2', name: 'Second' } + } + ]) + + expect(response).toBeInstanceOf(MultiStatusResponse) + expect(response!.length()).toBe(2) + expect(response!.getResponseAtIndex(0).value()).toEqual({ + status: 400, + errormessage: 'Bad Request', + errortype: 'BAD_REQUEST', + body: { person_id: 'user-1', name: 'First' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-1' }, name: 'First' } + }) + expect(response!.getResponseAtIndex(1).value()).toEqual({ + status: 400, + errormessage: 'Bad Request', + errortype: 'BAD_REQUEST', + body: { person_id: 'user-2', name: 'Second' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-2' }, name: 'Second' } + }) + }) + + it('should convert 429 HTTP errors into per-item MultiStatusResponse entries', async () => { + const error = new HTTPError( + { status: 429, statusText: 'Too Many Requests' } as any, + { url: 'https://track.customer.io/api/v2/batch' } as any, + {} as any + ) + const request = jest.fn().mockRejectedValue(error) + + const response = await sendBatch(request, [ + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-1', name: 'First' } + } + ]) + + expect(response).toBeInstanceOf(MultiStatusResponse) + expect(response!.getResponseAtIndex(0).value()).toEqual({ + status: 429, + errormessage: 'Too Many Requests', + errortype: 'TOO_MANY_REQUESTS', + body: { person_id: 'user-1', name: 'First' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-1' }, name: 'First' } + }) + }) + + it('should convert 500 HTTP errors into per-item MultiStatusResponse entries', async () => { + const error = new HTTPError( + { status: 500, statusText: 'Internal Server Error' } as any, + { url: 'https://track.customer.io/api/v2/batch' } as any, + {} as any + ) + const request = jest.fn().mockRejectedValue(error) + + const response = await sendBatch(request, [ + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-1', name: 'First' } + } + ]) + + expect(response).toBeInstanceOf(MultiStatusResponse) + expect(response!.getResponseAtIndex(0).value()).toEqual({ + status: 500, + errormessage: 'Internal Server Error', + errortype: 'INTERNAL_SERVER_ERROR', + body: { person_id: 'user-1', name: 'First' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-1' }, name: 'First' } + }) + }) + + it('should convert 401 auth errors into per-item MultiStatusResponse entries', async () => { + const error = new HTTPError( + { status: 401, statusText: 'Unauthorized' } as any, + { url: 'https://track.customer.io/api/v2/batch' } as any, + {} as any + ) + const request = jest.fn().mockRejectedValue(error) + + const response = await sendBatch(request, [ + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-1', name: 'First' } + } + ]) + + expect(response).toBeInstanceOf(MultiStatusResponse) + expect(response!.getResponseAtIndex(0).value()).toEqual({ + status: 401, + errormessage: 'Unauthorized', + errortype: 'UNAUTHORIZED', + body: { person_id: 'user-1', name: 'First' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-1' }, name: 'First' } + }) + }) + + it('should handle non-HTTP errors with status 500', async () => { + const error = new Error('Network failure') + const request = jest.fn().mockRejectedValue(error) + + const response = await sendBatch(request, [ + { + type: 'person', + action: 'event', + settings: {}, + payload: { person_id: 'user-1', name: 'First' } + } + ]) + + expect(response).toBeInstanceOf(MultiStatusResponse) + expect(response!.getResponseAtIndex(0).value()).toEqual({ + status: 500, + errormessage: 'Network failure', + errortype: 'INTERNAL_SERVER_ERROR', + body: { person_id: 'user-1', name: 'First' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-1' }, name: 'First' } + }) + }) +}) + +describe('parseResponse', () => { + it('should create error entries for items referenced by batch errors', () => { + const payloads = [{ person_id: 'user-0' }, { person_id: 'user-1' }, { person_id: 'user-2' }] + const batch = [ + { type: 'person', action: 'event', identifiers: { id: 'user-0' } }, + { type: 'person', action: 'event', identifiers: { id: 'user-1' } }, + { type: 'person', action: 'event', identifiers: { id: 'user-2' } } + ] + + const response = parseResponse( + { + status: 207, + data: { + errors: [{ batch_index: 1, reason: 'required', field: 'name', message: 'Name is required' }] + } + } as any, + payloads, + batch + ) + + expect(response.getAllResponses().map((result) => result.value())).toEqual([ + { + status: 207, + body: { person_id: 'user-0' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-0' } } + }, + { + status: 400, + errormessage: 'Name is required', + errortype: 'BAD_REQUEST', + body: { person_id: 'user-1' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-1' } } + }, + { + status: 207, + body: { person_id: 'user-2' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-2' } } + } + ]) + }) + + it('should use reason as fallback when message is missing', () => { + const payloads = [{ person_id: 'user-0' }] + const batch = [{ type: 'person', action: 'event', identifiers: { id: 'user-0' } }] + + const response = parseResponse( + { + status: 207, + data: { + errors: [{ batch_index: 0, reason: 'invalid' }] + } + } as any, + payloads, + batch + ) + + expect(response.getResponseAtIndex(0).value()).toMatchObject({ + status: 400, + errormessage: 'invalid' + }) + }) + + it('should treat missing errors array as all-success', () => { + const payloads = [{ person_id: 'user-0' }] + const batch = [{ type: 'person', action: 'event', identifiers: { id: 'user-0' } }] + + const response = parseResponse( + { + status: 200, + data: undefined + } as any, + payloads, + batch + ) + + expect(response).toBeInstanceOf(MultiStatusResponse) + expect(response.getResponseAtIndex(0).value()).toEqual({ + status: 200, + body: { person_id: 'user-0' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-0' } } + }) + }) + + it('should treat an empty errors array as all-success', () => { + const payloads = [{ person_id: 'user-0' }] + const batch = [{ type: 'person', action: 'event', identifiers: { id: 'user-0' } }] + + const response = parseResponse( + { + status: 200, + data: { errors: [] } + } as any, + payloads, + batch + ) + + expect(response).toBeInstanceOf(MultiStatusResponse) + expect(response.getResponseAtIndex(0).value()).toEqual({ + status: 200, + body: { person_id: 'user-0' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-0' } } + }) + }) + + it('should skip errors without batch_index', () => { + const payloads = [{ person_id: 'user-0' }] + const batch = [{ type: 'person', action: 'event', identifiers: { id: 'user-0' } }] + + const response = parseResponse( + { + status: 207, + data: { + errors: [{ reason: 'invalid', message: 'some error' }] + } + } as any, + payloads, + batch + ) + + expect(response.getResponseAtIndex(0).value()).toEqual({ + status: 207, + body: { person_id: 'user-0' }, + sent: { type: 'person', action: 'event', identifiers: { id: 'user-0' } } + }) + }) +}) + describe('convertValidTimestamp', () => { it('should leave decimal unix timestamps unchanged', () => { expect(convertValidTimestamp('1712345678.123')).toBe('1712345678.123') }) + + it('should leave integer unix timestamps unchanged', () => { + expect(convertValidTimestamp('1712345678')).toBe('1712345678') + }) + + it('should convert ISO date strings to unix timestamps', () => { + expect(convertValidTimestamp('2023-12-25T14:30:45Z')).toBe(1703514645) + }) + + it('should return non-string values unchanged', () => { + expect(convertValidTimestamp(12345)).toBe(12345) + expect(convertValidTimestamp(null)).toBe(null) + expect(convertValidTimestamp(undefined)).toBe(undefined) + }) + + it('should return invalid date strings unchanged', () => { + expect(convertValidTimestamp('not-a-date')).toBe('not-a-date') + }) }) diff --git a/packages/destination-actions/src/destinations/customerio/createUpdateObject/index.ts b/packages/destination-actions/src/destinations/customerio/createUpdateObject/index.ts index ab2d15fd628..0aef39f666e 100644 --- a/packages/destination-actions/src/destinations/customerio/createUpdateObject/index.ts +++ b/packages/destination-actions/src/destinations/customerio/createUpdateObject/index.ts @@ -1,11 +1,10 @@ import type { ActionDefinition } from '@segment/actions-core' +import { MultiStatusResponse } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { convertAttributeTimestamps, sendSingle, sendBatch, resolveIdentifiers } from '../utils' import { eventProperties } from '../customerio-properties' -type Action = 'identify' | 'identify_anonymous' - const action: ActionDefinition = { title: 'Create or Update Object', description: 'Create an object in Customer.io or update them if they exist.', @@ -91,26 +90,28 @@ const action: ActionDefinition = { ...eventProperties }, - performBatch: (request, { payload: payloads, settings }) => { - const payloadsByAction: Record[]> = { - identify: [], - identify_anonymous: [] - } + performBatch: async (request, { payload: payloads, settings }) => { + const identifyOptions: { index: number; payload: Record }[] = [] + const identifyAnonOptions: { index: number; payload: Record }[] = [] - for (const payload of payloads) { - const { action, body } = mapPayload(payload) + for (let i = 0; i < payloads.length; i++) { + const { action, body } = mapPayload(payloads[i]) - payloadsByAction[action as Action].push(body) + if (action === 'identify_anonymous') { + identifyAnonOptions.push({ index: i, payload: body }) + } else { + identifyOptions.push({ index: i, payload: body }) + } } - return Promise.all([ + const [identifyResult, identifyAnonResult] = await Promise.all([ sendBatch( request, - payloadsByAction.identify.map((payload) => ({ action: 'identify', payload, settings, type: 'object' })) + identifyOptions.map(({ payload }) => ({ action: 'identify', payload, settings, type: 'object' })) ), sendBatch( request, - payloadsByAction.identify_anonymous.map((payload) => ({ + identifyAnonOptions.map(({ payload }) => ({ action: 'identify_anonymous', payload, settings, @@ -118,6 +119,28 @@ const action: ActionDefinition = { })) ) ]) + + const merged = new MultiStatusResponse() + + if (identifyResult) { + for (let i = 0; i < identifyOptions.length; i++) { + const item = identifyResult.getResponseAtIndex(i) + if (item) { + merged.pushResponseObjectAtIndex(identifyOptions[i].index, item) + } + } + } + + if (identifyAnonResult) { + for (let i = 0; i < identifyAnonOptions.length; i++) { + const item = identifyAnonResult.getResponseAtIndex(i) + if (item) { + merged.pushResponseObjectAtIndex(identifyAnonOptions[i].index, item) + } + } + } + + return merged }, perform: (request, { payload, settings }) => { diff --git a/packages/destination-actions/src/destinations/customerio/types.ts b/packages/destination-actions/src/destinations/customerio/types.ts new file mode 100644 index 00000000000..17068bf9cc3 --- /dev/null +++ b/packages/destination-actions/src/destinations/customerio/types.ts @@ -0,0 +1,11 @@ + +export type CustomerIOBatchResponse = { + errors: TrackApiError[] +} + +export type TrackApiError = { + batch_index?: number + reason?: string + field?: string + message?: string +} diff --git a/packages/destination-actions/src/destinations/customerio/utils.ts b/packages/destination-actions/src/destinations/customerio/utils.ts index 94718c56a00..e29af2e453b 100644 --- a/packages/destination-actions/src/destinations/customerio/utils.ts +++ b/packages/destination-actions/src/destinations/customerio/utils.ts @@ -2,6 +2,8 @@ import dayjs from '../../lib/dayjs' import isPlainObject from 'lodash/isPlainObject' import { fullFormats } from 'ajv-formats/dist/formats' import { CUSTOMERIO_TRACK_API_VERSION } from './versioning-info' +import { CustomerIOBatchResponse } from './types' +import { MultiStatusResponse, JSONLikeObject, ModifiedResponse, HTTPError } from '@segment/actions-core' const isEmail = (value: string): boolean => { return (fullFormats.email as RegExp).test(value) @@ -197,20 +199,41 @@ export const resolveIdentifiers = ({ } } -export const sendBatch = (request: Function, options: RequestPayload[]) => { +export const sendBatch = async (request: Function, options: RequestPayload[]) => { if (!options?.length) { return } + const payloads = options.map((opts) => ({ ...opts.payload })) + const [{ settings }] = options const batch = options.map((opts) => buildPayload(opts)) - return request(`${trackApiEndpoint(settings)}/api/${CUSTOMERIO_TRACK_API_VERSION}/batch`, { - method: 'post', - json: { - batch + try { + const response: ModifiedResponse = await request(`${trackApiEndpoint(settings)}/api/${CUSTOMERIO_TRACK_API_VERSION}/batch`, { + method: 'post', + json: { + batch + } + }) + + return parseResponse(response, payloads, batch) + } catch (error) { + const multiStatusResponse = new MultiStatusResponse() + const status = error instanceof HTTPError ? error.response.status : 500 + const errormessage = error instanceof Error ? error.message : 'Unknown error' + + for (let i = 0; i < payloads.length; i++) { + multiStatusResponse.setErrorResponseAtIndex(i, { + status, + errormessage, + sent: batch[i] as unknown as JSONLikeObject, + body: payloads[i] as unknown as JSONLikeObject + }) } - }) + + return multiStatusResponse + } } export const sendSingle = (request: Function, options: RequestPayload) => { @@ -221,4 +244,37 @@ export const sendSingle = (request: Function, optio }) } +export const parseResponse = ( + response: ModifiedResponse, + payloads: Payload[], + batch: Record[] +): MultiStatusResponse => { + const multiStatusResponse = new MultiStatusResponse() + + const errors = response.data?.errors ?? [] + const errorStatus = response.status >= 200 && response.status < 300 ? 400 : response.status + for (const error of errors) { + if (error.batch_index != null) { + multiStatusResponse.setErrorResponseAtIndex(error.batch_index, { + status: errorStatus, + errormessage: error.message || error.reason || 'Unknown error', + sent: batch[error.batch_index] as unknown as JSONLikeObject, + body: payloads[error.batch_index] as unknown as JSONLikeObject + }) + } + } + + for (let i = 0; i < payloads.length; i++) { + if (!multiStatusResponse.isErrorResponseAtIndex(i)) { + multiStatusResponse.setSuccessResponseAtIndex(i, { + status: response.status, + sent: batch[i] as unknown as JSONLikeObject, + body: payloads[i] as unknown as JSONLikeObject + }) + } + } + + return multiStatusResponse +} + export { isIsoDate }