Skip to content

Commit

Permalink
Merge pull request #289 from prezly/feature/dev-12883-tag-creator-las…
Browse files Browse the repository at this point in the history
…t_modified_user-created_at-updated_at

[DEV-12883] Feature - Declare Contact Tags API + extend it
  • Loading branch information
e1himself authored Apr 25, 2024
2 parents 3b62dab + 3a9ad1b commit 2bf4622
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 11 deletions.
5 changes: 4 additions & 1 deletion src/Client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { DeferredJobsApiClient } from './api';
import { createApiClient, createDeferredJobsApiClient, type Fetch } from './api';
import { Contacts } from './endpoints';
import {
Accounts,
Billing,
NewsroomSubscriptions,
Campaigns,
CampaignRecipients,
Contacts,
ContactTags,
ContactsExports,
Coverage,
Jobs,
Expand Down Expand Up @@ -47,6 +48,7 @@ export interface Client {
campaigns: Campaigns.Client;
campaignRecipients: CampaignRecipients.Client;
contacts: Contacts.Client;
contactTags: ContactTags.Client;
contactsExports: ContactsExports.Client;
coverage: Coverage.Client;
jobs: Jobs.Client;
Expand Down Expand Up @@ -93,6 +95,7 @@ export function createClient({
campaigns: Campaigns.createClient(api),
campaignRecipients: CampaignRecipients.createClient(api),
contacts: Contacts.createClient(api),
contactTags: ContactTags.createClient(api),
contactsExports: ContactsExports.createClient(api),
coverage: Coverage.createClient(api),
jobs: Jobs.createClient(api),
Expand Down
138 changes: 138 additions & 0 deletions src/endpoints/ContactTags/Client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { DeferredJobsApiClient } from '../../api';
import { routing } from '../../routing';
import { type Contact, type ContactTag, Query } from '../../types';
import { SortOrder } from '../../types';

import type {
CreateOptions,
CreateRequest,
CreateResponse,
ListOptions,
ListResponse,
MergeRequest,
MergeResponse,
SearchOptions,
SearchResponse,
UpdateOptions,
UpdateRequest,
UpdateResponse,
} from './types';

export type Client = ReturnType<typeof createClient>;

// Not putting these into the `./types` module to keep it local.
type RawModificationResponse = {
tag: ContactTag;
deleted_tags_ids: ContactTag['id'][];
};
type RawDeleteResponse = {
deleted_tags_ids: ContactTag['id'][];
};

export function createClient(api: DeferredJobsApiClient) {
async function list(options: ListOptions = {}): Promise<ListResponse> {
const { sortOrder } = options;
const { tags } = await api.get<{
tags: ContactTag[];
}>(routing.contactsTagsUrl, {
query: {
sort: SortOrder.stringify(sortOrder),
},
});

return { tags };
}

async function search(options: SearchOptions = {}): Promise<SearchResponse> {
const { query, sortOrder } = options;
const { tags } = await api.post<{
tags: ContactTag[];
}>(routing.contactsTagsUrl, {
query: {
sort: SortOrder.stringify(sortOrder),
query: Query.stringify(query),
},
});

return { tags };
}

async function get(id: ContactTag['id']): Promise<ContactTag> {
const url = `${routing.contactsTagsUrl}/${id}`;
const { tag } = await api.get<{ tag: ContactTag }>(url);

return tag;
}

async function create(
payload: CreateRequest,
{ force = false }: CreateOptions = {},
): Promise<CreateResponse> {
const data = await api.post<RawModificationResponse>(routing.contactsTagsUrl, {
payload,
query: {
force: force || undefined, // Convert `false` to `undefined`
},
});
return {
tag: data.tag,
deleted: data.deleted_tags_ids,
};
}

async function update(
id: Contact['id'],
payload: UpdateRequest,
{ force = false }: UpdateOptions = {},
): Promise<UpdateResponse> {
const url = `${routing.contactsTagsUrl}/${id}`;
const data = await api.patch<RawModificationResponse>(url, {
payload,
query: {
force: force || undefined, // Convert `false` to `undefined`
},
});
return {
tag: data.tag,
deleted: data.deleted_tags_ids,
};
}

async function merge({ name, tags }: MergeRequest): Promise<MergeResponse> {
const url = `${routing.contactsTagsUrl}/merge`;
const data = await api.post<RawModificationResponse>(url, {
payload: { name, tags },
});
return {
tag: data.tag,
deleted: data.deleted_tags_ids,
};
}

async function doDelete(id: ContactTag['id']) {
const url = `${routing.contactsTagsUrl}/${id}`;
const data = await api.delete<RawDeleteResponse>(url);
return { deleted: data.deleted_tags_ids };
}

async function doDeleteUnused() {
const data = await api.delete<RawDeleteResponse>(routing.contactsTagsUrl, {
query: {
filter: 'unused',
},
});

return { deleted: data.deleted_tags_ids };
}

return {
list,
search,
get,
create,
update,
merge,
delete: doDelete,
deleteUnused: doDeleteUnused,
};
}
2 changes: 2 additions & 0 deletions src/endpoints/ContactTags/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Client';
export * from './types';
52 changes: 52 additions & 0 deletions src/endpoints/ContactTags/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ContactTag, Query } from '../../types';
import type { SortOrder } from '../../types';

export interface ListOptions {
sortOrder?: SortOrder | string;
}

export interface ListResponse {
tags: ContactTag[];
}

export interface SearchOptions extends ListOptions {
query?: Query;
}

export type SearchResponse = ListResponse;

export interface CreateRequest {
name: string;
}

export interface CreateOptions {
force?: boolean;
}

export interface CreateResponse {
tag: ContactTag;
deleted: ContactTag['id'][];
}

export interface UpdateRequest {
name: string;
}

export interface UpdateOptions {
force?: boolean;
}

export interface UpdateResponse {
tag: ContactTag;
deleted: ContactTag['id'][];
}

export interface MergeRequest {
name: string;
tags: ContactTag.Identifier[];
}

export interface MergeResponse {
tag: ContactTag;
deleted: ContactTag['id'][];
}
17 changes: 7 additions & 10 deletions src/endpoints/Contacts/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ProgressPromise } from '@prezly/progress-promise';

import type { DeferredJobsApiClient } from '../../api';
import { routing } from '../../routing';
import type { Contact, Pagination } from '../../types';
import type { ContactTag, Contact, Pagination } from '../../types';
import { SortOrder } from '../../types';

import type {
Expand All @@ -16,16 +16,13 @@ import type {
UpdateRequest,
} from './types';

type TagId = number;
type TagName = string;

export type Client = ReturnType<typeof createClient>;

export function createClient(api: DeferredJobsApiClient) {
/**
* List Contacts Exports with sorting, and pagination.
*/
async function list(options: ListOptions): Promise<ListResponse> {
async function list(options: ListOptions = {}): Promise<ListResponse> {
const { limit, offset, sortOrder } = options;
const { contacts, pagination, sort } = await api.get<{
contacts: Contact[];
Expand All @@ -45,7 +42,7 @@ export function createClient(api: DeferredJobsApiClient) {
/**
* List Contacts Exports with sorting, pagination, and filtering.
*/
async function search(options: SearchOptions): Promise<SearchResponse> {
async function search(options: SearchOptions = {}): Promise<SearchResponse> {
const { limit, offset, query, sortOrder } = options;
const url = `${routing.contactsUrl}/search`;
const { contacts, pagination, sort } = await api.post<{
Expand Down Expand Up @@ -95,7 +92,7 @@ export function createClient(api: DeferredJobsApiClient) {
return contact;
}

async function tag(id: Contact['id'], tags: (TagId | TagName)[]): Promise<Contact> {
async function tag(id: Contact['id'], tags: ContactTag.Identifier[]): Promise<Contact> {
const url = `${routing.contactsUrl}/${id}`;
const { contact } = await api.patch<{ contact: Contact }>(url, {
payload: {
Expand All @@ -105,7 +102,7 @@ export function createClient(api: DeferredJobsApiClient) {
return contact;
}

async function untag(id: Contact['id'], tags: (TagId | TagName)[]): Promise<Contact> {
async function untag(id: Contact['id'], tags: ContactTag.Identifier[]): Promise<Contact> {
const url = `${routing.contactsUrl}/${id}`;
const { contact } = await api.patch<{ contact: Contact }>(url, {
payload: {
Expand All @@ -117,7 +114,7 @@ export function createClient(api: DeferredJobsApiClient) {

async function bulkTag(
selector: BulkSelector,
tags: (TagId | TagName)[],
tags: ContactTag.Identifier[],
): ProgressPromise<undefined> {
const { scope, query } = selector;
return api.patch(routing.contactsUrl, {
Expand All @@ -127,7 +124,7 @@ export function createClient(api: DeferredJobsApiClient) {

async function bulkUntag(
selector: BulkSelector,
tags: (TagId | TagName)[],
tags: ContactTag.Identifier[],
): ProgressPromise<undefined> {
const { query, scope } = selector;
return api.patch(routing.contactsUrl, {
Expand Down
1 change: 1 addition & 0 deletions src/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * as Accounts from './Accounts';
export * as Billing from './Billing';
export * as CampaignRecipients from './CampaignRecipients';
export * as Campaigns from './Campaigns';
export * as ContactTags from './ContactTags';
export * as Contacts from './Contacts';
export * as ContactsExports from './ContactsExports';
export * as Coverage from './Coverage';
Expand Down
1 change: 1 addition & 0 deletions src/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const routing = {
campaignRecipientsUrl: '/v2/campaigns/:campaign_id/recipients',
contactsExportsUrl: '/v2/contacts/exports',
contactsUrl: '/v2/contacts',
contactsTagsUrl: '/v2/contacts-tags',
coverageUrl: '/v2/coverage',
jobsUrl: '/v2/jobs',
licenseUrl: '/v2/licenses',
Expand Down
18 changes: 18 additions & 0 deletions src/types/ContactTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { UserRef } from './User';

type Iso8601DateTime = string;

export interface ContactTag {
id: number;
name: string;
contacts_number: number;
contacts_url: string;
created_at: Iso8601DateTime | undefined; // May be undefined for older tags
updated_at: Iso8601DateTime | undefined; // May be undefined for older tags
creator: UserRef | undefined; // May be undefined for older tags
last_updated_by_user: UserRef | undefined; // May be undefined for older tags
}

export namespace ContactTag {
export type Identifier = ContactTag['id'] | ContactTag['name'];
}
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './Campaign';
export * from './Category';
export * from './Contact';
export * from './ContactDuplicateSuggestion';
export * from './ContactTag';
export * from './ContactsBulkSelector';
export * from './ContactsExport';
export * from './ContactsScope';
Expand Down

0 comments on commit 2bf4622

Please sign in to comment.