-
Notifications
You must be signed in to change notification settings - Fork 90
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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 => |
There was a problem hiding this comment.
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 => |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
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
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)
There was a problem hiding this 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
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) |
There was a problem hiding this comment.
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 :)
const hasFlowReferenceError = ({ | ||
targetReferences, | ||
flowNodes, | ||
}: { | ||
targetReferences: Map<string, ElemID> | ||
flowNodes: Map<string, ElemID> | ||
}): boolean => |
There was a problem hiding this comment.
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?
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 } }, | ||
}, | ||
}) |
There was a problem hiding this comment.
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' }, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expect(errors).toHaveLength(0) | |
expect(errors).toBeEmpty() |
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.`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
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.`) |
There was a problem hiding this comment.
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 () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
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. | |
There was a problem hiding this comment.
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
There was a problem hiding this 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 |
There was a problem hiding this comment.
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.
kind?: string | |
kind?: typeof FLOW_NODE | typeof TARGET_REFERENCE |
type ReferenceToElemId = { | ||
referenceOrName: string | ||
elemId: ElemID | ||
kind?: string |
There was a problem hiding this comment.
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
c5f670b
to
885f71c
Compare
SALTO-6493: Change Validator that validates Flows have the referenced Elements
Additional context for reviewer:
Error message format:
Release Notes:
None
User Notifications:
None