Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: zoho record processing #4054

Merged
merged 20 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
44d9121
refactor: zoho to not asssume same action in all events of a batch
koladilip Feb 6, 2025
8b90258
refactor: zoho to not asssume same action in all events of a batch
koladilip Feb 7, 2025
07c62c0
refactor: zoho to not asssume same action in all events of a batch
koladilip Feb 7, 2025
844150c
Merge branch 'develop' into refactor/zoho-record-processing
koladilip Feb 7, 2025
098fcb5
refactor: remove unused import in zoho tests
koladilip Feb 10, 2025
34f977b
refactor: remove unused import in zoho tests
koladilip Feb 10, 2025
560bef4
refactor: add missing tests
koladilip Feb 10, 2025
8a8ef5a
refactor: zoho utils using cursor
koladilip Feb 10, 2025
09905f8
refactor: zoho utils using cursor
koladilip Feb 10, 2025
741080a
chore: add more tests for zoho utils
koladilip Feb 11, 2025
208c375
chore: add more tests for zoho utils
koladilip Feb 11, 2025
e49ae2d
Merge branch 'develop' into refactor/zoho-record-processing
koladilip Feb 11, 2025
2b6ff09
Merge branch 'develop' into refactor/zoho-record-processing
koladilip Feb 12, 2025
75d68aa
refactor: update zoho search record id function
koladilip Feb 12, 2025
b18e138
refactor: update zoho format multi select fields function
koladilip Feb 12, 2025
210f71f
refactor: add more util tests for zoho
koladilip Feb 12, 2025
a0c09f5
Merge branch 'develop' into refactor/zoho-record-processing
koladilip Feb 12, 2025
342c195
refactor: use function to express condition
koladilip Feb 12, 2025
2ae02f1
chore: add more test for zoho
koladilip Feb 13, 2025
2f86476
chore: add more test for zoho
koladilip Feb 13, 2025
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
4 changes: 2 additions & 2 deletions src/cdk/v2/destinations/zoho/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const DATA_CENTRE_BASE_ENDPOINTS_MAP = {
};

const getBaseEndpoint = (dataServer) => DATA_CENTRE_BASE_ENDPOINTS_MAP[dataServer];
const COMMON_RECORD_ENDPOINT = (dataCenter = 'US') =>
`${getBaseEndpoint(dataCenter)}/crm/v6/moduleType`;
const COMMON_RECORD_ENDPOINT = (dataCenter) =>
`${getBaseEndpoint(dataCenter || 'US')}/crm/v6/moduleType`;

