Files
Flash5/flash-extensions/flash-ext-routeviewer/routeviewer-ui/src/normalize.js
T

187 lines
6.7 KiB
JavaScript

/**
* Canonical Graph Normalization.
*
* Transform raw route/handler nodes into a class-centric DAG where:
* - Each handler class appears exactly ONCE
* - Inheritance is the primary relationship
* - Routes are attached to leaf (concrete) handlers
* - Middleware is extracted and attached to handlers
* - Lambda routes (no handler) become isolated leaf nodes
*/
export function normalizeGraph(rawNodes, rawEdges) {
const byId = Object.fromEntries(rawNodes.map(n => [n.id, n]))
const ofType = t => rawNodes.filter(n => n.type === t)
const routeRaw = ofType('route')
const handlerRaw = ofType('handler')
// ── Extract metadata per route ──────────────────────────────────────────
const routeMetadata = new Map() // routeId → {method, path, middleware: []}
routeRaw.forEach(route => {
routeMetadata.set(route.id, {
method: route.data?.method || 'GET',
path: route.data?.path || '/',
middleware: [],
})
})
// ── Extract middleware per route ───────────────────────────────────────
const wrapsEdges = rawEdges.filter(e => e.edgeType === 'wraps')
const wrapsOf = new Map() // wrappedNode → wrapperNode
wrapsEdges.forEach(e => wrapsOf.set(e.target, e.source))
routeRaw.forEach(route => {
const middleware = []
let cur = wrapsOf.get(route.id)
while (cur && byId[cur]?.type === 'middleware') {
middleware.unshift(byId[cur].data.name)
cur = wrapsOf.get(cur)
}
if (middleware.length) {
routeMetadata.get(route.id).middleware = middleware
}
})
// ── Build inheritance map ──────────────────────────────────────────────
// handlerClassName → parentClassName
const inheritanceMap = new Map()
const extendsEdges = rawEdges.filter(e => e.edgeType === 'extends')
extendsEdges.forEach(e => {
const childNode = byId[e.source]
const parentNode = byId[e.target]
if (childNode?.data?.name && parentNode?.data?.name) {
inheritanceMap.set(childNode.data.name, parentNode.data.name)
}
})
// ── Build handler chains per route ─────────────────────────────────────
const handlesEdges = rawEdges.filter(e => e.edgeType === 'handles')
const routeToChain = new Map() // routeId → [className, parentClassName, ...]
handlesEdges.forEach(e => {
const handlerNode = byId[e.target]
if (handlerNode?.data?.name) {
const chain = []
let cur = handlerNode.data.name
while (cur) {
chain.push(cur)
cur = inheritanceMap.get(cur)
}
routeToChain.set(e.source, chain)
}
})
// Ensure ALL routes are in the chain map (routes without handlers get empty chain)
routeRaw.forEach(route => {
if (!routeToChain.has(route.id)) {
routeToChain.set(route.id, [])
}
})
// ── Collect all unique handler class names ──────────────────────────────
const allClassNames = new Set()
routeToChain.forEach(chain => chain.forEach(name => allClassNames.add(name)))
// ── Create canonical handler nodes ────────────────────────────────────
const canonicalHandlers = new Map() // class:ClassName → {id, name, isAbstract, routes[], parentId, isLambda}
;[...allClassNames].forEach(className => {
const id = 'class:' + className
canonicalHandlers.set(id, {
id,
name: className,
isAbstract: true, // will be marked false if used as concrete
routes: [],
parentId: null,
isLambda: false,
})
})
// ── Wire up inheritance ────────────────────────────────────────────────
inheritanceMap.forEach((parentClassName, childClassName) => {
const childId = 'class:' + childClassName
const parentId = 'class:' + parentClassName
if (canonicalHandlers.has(childId) && canonicalHandlers.has(parentId)) {
canonicalHandlers.get(childId).parentId = parentId
}
})
// ── Attach routes to concrete handlers ──────────────────────────────────
routeToChain.forEach((chain, routeId) => {
if (chain.length === 0) {
// Lambda: no handler
const meta = routeMetadata.get(routeId)
const lambdaId = 'class:Lambda:' + meta.method + ':' + encodeURIComponent(meta.path)
canonicalHandlers.set(lambdaId, {
id: lambdaId,
name: 'Lambda Handler',
isAbstract: false,
isLambda: true,
method: meta.method,
path: meta.path,
routes: [{
method: meta.method,
path: meta.path,
middleware: meta.middleware,
}],
parentId: null,
})
} else {
// Normal handler chain: mark concrete (first = depth 0)
const concreteClassName = chain[0]
const concreteId = 'class:' + concreteClassName
if (canonicalHandlers.has(concreteId)) {
const handler = canonicalHandlers.get(concreteId)
handler.isAbstract = false
const meta = routeMetadata.get(routeId)
handler.routes.push({
method: meta.method,
path: meta.path,
middleware: meta.middleware,
})
}
}
})
// ── Build output nodes ────────────────────────────────────────────────
const nodes = [...canonicalHandlers.values()].map(handler => ({
id: handler.id,
type: 'handler',
data: {
name: handler.name,
isAbstract: handler.isAbstract,
isLambda: handler.isLambda,
method: handler.method,
path: handler.path,
routes: handler.routes,
middleware: handler.routes.length > 0
? [...new Set(handler.routes.flatMap(r => r.middleware))]
: [],
},
}))
// ── Build output edges (deduped extends only) ──────────────────────────
const edgeSet = new Set()
const edges = []
canonicalHandlers.forEach((handler, handlerId) => {
if (handler.parentId) {
const key = handler.parentId + '->' + handlerId
if (!edgeSet.has(key)) {
edgeSet.add(key)
edges.push({
id: key,
source: handler.parentId,
target: handlerId,
edgeType: 'extends',
})
}
}
})
return { nodes, edges }
}