El parser usaba querySelector() que retornaba solo el primer <bpmn:process>. En diagramas de colaboración con múltiples pools, las actividades de los pools 2..N nunca se cargaban en la store, haciendo imposible configurarlas. Ahora itera todos los <bpmn:process> hijos directos de <definitions> y combina sus nodos y flujos en un único grafo. Pools de soporte sin StartEvent propio (SAP, bandeja manual) son válidos — la validación exige al menos un StartEvent entre todos los pools del diagrama. Agrega 3 tests de regresión para el escenario de colaboración. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
163 lines
5.1 KiB
TypeScript
163 lines
5.1 KiB
TypeScript
import type {
|
|
ProcessGraph,
|
|
ProcessGraphNode,
|
|
ProcessGraphFlow,
|
|
BpmnNodeType,
|
|
BpmnElementId,
|
|
} from './types'
|
|
|
|
// Mapeo de tags XML BPMN → nuestro tipo normalizado
|
|
const TAG_TO_TYPE: Record<string, BpmnNodeType> = {
|
|
startevent: 'startEvent',
|
|
endevent: 'endEvent',
|
|
task: 'task',
|
|
usertask: 'userTask',
|
|
servicetask: 'serviceTask',
|
|
scripttask: 'scriptTask',
|
|
manualtask: 'manualTask',
|
|
businessruletask: 'businessRuleTask',
|
|
subprocess: 'subprocess',
|
|
exclusivegateway: 'exclusiveGateway',
|
|
parallelgateway: 'parallelGateway',
|
|
inclusivegateway: 'inclusiveGateway',
|
|
eventbasedgateway: 'eventBasedGateway',
|
|
intermediatethrowevent: 'intermediateThrowEvent',
|
|
intermediatecatchevent: 'intermediateCatchEvent',
|
|
}
|
|
|
|
function normalizeLocalName(tagName: string): string {
|
|
// Eliminar namespace prefix (bpmn:Task → task)
|
|
const local = tagName.includes(':') ? tagName.split(':')[1] : tagName
|
|
return local.toLowerCase()
|
|
}
|
|
|
|
function getAttr(el: Element, name: string): string {
|
|
return el.getAttribute(name) ?? ''
|
|
}
|
|
|
|
function parseFlowRefs(el: Element, tagName: string): string[] {
|
|
return Array.from(el.getElementsByTagNameNS('*', tagName)).map((ref) => ref.textContent?.trim() ?? '')
|
|
}
|
|
|
|
export class BpmnParseError extends Error {
|
|
constructor(message: string) {
|
|
super(message)
|
|
this.name = 'BpmnParseError'
|
|
}
|
|
}
|
|
|
|
export function parseBpmnXml(xml: string): ProcessGraph {
|
|
let doc: Document
|
|
try {
|
|
const parser = new DOMParser()
|
|
doc = parser.parseFromString(xml, 'application/xml')
|
|
} catch {
|
|
throw new BpmnParseError('El archivo no es un XML válido')
|
|
}
|
|
|
|
const parserError = doc.querySelector('parsererror')
|
|
if (parserError) {
|
|
throw new BpmnParseError('XML malformado: ' + parserError.textContent?.slice(0, 200))
|
|
}
|
|
|
|
const nodes = new Map<BpmnElementId, ProcessGraphNode>()
|
|
const flows = new Map<string, ProcessGraphFlow>()
|
|
const startEventIds: BpmnElementId[] = []
|
|
|
|
// Buscar TODOS los procesos del documento. Las colaboraciones tienen múltiples
|
|
// <bpmn:process> como hijos directos de <definitions>, uno por pool.
|
|
const definitions = doc.documentElement
|
|
const processEls = Array.from(definitions.children).filter(
|
|
(el) => normalizeLocalName(el.tagName) === 'process'
|
|
)
|
|
|
|
if (processEls.length === 0) {
|
|
throw new BpmnParseError('No se encontró un elemento <process> en el BPMN')
|
|
}
|
|
|
|
// Parsear cada proceso y acumular nodos y flujos en mapas compartidos
|
|
for (const processEl of processEls) {
|
|
for (const child of Array.from(processEl.children)) {
|
|
const local = normalizeLocalName(child.tagName)
|
|
const type: BpmnNodeType = TAG_TO_TYPE[local] ?? 'unknown'
|
|
|
|
if (local === 'sequenceflow') {
|
|
const flow: ProcessGraphFlow = {
|
|
id: getAttr(child, 'id'),
|
|
sourceRef: getAttr(child, 'sourceRef'),
|
|
targetRef: getAttr(child, 'targetRef'),
|
|
name: getAttr(child, 'name') || undefined,
|
|
}
|
|
if (flow.id) flows.set(flow.id, flow)
|
|
continue
|
|
}
|
|
|
|
if (type === 'unknown') continue
|
|
|
|
const id = getAttr(child, 'id')
|
|
if (!id) continue
|
|
|
|
const node: ProcessGraphNode = {
|
|
id,
|
|
name: getAttr(child, 'name'),
|
|
type,
|
|
outgoing: parseFlowRefs(child, 'outgoing'),
|
|
incoming: parseFlowRefs(child, 'incoming'),
|
|
}
|
|
|
|
nodes.set(id, node)
|
|
if (type === 'startEvent') startEventIds.push(id)
|
|
}
|
|
}
|
|
|
|
if (nodes.size === 0) {
|
|
throw new BpmnParseError('El proceso no contiene elementos reconocibles')
|
|
}
|
|
|
|
// Pools de soporte (SAP, bandeja manual, etc.) pueden no tener StartEvent propio —
|
|
// exigimos al menos uno entre todos los pools del diagrama.
|
|
if (startEventIds.length === 0) {
|
|
throw new BpmnParseError('El proceso no tiene evento de inicio (StartEvent)')
|
|
}
|
|
|
|
return { nodes, flows, startEventIds }
|
|
}
|
|
|
|
// Extrae solo los IDs y nombres de los task/subprocess — util para inicializar actividades
|
|
export function extractActivityElements(
|
|
graph: ProcessGraph
|
|
): Array<{ bpmnElementId: BpmnElementId; name: string; type: 'task' | 'subprocess' }> {
|
|
const result = []
|
|
for (const node of graph.nodes.values()) {
|
|
if (
|
|
['task', 'userTask', 'serviceTask', 'scriptTask', 'manualTask', 'businessRuleTask'].includes(node.type)
|
|
) {
|
|
result.push({ bpmnElementId: node.id, name: node.name || node.id, type: 'task' as const })
|
|
} else if (node.type === 'subprocess') {
|
|
result.push({ bpmnElementId: node.id, name: node.name || node.id, type: 'subprocess' as const })
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Extrae gateways divergentes (los que tienen >1 outgoing)
|
|
export function extractGatewayElements(
|
|
graph: ProcessGraph
|
|
): Array<{ bpmnElementId: BpmnElementId; name: string; gatewayType: string; outgoing: string[] }> {
|
|
const result = []
|
|
for (const node of graph.nodes.values()) {
|
|
if (
|
|
['exclusiveGateway', 'parallelGateway', 'inclusiveGateway', 'eventBasedGateway'].includes(node.type) &&
|
|
node.outgoing.length > 1
|
|
) {
|
|
result.push({
|
|
bpmnElementId: node.id,
|
|
name: node.name || node.id,
|
|
gatewayType: node.type,
|
|
outgoing: node.outgoing,
|
|
})
|
|
}
|
|
}
|
|
return result
|
|
}
|