diff --git a/packages/orgcheck-api/src/api/core/orgcheck-api-globalparameter.ts b/packages/orgcheck-api/src/api/core/orgcheck-api-globalparameter.ts index 076b51d8..a57ac996 100644 --- a/packages/orgcheck-api/src/api/core/orgcheck-api-globalparameter.ts +++ b/packages/orgcheck-api/src/api/core/orgcheck-api-globalparameter.ts @@ -13,6 +13,9 @@ const GROUP_TYPE_QUEUE = 'queue'; const OBJECTS_MODE = 'objectsmode'; const OBJECTS_MODE_FULL = 'full'; const OBJECTS_MODE_LITE = 'lite'; +const LFS_BETA_MODE = 'lfsbetamode'; +const LFS_MIN_SEVERITY = 'lfsminseverity'; +const LFS_SEVERITY_ALL = '*'; export class OrgCheckGlobalParameter { @@ -196,4 +199,47 @@ export class OrgCheckGlobalParameter { static getObjectsMode(parameters: Map): string { return parameters?.get(OBJECTS_MODE) ?? OBJECTS_MODE_FULL; } + + /** + * @description Key to enable Lightning Flow Scanner beta rules + * @returns {string} The value of the constant + * @static + */ + static get LFS_BETA_MODE(): string { return LFS_BETA_MODE; } + + /** + * @description Key for the minimum LFS violation severity to display ('error', 'warning', 'note', or '*' for all) + * @returns {string} The value of the constant + * @static + */ + static get LFS_MIN_SEVERITY(): string { return LFS_MIN_SEVERITY; } + + /** + * @description Wildcard value meaning all severities + * @returns {string} The value of the constant + * @static + */ + static get LFS_SEVERITY_ALL(): string { return LFS_SEVERITY_ALL; } + + /** + * @description Get the LFS beta mode setting from the parameters + * @param {Map} parameters - Map of parameters + * @returns {boolean} true if beta rules should be included + * @static + * @public + */ + static getLfsBetaMode(parameters: Map): boolean { + return (parameters?.get(LFS_BETA_MODE) as boolean) ?? false; + } + + /** + * @description Get the LFS minimum severity from the parameters + * @param {Map} parameters - Map of parameters + * @returns {string} The minimum severity or '*' (all) + * @static + * @public + */ + static getLfsMinSeverity(parameters: Map): string { + return (parameters?.get(LFS_MIN_SEVERITY) as string) ?? LFS_SEVERITY_ALL; + } } \ No newline at end of file diff --git a/packages/orgcheck-api/src/api/core/orgcheck-api-secretsauce.ts b/packages/orgcheck-api/src/api/core/orgcheck-api-secretsauce.ts index 343675ab..bc267572 100644 --- a/packages/orgcheck-api/src/api/core/orgcheck-api-secretsauce.ts +++ b/packages/orgcheck-api/src/api/core/orgcheck-api-secretsauce.ts @@ -738,7 +738,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ { id: 100, description: '[LFS] Inactive Flow', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('InactiveFlow') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'InactiveFlow') || false) as (data: unknown) => boolean, errorMessage: `This flow is inactive. Consider activating it or removing it from your org.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -746,7 +746,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 101, description: '[LFS] Process Builder', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('ProcessBuilder') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'ProcessBuilder') || false) as (data: unknown) => boolean, errorMessage: `Time to migrate this process builder to flow!`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -754,7 +754,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 102, description: '[LFS] Missing Flow Description', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('FlowDescription') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'FlowDescription') || false) as (data: unknown) => boolean, errorMessage: `This flow does not have a description. Add documentation about its purpose and usage.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -762,7 +762,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 103, description: '[LFS] Outdated API Version', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('APIVersion') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'APIVersion') || false) as (data: unknown) => boolean, errorMessage: `The API version of this flow is outdated. Update it to the newest version.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -770,7 +770,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 104, description: '[LFS] Unsafe Running Context', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('UnsafeRunningContext') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'UnsafeRunningContext') || false) as (data: unknown) => boolean, errorMessage: `This flow runs in System Mode without Sharing, which can lead to unsafe data access.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -778,7 +778,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 105, description: '[LFS] SOQL Query In Loop', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('SOQLQueryInLoop') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'SOQLQueryInLoop') || false) as (data: unknown) => boolean, errorMessage: `This flow has SOQL queries inside loops. Consolidate queries at the end of the flow to avoid governor limits.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -786,7 +786,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 106, description: '[LFS] DML Statement In Loop', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('DMLStatementInLoop') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'DMLStatementInLoop') || false) as (data: unknown) => boolean, errorMessage: `This flow has DML operations inside loops. Consolidate DML at the end to avoid governor limits.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -794,7 +794,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 107, description: '[LFS] Action Calls In Loop', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('ActionCallsInLoop') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'ActionCallsInLoop') || false) as (data: unknown) => boolean, errorMessage: `This flow has action calls inside loops. Bulkify apex calls using collection variables.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -802,7 +802,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 108, description: '[LFS] Hardcoded Id', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('HardcodedId') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'HardcodedId') || false) as (data: unknown) => boolean, errorMessage: `This flow contains hardcoded IDs which are org-specific. Use variables or merge fields instead.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -810,7 +810,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 109, description: '[LFS] Hardcoded Url', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('HardcodedUrl') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'HardcodedUrl') || false) as (data: unknown) => boolean, errorMessage: `This flow contains hardcoded URLs. Use $API formulas or custom labels instead.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -818,7 +818,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 110, description: '[LFS] Missing Null Handler', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('MissingNullHandler') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'MissingNullHandler') || false) as (data: unknown) => boolean, errorMessage: `This flow has Get Records operations without null checks. Use decision elements to validate results.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -826,7 +826,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 111, description: '[LFS] Missing Fault Path', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('MissingFaultPath') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'MissingFaultPath') || false) as (data: unknown) => boolean, errorMessage: `This flow has DML or action operations without fault handlers. Add fault paths for better error handling.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -834,7 +834,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 112, description: '[LFS] Recursive After Update', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('RecursiveAfterUpdate') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'RecursiveAfterUpdate') || false) as (data: unknown) => boolean, errorMessage: `This after-update flow modifies the same record that triggered it, risking recursion. Use before-save flows instead.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -842,7 +842,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 113, description: '[LFS] Duplicate DML Operation', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('DuplicateDMLOperation') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'DuplicateDMLOperation') || false) as (data: unknown) => boolean, errorMessage: `This flow allows navigation back after DML operations, which may cause duplicate changes.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -850,7 +850,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 114, description: '[LFS] Get Record All Fields', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('GetRecordAllFields') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'GetRecordAllFields') || false) as (data: unknown) => boolean, errorMessage: `This flow uses Get Records with "all fields". Specify only needed fields for better performance.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -858,7 +858,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 115, description: '[LFS] Record ID as String', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('RecordIdAsString') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'RecordIdAsString') || false) as (data: unknown) => boolean, errorMessage: `This flow uses a String recordId variable. Modern flows can receive the entire record object, eliminating Get Records queries.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -866,7 +866,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 116, description: '[LFS] Unconnected Element', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('UnconnectedElement') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'UnconnectedElement') || false) as (data: unknown) => boolean, errorMessage: `This flow has unconnected elements that are not in use. Remove them to maintain clarity.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -874,7 +874,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 117, description: '[LFS] Unused Variable', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('UnusedVariable') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'UnusedVariable') || false) as (data: unknown) => boolean, errorMessage: `This flow has unused variables. Remove them to maintain efficiency.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -882,7 +882,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 118, description: '[LFS] Copy API Name', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('CopyAPIName') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'CopyAPIName') || false) as (data: unknown) => boolean, errorMessage: `This flow has elements with copy-paste naming patterns like "Copy_X_Of_Element". Update API names for readability.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -890,7 +890,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 120, description: '[LFS] Same Record Field Updates', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('SameRecordFieldUpdates') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'SameRecordFieldUpdates') || false) as (data: unknown) => boolean, errorMessage: `This before-save flow uses Update Records on $Record. Use direct assignment instead for better performance.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -898,7 +898,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 122, description: '[LFS] Missing Metadata Description', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('MissingMetadataDescription') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'MissingMetadataDescription') || false) as (data: unknown) => boolean, errorMessage: `This flow has elements or variables without descriptions. Add documentation for better maintainability.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -906,7 +906,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 123, description: '[LFS] Missing Filter Record Trigger', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('MissingFilterRecordTrigger') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'MissingFilterRecordTrigger') || false) as (data: unknown) => boolean, errorMessage: `This record-triggered flow lacks filters on changed fields or entry conditions, causing unnecessary executions.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -914,7 +914,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 124, description: '[LFS] Transform Instead of Loop', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('TransformInsteadOfLoop') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'TransformInsteadOfLoop') || false) as (data: unknown) => boolean, errorMessage: `This flow uses Loop + Assignment which could be replaced with Transform element (10x faster).`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], @@ -922,7 +922,7 @@ const ALL_SCORE_RULES: ScoreRule[] = [ }, { id: 125, description: '[LFS] Missing Auto Layout', - formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.includes('AutoLayout') || false) as (data: unknown) => boolean, + formula: ((d: SfdcFlow) => d?.currentVersionRef?.lfsViolations?.some(v => v.name === 'AutoLayout') || false) as (data: unknown) => boolean, errorMessage: `This flow doesn't use Auto-Layout mode. Enable it to keep your flow organized automatically.`, badField: 'currentVersionRef.lfsViolations', applicable: [ DataAliases.SfdcFlow ], diff --git a/packages/orgcheck-api/src/api/core/salesforce/orgcheck-api-lfs-scanner.ts b/packages/orgcheck-api/src/api/core/salesforce/orgcheck-api-lfs-scanner.ts index eace447e..dbdce003 100644 --- a/packages/orgcheck-api/src/api/core/salesforce/orgcheck-api-lfs-scanner.ts +++ b/packages/orgcheck-api/src/api/core/salesforce/orgcheck-api-lfs-scanner.ts @@ -1,6 +1,20 @@ /** * @description Lightning Flow Scanner integration for OrgCheck. Scans flows using the LFS_Core.js static resource */ + +const SEVERITY_ORDER: Record = { error: 3, warning: 2, note: 1 }; + +/** + * @description Returns true if violationSeverity meets or exceeds the minimum required severity + * @param {string} violationSeverity - The severity of the individual violation + * @param {string} minSeverity - The minimum severity threshold ('error', 'warning', 'note', or '*' for all) + * @returns {boolean} + */ +export function meetsMinSeverity(violationSeverity: string, minSeverity: string): boolean { + if (!minSeverity || minSeverity === '*') return true; + return (SEVERITY_ORDER[violationSeverity] ?? 0) >= (SEVERITY_ORDER[minSeverity] ?? 0); +} + export class LFSScanner { /** @@ -39,9 +53,10 @@ export class LFSScanner { * @description Scan flows using Lightning Flow Scanner * @param {any[]} flowRecords - Flow metadata records from Tooling API * @param {Function} CaseSafeId - Function to convert 18-char IDs to 15-char + * @param {boolean} [betaMode] - Whether to include beta rules in the scan * @returns {Promise>} Map of flow version ID to LFS violations */ - static async scanFlows(flowRecords: Record[], CaseSafeId: (id: string) => string): Promise> { + static async scanFlows(flowRecords: Record[], CaseSafeId: (id: string) => string, betaMode = false): Promise> { let results = new Map(); try { // @ts-expect-error: lightningflowscanner is a global library injected at runtime and not declared in TypeScript's Window type definitions @@ -57,8 +72,8 @@ export class LFSScanner { }; }); - // Scan flows - const scanResults = lfsCore.scan(lfsFlows); + // Scan flows, passing betaMode option to include/exclude beta rules + const scanResults = lfsCore.scan(lfsFlows, { betaMode }); // Map results: flowVersionId -> violations results = this.mapResults(scanResults); @@ -73,15 +88,15 @@ export class LFSScanner { /** * @description Map LFS scan results to OrgCheck format * @param {any[]} scanResults - LFS scan results - * @returns {Map} Map of flow version ID to violations + * @returns {Map} Map of flow version ID to violations */ - static mapResults(scanResults: Record[]): Map { + static mapResults(scanResults: Record[]): Map { const violationsMap = new Map(); for (const result of scanResults) { - const ruleResults = result.ruleResults as { occurs: boolean; ruleName: string }[]; + const ruleResults = result.ruleResults as { occurs: boolean; ruleName: string; severity?: string }[]; const violations = ruleResults .filter((ruleResult) => ruleResult.occurs === true) - .map((ruleResult) => ruleResult.ruleName); + .map((ruleResult) => ({ name: ruleResult.ruleName, severity: ruleResult.severity ?? 'error' })); if (violations?.length > 0) { violationsMap.set((result.flow as { uri: string }).uri, violations); } diff --git a/packages/orgcheck-api/src/api/data/orgcheck-api-data-flow.ts b/packages/orgcheck-api/src/api/data/orgcheck-api-data-flow.ts index 4d6fe3eb..8611edb1 100644 --- a/packages/orgcheck-api/src/api/data/orgcheck-api-data-flow.ts +++ b/packages/orgcheck-api/src/api/data/orgcheck-api-data-flow.ts @@ -98,6 +98,22 @@ export interface SfdcFlow extends DataWithScoreAndDependencies { lastModifiedDate: number; } +/** + * Represents a single LFS rule violation with its name and severity + */ +export interface LfsViolation { + /** + * @description The rule name (e.g. 'SOQLQueryInLoop') + * @type {string} + */ + name: string; + /** + * @description The severity of the violation: 'error', 'warning', or 'note' + * @type {string} + */ + severity: string; +} + /** * Represents a Flow Version */ @@ -237,9 +253,9 @@ export interface SfdcFlowVersion extends DataWithoutScore { recordTriggerType: string; /** - * @description LFS Violations (list of rule names) for this flow version - * @type {string[]} + * @description LFS Violations (list of violations with rule name and severity) for this flow version + * @type {LfsViolation[]} * @public */ - lfsViolations: string[]; + lfsViolations: LfsViolation[]; } \ No newline at end of file diff --git a/packages/orgcheck-api/src/api/dataset/orgcheck-api-dataset-flows.ts b/packages/orgcheck-api/src/api/dataset/orgcheck-api-dataset-flows.ts index ea7e59e5..b72f2be4 100644 --- a/packages/orgcheck-api/src/api/dataset/orgcheck-api-dataset-flows.ts +++ b/packages/orgcheck-api/src/api/dataset/orgcheck-api-dataset-flows.ts @@ -7,6 +7,7 @@ import { SalesforceMetadataTypes } from 'src/api/core/salesforce/orgcheck-api-sa import { SalesforceManagerIntf } from 'src/api/core/salesforce/orgcheck-api-salesforcemanager'; import { SfdcFlow, SfdcFlowVersion } from 'src/api/data/orgcheck-api-data-flow'; import { LFSScanner } from 'src/api/core/salesforce/orgcheck-api-lfs-scanner'; +import { OrgCheckGlobalParameter } from 'src/api/core/orgcheck-api-globalparameter'; // Limited list of known types of Flow ProcessType // see all the list at https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_visual_workflow.htm @@ -29,7 +30,7 @@ export class DatasetFlows implements Dataset { * @param {SimpleLoggerIntf} logger - Logger * @returns {Promise>} The result of the dataset */ - async run(sfdcManager: SalesforceManagerIntf, dataFactory: DataFactoryIntf, logger: SimpleLoggerIntf): Promise> { + async run(sfdcManager: SalesforceManagerIntf, dataFactory: DataFactoryIntf, logger: SimpleLoggerIntf, parameters?: Map): Promise> { // First SOQL query logger?.log(`Querying Tooling API about FlowDefinition in the org...`); @@ -125,8 +126,9 @@ export class DatasetFlows implements Dataset { const records = await sfdcManager.readMetadataAtScale('Flow', activeFlowIds, [ 'UNKNOWN_EXCEPTION' ], logger); // There are GACKs throwing that errors for some flows! // Scan flow versions with Lightning Flow Scanner - logger?.log(`Scanning ${records?.length} flows with Lightning Flow Scanner...`); - const lfsViolations = await LFSScanner.scanFlows(records, sfdcManager.caseSafeId); + const betaMode = OrgCheckGlobalParameter.getLfsBetaMode(parameters); + logger?.log(`Scanning ${records?.length} flows with Lightning Flow Scanner (betaMode=${betaMode})...`); + const lfsViolations = await LFSScanner.scanFlows(records, sfdcManager.caseSafeId, betaMode); logger?.log(`LFS gave us ${lfsViolations.size} violations.`); // Lets parse the flow versions by ourselves diff --git a/packages/orgcheck-api/src/api/orgcheck-api-impl.ts b/packages/orgcheck-api/src/api/orgcheck-api-impl.ts index 2b15de05..5393b654 100644 --- a/packages/orgcheck-api/src/api/orgcheck-api-impl.ts +++ b/packages/orgcheck-api/src/api/orgcheck-api-impl.ts @@ -538,14 +538,15 @@ export class API implements ApiIntf { * @returns {string} cachestamp of the data * @public */ - public cachestampData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string): string { + public cachestampData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string, extraParams?: Map): string { const logger = this._loggerFactory?.create('Cache Stamp Data', false); try { logger?.log(`Calling the cachestamp method for recipe: ${alias} with namespace: ${namespace}, sobjectType: ${sobjectType} and sobject: ${sobject}`); const results = this._recipeManager.cachestamp(alias, new Map([ [OrgCheckGlobalParameter.SOBJECT_NAME, sobject], [OrgCheckGlobalParameter.PACKAGE_NAME, namespace], - [OrgCheckGlobalParameter.SOBJECT_TYPE_NAME, sobjectType] + [OrgCheckGlobalParameter.SOBJECT_TYPE_NAME, sobjectType], + ...(extraParams ?? []) ])); logger?.log(`Done.`); return results; @@ -569,7 +570,7 @@ export class API implements ApiIntf { * @async * @public */ - public async prepareData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string): Promise | DataCollectionStatisticsIntf[]> { + public async prepareData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string, extraParams?: Map): Promise | DataCollectionStatisticsIntf[]> { const logger = this._loggerFactory?.create(`Preparing data for recipe: ${alias}`, false); try { // Check if usage terms were accepted @@ -581,7 +582,8 @@ export class API implements ApiIntf { const results = await this._recipeManager.prepare(alias, new Map([ [OrgCheckGlobalParameter.SOBJECT_NAME, sobject], [OrgCheckGlobalParameter.PACKAGE_NAME, namespace], - [OrgCheckGlobalParameter.SOBJECT_TYPE_NAME, sobjectType] + [OrgCheckGlobalParameter.SOBJECT_TYPE_NAME, sobjectType], + ...(extraParams ?? []) ]), logger?.toSimpleLogger()); logger?.log(`Done.`); return results; @@ -665,7 +667,7 @@ export class API implements ApiIntf { * @param sobject * @public */ - public cleanData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string): void { + public cleanData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string, extraParams?: Map): void { const logger = this._loggerFactory?.create(`Clean Data for recipe: ${alias}`, false); try { // Clean the data @@ -673,7 +675,8 @@ export class API implements ApiIntf { const results = this._recipeManager.clean(alias, new Map([ [OrgCheckGlobalParameter.SOBJECT_NAME, sobject], [OrgCheckGlobalParameter.PACKAGE_NAME, namespace], - [OrgCheckGlobalParameter.SOBJECT_TYPE_NAME, sobjectType] + [OrgCheckGlobalParameter.SOBJECT_TYPE_NAME, sobjectType], + ...(extraParams ?? []) ])); logger?.log(`Done.`); return results; diff --git a/packages/orgcheck-api/src/api/orgcheck-api.ts b/packages/orgcheck-api/src/api/orgcheck-api.ts index 63e889c4..9cc2cc47 100644 --- a/packages/orgcheck-api/src/api/orgcheck-api.ts +++ b/packages/orgcheck-api/src/api/orgcheck-api.ts @@ -234,7 +234,7 @@ export interface ApiIntf { * @returns {string} cachestamp of the data * @public */ - cachestampData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string): string; + cachestampData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string, extraParams?: Map): string; /** * @description Prepare data for a specific recipe. This method will retrieve the data from the org, compute the @@ -248,7 +248,7 @@ export interface ApiIntf { * @async * @public */ - prepareData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string): Promise | DataCollectionStatisticsIntf[]>; + prepareData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string, extraParams?: Map): Promise | DataCollectionStatisticsIntf[]>; /** * @description Serve data for a specific recipe. This method will format the data in a way that can be used by the UI. @@ -287,5 +287,5 @@ export interface ApiIntf { * @param sobject * @public */ - cleanData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string): void; + cleanData(alias: RecipeAliases, namespace: string, sobjectType: string, sobject: string, extraParams?: Map): void; } \ No newline at end of file diff --git a/packages/orgcheck-api/src/api/recipe/orgcheck-api-recipe-flows.ts b/packages/orgcheck-api/src/api/recipe/orgcheck-api-recipe-flows.ts index b1a4c907..c0d07178 100644 --- a/packages/orgcheck-api/src/api/recipe/orgcheck-api-recipe-flows.ts +++ b/packages/orgcheck-api/src/api/recipe/orgcheck-api-recipe-flows.ts @@ -4,8 +4,11 @@ import { TableFactory } from 'src/ui/table/orgcheck-ui-table-factory'; import { MediumProcessor } from 'src/api/core/orgcheck-api-processor'; import { DatasetRunInformation } from 'src/api/core/dataset/orgcheck-api-dataset-runinformation'; import { DatasetAliases } from 'src/api/core/dataset/orgcheck-api-datasets-aliases'; -import { SfdcFlow }from 'src/api/data/orgcheck-api-data-flow'; +import { SfdcFlow } from 'src/api/data/orgcheck-api-data-flow'; import { FlowsTableDefinition } from 'src/ui/table/definitions/orgcheck-ui-tabledef-flows'; +import { SimpleLoggerIntf } from 'src/api/core/logger/orgcheck-api-logger'; +import { OrgCheckGlobalParameter } from 'src/api/core/orgcheck-api-globalparameter'; +import { meetsMinSeverity } from 'src/api/core/salesforce/orgcheck-api-lfs-scanner'; export class RecipeFlows implements ServedRecipe { @@ -18,31 +21,38 @@ export class RecipeFlows implements ServedRecipe { /** * @description List all ingredients (aka dataset aliases or datasetRunInfos) that Org Check will use in this recipe + * @param {SimpleLoggerIntf} _logger - Logger + * @param {Map} parameters - Parameters including LFS options * @returns {Array} The ingredients to use in this recipe * @public */ - public ingredients(): Array { - return [DatasetAliases.FLOWS]; + public ingredients(_logger: SimpleLoggerIntf, parameters: Map): Array { + return [new DatasetRunInformation( + DatasetAliases.FLOWS, + `${DatasetAliases.FLOWS}-${OrgCheckGlobalParameter.getLfsBetaMode(parameters) ? 'beta' : 'stable'}`, + new Map([[OrgCheckGlobalParameter.LFS_BETA_MODE, OrgCheckGlobalParameter.getLfsBetaMode(parameters)]]) + )]; } /** - * @description List the parameters that this mix depends on on - * @returns {string[]} List of parameters that this mix dependes on + * @description List the parameters that this mix depends on + * @returns {string[]} List of parameters that this mix depends on * @public */ public mixDependencies(): string[] { - return []; + return [OrgCheckGlobalParameter.LFS_BETA_MODE, OrgCheckGlobalParameter.LFS_MIN_SEVERITY]; } /** * @description mix the ingredients all together and return the result * @param {Map} ingredients - Records or information grouped by their alias in a Map * @param {SimpleLoggerIntf} _logger - Logger + * @param {Map} parameters - Parameters including LFS severity filter * @returns {Promise} Returns the mixture * @async * @public */ - public async mix(ingredients: Map): Promise { + public async mix(ingredients: Map, _logger: SimpleLoggerIntf, parameters: Map): Promise { // Get data const flows = ingredients.get(DatasetAliases.FLOWS) as Map; @@ -50,11 +60,19 @@ export class RecipeFlows implements ServedRecipe { // Checking data and filter if (!flows) throw new Error(`RecipeFlows: Data from dataset alias 'FLOWS' was undefined.`); + const minSeverity = OrgCheckGlobalParameter.getLfsMinSeverity(parameters); + // Filter data const array: SfdcFlow[] = []; await MediumProcessor.forEach(flows, async (flow: SfdcFlow) => { if (flow.isProcessBuilder === false) { - array.push(flow); + // Apply severity filter: keep the flow if it has no LFS violations, or if at least + // one violation meets the minimum severity threshold + if (minSeverity === OrgCheckGlobalParameter.LFS_SEVERITY_ALL || + !flow.currentVersionRef?.lfsViolations?.length || + flow.currentVersionRef.lfsViolations.some(v => meetsMinSeverity(v.severity, minSeverity))) { + array.push(flow); + } } }); diff --git a/packages/orgcheck-api/src/ui/table/definitions/orgcheck-ui-tabledef-flows.ts b/packages/orgcheck-api/src/ui/table/definitions/orgcheck-ui-tabledef-flows.ts index 1a2f3a10..edd986a3 100644 --- a/packages/orgcheck-api/src/ui/table/definitions/orgcheck-ui-tabledef-flows.ts +++ b/packages/orgcheck-api/src/ui/table/definitions/orgcheck-ui-tabledef-flows.ts @@ -29,7 +29,7 @@ export class FlowsTableDefinition implements TableDefinition { { label: '# DML Delete Nodes', type: ColumnType.NUM, data: { value: 'currentVersionRef.dmlDeleteNodeCount' }}, { label: '# DML Update Nodes', type: ColumnType.NUM, data: { value: 'currentVersionRef.dmlUpdateNodeCount' }}, { label: '# Screen Nodes', type: ColumnType.NUM, data: { value: 'currentVersionRef.screenNodeCount' }}, - { label: 'Its LFS Violations', type: ColumnType.TXTS, data: { values: 'currentVersionRef.lfsViolations', value: '.' }}, + { label: 'Its LFS Violations', type: ColumnType.TXTS, data: { values: 'currentVersionRef.lfsViolations', value: 'name' }}, { label: 'Its created date', type: ColumnType.DTM, data: { value: 'currentVersionRef.createdDate' }}, { label: 'Its modified date', type: ColumnType.DTM, data: { value: 'currentVersionRef.lastModifiedDate' }}, { label: 'Its description', type: ColumnType.TXT, data: { value: 'currentVersionRef.description' }, modifier: { maximumLength: 45, valueIfEmpty: 'No description.' }}, diff --git a/packages/orgcheck-api/src/ui/table/definitions/orgcheck-ui-tabledef-processbuilders.ts b/packages/orgcheck-api/src/ui/table/definitions/orgcheck-ui-tabledef-processbuilders.ts index c3e0f5e6..07e7cb6f 100644 --- a/packages/orgcheck-api/src/ui/table/definitions/orgcheck-ui-tabledef-processbuilders.ts +++ b/packages/orgcheck-api/src/ui/table/definitions/orgcheck-ui-tabledef-processbuilders.ts @@ -27,7 +27,7 @@ export class ProcessBuildersTableDefinition implements TableDefinition { { label: '# DML Delete Nodes', type: ColumnType.NUM, data: { value: 'currentVersionRef.dmlDeleteNodeCount' }}, { label: '# DML Update Nodes', type: ColumnType.NUM, data: { value: 'currentVersionRef.dmlUpdateNodeCount' }}, { label: '# Screen Nodes', type: ColumnType.NUM, data: { value: 'currentVersionRef.screenNodeCount' }}, - { label: 'Its LFS Violations', type: ColumnType.TXTS, data: { values: 'currentVersionRef.lfsViolations', value: '.' }}, + { label: 'Its LFS Violations', type: ColumnType.TXTS, data: { values: 'currentVersionRef.lfsViolations', value: 'name' }}, { label: 'Its created date', type: ColumnType.DTM, data: { value: 'currentVersionRef.createdDate' }}, { label: 'Its modified date', type: ColumnType.DTM, data: { value: 'currentVersionRef.lastModifiedDate' }}, { label: 'Its description', type: ColumnType.TXT, data: { value: 'currentVersionRef.description' }, modifier: { maximumLength: 45, valueIfEmpty: 'No description.' }}, diff --git a/packages/orgcheck-salesforce-app/force-app/main/default/lwc/orgcheckApp/orgcheckApp.html b/packages/orgcheck-salesforce-app/force-app/main/default/lwc/orgcheckApp/orgcheckApp.html index 5fe05b77..745e3a29 100644 --- a/packages/orgcheck-salesforce-app/force-app/main/default/lwc/orgcheckApp/orgcheckApp.html +++ b/packages/orgcheck-salesforce-app/force-app/main/default/lwc/orgcheckApp/orgcheckApp.html @@ -859,7 +859,35 @@

