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

SALTO-6493: Change Validator that validates Flows have the referenced Elements #6978

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

AlmogMesilaty
Copy link
Contributor

SALTO-6493: Change Validator that validates Flows have the referenced Elements


Additional context for reviewer:
Error message format:
image


Release Notes:
None


User Notifications:
None

@coveralls
Copy link

coveralls commented Dec 19, 2024

Coverage Status

coverage: 93.787% (+0.1%) from 93.66%
when pulling 885f71c on AlmogMesilaty:SALTO-6493
into b65522e on salto-io:main.

Copy link
Contributor

@idoamit8 idoamit8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job!! 💯
I added some comments but will leave the rest of the review to @Uri-Bar and @tamtamirr

As we discussed, I noticed you're not using any get methods from the Map and are treating it purely as key-value pairs.
In this case, I would personally recommend using Record. While both have similar capabilities, Record is a simpler data structure and should be more than sufficient here.


const isFlowNode = (fields: FieldMap): boolean => 'locationX' in fields && 'locationY' in fields

const isFlowNodeName = (value: unknown, path: ElemID, parent: ObjectType): value is string =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am might missing something, but I don't like the return value here because it doesn't just check if the value is a string; it also includes additional logic. I think the function should return a boolean instead.

const isFlowNodeName = (value: unknown, path: ElemID, parent: ObjectType): value is string =>
isFlowNode(parent.fields) && path.name === 'name' && _.isString(value)

const isTargetReference = (value: unknown, path: ElemID): value is string =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

