Skip to content
This repository has been archived by the owner on Mar 10, 2024. It is now read-only.

feat: upsert accounts #2006

Merged
merged 2 commits into from
Nov 30, 2023
Merged
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
29 changes: 29 additions & 0 deletions apps/api/routes/engagement/v2/account.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
GetAccountResponse,
ListAccountsResponse,
UpdateAccountResponse,
UpsertAccountResponse,
} from '@supaglue/schemas/v2/engagement';

describe('account', () => {
Expand Down Expand Up @@ -71,6 +72,34 @@ describe('account', () => {
expect(found?.domain).toEqual(testAccount.domain);
}, 120_000);

test('Test that 2 identical upserts only creates 1 record', async () => {
const response = await apiClient.post<UpsertAccountResponse>('/engagement/v2/accounts', {
record: testAccount,
upsert_on: {
name: testAccount.name,
domain: providerName === 'apollo' ? undefined : testAccount.domain,
},
});
expect(response.status).toEqual(201);
expect(response.data.record?.id).toBeTruthy();
addedObjects.push({
id: response.data.record?.id as string,
providerName,
objectName: 'account',
});

const response2 = await apiClient.post<UpsertAccountResponse>('/engagement/v2/accounts', {
record: testAccount,
upsert_on: {
name: testAccount.name,
domain: providerName === 'apollo' ? undefined : testAccount.domain,
},
});
expect(response2.status).toEqual(201);
expect(response2.data.record?.id).toBeTruthy();
expect(response2.data.record?.id).toEqual(response.data.record?.id);
}, 120_000);

test('Test that POST followed by PATCH followed by GET has correct data and cache invalidates', async () => {
const response = await apiClient.post<CreateAccountResponse>(
'/engagement/v2/accounts',
Expand Down
18 changes: 18 additions & 0 deletions apps/api/routes/engagement/v2/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import type {
UpdateAccountQueryParams,
UpdateAccountRequest,
UpdateAccountResponse,
UpsertAccountPathParams,
UpsertAccountRequest,
UpsertAccountResponse,
} from '@supaglue/schemas/v2/engagement';
import { camelcaseKeysSansCustomFields } from '@supaglue/utils/camelcase';
import type { Request, Response } from 'express';
Expand Down Expand Up @@ -90,6 +93,21 @@ export default function init(app: Router): void {
}
);

router.post(
'/_upsert',
async (
req: Request<UpsertAccountPathParams, UpsertAccountResponse, UpsertAccountRequest>,
res: Response<UpsertAccountResponse>
) => {
const id = await engagementCommonObjectService.upsert(
'account',
req.customerConnection,
camelcaseKeysSansCustomFields(req.body)
);
return res.status(201).send({ record: { id } });
}
);

router.patch<string, UpdateAccountPathParams, UpdateAccountResponse, UpdateAccountRequest, UpdateAccountQueryParams>(
'/:account_id',
async (
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/api/v2/engagement/sidebar.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions docs/docs/api/v2/engagement/upsert-account.api.mdx

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions openapi/v2/engagement/openapi.bundle.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions openapi/v2/engagement/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ paths:
$ref: paths/accounts.yaml
'/accounts/{account_id}':
$ref: paths/accounts@{account_id}.yaml
'/accounts/_upsert':
$ref: paths/[email protected]
'/contacts':
$ref: paths/contacts.yaml
'/contacts/_search':
Expand Down
50 changes: 50 additions & 0 deletions openapi/v2/engagement/paths/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
post:
operationId: upsertAccount
summary: Upsert account
description: |
Upsert an account. If the account matching the given criteria does not exist, it will be created. If the account does exist, it will be updated.
Only supported for Salesforce, Hubspot, and Pipedrive.
tags:
- Accounts
security:
- x-api-key: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
record:
$ref: ../components/schemas/create_update_account.yaml
upsert_on:
type: object
description: The criteria to upsert on. If both name and domain are specified, it would perform an AND operation. If more than one account is found that matches, then an error will be thrown.
properties:
name:
type: string
description: The name of the account to upsert on. Supported for all providers.
domain:
type: string
description: The domain of the account to upsert on. Only supported for Outreach and Salesloft.
required:
- record
- upsert_on
responses:
'201':
description: Account upserted
content:
application/json:
schema:
type: object
properties:
errors:
$ref: ../../../common/components/schemas/errors.yaml
record:
$ref: ../../../common/components/schemas/created_model.yaml
warnings:
$ref: ../../../common/components/schemas/warnings.yaml
parameters:
- $ref: ../../../common/components/parameters/header/x-customer-id.yaml
- $ref: ../../../common/components/parameters/header/x-provider-name.yaml
13 changes: 13 additions & 0 deletions packages/core/remotes/categories/engagement/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export type CreateCommonObjectRecordResponse<T extends EngagementCommonObjectTyp
record?: EngagementCommonObjectTypeMap<T>['object'];
};

export type UpsertCommonObjectRecordResponse<T extends EngagementCommonObjectType> =
CreateCommonObjectRecordResponse<T>;

export type UpdateCommonObjectRecordResponse<T extends EngagementCommonObjectType> =
CreateCommonObjectRecordResponse<T>;

Expand All @@ -27,6 +30,10 @@ export interface EngagementRemoteClient extends RemoteClient {
commonObjectType: T,
params: EngagementCommonObjectTypeMap<T>['createParams']
): Promise<CreateCommonObjectRecordResponse<T>>;
upsertCommonObjectRecord<T extends EngagementCommonObjectType>(
commonObjectType: T,
params: EngagementCommonObjectTypeMap<T>['upsertParams']
): Promise<UpsertCommonObjectRecordResponse<T>>;
updateCommonObjectRecord<T extends EngagementCommonObjectType>(
commonObjectType: T,
params: EngagementCommonObjectTypeMap<T>['updateParams']
Expand Down Expand Up @@ -71,6 +78,12 @@ export abstract class AbstractEngagementRemoteClient extends AbstractRemoteClien
): Promise<CreateCommonObjectRecordResponse<T>> {
throw new NotImplementedError();
}
public async upsertCommonObjectRecord<T extends EngagementCommonObjectType>(
commonObjectType: T,
params: EngagementCommonObjectTypeMap<T>['upsertParams']
): Promise<UpsertCommonObjectRecordResponse<T>> {
throw new NotImplementedError();
}
public async updateCommonObjectRecord<T extends EngagementCommonObjectType>(
commonObjectType: T,
params: EngagementCommonObjectTypeMap<T>['updateParams']
Expand Down
52 changes: 52 additions & 0 deletions packages/core/remotes/impl/apollo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
import type {
AccountCreateParams,
AccountUpdateParams,
AccountUpsertParams,
Contact,
ContactCreateParams,
ContactSearchParams,
Expand All @@ -34,6 +35,7 @@ import type { ConnectorAuthConfig } from '../../base';
import type {
CreateCommonObjectRecordResponse,
UpdateCommonObjectRecordResponse,
UpsertCommonObjectRecordResponse,
} from '../../categories/engagement/base';
import { AbstractEngagementRemoteClient } from '../../categories/engagement/base';
import { paginator } from '../../utils/paginator';
Expand Down Expand Up @@ -515,6 +517,36 @@ class ApolloClient extends AbstractEngagementRemoteClient {
}
}

async upsertAccount(params: AccountUpsertParams): Promise<UpsertCommonObjectRecordResponse<'account'>> {
if (params.upsertOn.domain) {
throw new BadRequestError('Domain is not supported when upserting an account in Apollo');
}
if (!params.upsertOn.name) {
throw new BadRequestError('Name is required when upserting an account in Apollo');
}
// search account
const response = await axios.post<ApolloPaginatedAccounts>(
`${this.#baseURL}/v1/accounts/search`,
{
q_organization_name: params.upsertOn.name,
api_key: this.#apiKey,
per_page: MAX_PAGE_SIZE,
},
{
headers: this.#headers,
}
);
console.log(`response.data: `, response.data);

if (response.data.accounts.length > 1) {
throw new BadRequestError('More than one account found for upsert query');
}
if (response.data.accounts.length) {
return this.updateAccount({ ...params.record, id: response.data.accounts[0].id });
}
return await this.createAccount(params.record);
}

async createAccount(params: AccountCreateParams): Promise<CreateCommonObjectRecordResponse<'account'>> {
const response = await axios.post<{ account: Record<string, any> }>(
`${this.#baseURL}/v1/accounts`,
Expand Down Expand Up @@ -733,6 +765,26 @@ class ApolloClient extends AbstractEngagementRemoteClient {
}
}

public override async upsertCommonObjectRecord<T extends EngagementCommonObjectType>(
commonObjectType: T,
params: EngagementCommonObjectTypeMap<T>['upsertParams']
): Promise<UpsertCommonObjectRecordResponse<T>> {
// TODO: figure out why type assertion is required here
switch (commonObjectType) {
case 'account':
return (await this.upsertAccount(params as AccountUpsertParams)) as UpsertCommonObjectRecordResponse<T>;
case 'sequence_state':
case 'contact':
case 'sequence':
case 'sequence_step':
case 'mailbox':
case 'user':
throw new BadRequestError(`Create operation not supported for ${commonObjectType} object`);
default:
throw new BadRequestError(`Common object ${commonObjectType} not supported`);
}
}

public override async updateCommonObjectRecord<T extends EngagementCommonObjectType>(
commonObjectType: T,
params: EngagementCommonObjectTypeMap<T>['updateParams']
Expand Down
Loading