Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ _An Extensible Rule Engine capable of conducting static analysis on the metadata
| **Trigger Order** ([`TriggerOrder`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/TriggerOrder.ts)) | Guarantee your flow execution order with the Trigger Order property introduced in Spring '22 |
| **Cyclomatic Complexity** ([`CyclomaticComplexity`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/CyclomaticComplexity.ts)) | The number of loops and decision rules, plus the number of decisions. Use a combination of 1) subflows and 2) breaking flows into multiple concise trigger ordered flows, to reduce the cyclomatic complexity within a single flow, ensuring maintainability and simplicity. |
| **Recursive After Update** ([`RecursiveAfterUpdate`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/RecursiveAfterUpdate.ts)) | After updates are meant to be used for record modifications that are not the same record that triggered the flow. Using after updates on the same record can lead to recursion and unexpected behavior. Consider using before save flows for same record updates. |
| **Get Record All Fields** ([`GetRecordAllFields`](https://github.com/Lightning-Flow-Scanner/lightning-flow-scanner-core/tree/master/src/main/rules/GetRecordAllFields.ts)) | Following the principle of least privilege (PoLP), avoid using Get Records with 'Automatically store all fields' unless necessary. |

## Core Functions

Expand Down
68 changes: 68 additions & 0 deletions src/main/rules/GetRecordAllFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as core from "../internals/internals";
import { RuleCommon } from "../models/RuleCommon";

export class GetRecordAllFields extends RuleCommon implements core.IRuleDefinition {
constructor() {
super(
{
name: "GetRecordAllFields",
label: "Get Record All Fields",
description:
"Following the principle of least privilege (PoLP), avoid using Get Records with 'Automatically store all fields' unless necessary.",
supportedTypes: [...core.FlowType.visualTypes, ...core.FlowType.backEndTypes],
docRefs: [
{
label: "SOQL and SOSL | Best Practices for Deployments with Large Data Volumes",
path: "https://developer.salesforce.com/docs/atlas.en-us.salesforce_large_data_volumes_bp.meta/salesforce_large_data_volumes_bp/ldv_deployments_best_practices_soql_and_sosl.htm",
},
{
label: "Indexes | Best Practices",
path: "https://developer.salesforce.com/docs/atlas.en-us.salesforce_large_data_volumes_bp.meta/salesforce_large_data_volumes_bp/ldv_deployments_infrastructure_indexes.htm",
},
],
isConfigurable: false,
autoFixable: false,
},
{ severity: "warning" }
);
}

public execute(flow: core.Flow): core.RuleResult {
const results: core.ResultDetails[] = [];
const getElementNodes = flow.elements?.filter((element) => element.subtype === "recordLookups");
if (getElementNodes == null || getElementNodes.length === 0) {
return new core.RuleResult(this, results);
}

const errorNodes: core.ResultDetails[] = getElementNodes
.filter((element) => {
const getRecordElement = element as core.FlowNode;
const hasQualifiedElementDefinition = typeof getRecordElement.element === "object";
if (!hasQualifiedElementDefinition) {
return false;
}

const concreteChildElement = getRecordElement.element as core.FlowElement;

const storeAllFields =
"storeOutputAutomatically" in concreteChildElement &&
concreteChildElement["storeOutputAutomatically"];
const hasQueriedFields =
"queriedFields" in concreteChildElement &&
(concreteChildElement["queriedFields"] as string[]).length > 0;

if (storeAllFields && !hasQueriedFields) {
return true;
}

return false;
})
.map((element) => {
const getRecordElement = element as core.FlowNode;
return new core.ResultDetails(getRecordElement);
});

results.push(...errorNodes);
return new core.RuleResult(this, results);
}
}
180 changes: 180 additions & 0 deletions tests/GetRecordElementAllFields.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { ParsedFlow } from "../src/main/models/ParsedFlow";
import { RuleResult, Flow, scan, ScanResult } from "../src";
import { GetRecordAllFields } from "../src/main/rules/GetRecordAllFields";

import { describe, it, expect } from "@jest/globals";

describe("GetRecordAllFields", () => {
it("should be defined", () => {
expect(GetRecordAllFields).toBeDefined();
});

let rule: GetRecordAllFields;
beforeEach(() => {
rule = new GetRecordAllFields();
});

describe("e2e", () => {
it("should be empty when no Get Record elements are present", () => {
const config = {
rules: {
GetRecordAllFields: {
severity: "error",
},
},
};

const flows: ParsedFlow[] = [
{
flow: {
type: "AutoLaunchedFlow",
},
} as Partial<ParsedFlow> as ParsedFlow,
];

const results: ScanResult[] = scan(flows, config);
const scanResults = results.pop();
const ruleResults = scanResults?.ruleResults.filter((rule) => {
return rule.ruleDefinition.name === "GetRecordAllFields" && rule.occurs;
});
expect(ruleResults).toHaveLength(0);
});

it("should error when getRecord element has storeOutputAutomatically", () => {
const config = {
rules: {
GetRecordAllFields: {
severity: "error",
},
},
};

const flows: ParsedFlow[] = [
{
flow: {
type: "AutoLaunchedFlow",
elements: [
{
name: "GetRecord",
subtype: "recordLookups",
metaType: "node",
element: {
storeOutputAutomatically: true,
},
},
],
},
} as Partial<ParsedFlow> as ParsedFlow,
];

const results: ScanResult[] = scan(flows, config);
const scanResults = results.pop();
const ruleResults = scanResults?.ruleResults.filter((rule) => {
return rule.ruleDefinition.name === "GetRecordAllFields" && rule.occurs;
});
expect(ruleResults).toHaveLength(1);
});
});

describe("empty unit", () => {
it("should be empty results when no Get Record elements are present", () => {
const flow: Flow = {
type: "AutoLaunchedFlow",
} as Partial<Flow> as Flow;
const result: RuleResult = rule.execute(flow);
expect(result.occurs).toBeFalsy();
});

it("should be empty when no qualified node", () => {
const flow: Flow = {
type: "AutoLaunchedFlow",
elements: [
{
name: "GetRecord",
subtype: "recordLookups",
metaType: "node",
element: "attribute",
},
],
} as Partial<Flow> as Flow;
const result: RuleResult = rule.execute(flow);
expect(result.occurs).toBeFalsy();
});

it("should be empty when outputReference and queriedFields are present", () => {
const flow: Flow = {
type: "AutoLaunchedFlow",
elements: [
{
name: "GetRecord",
subtype: "recordLookups",
metaType: "node",
element: {
queriedFields: ["Id", "AccountId"],
outputReference: "outputReference",
},
},
],
} as Partial<Flow> as Flow;
const result: RuleResult = rule.execute(flow);
expect(result.occurs).toBeFalsy();
});

it("should be empty when outputAssignments are present", () => {
const flow: Flow = {
type: "AutoLaunchedFlow",
elements: [
{
name: "GetRecord",
subtype: "recordLookups",
metaType: "node",
element: {
outputAssignments: [{ assignToReference: "testVar", field: "AccountId" }],
},
},
],
} as Partial<Flow> as Flow;
const result: RuleResult = rule.execute(flow);
expect(result.occurs).toBeFalsy();
});

it("should be empty when storeOutputAutomatically and queriedFields", () => {
const flow: Flow = {
type: "AutoLaunchedFlow",
elements: [
{
name: "GetRecord",
subtype: "recordLookups",
metaType: "node",
element: {
storeOutputAutomatically: true,
queriedFields: ["Id", "AccountId"],
},
},
],
} as Partial<Flow> as Flow;
const result: RuleResult = rule.execute(flow);
expect(result.occurs).toBeFalsy();
});
});

describe("error unit", () => {
it("should error when Get Record element has storeOutputAutomatically and no queriedFields", () => {
const flow: Flow = {
type: "AutoLaunchedFlow",
elements: [
{
name: "GetRecord",
subtype: "recordLookups",
metaType: "node",
element: {
storeOutputAutomatically: true,
},
},
],
} as Partial<Flow> as Flow;
const result: RuleResult = rule.execute(flow);
expect(result.occurs).toBe(true);
});
});
});