Skip to content

Commit

Permalink
SALTO-7475: Resolving Workflows in SFDX dump (#7285)
Browse files Browse the repository at this point in the history
  • Loading branch information
yelly authored Feb 20, 2025
1 parent 3520472 commit 62904dd
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 70 deletions.
58 changes: 7 additions & 51 deletions packages/salesforce-adapter/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,20 @@ import {
ElemIdGetter,
FetchOptions,
FetchResult,
Field,
FixElementsFunc,
getChangeData,
InstanceElement,
isAdditionChange,
isField,
isInstanceElement,
isObjectType,
ObjectType,
PartialFetchData,
ReadOnlyElementsSource,
TypeElement,
CancelServiceAsyncTaskInput,
CancelServiceAsyncTaskResult,
} from '@salto-io/adapter-api'
import {
filter,
GetLookupNameFunc,
inspectValue,
logDuration,
ResolveValuesFunc,
safeJsonStringify,
} from '@salto-io/adapter-utils'
import { resolveChangeElement, resolveValues, restoreChangeElement } from '@salto-io/adapter-components'
import { filter, inspectValue, logDuration, safeJsonStringify } from '@salto-io/adapter-utils'
import { restoreChangeElement } from '@salto-io/adapter-components'
import { MetadataObject } from '@salto-io/jsforce'
import _ from 'lodash'
import { logger } from '@salto-io/logging'
Expand Down Expand Up @@ -159,7 +149,6 @@ import {
isInstanceOfCustomObjectSync,
isInstanceOfTypeSync,
isMetadataInstanceElementSync,
isOrderedMapTypeOrRefType,
listMetadataObjects,
metadataTypeSync,
queryClient,
Expand All @@ -171,7 +160,11 @@ import {
retrieveMetadataInstances,
} from './fetch'
import { deployCustomObjectInstancesGroup, isCustomObjectInstanceChanges } from './custom_object_instances_deploy'
import { getLookUpName, getLookupNameForDataInstances } from './transformers/reference_mapping'
import {
getLookUpName,
getLookupNameForDataInstances,
resolveSalesforceChanges,
} from './transformers/reference_mapping'
import { deployMetadata, NestedMetadataTypeInfo } from './metadata_deploy'
import nestedInstancesAuthorInformation from './filters/author_information/nested_instances'
import { buildFetchProfile } from './fetch_profile/fetch_profile'
Expand Down Expand Up @@ -469,43 +462,6 @@ type CreateFiltersRunnerParams = {
contextOverrides?: Partial<FilterContext>
}

const isFieldWithOrderedMapAnnotation = (field: Field): boolean =>
Object.values(field.getTypeSync().annotationRefTypes).some(isOrderedMapTypeOrRefType)

const isElementWithOrderedMap = (element: Element): boolean => {
if (isField(element)) {
return isFieldWithOrderedMapAnnotation(element)
}
if (isInstanceElement(element)) {
return Object.values(element.getTypeSync().fields).some(field => isOrderedMapTypeOrRefType(field.getTypeSync()))
}
if (isObjectType(element)) {
return Object.values(element.fields).some(isFieldWithOrderedMapAnnotation)
}
return false
}

export const salesforceAdapterResolveValues: ResolveValuesFunc = async (
element,
getLookUpNameFunc,
elementsSource,
allowEmpty = true,
) => {
const resolvedElement = await resolveValues(element, getLookUpNameFunc, elementsSource, allowEmpty)
// Since OrderedMaps' order values reference values that may contain references, we should resolve the Element twice
// in order to fully resolve it. An example use-case for this is the Field `SBQQ__ProductRule__c.SBQQ__LookupObject__c`
// Where the `fullName` of the Picklist values is a Reference to a Custom Object.
return isElementWithOrderedMap(resolvedElement)
? resolveValues(resolvedElement, getLookUpNameFunc, elementsSource, allowEmpty)
: resolvedElement
}

export const resolveSalesforceChanges = (
changes: readonly Change[],
getLookupNameFunc: GetLookupNameFunc,
): Promise<Change[]> =>
Promise.all(changes.map(change => resolveChangeElement(change, getLookupNameFunc, salesforceAdapterResolveValues)))

type SalesforceAdapterOperations = Omit<AdapterOperations, 'deploy' | 'validate'> & {
deploy: (deployOptions: SalesforceAdapterDeployOptions) => Promise<DeployResult>
validate: (deployOptions: SalesforceAdapterDeployOptions) => Promise<DeployResult>
Expand Down
34 changes: 24 additions & 10 deletions packages/salesforce-adapter/src/filters/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ import {
MetadataTypeAnnotations,
toMetadataInfo,
} from '../transformers/transformer'
import { fullApiName, parentApiName, getDataFromChanges, isInstanceOfTypeChange, isInstanceOfTypeSync } from './utils'
import { getLookUpName, salesforceAdapterResolveValues } from '../transformers/reference_mapping'
import {
fullApiName,
parentApiName,
getDataFromChanges,
isInstanceOfTypeSync,
isInstanceOfTypeChangeSync,
} from './utils'
import { WorkflowField } from '../fetch_profile/metadata_types'

const { awu, groupByAsync } = collections.asynciterable
Expand Down Expand Up @@ -96,13 +103,12 @@ const createPartialWorkflowInstance = async (
..._.omit(fullInstance.value, Object.keys(WORKFLOW_FIELD_TO_TYPE)),
...(await mapValuesAsync(WORKFLOW_FIELD_TO_TYPE, async fieldType =>
Promise.all(
getDataFromChanges(
dataField,
(await awu(changes).filter(isInstanceOfTypeChange(fieldType)).toArray()) as Change<InstanceElement>[],
).map(async nestedInstance => ({
...(await toMetadataInfo(nestedInstance)),
[INSTANCE_FULL_NAME_FIELD]: await apiName(nestedInstance, true),
})),
getDataFromChanges(dataField, changes.filter(isInstanceOfTypeChangeSync(fieldType))).map(
async nestedInstance => ({
...(await toMetadataInfo(nestedInstance)),
[INSTANCE_FULL_NAME_FIELD]: await apiName(nestedInstance, true),
}),
),
),
)),
},
Expand Down Expand Up @@ -248,8 +254,16 @@ const filterCreator: FilterCreator = ({ config, client }) => {
.filter(([parent, elem]) =>
originalWorkflowChanges[parent].every(change => !elem.elemID.isEqual(getChangeData(change).elemID)),
)
.forEach(([parent, elem]) => {
originalWorkflowChanges[parent].push(toChange({ after: elem }))
.forEach(async ([parent, elem]) => {
originalWorkflowChanges[parent].push(
toChange({
after: await salesforceAdapterResolveValues(
elem,
getLookUpName(config.fetchProfile),
config.elementsSource,
),
}),
)
})
}

Expand Down
4 changes: 2 additions & 2 deletions packages/salesforce-adapter/src/sfdx_parser/sfdx_dump.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { logger } from '@salto-io/logging'
import { AdapterFormat, Change, getChangeData, isField, isObjectType } from '@salto-io/adapter-api'
import { filter } from '@salto-io/adapter-utils'
import { objects, promises, values } from '@salto-io/lowerdash'
import { allFilters, NESTED_METADATA_TYPES, resolveSalesforceChanges } from '../adapter'
import { allFilters, NESTED_METADATA_TYPES } from '../adapter'
import { CUSTOM_METADATA, SYSTEM_FIELDS, UNSUPPORTED_SYSTEM_FIELDS, API_NAME } from '../constants'
import { getLookUpName } from '../transformers/reference_mapping'
import { getLookUpName, resolveSalesforceChanges } from '../transformers/reference_mapping'
import { buildFetchProfile } from '../fetch_profile/fetch_profile'
import { createDeployPackage, DeployPackage, PACKAGE } from '../transformers/xml_transformer'
import { addChangeToPackage, validateChanges } from '../metadata_deploy'
Expand Down
46 changes: 43 additions & 3 deletions packages/salesforce-adapter/src/transformers/reference_mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import {
ElemID,
isInstanceElement,
InstanceElement,
isField,
isObjectType,
Change,
} from '@salto-io/adapter-api'
import { references as referenceUtils } from '@salto-io/adapter-components'
import { GetLookupNameFunc, GetLookupNameFuncArgs } from '@salto-io/adapter-utils'
import { references as referenceUtils, resolveChangeElement, resolveValues } from '@salto-io/adapter-components'
import { GetLookupNameFunc, GetLookupNameFuncArgs, ResolveValuesFunc } from '@salto-io/adapter-utils'
import _ from 'lodash'
import { logger } from '@salto-io/logging'
import { collections } from '@salto-io/lowerdash'
Expand Down Expand Up @@ -67,7 +70,7 @@ import {
FLOW_FIELD_TYPE_NAMES,
ASSIGN_TO_REFERENCE,
} from '../constants'
import { instanceInternalId } from '../filters/utils'
import { instanceInternalId, isOrderedMapTypeOrRefType } from '../filters/utils'
import { FetchProfile } from '../types'

const log = logger(module)
Expand Down Expand Up @@ -1171,6 +1174,43 @@ export const getDefsFromFetchProfile = (fetchProfile: FetchProfile): FieldRefere
/**
* Translate a reference expression back to its original value before deploy.
*/
const isFieldWithOrderedMapAnnotation = (field: Field): boolean =>
Object.values(field.getTypeSync().annotationRefTypes).some(isOrderedMapTypeOrRefType)

const isElementWithOrderedMap = (element: Element): boolean => {
if (isField(element)) {
return isFieldWithOrderedMapAnnotation(element)
}
if (isInstanceElement(element)) {
return Object.values(element.getTypeSync().fields).some(field => isOrderedMapTypeOrRefType(field.getTypeSync()))
}
if (isObjectType(element)) {
return Object.values(element.fields).some(isFieldWithOrderedMapAnnotation)
}
return false
}

export const salesforceAdapterResolveValues: ResolveValuesFunc = async (
element,
getLookUpNameFunc,
elementsSource,
allowEmpty = true,
) => {
const resolvedElement = await resolveValues(element, getLookUpNameFunc, elementsSource, allowEmpty)
// Since OrderedMaps' order values reference values that may contain references, we should resolve the Element twice
// in order to fully resolve it. An example use-case for this is the Field `SBQQ__ProductRule__c.SBQQ__LookupObject__c`
// Where the `fullName` of the Picklist values is a Reference to a Custom Object.
return isElementWithOrderedMap(resolvedElement)
? resolveValues(resolvedElement, getLookUpNameFunc, elementsSource, allowEmpty)
: resolvedElement
}

export const resolveSalesforceChanges = (
changes: readonly Change[],
getLookupNameFunc: GetLookupNameFunc,
): Promise<Change[]> =>
Promise.all(changes.map(change => resolveChangeElement(change, getLookupNameFunc, salesforceAdapterResolveValues)))

export const getLookUpName = (fetchProfile: FetchProfile): GetLookupNameFunc =>
getLookUpNameImpl({
defs: getDefsFromFetchProfile(fetchProfile),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ import { mockTypes } from '../mock_elements'
import { FilterWith } from './mocks'
import { buildFetchProfile } from '../../src/fetch_profile/fetch_profile'
import { FIELD_ANNOTATIONS, ORDERED_MAP_PREFIX } from '../../src/constants'
import { getLookUpName } from '../../src/transformers/reference_mapping'
import { salesforceAdapterResolveValues } from '../../src/adapter'
import { getLookUpName, salesforceAdapterResolveValues } from '../../src/transformers/reference_mapping'
import { isOrderedMapTypeOrRefType } from '../../src/filters/utils'

type layoutAssignmentType = { layout: string; recordType?: string }
Expand Down
3 changes: 1 addition & 2 deletions packages/salesforce-adapter/test/resolve_changes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ import { GetLookupNameFunc } from '@salto-io/adapter-utils'
import { SALESFORCE } from '../src/constants'
import { createOrderedMapType } from '../src/filters/convert_maps'
import { mockTypes } from './mock_elements'
import { salesforceAdapterResolveValues } from '../src/adapter'

import { getLookUpName } from '../src/transformers/reference_mapping'
import { getLookUpName, salesforceAdapterResolveValues } from '../src/transformers/reference_mapping'
import { buildFetchProfile } from '../src/fetch_profile/fetch_profile'

describe('Resolve Salesforce Changes', () => {
Expand Down
29 changes: 29 additions & 0 deletions packages/salesforce-adapter/test/sfdx_parser/sfdx_dump.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ describe('dumpElementsToFolder', () => {
// The SFDX code has special treatment for types that are "non-decomposed", meaning, remain nested within their parent XML
// this is in contrast to "decomposed" types, like, CustomField for example, where they are split into a different file
let existingWorkflowRules: InstanceElement[]
let existingEmailTemplate: InstanceElement
let existingWorkflowAlert: InstanceElement
beforeAll(() => {
existingWorkflowRules = [
createInstanceElement(
Expand All @@ -130,6 +132,25 @@ describe('dumpElementsToFolder', () => {
{ [CORE_ANNOTATIONS.PARENT]: [new ReferenceExpression(new ElemID('salesforce', 'Test__c'))] },
),
]
existingEmailTemplate = createInstanceElement(
{
fullName: 'Template 1',
content: 'email content',
},
mockTypes.EmailTemplate,
)
existingWorkflowAlert = createInstanceElement(
{
fullName: 'Test__c.Alert 1',
description: 'Alert 1',
protected: false,
senderType: 'currentUser',
template: new ReferenceExpression(existingEmailTemplate.elemID),
},
mockTypes.WorkflowAlert,
undefined,
{ [CORE_ANNOTATIONS.PARENT]: [new ReferenceExpression(new ElemID('salesforce', 'Test__c'))] },
)
})
describe('when adding a new nested instance', () => {
const project = setupTmpProject()
Expand All @@ -151,6 +172,8 @@ describe('dumpElementsToFolder', () => {
mockTypes.WorkflowRule,
newWorkflowRule,
...existingWorkflowRules,
existingEmailTemplate,
existingWorkflowAlert,
]),
})
})
Expand All @@ -160,6 +183,7 @@ describe('dumpElementsToFolder', () => {
})
describe('workflow xml content', () => {
let rules: Value[]
let alerts: Value[]
beforeAll(async () => {
const parentXmlPath = path.join(
project.name(),
Expand All @@ -169,6 +193,7 @@ describe('dumpElementsToFolder', () => {
const xmlContent = await readTextFile(parentXmlPath)
const values = xmlToValues(xmlContent)
rules = collections.array.makeArray(values.values.rules)
alerts = collections.array.makeArray(values.values.alerts)
})
it('should add the instance to the parent XML', () => {
expect(rules).toContainEqual(expect.objectContaining({ fullName: 'Rule' }))
Expand All @@ -180,6 +205,10 @@ describe('dumpElementsToFolder', () => {
expect.objectContaining({ fullName: 'TestRule2' }),
]),
)
expect(alerts).toEqual(expect.arrayContaining([expect.objectContaining({ fullName: 'Alert 1' })]))
})
it('should resolve references', () => {
expect(alerts[0].template).toEqual(existingEmailTemplate.value.fullName)
})
})
})
Expand Down

0 comments on commit 62904dd

Please sign in to comment.