const flowNodes = new Map<string, ElemID>()
const targetReferences = new Map<string, ElemID>()
const findFlowNodesAndTargetReferences: TransformFuncSync = ({ value, path, field }) => {
if (!field || !path) return value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a variable is not a boolean, avoid using ! to check if it's undefined, as it will also return the same result for values like 0, an empty string, or other falsy values.

Comment on lines 94 to 109
const errors: ChangeError[] = []
const isReferenceToMissingElement = ([reference, _elemId]: [string, ElemID]): boolean => !flowNodes.has(reference)
const missingReferencedElementsErrors = Array.from(targetReferences).filter(isReferenceToMissingElement)
const isUnreferencedElement = ([reference, _elemId]: [string, ElemID]): boolean => !targetReferences.has(reference)
const unreferencedElementsErrors = Array.from(flowNodes).filter(isUnreferencedElement)
if (missingReferencedElementsErrors.length > 0) {
missingReferencedElementsErrors.forEach(([targetReference, elemId]) =>
errors.push(createMissingReferencedElementChangeError(targetReference, elemId)),
)
}
if (unreferencedElementsErrors.length > 0) {
unreferencedElementsErrors.forEach(([flowNode, elemId]) =>
errors.push(createUnreferencedElementChangeError(flowNode, elemId)),
)
}
return errors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this a bit clearer

Suggested change
const errors: ChangeError[] = []
const isReferenceToMissingElement = ([reference, _elemId]: [string, ElemID]): boolean => !flowNodes.has(reference)
const missingReferencedElementsErrors = Array.from(targetReferences).filter(isReferenceToMissingElement)
const isUnreferencedElement = ([reference, _elemId]: [string, ElemID]): boolean => !targetReferences.has(reference)
const unreferencedElementsErrors = Array.from(flowNodes).filter(isUnreferencedElement)
if (missingReferencedElementsErrors.length > 0) {
missingReferencedElementsErrors.forEach(([targetReference, elemId]) =>
errors.push(createMissingReferencedElementChangeError(targetReference, elemId)),
)
}
if (unreferencedElementsErrors.length > 0) {
unreferencedElementsErrors.forEach(([flowNode, elemId]) =>
errors.push(createUnreferencedElementChangeError(flowNode, elemId)),
)
}
return errors
const targetEntries = Array.from(targetReferences)
const flowEntries = Array.from(flowNodes)
const missingReferenceErrors = targetEntries
.filter(([reference]) => !flowNodes.has(reference))
.map(([reference, elemId]) => createMissingReferencedElementChangeError(reference, elemId))
const unreferencedErrors = flowEntries
.filter(([reference]) => !targetReferences.has(reference))
.map(([reference, elemId]) => createUnreferencedElementChangeError(reference, elemId))
return [...missingReferenceErrors, ...unreferencedErrors]

(But if the lists can be very long sometimes ... is not a good approach and its better to push)

Copy link
Contributor

@tamtamirr tamtamirr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work 👑 Added some comments

Comment on lines 27 to 31
const isFlowNodeName = (value: unknown, path: ElemID, parent: ObjectType): value is string =>
isFlowNode(parent.fields) && path.name === 'name' && _.isString(value)

const isTargetReference = (value: unknown, path: ElemID): value is string =>
path.name === 'targetReference' && _.isString(value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods seem redundant to me. I think it makes more sense to have these assertions inlined within the code of your transformFunc instead of having a type-guard here :)

Comment on lines 63 to 69
const hasFlowReferenceError = ({
targetReferences,
flowNodes,
}: {
targetReferences: Map<string, ElemID>
flowNodes: Map<string, ElemID>
}): boolean =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we run assertion twice? Can we avoid that?

Comment on lines 15 to 45
const FlowConnector = createMetadataObjectType({
annotations: {
metadataType: 'FlowConnector',
},
fields: {
targetReference: { refType: BuiltinTypes.STRING },
},
})
const FlowNode = createMetadataObjectType({
annotations: {
metadataType: 'FlowNode',
},
fields: {
name: { refType: BuiltinTypes.STRING },
locationX: { refType: BuiltinTypes.NUMBER, annotations: { constant: 1 } },
locationY: { refType: BuiltinTypes.NUMBER, annotations: { constant: 1 } },
connector: { refType: FlowConnector, annotations: { required: false } },
},
})
const Flow = createMetadataObjectType({
annotations: {
metadataType: 'Flow',
},
fields: {
start: { refType: FlowNode },
actionCalls: { refType: new ListType(FlowNode), annotations: { required: false } },
assignments: { refType: new ListType(FlowNode), annotations: { required: false } },
decisions: { refType: new ListType(FlowNode), annotations: { required: false } },
recordCreates: { refType: new ListType(FlowNode), annotations: { required: false } },
},
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Move this into a beforeEach. Since Elements are mutable, it's better to initialize them for each test.

{
fullName: 'TestFlow',
start: {
connector: { targetReference: 'ActionCall' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we const targetReference and use it both in the code and UTs?

})
it('should not return any errors', async () => {
const errors = await flowReferencedElements([flowChange])
expect(errors).toHaveLength(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect(errors).toHaveLength(0)
expect(errors).toBeEmpty()

Comment on lines 102 to 106
expect(errors).toHaveLength(1)
const [error] = errors
expect(error.severity).toEqual('Error')
expect(error.message).toEqual('Reference to missing Flow Element')
expect(error.detailedMessage).toEqual(`The Flow Element "${'ActionCall'}" does not exist.`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect(errors).toHaveLength(1)
const [error] = errors
expect(error.severity).toEqual('Error')
expect(error.message).toEqual('Reference to missing Flow Element')
expect(error.detailedMessage).toEqual(`The Flow Element "${'ActionCall'}" does not exist.`)
expect(errors).toEqual([{
message: '...',
detailedMessge: '...',
severity: 'Error',
elemID: missingRefElemID,
}])

Nit: Can be greatly simplified

Comment on lines 126 to 130
expect(errors).toHaveLength(1)
const [error] = errors
expect(error.severity).toEqual('Info')
expect(error.message).toEqual('Unused Flow Element')
expect(error.detailedMessage).toEqual(`The Flow Element “${'ActionCall'}” isn’t being used in the Flow.`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can be greatly simplified

)
flowChange = toChange({ after: flow })
})
it('should create change error per error', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('should create change error per error', async () => {
it('should create change error per issue', async () => {

Nit: I think it's confusing to use the term error on both sides

})
it('should create change error per error', async () => {
const errors = await flowReferencedElements([flowChange])
expect(errors).toHaveLength(3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's validate the errors explicitly :)

@@ -355,3 +355,4 @@ For more details see the DeployOptions section in the [salesforce documentation
| accountSettings | true | Cannot set a value for enableAccountOwnerReport without proper org setting. |
| unknownPicklistValues | true | Disallow any usage of unknown pickList values. |
| customApplications | true | Check for multiple action overrides for the same action in customApplications instances. |
| flowReferencedElements | true | Check that the referenced elements in Flow exist. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Shouldn't the | be aligned with the rest? (I think prettier should solve that)

Updated tests according to CR

Added test
Copy link
Contributor

@idoamit8 idoamit8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NICE! 💯
Added two small nits questions

type ReferenceToElemId = {
referenceOrName: string
elemId: ElemID
kind?: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can kind ever be a string other than FLOW_NODE or TARGET_REFERENCE?
If not, it might be clearer to make this explicit.

Suggested change
kind?: string
kind?: typeof FLOW_NODE | typeof TARGET_REFERENCE

type ReferenceToElemId = {
referenceOrName: string
elemId: ElemID
kind?: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you allow kind to be undefined in the type? It seems it always defined in your code

Updated according to CR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants