From f987a7ddad7a44abe34eb1511548498bbe71db1a Mon Sep 17 00:00:00 2001 From: hadard Date: Thu, 12 Mar 2020 16:21:08 +0200 Subject: [PATCH] SALTO-582: custom obj translation reference fields and validation rules (#813) --- package.json | 2 +- packages/adapter-api/src/elements.ts | 3 +- packages/salesforce-adapter/src/adapter.ts | 3 + packages/salesforce-adapter/src/constants.ts | 1 + .../src/filters/custom_object_translation.ts | 94 +++++++++++ .../src/filters/custom_objects.ts | 19 ++- .../salesforce-adapter/src/filters/utils.ts | 19 ++- .../filters/custom_object_translation.test.ts | 151 ++++++++++++++++++ 8 files changed, 279 insertions(+), 13 deletions(-) create mode 100644 packages/salesforce-adapter/src/filters/custom_object_translation.ts create mode 100644 packages/salesforce-adapter/test/filters/custom_object_translation.test.ts diff --git a/package.json b/package.json index 8d85342520d..82743c661b6 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "packages/*" ], "nohoist": [ - "salto-vscode/**" + "vscode/**" ] }, "devDependencies": { diff --git a/packages/adapter-api/src/elements.ts b/packages/adapter-api/src/elements.ts index 475d070c340..81b33cb522f 100644 --- a/packages/adapter-api/src/elements.ts +++ b/packages/adapter-api/src/elements.ts @@ -298,6 +298,7 @@ export class InstanceElement extends Element { * @return {InstanceElement} the cloned instance */ clone(): InstanceElement { - return new InstanceElement(this.elemID.name, this.type, _.cloneDeep(this.value), this.path) + return new InstanceElement(this.elemID.name, this.type, _.cloneDeep(this.value), this.path, + _.cloneDeep(this.annotations)) } } diff --git a/packages/salesforce-adapter/src/adapter.ts b/packages/salesforce-adapter/src/adapter.ts index c164b056ea4..7ac22af2f12 100644 --- a/packages/salesforce-adapter/src/adapter.ts +++ b/packages/salesforce-adapter/src/adapter.ts @@ -56,6 +56,7 @@ import topicsForObjectsFilter from './filters/topics_for_objects' import globalValueSetFilter from './filters/global_value_sets' import instanceReferences from './filters/instance_references' import valueSetFilter from './filters/value_set' +import customObjectTranslationFilter from './filters/custom_object_translation' import { FilterCreator, Filter, filtersRunner, } from './filter' @@ -84,6 +85,8 @@ export const DEFAULT_FILTERS = [ topicsForObjectsFilter, valueSetFilter, globalValueSetFilter, + // customObjectTranslationFilter depends on customObjectsFilter + customObjectTranslationFilter, // The following filters should remain last in order to make sure they fix all elements convertListsFilter, convertTypeFilter, diff --git a/packages/salesforce-adapter/src/constants.ts b/packages/salesforce-adapter/src/constants.ts index ad208bf788d..0ff7257ea2e 100644 --- a/packages/salesforce-adapter/src/constants.ts +++ b/packages/salesforce-adapter/src/constants.ts @@ -235,6 +235,7 @@ export const TOPICS_FOR_OBJECTS_METADATA_TYPE = 'TopicsForObjects' export const PROFILE_METADATA_TYPE = 'Profile' export const WORKFLOW_METADATA_TYPE = 'Workflow' export const ASSIGNMENT_RULES_METADATA_TYPE = 'AssignmentRules' +export const VALIDATION_RULES_METADATA_TYPE = 'ValidationRule' export const LEAD_CONVERT_SETTINGS_METADATA_TYPE = 'LeadConvertSettings' export const QUICK_ACTION_METADATA_TYPE = 'QuickAction' export const CUSTOM_TAB_METADATA_TYPE = 'CustomTab' diff --git a/packages/salesforce-adapter/src/filters/custom_object_translation.ts b/packages/salesforce-adapter/src/filters/custom_object_translation.ts new file mode 100644 index 00000000000..c91bd8a8d73 --- /dev/null +++ b/packages/salesforce-adapter/src/filters/custom_object_translation.ts @@ -0,0 +1,94 @@ +/* +* Copyright 2020 Salto Labs Ltd. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import wu from 'wu' +import { + Element, ElemID, ReferenceExpression, Field, isObjectType, +} from '@salto-io/adapter-api' +import { findInstances, findElements } from '@salto-io/adapter-utils' +import { collections } from '@salto-io/lowerdash' +import _ from 'lodash' +import { logger } from '@salto-io/logging' +import { apiName } from '../transformers/transformer' +import { FilterWith } from '../filter' +import { relativeApiName, instanceParent, parentApiNameToMetadataTypeInstances } from './utils' +import { SALESFORCE, CUSTOM_OBJECT_TRANSLATION_METADATA_TYPE, VALIDATION_RULES_METADATA_TYPE } from '../constants' + +const log = logger(module) + +const { makeArray } = collections.array + +const FIELDS = 'fields' +const NAME = 'name' +const VALIDATION_RULES = 'validationRules' + +/** +* This filter change CustomObjectTranslation logical references to fields and validation rules to +* Salto referenes +*/ +const filterCreator = (): FilterWith<'onFetch'> => ({ + onFetch: async (elements: Element[]) => { + const allCustomObjectFields = (elemID: ElemID): Iterable => + wu(findElements(elements, elemID)) + .filter(isObjectType) + .map(elem => Object.values(elem.fields)) + .flatten() + + const customToRule = parentApiNameToMetadataTypeInstances( + elements, VALIDATION_RULES_METADATA_TYPE + ) + + wu(findInstances(elements, new ElemID(SALESFORCE, CUSTOM_OBJECT_TRANSLATION_METADATA_TYPE))) + .forEach(customTranslation => { + const customObjectElemId = instanceParent(customTranslation) + if (_.isUndefined(customObjectElemId)) { + log.warn('failed to find custom object for custom translation %s', + apiName(customTranslation)) + return + } + + // Change fields to reference + const customFields = new Map( + wu(allCustomObjectFields(customObjectElemId)) + .map(f => [apiName(f, true), f]) + ) + makeArray(customTranslation.value[FIELDS]).forEach(field => { + const customField = customFields.get(field[NAME]) + if (customField) { + field[NAME] = new ReferenceExpression(customField.elemID) + } else { + log.warn('failed to find field %s in %s', field[NAME], customObjectElemId.getFullName()) + } + }) + + // Change validation rules to refs + const objRules = new Map( + makeArray(customToRule[customObjectElemId.getFullName()]) + .map(r => [relativeApiName(r), r]) + ) + makeArray(customTranslation.value[VALIDATION_RULES]).forEach(rule => { + const ruleInstance = objRules.get(rule[NAME]) + if (ruleInstance) { + rule[NAME] = new ReferenceExpression(ruleInstance.elemID) + } else { + log.warn('failed to validation rule %s for %s', rule[NAME], + customObjectElemId.getFullName()) + } + }) + }) + }, +}) + +export default filterCreator diff --git a/packages/salesforce-adapter/src/filters/custom_objects.ts b/packages/salesforce-adapter/src/filters/custom_objects.ts index 0e0bf9e1e53..7a9a34f6a2e 100644 --- a/packages/salesforce-adapter/src/filters/custom_objects.ts +++ b/packages/salesforce-adapter/src/filters/custom_objects.ts @@ -35,6 +35,7 @@ import { FORMULA, LEAD_CONVERT_SETTINGS_METADATA_TYPE, ASSIGNMENT_RULES_METADATA_TYPE, WORKFLOW_METADATA_TYPE, QUICK_ACTION_METADATA_TYPE, CUSTOM_TAB_METADATA_TYPE, DUPLICATE_RULE_METADATA_TYPE, CUSTOM_OBJECT_TRANSLATION_METADATA_TYPE, + VALIDATION_RULES_METADATA_TYPE, } from '../constants' import { FilterCreator } from '../filter' import { @@ -43,7 +44,8 @@ import { } from '../transformers/transformer' import { id, addApiName, addMetadataType, addLabel, hasNamespace, getNamespace, boolValue, - buildAnnotationsObjectType, generateApiNameToCustomObject, addObjectParentReference, + buildAnnotationsObjectType, generateApiNameToCustomObject, addObjectParentReference, apiNameParts, + parentApiName, } from './utils' import { convertList } from './convert_lists' import { WORKFLOW_FIELD_TO_TYPE } from './workflow' @@ -68,7 +70,7 @@ export const NESTED_INSTANCE_VALUE_NAME = { export const NESTED_INSTANCE_TYPE_NAME = { WEB_LINK: 'WebLink', - VALIDATION_RULE: 'ValidationRule', + VALIDATION_RULE: VALIDATION_RULES_METADATA_TYPE, BUSINESS_PROCESS: 'BusinessProcess', RECORD_TYPE: 'RecordType', LIST_VIEW: 'ListView', @@ -200,7 +202,7 @@ const getFieldDependency = (values: Values): Values | undefined => { return undefined } -const transfromAnnotationsNames = (fields: Values, parentApiName: string): Values => { +const transfromAnnotationsNames = (fields: Values, parentName: string): Values => { const annotations: Values = {} const typeName = fields[INSTANCE_TYPE_FIELD] Object.entries(fields).forEach(([k, v]) => { @@ -209,7 +211,7 @@ const transfromAnnotationsNames = (fields: Values, parentApiName: string): Value annotations[CORE_ANNOTATIONS.REQUIRED] = v break case INSTANCE_FULL_NAME_FIELD: - annotations[API_NAME] = [parentApiName, v].join(API_NAME_SEPERATOR) + annotations[API_NAME] = [parentName, v].join(API_NAME_SEPERATOR) break case FIELD_ANNOTATIONS.DEFAULT_VALUE: if (typeName === FIELD_TYPE_NAMES.CHECKBOX) { @@ -260,7 +262,7 @@ const transfromAnnotationsNames = (fields: Values, parentApiName: string): Value export const transformFieldAnnotations = ( instanceFieldValues: Values, - parentApiName: string + parentName: string ): Values => { // Ignores typeless/unknown typed instances if (!_.has(instanceFieldValues, INSTANCE_TYPE_FIELD)) { @@ -272,7 +274,7 @@ export const transformFieldAnnotations = ( return {} } - const annotations = transfromAnnotationsNames(instanceFieldValues, parentApiName) + const annotations = transfromAnnotationsNames(instanceFieldValues, parentName) const annotationsType = buildAnnotationsObjectType(fieldType.annotationTypes) convertList(annotationsType, annotations) @@ -484,9 +486,6 @@ const hasCustomObjectParent = (instance: InstanceElement): boolean => dependentMetadataTypes.has(metadataType(instance)) const fixDependentInstancesPathAndSetParent = (elements: Element[]): void => { - const apiNameParts = (instance: InstanceElement): string[] => - apiName(instance).split(/\.|-/g) - const setDependingInstancePath = (instance: InstanceElement, customObject: ObjectType): void => { if (customObject.path) { @@ -502,7 +501,7 @@ const fixDependentInstancesPathAndSetParent = (elements: Element[]): void => { const apiNameToCustomObject = generateApiNameToCustomObject(elements) const getDependentCustomObj = (instance: InstanceElement): ObjectType | undefined => { - const customObject = apiNameToCustomObject.get(apiNameParts(instance)[0]) + const customObject = apiNameToCustomObject.get(parentApiName(instance)) if (_.isUndefined(customObject) && metadataType(instance) === LEAD_CONVERT_SETTINGS_METADATA_TYPE) { return apiNameToCustomObject.get('Lead') diff --git a/packages/salesforce-adapter/src/filters/utils.ts b/packages/salesforce-adapter/src/filters/utils.ts index 58e33ba213a..7282b1b0c23 100644 --- a/packages/salesforce-adapter/src/filters/utils.ts +++ b/packages/salesforce-adapter/src/filters/utils.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import _ from 'lodash' +import _, { Dictionary } from 'lodash' import { logger } from '@salto-io/logging' import { Element, Field, isObjectType, ObjectType, InstanceElement, isInstanceElement, isField, @@ -121,6 +121,18 @@ export const buildAnnotationsObjectType = (annotationTypes: TypeMap): ObjectType export const generateApiNameToCustomObject = (elements: Element[]): Map => new Map(getCustomObjects(elements).map(obj => [apiName(obj), obj])) +export const apiNameParts = (instance: InstanceElement): string[] => + apiName(instance).split(/\.|-/g) + +export const parentApiName = (instance: InstanceElement): string => + apiNameParts(instance)[0] + +export const relativeApiName = (instance: InstanceElement): string => + apiName(instance).slice(parentApiName(instance).length + 1) + +export const instanceParent = (instance: InstanceElement): ElemID | undefined => + instance.annotations[INSTANCE_ANNOTATIONS.PARENT]?.elemId + export const addObjectParentReference = (instance: InstanceElement, { elemID: objectID }: ObjectType): void => { const instanceDeps = makeArray(instance.annotations[INSTANCE_ANNOTATIONS.PARENT]) @@ -133,3 +145,8 @@ export const addObjectParentReference = (instance: InstanceElement, export const fullApiName = (parent: string, child: string): string => ([parent, child].join(API_NAME_SEPERATOR)) + +export const parentApiNameToMetadataTypeInstances = (elements: Element[], type: string): +Dictionary => _(getInstancesOfMetadataType(elements, type)) + .groupBy(instance => instanceParent(instance)?.getFullName()) + .value() as Dictionary diff --git a/packages/salesforce-adapter/test/filters/custom_object_translation.test.ts b/packages/salesforce-adapter/test/filters/custom_object_translation.test.ts new file mode 100644 index 00000000000..96d689880e4 --- /dev/null +++ b/packages/salesforce-adapter/test/filters/custom_object_translation.test.ts @@ -0,0 +1,151 @@ +/* +* Copyright 2020 Salto Labs Ltd. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { + ObjectType, InstanceElement, Field, BuiltinTypes, ElemID, ReferenceExpression, + INSTANCE_ANNOTATIONS, +} from '@salto-io/adapter-api' +import filterCreator from '../../src/filters/custom_object_translation' +import { + SALESFORCE, CUSTOM_OBJECT_TRANSLATION_METADATA_TYPE, METADATA_TYPE, CUSTOM_OBJECT, API_NAME, + VALIDATION_RULES_METADATA_TYPE, + INSTANCE_FULL_NAME_FIELD, +} from '../../src/constants' +import { FilterWith } from '../../src/filter' + +describe('custom object translation filter', () => { + const customObjName = 'MockCustomObject' + const customFieldName = 'MockField' + const customObjElemID = new ElemID(SALESFORCE, customObjName) + const customObjectAnno = new ObjectType( + { + elemID: customObjElemID, + annotations: { + [METADATA_TYPE]: CUSTOM_OBJECT, + [API_NAME]: customObjName, + }, + } + ) + const customObjectField = new ObjectType( + { + elemID: customObjElemID, + fields: { + [customFieldName]: new Field(customObjElemID, customFieldName, BuiltinTypes.STRING, + { [API_NAME]: `${customObjName}.${customFieldName}` }), + }, + } + ) + const customObjectAdditionalField = new ObjectType( + { + elemID: customObjElemID, + fields: { + additional: new Field(customObjElemID, 'additional', BuiltinTypes.STRING, + { [API_NAME]: `${customObjName}.additional` }), + }, + } + ) + const validationRuleName = 'Last_price_must_for_recently_sold' + const validationRuleType = new ObjectType({ + elemID: new ElemID(SALESFORCE, VALIDATION_RULES_METADATA_TYPE), + annotations: { [METADATA_TYPE]: VALIDATION_RULES_METADATA_TYPE }, + }) + const validationRuleInstance = new InstanceElement( + `${customObjName}_${validationRuleName}`, + validationRuleType, + { [INSTANCE_FULL_NAME_FIELD]: `${customObjName}.${validationRuleName}` }, + undefined, + { [INSTANCE_ANNOTATIONS.PARENT]: new ReferenceExpression(customObjElemID) } + ) + const fakeValidationRuleInstance = new InstanceElement( + `${customObjName}_BLA`, + validationRuleType, + { [INSTANCE_FULL_NAME_FIELD]: `${customObjName}.BLA` } + ) + const objTranslationType = new ObjectType({ + elemID: new ElemID(SALESFORCE, CUSTOM_OBJECT_TRANSLATION_METADATA_TYPE), + }) + const objTranslationInstance = new InstanceElement( + `${customObjName}-en_US`, + objTranslationType, + { + [INSTANCE_FULL_NAME_FIELD]: `${customObjName}-en_US`, + fields: [{ name: customFieldName }, { name: 'not-exists' }], + validationRules: [{ name: validationRuleName }, { name: 'not-exists' }], + }, + undefined, + { [INSTANCE_ANNOTATIONS.PARENT]: new ReferenceExpression(customObjElemID) } + ) + const objTranslationNoCustomObjInstance = new InstanceElement( + 'BLA-en_US', + objTranslationType, + { + [INSTANCE_FULL_NAME_FIELD]: 'BLA.en_US', + // Use here also single element and not a list + fields: { name: customFieldName }, + validationRules: { name: validationRuleName }, + }, + ) + + describe('on fetch', () => { + let postFilter: InstanceElement + let postFilterNoObj: InstanceElement + + beforeAll(async () => { + const filter = filterCreator() as FilterWith<'onFetch'> + const testElements = [ + objTranslationInstance.clone(), + objTranslationNoCustomObjInstance.clone(), + customObjectAnno.clone(), + customObjectField.clone(), + customObjectAdditionalField.clone(), + objTranslationType.clone(), + validationRuleType.clone(), + validationRuleInstance.clone(), + fakeValidationRuleInstance.clone(), + ] + await filter.onFetch(testElements) + postFilter = testElements[0] as InstanceElement + postFilterNoObj = testElements[1] as InstanceElement + }) + + describe('fields reference', () => { + it('should transform fields to reference', async () => { + expect(postFilter.value.fields[0].name) + .toEqual(new ReferenceExpression(customObjectField.fields[customFieldName].elemID)) + }) + it('should keep name as is if no referenced field was found', async () => { + expect(postFilter.value.fields[1].name).toBe('not-exists') + }) + + it('should keep name as is if no custom object', async () => { + expect(postFilterNoObj.value.fields.name).toBe(customFieldName) + }) + }) + + describe('validation rules reference', () => { + it('should transform validation rules to reference', async () => { + expect(postFilter.value.validationRules[0].name) + .toEqual(new ReferenceExpression(validationRuleInstance.elemID)) + }) + it('should keep name as is if no referenced validation rules was found', async () => { + expect(postFilter.value.validationRules[1].name).toBe('not-exists') + }) + + it('should keep name as is if no custom object', async () => { + expect(postFilterNoObj.value.validationRules.name).toBe(validationRuleName) + }) + }) + }) +})