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

Commit

Permalink
feat: upsert accounts (#2006)
Browse files Browse the repository at this point in the history
Tested locally + wrote integration tests
  • Loading branch information
asdfryan authored Nov 30, 2023
1 parent 13d1e90 commit 1c0c438
Show file tree
Hide file tree
Showing 22 changed files with 533 additions and 28 deletions.
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

1 comment on commit 1c0c438

@vercel
Copy link

@vercel vercel bot commented on 1c0c438 Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

supaglue-docs – ./docs

supaglue-docs-git-main-supaglue.vercel.app
supaglue-docs-supaglue.vercel.app
docs.supaglue.com

Please sign in to comment.