{object}

export-basename={exportBasenames.chattergroups}>
- +
+
+ +
+
+ + + + +
+
+ +
+
+
+
+
+ {object} infinite-scrolling-initial-nb-rows=25 infinite-scrolling-additional-nb-rows=100 table={tableData.flows} - show-export-button - export-source={exportData.flows} + show-export-button + export-source={exportData.flows} export-basename={exportBasenames.flows}>
diff --git a/packages/orgcheck-salesforce-app/force-app/main/default/lwc/orgcheckApp/orgcheckApp.js b/packages/orgcheck-salesforce-app/force-app/main/default/lwc/orgcheckApp/orgcheckApp.js index ed5e15eb..c7f40bb5 100644 --- a/packages/orgcheck-salesforce-app/force-app/main/default/lwc/orgcheckApp/orgcheckApp.js +++ b/packages/orgcheck-salesforce-app/force-app/main/default/lwc/orgcheckApp/orgcheckApp.js @@ -144,6 +144,13 @@ export default class OrgcheckApp extends LightningElement { */ @track exportBasenames = { }; + /** + * @description LFS options for the Flows section + * @type {{ betaMode: boolean, minSeverity: string }} + * @public + */ + @track lfsOptions = { betaMode: false, minSeverity: '*' } + /** * @description Is something is loading? * @type {boolean} @@ -479,7 +486,7 @@ export default class OrgcheckApp extends LightningElement { key: 'F', title: '🤖 Automations', items: { - FLOWS: { key: '1C', data: 'flows', recipe: Recipes.FLOWS }, + FLOWS: { key: '1C', data: 'flows', recipe: Recipes.FLOWS, storeData: true, getExtraParams: (that) => that._getLfsExtraParams() }, PBS: { key: '1D', data: 'processbuilders', recipe: Recipes.PROCESS_BUILDERS }, WORKFLOWS: { key: '1E', data: 'workflows', recipe: Recipes.WORKFLOWS }, } @@ -671,21 +678,22 @@ export default class OrgcheckApp extends LightningElement { if (appNavItem.recipe) { // -------------------------------------------------------------------------------- - // Potentially we need to refresh the data + // Potentially we need to refresh the data // -------------------------------------------------------------------------------- - // if forceRefresh = true, we want to refresh the data from the API + // if forceRefresh = true, we want to refresh the data from the API + const extraParams = appNavItem.getExtraParams ? appNavItem.getExtraParams(this) : undefined; if (forceRefresh === true) { - this._private_properties.api.cleanData(appNavItem.recipe, this.namespace, this.objectType, this.object); + this._private_properties.api.cleanData(appNavItem.recipe, this.namespace, this.objectType, this.object, extraParams); } - + // -------------------------------------------------------------------------------- // Alias is useful to check if the data has potentially changed - // Each data may have a dependency with global filters, so if one of the filter - // changed and the value has dependency with it, it's more likely that the data + // Each data may have a dependency with global filters, so if one of the filter + // changed and the value has dependency with it, it's more likely that the data // needs top be updated // -------------------------------------------------------------------------------- // get the current alias depending on the dependency with global filter values - const alias = this._private_properties.api.cachestampData(appNavItem.recipe, this.namespace, this.objectType, this.object); + const alias = this._private_properties.api.cachestampData(appNavItem.recipe, this.namespace, this.objectType, this.object, extraParams); if (alias === '-') forceRefresh = true; if (forceRefresh === true || appNavItem.lastAlias !== alias) { @@ -694,7 +702,7 @@ export default class OrgcheckApp extends LightningElement { // shall we proceed getting the data for this item? It depends if there is a onlyIf condition or not, and if yes if it is validated or not if (appNavItem.onlyIf ? appNavItem.onlyIf(this) === true : true) { // If yes we prepare the data - const mixture = await this._private_properties.api.prepareData(appNavItem.recipe, this.namespace, this.objectType, this.object); + const mixture = await this._private_properties.api.prepareData(appNavItem.recipe, this.namespace, this.objectType, this.object, extraParams); // serve the data /** @type {object} */ const plate = await this._private_properties.api.serveData(appNavItem.recipe, mixture); @@ -985,6 +993,48 @@ export default class OrgcheckApp extends LightningElement { } } + /** + * @description Handle the LFS beta mode toggle in the Flows section + * @param {Event} event - Change event from the checkbox + * @public + * @async + */ + async handleLfsBetaModeChange(event) { + try { + this.lfsOptions = { ...this.lfsOptions, betaMode: event.target.checked }; + await this._async_updateCurrentData(true); + } catch (error) { + this._private_properties.modal?.showErrors('handleLfsBetaModeChange', error); + } + } + + /** + * @description Handle the LFS minimum severity filter change in the Flows section + * @param {Event} event - Change event from the select + * @public + * @async + */ + async handleLfsMinSeverityChange(event) { + try { + this.lfsOptions = { ...this.lfsOptions, minSeverity: event.target.value }; + await this._async_updateCurrentData(false); + } catch (error) { + this._private_properties.modal?.showErrors('handleLfsMinSeverityChange', error); + } + } + + /** + * @description Build the LFS extra params Map for prepareData / cleanData / cachestampData + * @returns {Map} + * @private + */ + _getLfsExtraParams() { + return new Map([ + ['lfsbetamode', this.lfsOptions.betaMode], + ['lfsminseverity', this.lfsOptions.minSeverity] + ]); + } + /** * @description Method called when the user ask to log a specific cache item in the console * @param {Event} event - The event information