Skip to content

Commit

Permalink
feat(integrations): add validation rules to salesforce fetch-fields
Browse files Browse the repository at this point in the history
  • Loading branch information
hassan254-prog committed Sep 27, 2024
1 parent 3bdf6fd commit 4e75075
Show file tree
Hide file tree
Showing 10 changed files with 457 additions and 51 deletions.
11 changes: 9 additions & 2 deletions flows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3764,13 +3764,14 @@ integrations:
actions:
fetch-fields:
description: >
Fetch available task fields. If the input is not specified then it
defaults back to "Task"
Fetch available task fields, child relationships and validation rules.
If the input is not specified then it defaults back to "Task"
Data Validation: Parses all incoming data with Zod. Does not fail on
parsing error will instead log parse error and return result.
scopes: offline_access,api
input: SalesforceEntity
version: 1.0.0
output: SalesforceFieldSchema
endpoint: GET /salesforce/fetch-fields
syncs:
Expand Down Expand Up @@ -3867,6 +3868,7 @@ integrations:
SalesforceFieldSchema:
fields: Field[]
childRelationships: ChildField[]
validationRules: ValidationRule[]
NestedFieldSchema:
fields: Field[]
Field:
Expand All @@ -3887,6 +3889,11 @@ integrations:
method: string
url: string
code: string
ValidationRule:
id: string
name: string
errorConditionFormula: string
errorMessage: string
sharepoint-online:
actions:
list-sites:
Expand Down
166 changes: 118 additions & 48 deletions integrations/salesforce/actions/fetch-fields.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,61 @@
import type { NangoAction, SalesforceFieldSchema, ProxyConfiguration, ActionResponseError, ChildField, Field, SalesforceEntity } from '../../models';
import { fieldSchema, childFieldSchema } from '../schema.zod.js';
import type {
NangoAction,
SalesforceFieldSchema,
ProxyConfiguration,
ActionResponseError,
ChildField,
Field,
SalesforceEntity,
ValidationRule
} from '../../models';
import { fieldSchema, childFieldSchema, validationRuleSchema } from '../schema.zod.js';
import type { DescribeSObjectResult, SalesForceField, ChildRelationship, ValidationRecord, ValidationRuleResponse } from '../types';

