From da639cc097957a1faa5109d30a4d8183b3d62dd2 Mon Sep 17 00:00:00 2001 From: Anton CAZALET Date: Tue, 17 Jan 2023 12:29:19 +0100 Subject: [PATCH] Add getAttributes method (#129) --- README.md | 23 +++++++++++++++ index.ts | 1 + lib/api.ts | 16 +++++++++-- lib/track.ts | 6 +++- lib/utils.ts | 5 ++++ test/api.ts | 79 +++++++++++++++++++++++++++++++++++++++++++++------ test/track.ts | 10 +++++++ 7 files changed, 129 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1561602..cae61b3 100644 --- a/README.md +++ b/README.md @@ -373,6 +373,29 @@ api.getCustomersByEmail("test@test.com"); - **email**: String (required) +### api.getAttributes(id, id_type) + +Returns a list of attributes for a customer profile. + +```javascript +api.getAttributes("1", "id"); +``` + +OR + +```javascript +const { IdentifierType } = require("customerio-node"); + +api.getAttributes("1", IdentifierType.ID); +``` + +[You can learn more about the available recipient fields here](https://customer.io/docs/api/#operation/getPersonAttributes). + +#### Options + +- **id**: Customer identifier, String or number (required) +- **id_type**: One of the ID types - "id" / "email" / "cio_id" (default is "id") + ### api.listExports() Return a list of your exports. Exports are point-in-time people or campaign metrics. diff --git a/index.ts b/index.ts index e37559b..f50304d 100644 --- a/index.ts +++ b/index.ts @@ -1,4 +1,5 @@ export * from './lib/track'; export * from './lib/api'; export * from './lib/regions'; +export { IdentifierType } from './lib/types'; export { CustomerIORequestError } from './lib/utils'; diff --git a/lib/api.ts b/lib/api.ts index b088a68..25759f7 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -2,8 +2,8 @@ import type { RequestOptions } from 'https'; import Request, { BearerAuth, RequestData } from './request'; import { Region, RegionUS } from './regions'; import { SendEmailRequest } from './api/requests'; -import { cleanEmail, isEmpty, MissingParamError } from './utils'; -import { Filter } from './types'; +import { cleanEmail, isEmpty, isIdentifierType, MissingParamError } from './utils'; +import { Filter, IdentifierType } from './types'; type APIDefaults = RequestOptions & { region: Region; url?: string }; @@ -138,6 +138,18 @@ export class APIClient { return this.request.post(`${this.apiRoot}/exports/deliveries`, { newsletter_id: newsletterId, ...options }); } + + getAttributes(id: string | number, idType: IdentifierType = IdentifierType.Id) { + if (isEmpty(id)) { + throw new MissingParamError('customerId'); + } + + if (!isIdentifierType(idType)) { + throw new Error('idType must be one of "id", "cio_id", or "email"'); + } + + return this.request.get(`${this.apiRoot}/customers/${id}/attributes?id_type=${idType}`); + } } export { SendEmailRequest } from './api/requests'; diff --git a/lib/track.ts b/lib/track.ts index 7b7c5ce..507ebcc 100644 --- a/lib/track.ts +++ b/lib/track.ts @@ -1,7 +1,7 @@ import type { RequestOptions } from 'https'; import Request, { BasicAuth, RequestData, PushRequestData } from './request'; import { Region, RegionUS } from './regions'; -import { isEmpty, MissingParamError } from './utils'; +import { isEmpty, isIdentifierType, MissingParamError } from './utils'; import { IdentifierType } from './types'; type TrackDefaults = RequestOptions & { region: Region; url?: string }; @@ -156,6 +156,10 @@ export class TrackClient { throw new MissingParamError('secondaryId'); } + if (!isIdentifierType(primaryIdType) || !isIdentifierType(secondaryIdType)) { + throw new Error('primaryIdType and secondaryIdType must be one of "id", "cio_id", or "email"'); + } + return this.request.post(`${this.trackRoot}/merge_customers`, { primary: { [primaryIdType]: primaryId, diff --git a/lib/utils.ts b/lib/utils.ts index ac573d7..d93723d 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,4 +1,5 @@ import { IncomingMessage } from 'http'; +import { IdentifierType } from '../lib/types'; export const isEmpty = (value: unknown) => { return value === null || value === undefined || (typeof value === 'string' && value.trim() === ''); @@ -8,6 +9,10 @@ export const cleanEmail = (email: string) => { return email.split('@').map(encodeURIComponent).join('@'); }; +export const isIdentifierType = (value: unknown) => { + return Object.values(IdentifierType).includes(value as IdentifierType); +}; + export class CustomerIORequestError extends Error { statusCode: number; response: IncomingMessage; diff --git a/test/api.ts b/test/api.ts index 3072bfe..c79533f 100644 --- a/test/api.ts +++ b/test/api.ts @@ -2,7 +2,7 @@ import avaTest, { TestFn } from 'ava'; import sinon, { SinonStub } from 'sinon'; import { APIClient, DeliveryExportMetric, DeliveryExportRequestOptions, SendEmailRequest } from '../lib/api'; import { RegionUS, RegionEU } from '../lib/regions'; -import { Filter } from '../lib/types'; +import { Filter, IdentifierType } from '../lib/types'; type TestContext = { client: APIClient }; @@ -140,28 +140,27 @@ test('#getCustomersByEmail: searching for a customer email (default)', (t) => { const email = 'hello@world.com'; t.context.client.getCustomersByEmail(email); t.truthy((t.context.client.request.get as SinonStub).calledWith(`${RegionUS.apiUrl}/customers?email=${email}`)); -}) +}); test('#getCustomersByEmail: should throw error when email is empty', (t) => { const email = ''; t.throws(() => t.context.client.getCustomersByEmail(email)); -}) - +}); test('#getCustomersByEmail: should throw error when email is null', (t) => { const email: unknown = null; t.throws(() => t.context.client.getCustomersByEmail(email as string)); -}) +}); test('#getCustomersByEmail: should throw error when email is undefined', (t) => { const email: unknown = undefined; t.throws(() => t.context.client.getCustomersByEmail(email as string)); -}) +}); test('#getCustomersByEmail: should throw error when email is not a string object', (t) => { - const email: unknown = { "object": "test" }; + const email: unknown = { object: 'test' }; t.throws(() => t.context.client.getCustomersByEmail(email as string)); -}) +}); test('#sendEmail: adding attachments with encoding (default)', (t) => { sinon.stub(t.context.client.request, 'post'); @@ -387,3 +386,67 @@ test('#createDeliveriesExport: fails without id', (t) => { }); t.falsy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/exports/deliveries`)); }); + +test('#getAttributes: fails without customerId', (t) => { + sinon.stub(t.context.client.request, 'get'); + t.throws(() => (t.context.client.getAttributes as any)(), { + message: 'customerId is required', + }); + t.falsy( + (t.context.client.request.get as SinonStub).calledWith(`${RegionUS.apiUrl}/customers/1/attributes?id_type=id`), + ); +}); + +test('#getAttributes: fails if id_type is not id, cio_id nor email', (t) => { + sinon.stub(t.context.client.request, 'get'); + t.throws(() => (t.context.client.getAttributes as any)(1, 'first_name'), { + message: 'idType must be one of "id", "cio_id", or "email"', + }); + t.falsy( + (t.context.client.request.get as SinonStub).calledWith(`${RegionUS.apiUrl}/customers/1/attributes?id_type=id`), + ); +}); + +test('#getAttributes: fails if id_type is null', (t) => { + sinon.stub(t.context.client.request, 'get'); + t.throws(() => (t.context.client.getAttributes as any)(1, null), { + message: 'idType must be one of "id", "cio_id", or "email"', + }); + t.falsy( + (t.context.client.request.get as SinonStub).calledWith(`${RegionUS.apiUrl}/customers/1/attributes?id_type=id`), + ); +}); + +test('#getAttributes: success with default type id', (t) => { + sinon.stub(t.context.client.request, 'get'); + t.context.client.getAttributes('1'); + t.truthy( + (t.context.client.request.get as SinonStub).calledWith(`${RegionUS.apiUrl}/customers/1/attributes?id_type=id`), + ); +}); + +test('#getAttributes: success with type id', (t) => { + sinon.stub(t.context.client.request, 'get'); + t.context.client.getAttributes('1', IdentifierType.Id); + t.truthy( + (t.context.client.request.get as SinonStub).calledWith(`${RegionUS.apiUrl}/customers/1/attributes?id_type=id`), + ); +}); + +test('#getAttributes: success with type cio id', (t) => { + sinon.stub(t.context.client.request, 'get'); + t.context.client.getAttributes('1', IdentifierType.CioId); + t.truthy( + (t.context.client.request.get as SinonStub).calledWith(`${RegionUS.apiUrl}/customers/1/attributes?id_type=cio_id`), + ); +}); + +test('#getAttributes: success with type email', (t) => { + sinon.stub(t.context.client.request, 'get'); + t.context.client.getAttributes('test@email.com', IdentifierType.Email); + t.truthy( + (t.context.client.request.get as SinonStub).calledWith( + `${RegionUS.apiUrl}/customers/test@email.com/attributes?id_type=email`, + ), + ); +}); diff --git a/test/track.ts b/test/track.ts index 5dc261c..80e70ed 100644 --- a/test/track.ts +++ b/test/track.ts @@ -289,3 +289,13 @@ test('#mergeCustomers works', (t) => { ); }); }); + +test('#mergeCustomers: fails if id_type is not id, cio_id nor email', (t) => { + t.throws(() => (t.context.client.mergeCustomers as any)(undefined, 'id1', IdentifierType.Id, 'id2'), { + message: 'primaryIdType and secondaryIdType must be one of "id", "cio_id", or "email"', + }); + + t.throws(() => (t.context.client.mergeCustomers as any)(IdentifierType.Id, 'id1', undefined, 'id2'), { + message: 'primaryIdType and secondaryIdType must be one of "id", "cio_id", or "email"', + }); +});