build.ts 13 KB
// 本体 IR 合成器:从后端 /api/meta/model/{code}(解析后的 YAML→JSON)在浏览器端合成统一 IR。
// 等价于 onto-app 里 Python codegen 的 IR 构建,改为 TypeScript 实现(“换一种方式重写”)。
// 仅读取现有只读接口,不改后端;三个视图(ER / 网络图 / 流程图)共用产物。

import { api } from '../api'
import type {
  Ontology, OntoObject, OntoAttribute, OntoRelationship,
  OntoBehavior, OntoRule, OntoEvent, ProcessModel, ProcessNode, ProcessFlow,
} from './types'

const A = <T = any>(x: any): T[] => (Array.isArray(x) ? x : [])
const S = (x: any): string => (x == null ? '' : String(x))

/** 拉取相关本体层并合成 IR。 */
export async function loadOntology(): Promise<Ontology> {
  const [m1, m2, me, mr, m4] = await Promise.all([
    api.model('M1'), api.model('M2'), api.model('ME'), api.model('MetaRule'), api.model('M4'),
  ])
  return buildOntology(m1.content, m2.content, me.content, mr.content, m4.content)
}

export function buildOntology(m1: any, m2: any, me: any, mr: any, m4: any): Ontology {
  const objects = buildObjects(m1)
  const events = buildEvents(me)
  const rules = buildRules(mr)
  const behaviors = buildBehaviors(m2)
  linkRulesToBehaviors(behaviors, rules, m4)
  const processModels = buildProcessModels(m4)
  return {
    domain: '销售订单交易(M 系列本体)',
    version: '2.0',
    objects, behaviors, rules, events,
    navigations: [], // 本项目无显式导航树,网络图退化为按聚合平铺
    processModels,
  }
}

// ============================================================
// M1 → objects(聚合根 + 组合子实体,含属性/主键/外键/引用/组合关系)
// ============================================================
function buildObjects(m1: any): OntoObject[] {
  const out: OntoObject[] = []
  for (const agg of A(m1?.aggregates)) {
    const aggregateId = S(agg.aggregateId)
    const root = agg.aggregateRoot || {}
    const rootId = S(root.name) || aggregateId
    const rootAttrs = mapAttributes(root.identifier, A(root.attributes))
    const rootRels: OntoRelationship[] = []

    // 跨聚合引用(refAggregate 字段)→ 引用关系
    for (const a of rootAttrs) {
      if (a.ref) rootRels.push({
        name: a.ref.root, label: a.name, target_object_id: a.ref.root,
        kind: 'MANY_TO_ONE', refType: 'reference', fk_field: a.name,
      })
    }
    // 组合关系(relations[relationType=composition])→ 子实体
    const childNames = A(root.relations).filter((r: any) => S(r.relationType) === 'composition').map((r: any) => S(r.ref))
    for (const cn of childNames) rootRels.push({
      name: cn, label: '组合', target_object_id: cn, kind: 'ONE_TO_MANY', refType: 'composition',
    })

    out.push({
      id: rootId, name: rootId, alias: aggregateId, description: S(root.description) || S(agg.description),
      is_aggregate_root: true, parent_id: null, aggregateId, attributes: rootAttrs, relationships: rootRels,
    })

    // 子实体
    for (const ent of A(agg.entities)) {
      const entId = S(ent.name)
      const entAttrs = mapAttributes(ent.localIdentifier, A(ent.attributes))
      const entRels: OntoRelationship[] = []
      for (const a of entAttrs) {
        if (a.ref) entRels.push({
          name: a.ref.root, label: a.name, target_object_id: a.ref.root,
          kind: 'MANY_TO_ONE', refType: 'reference', fk_field: a.name,
        })
      }
      out.push({
        id: entId, name: entId, alias: `${aggregateId}.${entId}`, description: S(ent.description),
        is_aggregate_root: false, parent_id: rootId, aggregateId, attributes: entAttrs, relationships: entRels,
      })
    }
  }
  return out
}

/** identifier(主键,M1 中单列于外层)前置为合成主键属性 + 其余字段。 */
function mapAttributes(identifier: any, attrs: any[]): OntoAttribute[] {
  const out: OntoAttribute[] = []
  const idName = S(identifier)
  if (idName) out.push({ name: idName, label: idName, type: 'string', is_primary_key: true, required: true })
  for (const a of attrs) {
    const ref = a.refAggregate ? { aggregate: S(a.refAggregate), root: S(a.refRoot), identifier: S(a.refIdentifier) } : undefined
    const cons = a.constraints || undefined
    const desc = S(a.description)
    out.push({
      name: S(a.name), label: S(a.name), type: S(a.type) || 'string',
      is_foreign_key: !!ref, ref, constraints: cons,
      required: cons?.required || undefined, unique: cons?.unique || undefined,
      transient: /瞬态|不落库/.test(desc) || undefined,
      description: desc || undefined,
    })
  }
  return out
}

