Skip to content

Commit

Permalink
SALTO-4607: Add Author Information on Salesforce Sub-Instances (#4828)
Browse files Browse the repository at this point in the history
  • Loading branch information
tamtamirr authored Sep 13, 2023
1 parent 8878929 commit 669a08c
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 179 deletions.
4 changes: 2 additions & 2 deletions packages/salesforce-adapter/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import customTypeSplit from './filters/custom_type_split'
import customObjectAuthorFilter from './filters/author_information/custom_objects'
import dataInstancesAuthorFilter from './filters/author_information/data_instances'
import sharingRulesAuthorFilter from './filters/author_information/sharing_rules'
import validationRulesAuthorFilter from './filters/author_information/validation_rules'
import profileInstanceSplitFilter from './filters/profile_instance_split'
import customObjectsInstancesFilter from './filters/custom_objects_instances'
import profilePermissionsFilter from './filters/profile_permissions'
Expand Down Expand Up @@ -99,6 +98,7 @@ import { retrieveMetadataInstances, fetchMetadataType, fetchMetadataInstances, l
import { isCustomObjectInstanceChanges, deployCustomObjectInstancesGroup } from './custom_object_instances_deploy'
import { getLookUpName, getLookupNameWithFallbackToElement } from './transformers/reference_mapping'
import { deployMetadata, NestedMetadataTypeInfo } from './metadata_deploy'
import nestedInstancesAuthorInformation from './filters/author_information/nested_instances'
import { FetchProfile, buildFetchProfile } from './fetch_profile/fetch_profile'
import {
CUSTOM_OBJECT,
Expand Down Expand Up @@ -159,10 +159,10 @@ export const allFilters: Array<LocalFilterCreatorDefinition | RemoteFilterCreato
{ creator: profilePathsFilter, addsNewInformation: true },
{ creator: territoryFilter },
{ creator: elementsUrlFilter, addsNewInformation: true },
{ creator: nestedInstancesAuthorInformation, addsNewInformation: true },
{ creator: customObjectAuthorFilter, addsNewInformation: true },
{ creator: dataInstancesAuthorFilter, addsNewInformation: true },
{ creator: sharingRulesAuthorFilter, addsNewInformation: true },
{ creator: validationRulesAuthorFilter, addsNewInformation: true },
{ creator: hideReadOnlyValuesFilter },
{ creator: currencyIsoCodeFilter },
{ creator: splitCustomLabels },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2023 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 { Element } from '@salto-io/adapter-api'
import { logger } from '@salto-io/logging'
import _ from 'lodash'
import { RemoteFilterCreator } from '../../filter'
import { apiNameSync, ensureSafeFilterFetch, isMetadataInstanceElementSync } from '../utils'
import { WORKFLOW_FIELD_TO_TYPE } from '../workflow'
import { NESTED_INSTANCE_VALUE_TO_TYPE_NAME } from '../custom_objects_to_object_type'
import { getAuthorAnnotations, MetadataInstanceElement } from '../../transformers/transformer'
import SalesforceClient from '../../client/client'

const log = logger(module)

export const WARNING_MESSAGE = 'Encountered an error while trying to populate author information in some of the Salesforce configuration elements.'

const NESTED_INSTANCES_METADATA_TYPES = [
'CustomLabel',
'AssignmentRule',
'AutoResponseRule',
'EscalationRule',
'MatchingRule',
...Object.keys(WORKFLOW_FIELD_TO_TYPE),
...Object.keys(NESTED_INSTANCE_VALUE_TO_TYPE_NAME),
] as const

type NestedInstanceMetadataType = typeof NESTED_INSTANCES_METADATA_TYPES[number]


type SetAuthorInformationForTypeParams = {
client: SalesforceClient
typeName: NestedInstanceMetadataType
instances: MetadataInstanceElement[]
}

const setAuthorInformationForInstancesOfType = async (
{
client, typeName, instances,
}: SetAuthorInformationForTypeParams): Promise<void> => {
const { result: filesProps } = await client.listMetadataObjects([{ type: typeName }])
const filePropsByFullName = _.keyBy(filesProps, props => props.fullName)
const instancesWithMissingFileProps: MetadataInstanceElement[] = []
instances.forEach(instance => {
const instanceFullName = apiNameSync(instance)
if (instanceFullName === undefined) {
return
}
const fileProps = filePropsByFullName[instanceFullName]
if (fileProps === undefined) {
instancesWithMissingFileProps.push(instance)
return
}
Object.assign(instance.annotations, getAuthorAnnotations(fileProps))
})
if (instancesWithMissingFileProps.length > 0) {
log.debug(`Failed to populate author information for the following ${typeName} instances: ${instancesWithMissingFileProps.map(instance => apiNameSync(instance)).join(', ')}`)
}
}

/*
* add author information on nested instances
*/
const filterCreator: RemoteFilterCreator = ({ client, config }) => ({
name: 'nestedInstancesAuthorFilter',
remote: true,
onFetch: ensureSafeFilterFetch({
warningMessage: WARNING_MESSAGE,
config,
filterName: 'authorInformation',
fetchFilterFunc: async (elements: Element[]) => {
const instancesByType = _.groupBy(
elements.filter(isMetadataInstanceElementSync),
e => apiNameSync(e.getTypeSync())
)
await Promise.all(Object.entries(instancesByType)
.map(([typeName, instances]) => (
setAuthorInformationForInstancesOfType({ client, typeName, instances })
)))
},
}),
})

export default filterCreator

This file was deleted.

48 changes: 45 additions & 3 deletions packages/salesforce-adapter/src/filters/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ import SalesforceClient from '../client/client'
import { INSTANCE_SUFFIXES, OptionalFeatures } from '../types'
import {
API_NAME,
API_NAME_SEPARATOR,
API_NAME_SEPARATOR, CUSTOM_FIELD,
CUSTOM_METADATA_SUFFIX,
CUSTOM_OBJECT,
CUSTOM_OBJECT, CUSTOM_OBJECT_ID_FIELD,
INSTANCE_FULL_NAME_FIELD,
INTERNAL_ID_ANNOTATION,
INTERNAL_ID_FIELD,
Expand All @@ -68,12 +68,13 @@ import { JSONBool, SalesforceRecord } from '../client/types'
import {
apiName,
defaultApiName,
isCustomObject,
isCustomObject, isMetadataObjectType,
isNameField, MetadataInstanceElement,
metadataType,
MetadataValues,
Types,
} from '../transformers/transformer'
import * as transformer from '../transformers/transformer'
import { Filter, FilterContext } from '../filter'

const { toArrayAsync, awu } = collections.asynciterable
Expand Down Expand Up @@ -105,6 +106,47 @@ export const safeApiName = async (elem: Readonly<Element>, relative = false): Pr
apiName(elem, relative)
)


export const metadataTypeSync = (element: Readonly<Element>): string => {
if (isInstanceElement(element)) {
return metadataTypeSync(element.getTypeSync())
}
if (isField(element)) {
// We expect to reach to this place only with field of CustomObject
return CUSTOM_FIELD
}
return element.annotations[METADATA_TYPE] || 'unknown'
}
export const isCustomObjectSync = (element: Readonly<Element>): boolean => {
const res = isObjectType(element)
&& metadataTypeSync(element) === CUSTOM_OBJECT
// The last part is so we can tell the difference between a custom object
// and the original "CustomObject" type from salesforce (the latter will not have an API_NAME)
&& element.annotations[API_NAME] !== undefined
return res
}


const fullApiNameSync = (elem: Readonly<Element>): string | undefined => {
if (isInstanceElement(elem)) {
return (isCustomObjectSync(elem.getTypeSync()))
? elem.value[CUSTOM_OBJECT_ID_FIELD] : elem.value[INSTANCE_FULL_NAME_FIELD]
}
return elem.annotations[API_NAME] ?? elem.annotations[METADATA_TYPE]
}

export const apiNameSync = (elem: Readonly<Element>, relative = false): string | undefined => {
const name = fullApiNameSync(elem)
return name && relative ? transformer.relativeApiName(name) : name
}

export const isMetadataInstanceElementSync = (elem: Element): elem is MetadataInstanceElement => (
isInstanceElement(elem)
&& isMetadataObjectType(elem.getTypeSync())
&& elem.value[INSTANCE_FULL_NAME_FIELD] !== undefined
)


export const isCustomMetadataRecordType = async (elem: Element): Promise<boolean> => {
const elementApiName = await apiName(elem)
return isObjectType(elem) && (elementApiName?.endsWith(CUSTOM_METADATA_SUFFIX) ?? false)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2023 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 { CORE_ANNOTATIONS, InstanceElement } from '@salto-io/adapter-api'
import { mockTypes } from '../../mock_elements'
import { CUSTOM_LABEL_METADATA_TYPE, INSTANCE_FULL_NAME_FIELD } from '../../../src/constants'
import { mockFileProperties } from '../../connection'
import mockClient from '../../client'
import filterCreator from '../../../src/filters/author_information/nested_instances'
import { FilterWith } from '../mocks'
import { defaultFilterContext } from '../../utils'

describe('nestedInstancesAuthorInformationFilter', () => {
const CREATED_BY_NAME = 'Test User'
const CREATED_DATE = '2021-01-01T00:00:00.000Z'
const LAST_MODIFIED_BY_NAME = 'Test User 2'
const LAST_MODIFIED_DATE = '2021-01-02T00:00:00.000Z'

let customLabelInstance: InstanceElement
let filter: FilterWith<'onFetch'>
describe('onFetch', () => {
beforeEach(() => {
customLabelInstance = new InstanceElement(
'TestCustomLabel',
mockTypes.CustomLabel,
{
[INSTANCE_FULL_NAME_FIELD]: 'TestCustomLabel',
},
)
const fileProperties = mockFileProperties({
fullName: 'TestCustomLabel',
type: CUSTOM_LABEL_METADATA_TYPE,
createdByName: CREATED_BY_NAME,
createdDate: CREATED_DATE,
lastModifiedByName: LAST_MODIFIED_BY_NAME,
lastModifiedDate: LAST_MODIFIED_DATE,
})
const { client, connection } = mockClient()
connection.metadata.list.mockResolvedValue([fileProperties])
filter = filterCreator({ client, config: defaultFilterContext }) as FilterWith<'onFetch'>
})
it('should add author information to nested instances', async () => {
await filter.onFetch([customLabelInstance])
expect(customLabelInstance.annotations).toEqual({
[CORE_ANNOTATIONS.CREATED_BY]: CREATED_BY_NAME,
[CORE_ANNOTATIONS.CREATED_AT]: CREATED_DATE,
[CORE_ANNOTATIONS.CHANGED_BY]: LAST_MODIFIED_BY_NAME,
[CORE_ANNOTATIONS.CHANGED_AT]: LAST_MODIFIED_DATE,
})
})
})
})
Loading

0 comments on commit 669a08c

Please sign in to comment.