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

Commit

Permalink
fix: support custom object in dynamics (#1093)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasmarshall committed Nov 16, 2023
1 parent b3b62f0 commit d32077c
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 60 deletions.
6 changes: 5 additions & 1 deletion apps/api/integration-test-environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ const getDeletePassthroughRequest = (id, objectName, providerName) => {
method: 'DELETE',
path: `/v2/${toSalesloftObjectName[objectName]}/${id}`,
};
case 'ms_dynamics_365_sales':
return {
method: 'DELETE',
path: `/api/data/v9.2/${objectName}s(${id})`,
};
default:
throw new Error('Unsupported provider');
}
Expand Down Expand Up @@ -146,7 +151,6 @@ class IntegrationEnvironment extends TestEnvironment {
);
}
}
await this.global.db?.end();
await super.teardown();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@ import type {

jest.retryTimes(3);

export const PERMANENT_CUSTOM_OBJECT_NAME = 'PermanentCustomObject';

type PermanentCustomObject = {
name: string;
description__c: string;
int__c?: number;
double__c?: number;
bool__c?: boolean;
};

// Permanent custom object schema:
// {
// "name": "PermanentCustomObject",
Expand Down Expand Up @@ -71,8 +61,14 @@ type PermanentCustomObject = {
// ]
// }

const CUSTOM_OBJECT_NAME_MAP: Record<string, string> = {
hubspot: 'PermanentCustomObject',
salesforce: 'PermanentCustomObject__c',
ms_dynamics_365_sales: 'permanentcustomobject',
};

describe('custom_objects_records', () => {
let testCustomObjectRecord: PermanentCustomObject;
let testCustomObjectRecord: Record<string, unknown>;

beforeEach(() => {
const randomNumber = Math.floor(Math.random() * 100_000);
Expand All @@ -85,9 +81,13 @@ describe('custom_objects_records', () => {
};
});

describe.each(['hubspot', 'salesforce'])('%s', (providerName) => {
describe.each(['hubspot', 'salesforce', 'ms_dynamics_365_sales'])('%s', (providerName) => {
let fullObjectName: string;
beforeEach(() => {
fullObjectName = CUSTOM_OBJECT_NAME_MAP[providerName];
});

test(`Test that POST followed by GET has correct data and properly cache invalidates`, async () => {
const fullObjectName = providerName === 'hubspot' ? 'PermanentCustomObject' : 'PermanentCustomObject__c';
const response = await apiClient.post<CreateCustomObjectRecordResponse>(
`/crm/v2/custom_objects/${fullObjectName}/records`,
{ record: testCustomObjectRecord },
Expand All @@ -101,7 +101,7 @@ describe('custom_objects_records', () => {
addedObjects.push({
id: response.data.record!.id,
providerName,
objectName: PERMANENT_CUSTOM_OBJECT_NAME,
objectName: providerName === 'ms_dynamics_365_sales' ? `cr50e_${fullObjectName}` : fullObjectName,
});
const getResponse = await apiClient.get<GetCustomObjectRecordResponse>(
`/crm/v2/custom_objects/${fullObjectName}/records/${response.data.record!.id}`,
Expand All @@ -112,12 +112,12 @@ describe('custom_objects_records', () => {

expect(getResponse.status).toEqual(200);
expect(getResponse.data.id).toEqual(response.data.record!.id);
expect(getResponse.data.custom_object_name).toEqual(
providerName === 'hubspot' ? 'PermanentCustomObject' : 'PermanentCustomObject__c'
);
expect(providerName === 'hubspot' ? getResponse.data.data.name : getResponse.data.data.Name).toEqual(
testCustomObjectRecord.name
);
expect(getResponse.data.custom_object_name).toEqual(fullObjectName);
expect(
providerName === 'hubspot' || providerName === 'ms_dynamics_365_sales'
? getResponse.data.data.name
: getResponse.data.data.Name
).toEqual(testCustomObjectRecord.name);
expect(getResponse.data.data.int__c?.toString()).toEqual(testCustomObjectRecord.int__c?.toString());
expect(getResponse.data.data.description__c).toEqual(testCustomObjectRecord.description__c);
expect(getResponse.data.data.double__c?.toString()).toEqual(testCustomObjectRecord.double__c?.toString());
Expand All @@ -144,7 +144,6 @@ describe('custom_objects_records', () => {
}, 120_000);

test(`Test that POST followed by PATCH followed by GET has correct data and cache invalidates`, async () => {
const fullObjectName = providerName === 'hubspot' ? 'PermanentCustomObject' : 'PermanentCustomObject__c';
const response = await apiClient.post<CreateCustomObjectRecordResponse>(
`/crm/v2/custom_objects/${fullObjectName}/records`,
{ record: testCustomObjectRecord },
Expand All @@ -158,7 +157,7 @@ describe('custom_objects_records', () => {
addedObjects.push({
id: response.data.record!.id,
providerName,
objectName: PERMANENT_CUSTOM_OBJECT_NAME,
objectName: providerName === 'ms_dynamics_365_sales' ? `cr50e_${fullObjectName}` : fullObjectName,
});

const updatedCustomObjectRecord = {
Expand Down Expand Up @@ -194,10 +193,12 @@ describe('custom_objects_records', () => {

expect(getResponse.status).toEqual(200);
expect(getResponse.data.id).toEqual(response.data.record!.id);
expect(getResponse.data.custom_object_name).toEqual(
providerName === 'hubspot' ? 'PermanentCustomObject' : 'PermanentCustomObject__c'
);
expect(providerName === 'hubspot' ? getResponse.data.data.name : getResponse.data.data.Name).toEqual('updated');
expect(getResponse.data.custom_object_name).toEqual(fullObjectName);
expect(
providerName === 'hubspot' || providerName === 'ms_dynamics_365_sales'
? getResponse.data.data.name
: getResponse.data.data.Name
).toEqual('updated');
expect(getResponse.data.data.int__c?.toString()).toEqual('2');
expect(getResponse.data.data.description__c).toEqual('updated_description');
expect(getResponse.data.data.double__c?.toString()).toEqual('0.2');
Expand All @@ -223,21 +224,27 @@ describe('custom_objects_records', () => {
expect(rawData?.bool__c?.toString()).toEqual('false');
}, 120_000);

test(`Test that Bad Requests have useful errors`, async () => {
const response = await apiClient.post<CreateCustomObjectRecordResponse>(
`/crm/v2/custom_objects/${PERMANENT_CUSTOM_OBJECT_NAME}/records`,
// This will fail because description__c is required
{ record: { ...testCustomObjectRecord, description__c: undefined } },
{
headers: { 'x-provider-name': providerName },
}
);
expect(response.status).toEqual(400);
expect(response.data.errors?.[0].title).toEqual(
providerName === 'hubspot'
? 'Error creating PermanentCustomObject. Some required properties were not set.'
: 'Required fields are missing'
);
}, 120_000);
testIf(
// ms_dynamics_365_sales doesn't seem to do validation of required fields in their API
providerName !== 'ms_dynamics_365_sales',
`Test that Bad Requests have useful errors`,
async () => {
const response = await apiClient.post<CreateCustomObjectRecordResponse>(
`/crm/v2/custom_objects/${fullObjectName}/records`,
// This will fail because description__c is required
{ record: { ...testCustomObjectRecord, description__c: undefined } },
{
headers: { 'x-provider-name': providerName },
}
);
expect(response.status).toEqual(400);
expect(response.data.errors?.[0].title).toEqual(
providerName === 'hubspot'
? 'Error creating PermanentCustomObject. Some required properties were not set.'
: 'Required fields are missing'
);
},
120_000
);
});
});
2 changes: 1 addition & 1 deletion apps/api/services/managed_data_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export class ManagedDataService {
if (pageSize > MAX_PAGE_SIZE) {
throw new BadRequestError(`Page size cannot exceed ${MAX_PAGE_SIZE}`);
}
if (!['hubspot', 'salesforce'].includes(providerName)) {
if (!['hubspot', 'salesforce', 'ms_dynamics_365_sales'].includes(providerName)) {
throw new BadRequestError(`Provider ${providerName} does not support custom object list reads`);
}
const records = await this.#getRecords<SupaglueStandardRecord>(
Expand Down
4 changes: 0 additions & 4 deletions apps/api/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { SupaglueClient } from '@supaglue/schemas';
import type { AxiosInstance } from 'axios';
import type { Pool } from 'pg';

export type AddedObject = {
id: string;
Expand All @@ -21,9 +20,6 @@ declare global {

// eslint-disable-next-line no-var
var testStartTime: Date;

// eslint-disable-next-line no-var
var db: Pool;
}

global.testIf = (condition: boolean, ...args: Parameters<typeof test>) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ function SyncConfigDetailsPanelImpl({ syncConfigId }: SyncConfigDetailsPanelImpl

const selectedProvider = providers.find((p) => p.name === providerName);
const supportsStandardObjects = ['hubspot', 'salesforce', 'ms_dynamics_365_sales', 'gong', 'intercom', 'linear'];
const supportsCustomObjects = ['hubspot', 'salesforce'];
const supportsCustomObjects = ['hubspot', 'salesforce', 'ms_dynamics_365_sales'];

const commonObjectsSupported = selectedProvider?.category !== 'no_category';

Expand Down
8 changes: 6 additions & 2 deletions docs/docs/providers/ms_dynamics_365_sales.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ Supaglue uses the Microsoft Dynamics 365 v9.2 API.

#### Supported standard objects:

N/A
Use the logical name (i.e. `mailbox`) when specifying [standard entities](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/about-entity-reference) to sync.

Syncs are Full or Incremental, and soft deletes are supported.

#### Supported custom objects:

N/A
When specifying a custom entity you'd like to sync, omit the `new_` prefix and use the singular noun. For instance, if your custom entity's table is `new_customobjects`, you would specify `customobject` as the object name.

Syncs are Full or Incremental, and soft deletes are supported.

## Provider setup

Expand Down
Loading

1 comment on commit d32077c

@vercel
Copy link

@vercel vercel bot commented on d32077c Nov 16, 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.