Skip to content

Commit

Permalink
Add getAttributes method (#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoncazalet authored Jan 17, 2023
1 parent ccb00b2 commit da639cc
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 11 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,29 @@ api.getCustomersByEmail("[email protected]");

- **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.
Expand Down
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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';
16 changes: 14 additions & 2 deletions lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -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';
6 changes: 5 additions & 1 deletion lib/track.ts
Original file line number Diff line number Diff line change
@@ -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 };
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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() === '');
Expand All @@ -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;
Expand Down
79 changes: 71 additions & 8 deletions test/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -140,28 +140,27 @@ test('#getCustomersByEmail: searching for a customer email (default)', (t) => {
const email = '[email protected]';
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');
Expand Down Expand Up @@ -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('[email protected]', IdentifierType.Email);
t.truthy(
(t.context.client.request.get as SinonStub).calledWith(
`${RegionUS.apiUrl}/customers/[email protected]/attributes?id_type=email`,
),
);
});
10 changes: 10 additions & 0 deletions test/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"',
});
});

0 comments on commit da639cc

Please sign in to comment.