All files / src/validation bpmn-validation.ts

100% Statements 42/42
100% Branches 36/36
100% Functions 6/6
100% Lines 42/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132        1048x 4x   1044x 5x   1039x                                   85x 603x 265x   338x 338x 12x                   265x 265x 2x             263x 263x 3x             260x             260x 260x   260x 260x 2x             258x 258x 3x             255x             265x               74x       74x   74x 74x 82x 73x       74x 265x 265x 10x 4x   6x 6x       70x    
import type { AutoLayoutOptions } from '../types';
import { addWarning, type LayoutWarning } from '../layout-warnings';
 
function getRefId(ref: any): string | undefined {
  if (!ref) {
    return undefined;
  }
  if (typeof ref === 'string') {
    return ref;
  }
  return ref.id;
}
 
interface FlowEntry {
  flow: any;
  containerId: string;
}
 
interface FlowCollectorContext {
  containerOf: Map<string, string>;
  flows: FlowEntry[];
}
 
function collectContainersAndFlows(
  elements: any[],
  containerId: string,
  ctx: FlowCollectorContext
): void {
  for (const el of elements) {
    if (el.$type === 'bpmn:SequenceFlow') {
      ctx.flows.push({ flow: el, containerId });
    } else {
      ctx.containerOf.set(el.id, containerId);
      if (Array.isArray(el.flowElements)) {
        collectContainersAndFlows(el.flowElements, el.id, ctx);
      }
    }
  }
}
 
function checkFlowEndpoints(
  entry: FlowEntry,
  containerOf: Map<string, string>
): LayoutWarning | undefined {
  const srcId = getRefId(entry.flow.sourceRef);
  if (!srcId || !containerOf.has(srcId)) {
    return {
      code: 'UNRESOLVED_SEQUENCE_FLOW',
      elementId: entry.flow.id,
      message: `Sequence flow "${entry.flow.id}" references unresolvable sourceRef "${srcId || 'undefined'}"`,
    };
  }
 
  const tgtId = getRefId(entry.flow.targetRef);
  if (!tgtId || !containerOf.has(tgtId)) {
    return {
      code: 'UNRESOLVED_SEQUENCE_FLOW',
      elementId: entry.flow.id,
      message: `Sequence flow "${entry.flow.id}" references unresolvable targetRef "${tgtId || 'undefined'}"`,
    };
  }
 
  return undefined;
}
 
function checkFlowBoundaries(
  entry: FlowEntry,
  containerOf: Map<string, string>
): LayoutWarning | undefined {
  const srcId = getRefId(entry.flow.sourceRef)!;
  const tgtId = getRefId(entry.flow.targetRef)!;
 
  const srcContainer = containerOf.get(srcId);
  if (srcContainer !== entry.containerId) {
    return {
      code: 'CROSS_CONTAINER_FLOW',
      elementId: entry.flow.id,
      message: `Sequence flow "${entry.flow.id}" belongs to container "${entry.containerId}" but its source "${srcId}" lives in container "${srcContainer}" -- sequence flows cannot cross container boundaries`,
    };
  }
 
  const tgtContainer = containerOf.get(tgtId);
  if (tgtContainer !== entry.containerId) {
    return {
      code: 'CROSS_CONTAINER_FLOW',
      elementId: entry.flow.id,
      message: `Sequence flow "${entry.flow.id}" belongs to container "${entry.containerId}" but its target "${tgtId}" lives in container "${tgtContainer}" -- sequence flows cannot cross container boundaries`,
    };
  }
 
  return undefined;
}
 
function validateSingleFlow(
  entry: FlowEntry,
  containerOf: Map<string, string>
): LayoutWarning | undefined {
  return checkFlowEndpoints(entry, containerOf) || checkFlowBoundaries(entry, containerOf);
}
 
export function validateFlowContainers(
  definitions: any,
  options?: AutoLayoutOptions,
  warningsOut?: LayoutWarning[]
): LayoutWarning[] {
  const ctx: FlowCollectorContext = {
    containerOf: new Map<string, string>(),
    flows: [],
  };
  const warnings: LayoutWarning[] = [];
 
  const rootElements = definitions?.rootElements || [];
  for (const root of rootElements) {
    if (root.$type === 'bpmn:Process') {
      collectContainersAndFlows(root.flowElements || [], root.id, ctx);
    }
  }
 
  for (const entry of ctx.flows) {
    const warning = validateSingleFlow(entry, ctx.containerOf);
    if (warning) {
      if (!options?.lenientFlowValidation) {
        throw new Error(warning.message);
      }
      warnings.push(warning);
      addWarning(warningsOut, warning);
    }
  }
 
  return warnings;
}