import Model = Type.Topology.Model import * as Leaf from "leaf-ts" import * as CoreLogic from "./coreLogic" import { ExecutorContext, ExecutorBuilder, MatchConfigs } from "./executor"; import { Backend, BackendProvider } from "./backend" import { Errors } from "../errors" import { Roll, RollEvals } from "./roll"; import { CriteriaBuilder } from "./condition"; import { Aggregator, Categorizer, Mapper, Joiner } from "./aggregator"; import { MapHandler, ValueProvider } from "./valueProvider"; import * as asyncModule from "async" export class Topology { include(spec: Type.Topology.Specification.Definition) { this.specs.push(spec) } executorContext = new ExecutorContext(this) specs: Type.Topology.Specification.Definition[] = [] backends: { [hash: string]: Backend } = {} getBackend(modelName: string): Backend { let id = modelName let model = this.getModel(modelName) let backend = this.backends[id] if (backend) return backend backend = this.backends[id] = this.createBackend(model) return backend } providers: BackendProvider[] = [] registerBackendProvider(provider: BackendProvider) { this.providers.push(provider) } createBackend(def: Type.Topology.Model.Definition): Backend { if (!def) { throw new Error("Invalid backend definition.") } for (let provider of this.providers) { let backend = provider.createBackend(def) if (backend) { backend.initialize() return backend } } return null } private initDatas: { [key: string]: any[] } = {} inits(object: { [key: string]: any[] }) { for (let key in object) { if (!this.initDatas[key]) this.initDatas[key] = [] this.initDatas[key].push(...object[key]) } } initialize(callback: Callback) { this.seal() asyncModule.eachSeries(Object.keys(this.models), (name, done) => { let model = this.models[name] asyncModule.eachSeries(model.inits, (data, done) => { console.log() let unique = model.unique[0] let value = data[unique] if (!value) { console.error("Invalid initial value", data) throw new Errors.LogicError(`Initial value missing unique key ${unique}`) } this.about(model.name).as("root").match({ [unique]: this.criteria().eq(value) }).one().evaluate((err, result) => { if (result) { console.log(`Skip existing init value for ${model.name}:`, value) done() return } this.about(model.name).as("root").create(data).eval((err) => { done(err) }) }) }, (err) => { done(err) }) }, (err) => { callback(err) }) } seal() { let start = Date.now() let uses: string[] = [] for (let spec of this.specs) { uses.push(spec.name) for (let base of spec.base || []) { if (uses.indexOf(base) < 0) { throw new Error(`${spec.name} requires ${base} not specified`) } } for (let key in spec.model) { let model = Leaf.Util.clone(spec.model[key]) if (typeof model.unique == "string") { model.unique = [model.unique] } if (!model.unique || model.unique.length == 0) { throw new Errors.LogicError(`Model ${model.name} missing unique key`) } if (!model.inits) { model.inits = [] } if (this.initDatas[model.name]) { model.inits.push(...this.initDatas[model.name]) } if (this.models[model.name]) { throw new Error(`Model ${model.name} already exists`) } if ((!model.unique || model.unique.length < 0) && model.backend.engine !== "single") { throw new Errors.LogicError("Missing unique key for ${model}") } this.models[model.name] = model } for (let topicName in spec.topic) { let joints = spec.topic[topicName] let extend = false if (topicName[0] == "$") { extend = true topicName = topicName.slice(1) } if (this.topics[topicName] && !extend) { throw new Error(`Duplicate topic ${topicName} `) } let topic = this.topics[topicName] = this.topics[topicName] || { name: topicName, joints: [], links: {}, models: [] } for (let joint of joints) { if (topic.joints.indexOf(joint) < 0) { topic.joints.push(joint) } let model = joint.split(".")[0] if (topic.models.indexOf(model) < 0) { topic.models.push(model) } if (!this.models[model]) { throw new Error(`Model ${model} not found of joint ${joint} in topic ${topic.name}, it that a typo ? `) } let topics = this.models[model].topics = this.models[model].topics || [] if (topics.indexOf(topicName) < 0) topics.push(topicName) } } } this.linkTopic() this.linkModel() let duration = Date.now() - start console.log(`Topology sealed in ${duration} ms`.cyan) } private linkTopic() { let links = CoreLogic.selfCross(Object.keys(this.topics)) for (let link of links) { let [n1, n2] = link let joints1 = this.topics[n1].joints let joints2 = this.topics[n2].joints let m1s = joints1.map(item => item.split(".")[0]) let m2s = joints2.map(item => item.split(".")[0]) let diff = Leaf.Util.arrayDiff(m1s, m2s) if (diff.intersect && diff.intersect.length > 0) { let intersect = CoreLogic.makeSet(diff.intersect) for (let share of intersect) { let model = share let topics = this.models[model].topics = this.models[model].topics || [] if (topics.indexOf(n1) < 0) topics.push(n1) if (topics.indexOf(n2) < 0) topics.push(n2) let ins = joints1.filter(item => item.split(".")[0] == model) let outs = joints2.filter(item => item.split(".")[0] == model) this.topics[n1].links[n2] = this.topics[n1].links[n2] || {} this.topics[n2].links[n1] = this.topics[n2].links[n1] || {} this.topics[n1].links[n2][model] = { ins, outs } this.topics[n2].links[n1][model] = { ins: outs, outs: ins } } } } } private linkModel() { let models = Object.keys(this.models) for (let m1 of models) { for (let m2 of models) { if (m1 == m2) continue let links = this.models[m1].links = this.models[m1].links || {} links[m2] = this.calculateLink(m1, m2) } } } private calculateLink(from: string, to: string): Type.Topology.Model.Link[] { let stepsList = this.calculateStep(from, to) return stepsList.map(steps => { return { from, to, signature: CoreLogic.hashStrings(steps.map(item => item.joint.from + "_" + item.joint.to).join("_")), steps } }).sort((a, b) => a.steps.length - b.steps.length) } maxLinkLength = 3 private calculateStep(from: string, to: string, blacklist: string[] = [], topicBlacklist: string[] = [], depth = 0): Type.Topology.Model.LinkStep[][] { if (depth > this.maxLinkLength) { return [] } if (from == to) return [] let results: Type.Topology.Model.LinkStep[][] = [] if (blacklist.indexOf(from) >= 0) return results blacklist.push(from) let topics = this.models[from].topics || [] for (let topicName of topics) { if (topicBlacklist.indexOf(topicName) >= 0) continue let topic = this.topics[topicName] topicBlacklist.push(topicName) let joints = topic.joints.filter(item => item.split(".")[0] == from) for (let next of topic.models) { if (blacklist.indexOf(next) >= 0) { continue } if (next === to) { let toJoints = topic.joints.filter(item => item.split(".")[0] == to) for (let fromJoint of joints) { for (let toJoint of toJoints) { if (fromJoint == "Account.id" && toJoint == "Mine.plotterId") { throw new Error("~~~") } results.push([{ topic: topicName, joint: { from: fromJoint, to: toJoint }, model: { from, to } }]) } } continue } let stepsSets = this.calculateStep(next, to, blacklist, topicBlacklist, depth + 1) for (let steps of stepsSets) { for (let joint of joints) { let toJoints = topic.joints.filter(item => item.split(".")[0] == steps[0].model.from) for (let toJoint of toJoints) { let toadd = steps.slice() toadd.unshift({ topic: topicName, joint: { from: joint, to: toJoint }, model: { from, to: steps[0].model.from } }) results.push(toadd) } } } } topicBlacklist.pop() } blacklist.pop() return results } models: { [name: string]: Model.Definition & { topics?: string[] links?: { [model: string]: Type.Topology.Model.Link[] } } } = {} topics: { [name: string]: Model.TopicInfo & { models: string[] links: { [name: string]: { [model: string]: { ins: Type.Topology.Model.Joint[] outs: Type.Topology.Model.Joint[] } } } } } = {} joints: { [joint: string]: Model.JointInfo } = {} getModel(name: string) { return this.models[name] } from(info: MatchConfigs) { return new ExecutorBuilder(this, this.executorContext).from(info) } about(name: string) { return new ExecutorBuilder(this, this.executorContext).about(name) } roll(routine: RollEvals) { return new Roll(routine) } aggregate } = any>(data: T, zero = null) { return new Aggregator(data, zero) } categorize(option: { [K in keyof TInfo]: ValueProvider }, next?: MapHandler) { return new Categorizer(option, next) } map(option: MapHandler) { return new Mapper(option) } join(path?: string) { return new Joiner(path) } criteria() { return new CriteriaBuilder() } }