// ============================================================
// M2 → behaviors(命令)
// ============================================================
function buildBehaviors(m2: any): OntoBehavior[] {
  const out: OntoBehavior[] = []
  for (const beh of A(m2?.behaviors)) {
    const owner = S(beh.aggregateRoot)
    for (const cmd of A(beh.commands)) {
      out.push({
        id: S(cmd.cmdId), name: S(cmd.cmdId), owner_object_id: owner,
        type: 'COMMAND', commandType: S(cmd.commandType) || 'create',
        description: S(cmd.desc),
        applied_rules: [],
        produced_events: A(cmd.emitEvents).map(S),
        preconditions: A(cmd.validations).map(S),
        effects: A(cmd.derivations).map((d: any) => `${S(d.targetField)} = ${S(d.expression)}`)
          .concat(cmd.effect ? [`${S(cmd.effect.op)} ${S(cmd.effect.targetField)}`] : []),
      })
    }
  }
  return out
}

// ============================================================
// MetaRule → rules;并按 bindScene→绑定命令,回填 behavior.applied_rules
// ============================================================
function buildRules(mr: any): OntoRule[] {
  const out: OntoRule[] = []
  for (const g of A(mr?.metaRuleModel?.ruleGroups)) {
    for (const r of A(g.rules)) {
      out.push({
        id: S(r.ruleId), name: S(r.ruleName), type: S(r.effect),
        expression: S(r.matchCondition), enforced: true, bindScene: S(g.bindScene),
        description: S(r.message),
      })
    }
  }
  return out
}

function linkRulesToBehaviors(behaviors: OntoBehavior[], rules: OntoRule[], m4: any) {
  const byId = new Map(behaviors.map((b) => [b.id, b]))
  const scenes = A(m4?.sceneModel?.sceneDefinitions)
  for (const rule of rules) {
    if (!rule.bindScene) continue
    const scene = scenes.find((s: any) => S(s.sceneId) === rule.bindScene)
    if (!scene) continue
    for (const cmdId of collectBindCommands(A(scene.flowSteps))) {
      const b = byId.get(cmdId)
      if (b && !b.applied_rules.includes(rule.id)) b.applied_rules.push(rule.id)
    }
  }
}

/** 递归收集 flowSteps 里所有 bindCommand 的命令 id(含 then/else/body/branches/cases/default 嵌套)。 */
function collectBindCommands(steps: any[]): string[] {
  const acc: string[] = []
  const walk = (arr: any[]) => {
    for (const st of A(arr)) {
      if (S(st.stepType) === 'bindCommand' && st.bindCommand) acc.push(S(st.bindCommand))
      walk(st.then); walk(st.else); walk(st.body); walk(st.default)
      for (const br of A(st.branches)) walk(br.steps)
      for (const c of A(st.cases)) walk(c.steps)
    }
  }
  walk(steps)
  return acc
}

// ============================================================
// ME → events
// ============================================================
function buildEvents(me: any): OntoEvent[] {
  const out: OntoEvent[] = []
  for (const def of A(me?.eventModel?.aggregateEventDefinitions)) {
    const aggregate = S(def.aggregateId)
    const events = def.events || {}
    for (const name of Object.keys(events)) {
      const e = events[name] || {}
      out.push({
        id: name, name, aggregate,
        topic: S(e.topic), payload: A(e.payload).map(S), consumers: A(e.consumers).map(S),
      })
    }
  }
  return out
}

// ============================================================
// M4 flowSteps → ProcessModel[](BPMN 语义骨架,供 bpmn-elk-layout 布局)
// ============================================================
function buildProcessModels(m4: any): ProcessModel[] {
  const scenes = A(m4?.sceneModel?.sceneDefinitions)
  return scenes.map((sc: any) => synthProcess(sc)).filter((p: ProcessModel) => p.nodes.length > 1)
}