/**
* This action fetches the available objects for a given organization in Salesforce.
* This action retrieves the available properties of a custom object, including fields, child relationships, and validation rules, for a given organization in Salesforce.
* https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_describe.htm
* @param nango - The NangoAction object
* @returns SalesforceFieldSchema - The fields for the object
* https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_validationrule.htm
*
* @param nango - The NangoAction instance used for making API requests.
* @param input - SalesforceEntity defining the object to describe
* @returns A promise that resolves to a SalesforceFieldSchema object containing: fields, child relationships, and validation rules for the object
*/
export default async function runAction(nango: NangoAction, input: SalesforceEntity): Promise<SalesforceFieldSchema> {
try {
const entity = input?.name || 'Task';
const proxyConfig: ProxyConfiguration = {

const proxyConfigFields: ProxyConfiguration = {
endpoint: `/services/data/v60.0/sobjects/${entity}/describe`,
retries: 5
retries: 10
};

const response = await nango.get(proxyConfig);
const proxyConfigValidationIds: ProxyConfiguration = {
endpoint: `/services/data/v60.0/tooling/query`,
retries: 10,
params: {
q: `SELECT Id, ValidationName FROM ValidationRule WHERE EntityDefinition.QualifiedApiName='${entity}'`
}
};

const { data } = response;
const { fields, childRelationships } = data;
// Parallelize both requests as we won't get rate limited in here.
const [fieldsResponse, validationResponse] = await Promise.all([
nango.get<DescribeSObjectResult>(proxyConfigFields),
nango.get<ValidationRuleResponse>(proxyConfigValidationIds)
]);

const fieldResults = mapFields({ fields });
const childRelationshipsResults = mapChildRelationships({
relationships: childRelationships
});
const { fields, childRelationships } = fieldsResponse.data;
const validationRulesIds = validationResponse.data.records;

const validationRulesData: ValidationRecord[] = await fetchValidationRuleMetadata(nango, validationRulesIds);

const fieldResults = mapFields(fields);
const childRelationshipsResults = mapChildRelationships(childRelationships);
const validationRulesResults = mapValidationRules(validationRulesData);

return {
fields: fieldResults,
childRelationships: childRelationshipsResults
childRelationships: childRelationshipsResults,
validationRules: validationRulesResults
};
} catch (error: unknown) {
const errorResponse = error as Record<string, unknown>;
Expand All @@ -45,44 +73,86 @@ export default async function runAction(nango: NangoAction, input: SalesforceEnt
}

/**
* Maps the fields from the Salesforce API response to the Field schema
* @param fields - The unverified data returned from the Salesforce API
* @param nango - The NangoAction object
* @returns The mapped fields and a boolean indicating if the mapping was successful
* Fetches metadata for multiple validation rules from the Salesforce Tooling API.
* Note: The maximum number of active validation rules per object is limited to 500,
* and this limit can vary based on the Salesforce edition.
* For more details, refer to the Salesforce documentation:
* https://help.salesforce.com/s/articleView?id=000383591&type=1
*
* @param nango - The NangoAction instance used for making API requests.
* @param validationRulesIds - An array of objects containing the IDs and names of the validation rules.
* @returns A promise that resolves to an array of ValidationRecord objects with metadata for each validation rule.
*/
function mapFields({ fields }: { fields: Field[] }): Field[] {
const validatedFields: Field[] = [];
for (const field of fields) {
const resultData = field;
const parsedField = fieldSchema.parse({
name: resultData['name'],
label: resultData['label'],
type: resultData['type'],
referenceTo: resultData['referenceTo'],
relationshipName: resultData['relationshipName']
});
async function fetchValidationRuleMetadata(nango: NangoAction, validationRulesIds: { Id: string; ValidationName: string }[]): Promise<ValidationRecord[]> {
const metadataFetchPromises: Promise<ValidationRecord>[] = validationRulesIds.map((rule) =>
nango
.get<ValidationRuleResponse>({
endpoint: `/services/data/v60.0/tooling/query`,
retries: 10,
params: {
q: `SELECT Id, ValidationName, Metadata FROM ValidationRule WHERE Id='${rule.Id}'`
}
})
.then((response: { data: ValidationRuleResponse }) => {
const record = response.data.records[0];
if (record) {
return { ...record, ValidationName: rule.ValidationName };
}
throw new nango.ActionError({
message: `Validation rule with ID ${rule.Id} not found.`
});
})
);

validatedFields.push(parsedField);
}
const settledResults = await Promise.allSettled(metadataFetchPromises);

return validatedFields;
return settledResults.filter((result) => result.status === 'fulfilled').map((result) => (result as PromiseFulfilledResult<ValidationRecord>).value);
}

/**
* Maps the child relationships from the Salesforce API response to the ChildField schema
* @param relationships - The unverified data returned from the Salesforce API
* @returns The mapped child relationships and a boolean indicating if the mapping was successful
* Maps the fields from the Salesforce API response to the Field schema.
* @param fields - Array of fields from Salesforce
* @returns An array of mapped validation rules conforming to the Field schema.
*/
function mapChildRelationships({ relationships }: { relationships: Record<string, unknown>[] }): ChildField[] {
const validatedRelationships: ChildField[] = [];
for (const relationship of relationships) {
const resultData = relationship;
const parsedChildField = childFieldSchema.parse({
object: resultData['childSObject'],
field: resultData['field'],
relationshipName: resultData['relationshipName']
});
validatedRelationships.push(parsedChildField);
}
return validatedRelationships;
function mapFields(fields: SalesForceField[]): Field[] {
return fields.map((field) =>
fieldSchema.parse({
name: field.name,
label: field.label,
type: field.type,
referenceTo: field.referenceTo,
relationshipName: field.relationshipName
})
);
}

/**
* Maps child relationships from Salesforce API to the ChildField schema.
* @param relationships - Array of child relationships from Salesforce
* @returns An array of mapped child relationships conforming to the ChildField schema.
*/
function mapChildRelationships(relationships: ChildRelationship[]): ChildField[] {
return relationships.map((relationship) =>
childFieldSchema.parse({
object: relationship.childSObject,
field: relationship.field,
relationshipName: relationship.relationshipName
})
);
}

/**
* Maps validation rules from Salesforce Tooling API response to the ValidationRule schema.
* @param validationRules - Array of validation rules from Salesforce Tooling API
* @returns An array of mapped validation rules conforming to the ValidationRule schema.
*/
function mapValidationRules(validationRules: ValidationRecord[]): ValidationRule[] {
return validationRules.map((rule) =>
validationRuleSchema.parse({
id: rule.Id,
name: rule.ValidationName,
errorConditionFormula: rule.Metadata.errorConditionFormula,
errorMessage: rule.Metadata.errorMessage
})
);
}
9 changes: 8 additions & 1 deletion integrations/salesforce/nango.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ integrations:
actions:
fetch-fields:
description: |
Fetch available task fields. If the input is not specified then it defaults back to "Task"
Fetch available task fields, child relationships and validation rules. If the input is not specified then it defaults back to "Task"
Data Validation: Parses all incoming data with Zod. Does not fail on parsing error will instead log parse error and return result.
scopes: offline_access,api
input: SalesforceEntity
version: 1.0.0
output: SalesforceFieldSchema
endpoint: GET /salesforce/fetch-fields
syncs:
Expand Down Expand Up @@ -103,6 +104,7 @@ models:
SalesforceFieldSchema:
__extends: NestedFieldSchema
childRelationships: ChildField[]
validationRules: ValidationRule[]
NestedFieldSchema:
fields: Field[]
Field:
Expand All @@ -123,3 +125,8 @@ models:
method: string
url: string
code: string
ValidationRule:
id: string
name: string
errorConditionFormula: string
errorMessage: string
7 changes: 7 additions & 0 deletions integrations/salesforce/schema.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,10 @@ export const actionResponseErrorSchema = z.object({
message: z.string(),
details: actionResponseErrorDetailsSchema.optional()
});

export const validationRuleSchema = z.object({
id: z.string(),
name: z.string(),
errorConditionFormula: z.string(),
errorMessage: z.string()
});
27 changes: 27 additions & 0 deletions integrations/salesforce/tests/salesforce-accounts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { vi, expect, it, describe } from "vitest";
import type { NangoSync } from "../models.js";

import fetchData from "../syncs/accounts.js";

describe("salesforce accounts tests", () => {
const nangoMock = new global.vitest.NangoSyncMock({
dirname: __dirname,
name: "accounts",
Model: "SalesforceAccount"
});
it("should get, map correctly the data and batchSave the result", async () => {
await fetchData(nangoMock);

const batchSaveData = await nangoMock.getBatchSaveData();
expect(nangoMock.batchSave).toHaveBeenCalledWith(batchSaveData, "SalesforceAccount");
});

it('should get, map correctly the data and batchDelete the result', async () => {
await fetchData(nangoMock);

const batchDeleteData = await nangoMock.getBatchDeleteData();
if (batchDeleteData && batchDeleteData.length > 0) {
expect(nangoMock.batchDelete).toHaveBeenCalledWith(batchDeleteData, "SalesforceAccount");
}
});
});
27 changes: 27 additions & 0 deletions integrations/salesforce/tests/salesforce-articles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { vi, expect, it, describe } from "vitest";
import type { NangoSync } from "../models.js";

import fetchData from "../syncs/articles.js";

describe("salesforce articles tests", () => {
const nangoMock = new global.vitest.NangoSyncMock({
dirname: __dirname,
name: "articles",
Model: "SalesforceArticle"
});
it("should get, map correctly the data and batchSave the result", async () => {
await fetchData(nangoMock);

const batchSaveData = await nangoMock.getBatchSaveData();
expect(nangoMock.batchSave).toHaveBeenCalledWith(batchSaveData, "SalesforceArticle");
});

it('should get, map correctly the data and batchDelete the result', async () => {
await fetchData(nangoMock);

const batchDeleteData = await nangoMock.getBatchDeleteData();
if (batchDeleteData && batchDeleteData.length > 0) {
expect(nangoMock.batchDelete).toHaveBeenCalledWith(batchDeleteData, "SalesforceArticle");
}
});
});
27 changes: 27 additions & 0 deletions integrations/salesforce/tests/salesforce-contacts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { vi, expect, it, describe } from "vitest";
import type { NangoSync } from "../models.js";

import fetchData from "../syncs/contacts.js";

describe("salesforce contacts tests", () => {
const nangoMock = new global.vitest.NangoSyncMock({
dirname: __dirname,
name: "contacts",
Model: "SalesforceContact"
});
it("should get, map correctly the data and batchSave the result", async () => {
await fetchData(nangoMock);

const batchSaveData = await nangoMock.getBatchSaveData();
expect(nangoMock.batchSave).toHaveBeenCalledWith(batchSaveData, "SalesforceContact");
});

it('should get, map correctly the data and batchDelete the result', async () => {
await fetchData(nangoMock);

const batchDeleteData = await nangoMock.getBatchDeleteData();
if (batchDeleteData && batchDeleteData.length > 0) {
expect(nangoMock.batchDelete).toHaveBeenCalledWith(batchDeleteData, "SalesforceContact");
}
});
});
27 changes: 27 additions & 0 deletions integrations/salesforce/tests/salesforce-deals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { vi, expect, it, describe } from "vitest";
import type { NangoSync } from "../models.js";

import fetchData from "../syncs/deals.js";

describe("salesforce deals tests", () => {
const nangoMock = new global.vitest.NangoSyncMock({
dirname: __dirname,
name: "deals",
Model: "SalesforceDeal"
});
it("should get, map correctly the data and batchSave the result", async () => {
await fetchData(nangoMock);

const batchSaveData = await nangoMock.getBatchSaveData();
expect(nangoMock.batchSave).toHaveBeenCalledWith(batchSaveData, "SalesforceDeal");
});

it('should get, map correctly the data and batchDelete the result', async () => {
await fetchData(nangoMock);

const batchDeleteData = await nangoMock.getBatchDeleteData();
if (batchDeleteData && batchDeleteData.length > 0) {
expect(nangoMock.batchDelete).toHaveBeenCalledWith(batchDeleteData, "SalesforceDeal");
}
});
});
Loading

0 comments on commit 4e75075

Please sign in to comment.