| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| import angular from 'angular'; |
| import {Entity, EntityFamily} from "../util/model/entity.model"; |
| import {Issue, ISSUE_LEVEL} from '../util/model/issue.model'; |
| import {Dsl} from "../util/model/dsl.model"; |
| import jsYaml from "js-yaml"; |
| import typeNotFoundIcon from "../../img/icon-not-found.svg"; |
| |
| const MODULE_NAME = 'brooklyn.composer.service.blueprint-service'; |
| const TAG = 'SERVICE :: BLUEPRINT :: '; |
| |
| angular.module(MODULE_NAME, []) |
| .provider('blueprintService', blueprintServiceProvider); |
| |
| export default MODULE_NAME; |
| |
| export const RESERVED_KEYS = ['name', 'location', 'locations', 'type', 'services', 'brooklyn.config', 'brooklyn.children', 'brooklyn.enrichers', 'brooklyn.policies']; |
| export const DSL_ENTITY_SPEC = '$brooklyn:entitySpec'; |
| |
| export function blueprintServiceProvider() { |
| return { |
| $get: ['$log', '$q', '$sce', 'paletteApi', 'iconGenerator', 'dslService', |
| function ($log, $q, $sce, paletteApi, iconGenerator, dslService) { |
| return new BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService); |
| }] |
| } |
| } |
| |
| function BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService) { |
| let blueprint = new Entity(); |
| |
| return { |
| setFromJson: setBlueprintFromJson, |
| setFromYaml: setBlueprintFromYaml, |
| get: getBlueprint, |
| getAsJson: getBlueprintAsJson, |
| getAsYaml: getBlueprintAsYaml, |
| reset: resetBlueprint, |
| find: findEntity, |
| findAny: findAnyEntity, |
| getEntityMetadata: getEntityMetadata, |
| entityHasMetadata: entityHasMetadata, |
| refreshBlueprintMetadata: refreshBlueprintMetadata, |
| refreshEntityMetadata: refreshEntityMetadata, |
| refreshConfigConstraints: refreshConfigConstraints, |
| refreshRelationships: refreshRelationships, |
| refreshAllRelationships: refreshAllRelationships, |
| isReservedKey: isReservedKey, |
| getIssues: getIssues, |
| hasIssues: hasIssues, |
| populateEntityFromApi: populateEntityFromApiSuccess, |
| populateLocationFromApi: populateLocationFromApiSuccess, |
| addConfigKeyDefinition: addConfigKeyDefinition, |
| getRelationships: getRelationships, |
| }; |
| |
| function setBlueprintFromJson(jsonBlueprint) { |
| if (jsonBlueprint) { |
| blueprint.setEntityFromJson(jsonBlueprint); |
| $log.debug(TAG + 'Blueprint set from JS', blueprint); |
| } else { |
| resetBlueprint(); |
| } |
| } |
| |
| function setBlueprintFromYaml(yamlBlueprint, reset = false) { |
| let newBlueprint = jsYaml.safeLoad(yamlBlueprint); |
| if (reset) { |
| resetBlueprint(); |
| } |
| setBlueprintFromJson(newBlueprint); |
| $log.debug(TAG + 'Blueprint set from YAML', blueprint); |
| // TODO refresh the blueprint now? see comments in yaml.state.js and on refreshBlueprint |
| } |
| |
| function getBlueprint() { |
| return blueprint; |
| } |
| |
| function getBlueprintAsJson() { |
| return blueprint.getData(); |
| } |
| |
| function getBlueprintAsYaml() { |
| try { |
| let currentModel = blueprint.getData(); |
| return (Object.keys(currentModel).length === 0) ? '' : jsYaml.safeDump(currentModel, { lineWidth: 127 }); |
| } catch (err) { |
| $log.error("Error converting brooklyn blueprint (rethrowing)", err); |
| throw 'Could not convert the Brooklyn blueprint into YAML format'; |
| } |
| } |
| |
| function resetBlueprint() { |
| blueprint.reset(); |
| } |
| |
| function findEntity(id) { |
| if (!id) { |
| return null; |
| } |
| return lookup(blueprint, id); |
| } |
| |
| |
| function findAnyEntity(id) { |
| if (!id) { |
| return null; |
| } |
| return lookup(blueprint, id, true); |
| } |
| |
| function getEntityMetadata(entity) { |
| let metadata = new Map(); |
| if (entity instanceof Entity) { |
| entity.metadata.forEach((value, key)=> { |
| if (RESERVED_KEYS.indexOf(key) === -1) { |
| metadata.set(key, value); |
| } |
| }); |
| } |
| return metadata; |
| } |
| |
| function entityHasMetadata(entity) { |
| return this.getEntityMetadata(entity).size > 0; |
| } |
| |
| function isReservedKey(key) { |
| return RESERVED_KEYS.indexOf(key) > -1; |
| } |
| |
| function getIssues(entity = blueprint) { |
| let issues = []; |
| |
| if (entity.hasIssues()) { |
| issues = issues.concat(entity.issues.map((issue)=> { |
| let newIssue = Issue.builder().message(issue.message).group(issue.group).ref(issue.ref).level(issue.level).build(); |
| newIssue.entity = entity; |
| return newIssue; |
| })); |
| } |
| |
| entity.policies.forEach((policy)=> { |
| issues = issues.concat(getIssues(policy)); |
| }); |
| |
| entity.enrichers.forEach((enricher)=> { |
| issues = issues.concat(getIssues(enricher)); |
| }); |
| |
| entity.children.forEach((child)=> { |
| issues = issues.concat(getIssues(child)); |
| }); |
| |
| return issues; |
| } |
| |
| function hasIssues() { |
| return this.getIssues().length > 0; |
| } |
| |
| function lookup(entity, id, any = false) { |
| if (entity._id === id) { |
| return entity; |
| } |
| if (entity.childrenAsMap.has(id)) { |
| return entity.childrenAsMap.get(id); |
| } |
| if (entity.policies.has(id)) { |
| return entity.policies.get(id); |
| } |
| if (entity.enrichers.has(id)) { |
| return entity.enrichers.get(id); |
| } |
| let memberSpecs = Object.values(entity.getClusterMemberspecEntities()).filter((memberSpec)=>(memberSpec._id === id)); |
| if (memberSpecs.length === 1) { |
| return memberSpecs[0]; |
| } |
| for (let child of entity.childrenAsMap.values()) { |
| let ret = lookup(child, id, any); |
| if (ret !== null) { |
| return ret; |
| } |
| } |
| return null; |
| } |
| |
| function refreshBlueprintMetadata(entity = blueprint, family = 'ENTITY') { |
| // TODO ideally we'd have a cache for all types |
| return refreshEntityMetadata(entity, family).then(()=> { |
| return $q.all(entity.children.reduce((result, child) => { |
| result.push(refreshBlueprintMetadata(child)); |
| return result; |
| }, [])); |
| }).then(() => { |
| return entity; |
| }); |
| } |
| |
| function refreshEntityMetadata(entity, family) { |
| entity.miscData.set('loading', true); |
| return $q.all([refreshTypeMetadata(entity, family), refreshLocationMetadata(entity)]).then(()=> { |
| entity.miscData.set('loading', false); |
| return refreshRelationships(entity); |
| }).then(() => { |
| return $q.all([ |
| refreshConfigConstraints(entity), |
| refreshConfigMemberspecsMetadata(entity), |
| refreshPoliciesMetadata(entity), |
| refreshEnrichersMetadata(entity), |
| ]); |
| }).then(()=> { |
| return entity; |
| }); |
| } |
| |
| function refreshTypeMetadata(entity, family) { |
| let deferred = $q.defer(); |
| |
| if (entity.hasType()) { |
| entity.family = family.id; |
| |
| let promise = entity.miscData.has('bundle') |
| ? paletteApi.getBundleType(entity.miscData.get('bundle').symbolicName, entity.miscData.get('bundle').version, entity.type, entity.version, entity.config) |
| : paletteApi.getType(entity.type, entity.version, entity.config); |
| |
| promise.then((data)=> { |
| deferred.resolve(populateEntityFromApiSuccess(entity, data)); |
| }).catch(function (error) { |
| deferred.resolve(populateEntityFromApiError(entity, error)); |
| }); |
| } else if (entity.parent) { |
| entity.clearIssues({group: 'type'}).addIssue(Issue.builder().group('type').message('Entity needs a type').level(ISSUE_LEVEL.WARN).build()); |
| entity.miscData.set('sensors', []); |
| entity.miscData.set('traits', []); |
| deferred.resolve(entity); |
| addUnlistedConfigKeysDefinitions(entity); |
| addUnlistedParameterDefinitions(entity); |
| } else { |
| entity.miscData.set('sensors', []); |
| entity.miscData.set('traits', []); |
| deferred.resolve(entity); |
| addUnlistedConfigKeysDefinitions(entity); |
| addUnlistedParameterDefinitions(entity); |
| } |
| |
| return deferred.promise; |
| } |
| |
| function locationType(location) { |
| if (!location || typeof location === 'string') return location; |
| if (typeof location === 'object' && Object.keys(location).length==1) return Object.keys(location)[0]; |
| return null; |
| } |
| |
| function refreshLocationMetadata(entity) { |
| let deferred = $q.defer(); |
| |
| if (entity.hasLocation()) { |
| let type = locationType(entity.location); |
| if (type.startsWith("jclouds:")) { |
| // types eg jclouds:aws-ec2 are low-level, not in the catalog |
| deferred.resolve(populateLocationFromApiSuccess(entity, { yamlHere: entity.location })); |
| } else { |
| paletteApi.getLocation(locationType(entity.location)).then((location)=> { |
| let loc = Object.assign({}, location.catalog || location, { yamlHere: entity.location }); |
| deferred.resolve(populateLocationFromApiSuccess(entity, loc)); |
| }).catch(function () { |
| deferred.resolve(populateLocationFromApiError(entity)); |
| }); |
| } |
| } else { |
| deferred.resolve(entity); |
| } |
| |
| return deferred.promise; |
| } |
| |
| function refreshConfigConstraints(entity) { |
| return $q((resolve) => { |
| if (entity.miscData.has('config')) { |
| entity.miscData.get('config') |
| .filter(config => config.constraints && config.constraints.length > 0) |
| .forEach(config => { |
| for (let constraintO of config.constraints) { |
| let message = null; |
| let key = null, args = null; |
| if (constraintO instanceof String || typeof constraintO=='string') { |
| key = constraintO; |
| } else if (Object.keys(constraintO).length==1) { |
| key = Object.keys(constraintO)[0]; |
| args = constraintO[key]; |
| } else { |
| $log.warn("Unknown constraint object", typeof constraintO, constraintO, config); |
| key = constraintO; |
| } |
| let val = (k) => entity.config.get(k || config.name); |
| let isSet = (k) => entity.config.has(k || config.name) && angular.isDefined(val(k)); |
| let isAnySet = (k) => { |
| if (!k || !Array.isArray(k)) return false; |
| return k.some(isSet); |
| } |
| let hasDefault = () => angular.isDefined(config.defaultValue); |
| switch (key) { |
| case 'Predicates.notNull()': |
| case 'Predicates.notNull': |
| if (!isSet() && !hasDefault()) { |
| message = `<samp>${config.name}</samp> is required`; |
| } |
| break; |
| case 'required': |
| if (!isSet() && !hasDefault() && val()!='') { |
| message = `<samp>${config.name}</samp> is required`; |
| } |
| break; |
| case 'regex': |
| if (isSet() && !(new RegExp(args).test(val))) { |
| message = `<samp>${config.name}</samp> does not match the required format: <samp>${args}</samp>`; |
| } |
| break; |
| case 'forbiddenIf': |
| if (isSet() && isSet(args)) { |
| message = `<samp>${config.name}</samp> and <samp>${args}</samp> cannot both be set`; |
| } |
| break; |
| case 'forbiddenUnless': |
| if (isSet() && !isSet(args)) { |
| message = `<samp>${config.name}</samp> can only be set when <samp>${args}</samp> is set`; |
| } |
| break; |
| case 'requiredIf': |
| if (!isSet() && isSet(args)) { |
| message = `<samp>${config.name}</samp> must be set if <samp>${args}</samp> is set`; |
| } |
| break; |
| case 'requiredUnless': |
| if (!isSet() && !isSet(args)) { |
| message = `<samp>${config.name}</samp> or <samp>${args}</samp> is required`; |
| } |
| break; |
| case 'requiredUnlessAnyOf': |
| if (!isSet() && !isAnySet(args)) { |
| message = `<samp>${config.name}</samp> or one of <samp>${args}</samp> is required`; |
| } |
| break; |
| case 'forbiddenUnlessAnyOf': |
| if (isSet() && !isAnySet(args)) { |
| message = `<samp>${config.name}</samp> cannot be set if any of <samp>${args}</samp> are set`; |
| } |
| break; |
| default: |
| $log.warn("Unknown constraint predicate", constraintO, config); |
| } |
| if (message !== null) { |
| entity.addIssue(Issue.builder().group('config').ref(config.name).message($sce.trustAsHtml(message)).build()); |
| } |
| } |
| }); |
| } |
| resolve(); |
| }); |
| } |
| |
| function refreshConfigMemberspecsMetadata(entity) { |
| let promiseArray = []; |
| Object.values(entity.getClusterMemberspecEntities()).forEach((memberSpec)=> { |
| // memberSpec can be `undefined` if the member spec is not a `$brooklyn:entitySpec`, e.g. it is `$brooklyn:config("spec")`. |
| // there may be a better way but this seems to handle it. |
| if (memberSpec) promiseArray.push(refreshBlueprintMetadata(memberSpec, 'SPEC')); |
| }); |
| return $q.all(promiseArray); |
| } |
| |
| function refreshPoliciesMetadata(entity) { |
| return $q.all(entity.getPoliciesAsArray().reduce((result, policy)=> { |
| policy.miscData.set('loading', true); |
| policy.family = 'POLICY'; |
| |
| let deferred = $q.defer(); |
| |
| paletteApi.getType(policy.type, policy.version).then((data)=> { |
| deferred.resolve(populateEntityFromApiSuccess(policy, data)); |
| }).catch(function (error) { |
| deferred.resolve(populateEntityFromApiError(policy, error)); |
| }).finally(()=> { |
| policy.miscData.set('loading', false); |
| }); |
| result.push(deferred); |
| return result; |
| }, [])); |
| } |
| |
| function refreshEnrichersMetadata(entity) { |
| return $q.all(entity.getEnrichersAsArray().reduce((result, enricher)=> { |
| enricher.miscData.set('loading', true); |
| enricher.family = 'ENRICHER'; |
| |
| let deferred = $q.defer(); |
| |
| paletteApi.getType(enricher.type, enricher.version).then((data)=> { |
| deferred.resolve(populateEntityFromApiSuccess(enricher, data)); |
| }).catch(function (error) { |
| deferred.resolve(populateEntityFromApiError(enricher, error)); |
| }).finally(()=> { |
| enricher.miscData.set('loading', false); |
| }); |
| result.push(deferred); |
| return result; |
| }, [])); |
| } |
| |
| function parseInput(input, entity) { |
| return $q((resolve, reject) => { |
| try { |
| let parsed = dslService.parse(input, entity, getBlueprint()); |
| if (parsed.kind && parsed.kind.family === 'constant') { |
| reject('constants not interpreted as DSL when parsed', input); |
| } else { |
| resolve(parsed); |
| } |
| } catch (ex) { |
| $log.debug(ex); |
| reject(ex, input); |
| } |
| }); |
| } |
| |
| function refreshRelationships(entity) { |
| return $q.all(Array.from(entity.config.keys()).reduce((promises, key) => { |
| let value = entity.config.get(key); |
| |
| // Return promises that returns an object like { key: "my-config-key-key", issues: [] } |
| if (value instanceof Dsl) { |
| promises.push( |
| parseInput(value.toString(), entity).then(dsl => { |
| entity.config.set(key, dsl); |
| return { |
| key: key, |
| issues: dsl.getAllIssues() |
| }; |
| }).catch(() => $q.resolve({ |
| key: key, |
| issues: [] |
| })) |
| ); |
| } else if (value instanceof Array) { |
| promises.push( |
| $q.all(value.reduce((issues, itemValue, itemIndex) => { |
| return issues.concat( |
| parseInput(itemValue, entity).then(dsl => { |
| value[itemIndex] = dsl; |
| return dsl.getAllIssues(); |
| }).catch(() => []) |
| ); |
| }, [])).then(issues => { |
| return { |
| key: key, |
| issues: issues.reduce((acc, issue) => acc.concat(issue), []) |
| } |
| }) |
| ); |
| } else if (value instanceof Object) { |
| promises.push( |
| $q.all(Object.keys(value).reduce((issues, itemKey) => { |
| return issues.concat( |
| parseInput(value[itemKey], entity).then(dsl => { |
| value[itemKey] = dsl; |
| return dsl.getAllIssues(); |
| }).catch(() => []) |
| ); |
| }, [])).then(issues => { |
| return { |
| key: key, |
| issues: issues.reduce((acc, issue) => acc.concat(issue), []) |
| } |
| }) |
| ); |
| } else { |
| promises.push( |
| parseInput(value, entity).then(dsl => { |
| entity.config.set(key, dsl); |
| return { |
| key: key, |
| issues: dsl.getAllIssues() |
| }; |
| }).catch(() => $q.resolve({ |
| key: key, |
| issues: [] |
| })) |
| ); |
| } |
| |
| return promises; |
| }, [])).then(results => { |
| results.forEach(result => { |
| entity.clearIssues({ref: result.key}); |
| result.issues.forEach(issue => { |
| entity.addIssue(Issue.builder().group('config').ref(result.key).message($sce.trustAsHtml(issue)).build()); |
| }); |
| }) |
| }); |
| } |
| |
| function refreshAllRelationships(entity = blueprint) { |
| let promises = []; |
| promises.push(refreshRelationships(entity)); |
| promises.concat(entity.children.map(child => refreshAllRelationships(child))); |
| |
| return $q.all(promises); |
| } |
| |
| function addConfigKeyDefinition(config, key) { |
| config.push({ |
| "constraints": [], |
| "description": "", |
| "name": key, |
| "label": key, |
| "priority": 1, |
| "pinned": true, |
| "type": "java.lang.String", |
| }); |
| } |
| |
| function addUnlistedConfigKeysDefinitions(entity) { |
| // copy config key definitions set on this entity into the miscData aggregated view |
| let allConfig = entity.miscData.get('config') || []; |
| entity.config.forEach((value, key) => { |
| if (!allConfig.some((e) => e.name === key)) { |
| addConfigKeyDefinition(allConfig, key); |
| } |
| }); |
| entity.miscData.set('config', allConfig); |
| } |
| |
| function addUnlistedParameterDefinitions(entity) { |
| // copy parameter definitions set on this entity into the miscData aggregated view; |
| // TODO see discussions in PR 112 about whether this is necessary and/or there is a better way |
| let allParams = entity.miscData.get('parameters') || []; |
| entity.parameters.forEach((param) => { |
| if (!allParams.some((e) => e.name === param.name)) { |
| allParams.push(param); |
| } |
| }); |
| entity.miscData.set('parameters', allParams); |
| } |
| |
| function populateEntityFromApiSuccess(entity, data) { |
| entity.clearIssues({group: 'type'}); |
| entity.type = data.symbolicName; |
| entity.icon = data.iconUrl || iconGenerator(data.symbolicName); |
| entity.miscData.set('important', !!data.iconUrl); |
| entity.miscData.set('bundle', { |
| symbolicName: data.containingBundle.split(':')[0], |
| version: data.containingBundle.split(':')[1] |
| }); |
| entity.miscData.set('typeName', data.displayName || data.symbolicName); |
| entity.miscData.set('config', data.config || []); |
| entity.miscData.set('parameters', data.parameters || []); |
| entity.miscData.set('sensors', data.sensors || []); |
| entity.miscData.set('traits', data.supertypes || []); |
| entity.miscData.set('tags', data.tags || []); |
| var uiHints = {}; |
| data.tags.forEach( (t) => { |
| mergeAppendingLists(uiHints, t['ui-composer-hints']); |
| }); |
| entity.miscData.set('ui-composer-hints', uiHints); |
| entity.miscData.set('virtual', data.virtual || null); |
| addUnlistedConfigKeysDefinitions(entity); |
| addUnlistedParameterDefinitions(entity); |
| return entity; |
| } |
| function mergeAppendingLists(dst, src) { |
| for (let p in src) { |
| if (Array.isArray(dst[p]) || Array.isArray(src[p])) { |
| dst[p] = [].concat(dst[p] || [], src[p]); |
| } else { |
| dst[p] = Object.assign({}, dst[p], src[p]); |
| } |
| } |
| return dst; |
| } |
| |
| function populateEntityFromApiError(entity, error) { |
| $log.warn("Error loading/populating type, data will be incomplete.", entity, error); |
| entity.clearIssues({group: 'type'}); |
| entity.addIssue(Issue.builder().group('type').message($sce.trustAsHtml(`Type <samp>${entity.type + (entity.hasVersion ? ':' + entity.version : '')}</samp> does not exist`)).build()); |
| entity.miscData.set('typeName', entity.type || ''); |
| entity.miscData.set('config', []); |
| entity.miscData.set('parameters', []); |
| entity.miscData.set('sensors', []); |
| entity.miscData.set('traits', []); |
| entity.miscData.set('virtual', null); |
| entity.icon = typeNotFoundIcon; |
| addUnlistedConfigKeysDefinitions(entity); |
| addUnlistedParameterDefinitions(entity); |
| return entity; |
| } |
| |
| function populateLocationFromApiCommon(entity, data) { |
| entity.clearIssues({group: 'location'}); |
| entity.location = data.yamlHere || data.symbolicName; |
| |
| let name = data.name || data.displayName; |
| if (!name && data.yamlHere) { |
| name = typeof data.yamlHere === 'object' ? Object.keys(data.yamlHere)[0] : data.yamlHere; |
| } |
| if (!name) name = data.symbolicName; |
| entity.miscData.set('locationName', name); |
| |
| // use icon on item, but if none then generate using *yaml* to distinguish when someone has changed it |
| // (especially for things like jclouds:aws-ec2 -- the config is more interesting than the type name) |
| entity.miscData.set('locationIcon', data==null ? null : data.iconUrl || iconGenerator(data.yamlHere ? JSON.stringify(data.yamlHere) : data.symbolicName)); |
| return entity; |
| } |
| |
| function populateLocationFromApiSuccess(entity, data) { |
| populateLocationFromApiCommon(entity, data); |
| } |
| |
| function populateLocationFromApiError(entity) { |
| populateLocationFromApiCommon(entity, { yamlHere: entity.location }); |
| entity.addIssue(Issue.builder().level(ISSUE_LEVEL.WARN).group('location').message($sce.trustAsHtml(`Location <samp>${!(entity.location instanceof String) ? JSON.stringify(entity.location) : entity.location}</samp> does not exist in your local catalog. Deployment might fail.`)).build()); |
| entity.miscData.set('locationIcon', typeNotFoundIcon); |
| return entity; |
| } |
| |
| /** |
| * Retrieves all the Entities referenced by an Entity |
| * @param {Entity} entity the Entity to resolve relative references from |
| * @return {Array} of objects that contains source and target entities |
| */ |
| function getRelationships(entity = blueprint) { |
| let set = Array.from(entity.config.values()) |
| .reduce((set, config)=> { |
| if (config instanceof Dsl) { |
| config.relationships.forEach((entity) => { |
| if (entity !== null) { |
| set.add(entity); |
| } |
| }); |
| } |
| if (config instanceof Array) { |
| config |
| .filter(conf => conf instanceof Dsl) |
| .reduce((set, config)=> { |
| config.relationships.forEach((entity)=> { |
| if (entity !== null) { |
| set.add(entity); |
| } |
| }); |
| return set; |
| }, set); |
| } |
| if (config instanceof Object) { |
| Object.keys(config) |
| .filter(key => config[key] instanceof Dsl) |
| .reduce((set, key)=> { |
| config[key].relationships.forEach((entity)=> { |
| if (entity !== null) { |
| set.add(entity); |
| } |
| }); |
| return set; |
| }, set); |
| } |
| return set; |
| }, new Set()); |
| |
| let relationships = Array.from(set).map((relation) => { |
| return { |
| source: entity, |
| target: relation |
| }; |
| }); |
| |
| relationships = Array.from(entity.config.values()) |
| .filter(config => config[DSL_ENTITY_SPEC] && config[DSL_ENTITY_SPEC] instanceof Entity) |
| .map(config => config[DSL_ENTITY_SPEC]) |
| .reduce((relationships, spec) => { |
| return relationships.concat(getRelationships(spec)); |
| }, relationships); |
| |
| return entity.children.reduce((relationships, child) => { |
| return relationships.concat(getRelationships(child)) |
| }, relationships); |
| } |
| |
| } |