/** 单场景 flowSteps → 节点 + 序列流(递归处理控制流)。 */
function synthProcess(scene: any): ProcessModel {
  const nodes: ProcessNode[] = []
  const flows: ProcessFlow[] = []
  let c = 0
  const nid = (p: string) => `${p}_${++c}`
  const push = (n: ProcessNode) => { nodes.push(n); return n.id }
  const connect = (froms: string[], to: string, opts?: Partial<ProcessFlow>) =>
    froms.forEach((f) => flows.push({ id: nid('Flow'), source: f, target: to, ...opts }))

  const start = push({ id: nid('Start'), type: 'startEvent', name: '开始' })

  // build:把一段 steps 接到 incoming 之后,firstEdge 修饰 incoming→首节点的连线;返回“尾部”节点集合
  const build = (steps: any[], incoming: string[], firstEdge?: Partial<ProcessFlow>): string[] => {
    let tails = incoming
    let edge = firstEdge
    for (const st of A(steps)) {
      const r = node(st, tails, edge)
      tails = r
      edge = undefined // 仅第一段带修饰
    }
    return tails
  }

  const node = (st: any, incoming: string[], firstEdge?: Partial<ProcessFlow>): string[] => {
    const t = S(st.stepType)
    const simple = (type: ProcessNode['type'], name: string, extra?: Partial<ProcessNode>): string[] => {
      const id = push({ id: nid('Node'), type, name, ...extra }); connect(incoming, id, firstEdge); return [id]
    }
    switch (t) {
      case 'start': return incoming
      case 'return': case 'end': case 'errorEnd': {
        const id = push({ id: nid('End'), type: 'endEvent', name: t === 'errorEnd' ? '异常结束' : '结束' })
        connect(incoming, id, firstEdge); return []
      }
      case 'bindCommand':
        return simple('task', S(st.bindCommand), { behaviorId: S(st.bindCommand), lane: S(st.bindAggregate) })
      case 'readOnlyCheck': return simple('task', `只读校验 ${S(st.bindAggregate)}`)
      case 'validate': return simple('task', '校验')
      case 'derive': return simple('task', '派生')
      case 'assign': {
        const label = A(st.assignments).length
          ? `赋值 ${A(st.assignments).map((x: any) => S(x.targetField)).join(', ')}`
          : `赋值 ${S(st.targetField)}`
        return simple('task', label)
      }
      case 'script': return simple('task', `脚本 → ${S(st.assignTo) || S(st.expression)}`)
      case 'transform': return simple('task', '转换')
      case 'callService': return simple('task', `调用服务 ${S(st.method)} ${S(st.endpoint)}`)
      case 'userTask': return simple('userTask', `人工任务 ${S(st.role) ? '· ' + S(st.role) : ''}`)
      case 'timer': return simple('intermediateCatchEvent', `定时 ${S(st.delayMs)}ms`)
      case 'waitEvent': return simple('intermediateCatchEvent', `等待事件 ${S(st.waitEvent)}`)
      case 'emitEvent': return simple('intermediateCatchEvent', `发事件 ${S(st.event)}`)
      case 'condition': {
        const g = push({ id: nid('Gw'), type: 'exclusiveGateway', name: '判断' })
        connect(incoming, g, firstEdge)
        const thenT = build(A(st.then), [g], { name: '是', condition: S(st.when) })
        const elseT = build(A(st.else), [g], { name: '否' })
        return [...thenT, ...elseT]
      }
      case 'switch': {
        const g = push({ id: nid('Gw'), type: 'exclusiveGateway', name: `分派 ${S(st.switchOn)}` })
        connect(incoming, g, firstEdge)
        const tails: string[] = []
        for (const cs of A(st.cases)) tails.push(...build(A(cs.steps), [g], { name: `= ${S(cs.equals)}` }))
        tails.push(...build(A(st.default), [g], { name: 'default' }))
        return tails.length ? tails : [g]
      }
      case 'while': {
        const g = push({ id: nid('Gw'), type: 'exclusiveGateway', name: '循环判断' })
        connect(incoming, g, firstEdge)
        const bodyT = build(A(st.body), [g], { name: '循环', condition: S(st.when) })
        connect(bodyT, g) // 回边
        return [g] // 退出边由后续步骤接出
      }
      case 'parallel': {
        const split = push({ id: nid('Gw'), type: 'parallelGateway', name: '并行分叉' })
        connect(incoming, split, firstEdge)
        const join = push({ id: nid('Gw'), type: 'parallelGateway', name: '并行汇合' })
        for (const br of A(st.branches)) {
          const bt = build(A(br.steps), [split])
          connect(bt, join)
        }
        return [join]
      }
      case 'retry': {
        const id = push({ id: nid('Node'), type: 'task', name: `重试 ×${S(st.maxAttempts)}` })
        connect(incoming, id, firstEdge)
        return build(A(st.body), [id])
      }
      case 'compensate': return simple('task', `补偿 ${S(st.compensateCommand)}`)
      default: return simple('task', t || '步骤')
    }
  }

  const tails = build(A(scene.flowSteps), [start])
  if (tails.length) { const end = push({ id: nid('End'), type: 'endEvent', name: '结束' }); connect(tails, end) }
  return { id: S(scene.sceneId), name: S(scene.sceneName) || S(scene.sceneId), nodes, flows }
}