// ref: https://www.zoho.com/crm/developer/docs/api/v6/insert-records.html#:~:text=%2DX%20POST-,System%2Ddefined%20mandatory%20fields%20for%20each%20module,-While%20inserting%20records
const MODULE_MANDATORY_FIELD_CONFIG = {
Expand Down
17 changes: 6 additions & 11 deletions src/cdk/v2/destinations/zoho/transformRecord.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const responseBuilder = (
identifierType,
operationModuleType,
commonEndPoint,
action,
isUpsert,
metadata,
) => {
const { trigger, addDefaultDuplicateCheck, multiSelectFieldLevelDecision } = config;
Expand All @@ -43,7 +43,7 @@ const responseBuilder = (
Authorization: `Zoho-oauthtoken ${metadata[0].secret.accessToken}`,
};

if (action === 'insert' || action === 'update') {
if (isUpsert) {
const payload = {
duplicate_check_fields: handleDuplicateCheck(
addDefaultDuplicateCheck,
Expand All @@ -70,7 +70,6 @@ const batchResponseBuilder = (
identifierType,
operationModuleType,
upsertEndPoint,
action,
) => {
const upsertResponseArray = [];
const deletionResponseArray = [];
Expand Down Expand Up @@ -101,7 +100,7 @@ const batchResponseBuilder = (
identifierType,
operationModuleType,
upsertEndPoint,
action,
true,
upsertmetadataChunks.items[0],
),
);
Expand All @@ -115,7 +114,7 @@ const batchResponseBuilder = (
identifierType,
operationModuleType,
upsertEndPoint,
action,
false,
deletionmetadataChunks.items[0],
),
);
Expand Down Expand Up @@ -226,13 +225,12 @@ const handleDeletion = async (
*/
const processInput = async (
input,
action,
operationModuleType,
Config,
transformedResponseToBeBatched,
errorResponseList,
) => {
const { fields } = input.message;
const { fields, action } = input.message;

if (isEmptyObject(fields)) {
const emptyFieldsError = new InstrumentationError('`fields` cannot be empty');
Expand Down Expand Up @@ -285,7 +283,6 @@ const processRecordInputs = async (inputs, destination) => {
const response = [];
const errorResponseList = [];
const { Config } = destination;
const { action } = inputs[0].message;

const transformedResponseToBeBatched = {
upsertData: [],
Expand All @@ -296,13 +293,12 @@ const processRecordInputs = async (inputs, destination) => {

const { operationModuleType, identifierType, upsertEndPoint } = deduceModuleInfo(inputs, Config);

validateConfigurationIssue(Config, operationModuleType, action);
validateConfigurationIssue(Config, operationModuleType);

await Promise.all(
inputs.map((input) =>
processInput(
input,
action,
operationModuleType,
Config,
transformedResponseToBeBatched,
Expand All @@ -322,7 +318,6 @@ const processRecordInputs = async (inputs, destination) => {
identifierType,
operationModuleType,
upsertEndPoint,
action,
);

if (upsertResponseArray.length === 0 && deletionResponseArray.length === 0) {
Expand Down
165 changes: 90 additions & 75 deletions src/cdk/v2/destinations/zoho/utils.js
Original file line number Diff line number Diff line change
@@ -1,81 +1,88 @@
const {
MappedToDestinationKey,
getHashFromArray,
isDefinedAndNotNull,
ConfigurationError,
isDefinedAndNotNullAndNotEmpty,
} = require('@rudderstack/integrations-lib');
const get = require('get-value');
const { getDestinationExternalIDInfoForRetl, isHttpStatusSuccess } = require('../../../../v0/util');
const zohoConfig = require('./config');
const { handleHttpRequest } = require('../../../../adapters/network');
const { CommonUtils } = require('../../../../util/common');

const deduceModuleInfo = (inputs, Config) => {
const singleRecordInput = inputs[0].message;
const operationModuleInfo = {};
const mappedToDestination = get(singleRecordInput, MappedToDestinationKey);
if (mappedToDestination) {
const { objectType, identifierType } = getDestinationExternalIDInfoForRetl(
singleRecordInput,
'ZOHO',
);
operationModuleInfo.operationModuleType = objectType;
operationModuleInfo.upsertEndPoint = zohoConfig
.COMMON_RECORD_ENDPOINT(Config.region)
.replace('moduleType', objectType);
operationModuleInfo.identifierType = identifierType;
if (!Array.isArray(inputs) || inputs.length === 0) {
return {};
}

const firstRecord = inputs[0].message;
const mappedToDestination = firstRecord?.context?.mappedToDestination;

if (!mappedToDestination) {
return {};
}
return operationModuleInfo;

const { objectType, identifierType } = getDestinationExternalIDInfoForRetl(firstRecord, 'ZOHO');
return {
operationModuleType: objectType,
upsertEndPoint: zohoConfig
.COMMON_RECORD_ENDPOINT(Config.region)
.replace('moduleType', objectType),
identifierType,
};
};

// eslint-disable-next-line consistent-return
// Keeping the original function name and return structure
function validatePresenceOfMandatoryProperties(objectName, object) {
if (zohoConfig.MODULE_MANDATORY_FIELD_CONFIG.hasOwnProperty(objectName)) {
const requiredFields = zohoConfig.MODULE_MANDATORY_FIELD_CONFIG[objectName];
const missingFields =
requiredFields.filter(
(field) => !object.hasOwnProperty(field) || !isDefinedAndNotNullAndNotEmpty(object[field]),
) || [];
return { status: missingFields.length > 0, missingField: missingFields };
if (!zohoConfig.MODULE_MANDATORY_FIELD_CONFIG.hasOwnProperty(objectName)) {
return undefined; // Maintaining original undefined return for custom objects
}
// No mandatory check performed for custom objects

const requiredFields = zohoConfig.MODULE_MANDATORY_FIELD_CONFIG[objectName];
const missingFields = requiredFields.filter(
(field) => !object.hasOwnProperty(field) || !isDefinedAndNotNullAndNotEmpty(object[field]),
);

return {
status: missingFields.length > 0,
missingField: missingFields,
};
}

const formatMultiSelectFields = (config, fields) => {
// Convert multiSelectFieldLevelDecision array into a hash map for quick lookups
const multiSelectFields = getHashFromArray(
config.multiSelectFieldLevelDecision,
'from',
'to',
false,
);

Object.keys(fields).forEach((eachFieldKey) => {
if (multiSelectFields.hasOwnProperty(eachFieldKey)) {
// eslint-disable-next-line no-param-reassign
fields[eachFieldKey] = [fields[eachFieldKey]];
// Creating a shallow copy to avoid mutations
const formattedFields = { ...fields };

Object.keys(formattedFields).forEach((eachFieldKey) => {
if (
multiSelectFields.hasOwnProperty(eachFieldKey) &&
isDefinedAndNotNull(formattedFields[eachFieldKey])
) {
formattedFields[eachFieldKey] = [formattedFields[eachFieldKey]];
}
});
return fields;

return formattedFields;
};

// Utility to handle duplicate check
const handleDuplicateCheck = (addDefaultDuplicateCheck, identifierType, operationModuleType) => {
let duplicateCheckFields = [identifierType];
let additionalFields = [];

if (addDefaultDuplicateCheck) {
const moduleDuplicateCheckField =
zohoConfig.MODULE_WISE_DUPLICATE_CHECK_FIELD[operationModuleType];

if (isDefinedAndNotNull(moduleDuplicateCheckField)) {
duplicateCheckFields = [...moduleDuplicateCheckField];
duplicateCheckFields.unshift(identifierType);
} else {
duplicateCheckFields.push('Name'); // user chosen duplicate field always carries higher priority
}
additionalFields = isDefinedAndNotNull(moduleDuplicateCheckField)
? moduleDuplicateCheckField
: ['Name'];
}

return [...new Set(duplicateCheckFields)];
return Array.from(new Set([identifierType, ...additionalFields]));
};

function escapeAndEncode(value) {
Expand All @@ -93,42 +100,53 @@ function transformToURLParams(fields, Config) {
return `${regionBasedEndPoint}/crm/v6/Leads/search?criteria=${criteria}`;
}

// ref : https://www.zoho.com/crm/developer/docs/api/v6/search-records.html
const searchRecordId = async (fields, metadata, Config) => {
const searchURL = transformToURLParams(fields, Config);
const searchResult = await handleHttpRequest(
'get',
searchURL,
{
headers: {
Authorization: `Zoho-oauthtoken ${metadata.secret.accessToken}`,
try {
const searchURL = transformToURLParams(fields, Config);
const searchResult = await handleHttpRequest(
maheshkutty marked this conversation as resolved.
Show resolved Hide resolved
'get',
searchURL,
{
headers: {
Authorization: `Zoho-oauthtoken ${metadata.secret.accessToken}`,
},
},
},
{
destType: 'zoho',
feature: 'deleteRecords',
requestMethod: 'GET',
endpointPath: 'crm/v6/Leads/search?criteria=',
module: 'router',
},
);
if (!isHttpStatusSuccess(searchResult.processedResponse.status)) {
{
destType: 'zoho',
feature: 'deleteRecords',
requestMethod: 'GET',
endpointPath: 'crm/v6/Leads/search?criteria=',
module: 'router',
},
);

if (!isHttpStatusSuccess(searchResult.processedResponse.status)) {
return {
erroneous: true,
message: searchResult.processedResponse.response,
};
}

if (
searchResult.processedResponse.status === 204 ||
!CommonUtils.isNonEmptyArray(searchResult.processedResponse.response?.data)
) {
return {
erroneous: true,
message: 'No contact is found with record details',
};
}

return {
erroneous: true,
message: searchResult.processedResponse.response,
erroneous: false,
message: searchResult.processedResponse.response.data.map((record) => record.id),
koladilip marked this conversation as resolved.
Show resolved Hide resolved
};
}
if (searchResult.processedResponse.status === 204) {
} catch (error) {
return {
erroneous: true,
message: 'No contact is found with record details',
message: error.message,
};
}
const recordIds = searchResult.processedResponse.response.data.map((record) => record.id);
return {
erroneous: false,
message: recordIds,
};
};

// ref : https://www.zoho.com/crm/developer/docs/api/v6/upsert-records.html#:~:text=The%20trigger%20input%20can%20be%20workflow%2C%20approval%2C%20or%20blueprint.%20If%20the%20trigger%20is%20not%20mentioned%2C%20the%20workflows%2C%20approvals%20and%20blueprints%20related%20to%20the%20API%20will%20get%20executed.%20Enter%20the%20trigger%20value%20as%20%5B%5D%20to%20not%20execute%20the%20workflows.
Expand All @@ -142,18 +160,15 @@ const calculateTrigger = (trigger) => {
return [trigger];
};

const validateConfigurationIssue = (Config, operationModuleType, action) => {
const validateConfigurationIssue = (Config, operationModuleType) => {
const hashMapMultiselect = getHashFromArray(
Config.multiSelectFieldLevelDecision,
'from',
'to',
false,
);
if (
Object.keys(hashMapMultiselect).length > 0 &&
Config.module !== operationModuleType &&
action !== 'delete'
) {

if (Object.keys(hashMapMultiselect).length > 0 && Config.module !== operationModuleType) {
throw new ConfigurationError(
'Object Chosen in Visual Data Mapper is not consistent with Module type selected in destination configuration. Aborting Events.',
);
Expand Down
Loading
Loading