UNPKG

mlproj-core

Version:

Project management for MarkLogic, core implementation

1,341 lines (1,250 loc) 50.6 kB
"use strict"; (function() { const act = require('./action'); const cmp = require('./components'); // const err = require('./error'); const props = require('./properties'); class PlatformImporter { constructor(ctxt) { this.ctxt = ctxt; } resolve(href, path) { const base = this.ctxt.platform.dirname(path); const absolute = this.ctxt.platform.resolve(href, base); // TODO: Catch errors from require() and json() to display a // proper message (esp. with the correct path to the file...) const json = absolute.endsWith('.js') ? require(absolute)() : this.ctxt.platform.json(absolute); const module = new Module(this.ctxt, json, absolute, this); module.href = href; return module; } } class DumpImporter { // dumps = mlproj.environs from the "top-level dump" constructor(ctxt, dumps) { this.ctxt = ctxt; this.dumps = dumps; } resolve(href) { const dump = this.dumps.find(env => env.href === href); if ( ! dump ) { throw new Error(`There is no module ${href} in the dump`); } const module = new Module(this.ctxt, dump.json, dump.path, this); module.href = href; return module; } } /*~ * A fake environment, with only values from the command line. */ class FakeEnviron { constructor(ctxt, params, force) { this._params = {}; this.ctxt = ctxt; // set values from `force` if ( force ) { Object.keys(force).forEach(name => { this.param('@' + name, force[name]); }); } // set values from `params` if ( params ) { Object.keys(params).forEach(name => { this.param(name, params[name]); }); } } param(name, value) { if ( value === undefined ) { return this._params[name]; } else { this._params[name] = value; } } } /*~ * A complete environment. */ class Environ { constructor(ctxt, json, path, proj) { this._params = {}; this.ctxt = ctxt; this.proj = proj; if ( json.mlproj && json.mlproj.name ) { this.name = json.mlproj.name; } this.module = new Module(ctxt, json, path, new PlatformImporter(ctxt)); this.module.loadImports(); // needed for connect infos, find a nicer way to pass them if ( ctxt.platform.environ ) { throw new Error('Environ already set on the context'); } ctxt.platform.environ = this; } static fromName(ctxt, name, base, proj) { let json; let path; if ( name.includes('/') ) { path = ctxt.platform.resolve('xproject/mlenvs/@' + name.replace(/\//g, '+'), base); json = { "mlproj": { "format": '0.1', "import": name.split('/').map(n => { let pjson = ctxt.platform.resolve(n + '.json', 'xproject/mlenvs'); let pjs = ctxt.platform.resolve(n + '.js', 'xproject/mlenvs'); // return .js only if .json does not exist and .js does exist return (! ctxt.platform.exists(pjson) && ctxt.platform.exists(pjs)) ? n + '.js' : n + '.json'; }) }}; } else { let pjson = ctxt.platform.resolve('xproject/mlenvs/' + name + '.json', base); let pjs = ctxt.platform.resolve('xproject/mlenvs/' + name + '.js', base); // use .js only if .json does not exist and .js does exist if ( ! ctxt.platform.exists(pjson) && ctxt.platform.exists(pjs) ) { path = pjs; json = require(path)(); } else { path = pjson; json = ctxt.platform.json(path); } } let env = new Environ(ctxt, json, path, proj); env.name = name; return env; } configs() { var names = this.module.configs(); if ( this.proj ) { const extra = this.proj.configs().filter(n => ! names.includes(n)); return names.concat(extra); } else { return names; } } // Precedence: // - modules's config if exists // - if not project's config if exists // - if not global config if exists config(name) { var v = this.module.config(name); return v !== undefined ? v : this.proj && this.proj.config(name); } params() { var names = Object.keys(this._params).filter(n => ! n.startsWith('@')); this.module.params() .filter(n => ! names.includes(n)) .forEach(n => names.push(n)); return names; } param(name, value) { if ( value === undefined ) { var v = this._params[name]; return v === undefined ? this.module.param(name) : v; } else { this._params[name] = value; } } commands() { return this.module.commands(); } // TODO: Define the exact type/structure of the command values: // // - function // - string/array of strings // - object with different properties // // Then should we do some normalization here? Or let the caller do it? // command(name) { return this.module.command(name); } api(name) { let res = this._apis[name]; if ( ! res ) { throw new Error('Unknown API: ' + name); } return res; } hosts() { if ( this._topology ) { if ( ! this._hosts ) { this._hosts = this._topology; } else if ( ! this._hostsChecked ) { checkHosts(this.ctxt, this._hosts, this._topology); this._hostsChecked = true; } } return this._hosts; } databases() { return this._databases; } // ref is either ID or name database(ref) { let res = this.databases().filter(db => db.id === ref || db.name === ref); if ( ! res.length ) { return; } else if ( res.length === 1 ) { return res[0]; } else { let list = res.map(db => 'id:' + db.id + '/name:' + db.name).join(', '); throw new Error('More than one DB with ID or name "' + ref + '": ' + list); } } servers() { return this._servers; } // ref is either ID or name server(ref) { let res = this.servers().filter(srv => srv.id === ref || srv.name === ref); if ( ! res.length ) { return; } else if ( res.length === 1 ) { return res[0]; } else { let list = res.map(srv => 'id:' + srv.id + '/name:' + srv.name).join(', '); throw new Error('More than one server with ID or name "' + ref + '": ' + list); } } mimetypes() { return this._mimetypes; } execPrivileges() { return this._execPrivileges; } uriPrivileges() { return this._uriPrivileges; } roles() { return this._roles; } users() { return this._users; } sources() { return this._sources; } source(name) { let res = this.sources().filter(src => src.name === name); if ( ! res.length ) { return; } else if ( res.length === 1 ) { return res[0]; } else { let list = res.map(src => src.name).join(', '); throw new Error('More than one source with name "' + name + '": ' + list); } } substitute(val) { return this.module.resolveThing(this, val); } compile(params, force, defaults) { // if not set explicitly, use default values if ( defaults ) { Object.keys(defaults).forEach(name => { if ( this.param('@' + name) === undefined ) { this.param('@' + name, defaults[name]); } }); } // override values from `force` if ( force ) { Object.keys(force).forEach(name => { this.param('@' + name, force[name]); }); } // override values from `params` if ( params ) { Object.keys(params).forEach(name => { this.param(name, params[name]); }); } // compile databses, servers, source sets, mime types, privileges, // roles, users and apis (with import priority) this.module.compile(this); } show() { const addImports = (m, level) => { m.imports.forEach(i => { imports.push({ level: level, href: i.path }); addImports(i, level + 1); }); }; const imports = []; addImports(this.module, 1); let apis = {}; ['manage', 'admin', 'client', 'xdbc'].forEach(a => { let api = this._overridenApis[a]; if ( Object.keys(api).length ) { apis[a] = api; } }); this.ctxt.display.environ( this.name || this.module.path, this.param('@title'), this.param('@desc'), this.param('@host'), this.param('@user'), this.param('@password'), this.params().map(p => { return { name: p, value: this.param(p) }; }), apis, this.commands(), imports); } } /*~ * An environment module, that is a single one environment file. * * TODO: FIXME: The method `resolve()` modifies the JSON in place. It * should not. It would be nice to be able to keep it, so it is possible to * serialize it back (preserving variable references, for instance). */ class Module { constructor(ctxt, json, path, importer) { this.ctxt = ctxt; this.path = path; this.importer = importer; // check a few things if ( Object.keys(json).length !== 1 ) { throw new Error(`Invalid file, must have exactly one root: ${Object.keys(json)}`); } this.json = json.mlproj; if ( ! this.json ) { throw new Error(`Invalid file, must have the root 'mlproj'`); } if ( ! this.json.format ) { throw new Error(`Invalid file, must have the property 'format'`); } // check the format const format = this.json.format; if ( format === '0.1' ) { } else if ( format === 'dump/0.1' ) { this.importer = new DumpImporter(this.ctxt, this.json.environs); this.json = this.json.environs[0].json.mlproj; } else { throw new Error(`Invalid environ, 'format' neither 0.1 or dump/0.1: ${format}`); } // the params and commands hashes, empty by default this._params = this.json.params || {}; this._commands = this.json.commands || {}; // extract defined values from `obj` and put them in `this._params` const extract = (obj, props) => { props.forEach(p => { const v = obj[p]; if ( v !== undefined ) { this.param('@' + p, v); } }); }; extract(this.json, ['code', 'title', 'desc']); if ( this.json.connect ) { extract(this.json.connect, ['host', 'user', 'password']); } } loadImports() { this.imports = []; var imports = this.json['import']; if ( imports ) { if ( ! Array.isArray(imports) ) { imports = [ imports ]; } imports.concat().reverse().forEach(i => { const module = this.importer.resolve(i, this.path); this.imports.push(module); module.loadImports(); }); } } source(name) { let res = this._sources.filter(src => src.name === name); if ( ! res.length ) { return; } else if ( res.length === 1 ) { return res[0]; } else { let list = res.map(src => src.name).join(', '); throw new Error('More than one source with name "' + name + '": ' + list); } } configs() { let names = this.json.config ? Object.keys(this.json.config) : []; for ( let i = 0; i < this.imports.length; ++i ) { this.imports[i].configs() .filter(n => ! names.includes(n)) .forEach(n => names.push(n)); } return names; } config(name) { let v = this.json.config && this.json.config[name]; for ( let i = 0; v === undefined && i < this.imports.length; ++i ) { v = this.imports[i].config(name); } return v; } params() { var names = Object.keys(this._params).filter(n => ! n.startsWith('@')); this.imports.forEach(i => { i.params() .filter(n => ! names.includes(n)) .forEach(n => names.push(n)); }); return names; } param(name, value) { if ( value === undefined ) { var v = this._params[name]; if ( name !== '@title' && name !== '@desc' ) { for ( let i = 0; v === undefined && i < this.imports.length; ++i ) { v = this.imports[i].param(name); } } return v; } else { this._params[name] = value; } } commands() { var names = Object.keys(this._commands); this.imports.forEach(i => { i.commands() .filter(n => ! names.includes(n)) .forEach(n => names.push(n)); }); return names; } command(name) { var cmd = this._commands[name]; for ( let i = 0; cmd === undefined && i < this.imports.length; ++i ) { cmd = this.imports[i].command(name); } return cmd; } // `root` can be the root module, or the environ itself // resolve(root) { this.resolveObject(root, this.json.params, true); this.resolveArray(root, this.json.databases); this.resolveArray(root, this.json.servers); this.resolveArray(root, this.json.sources); this.resolveObject(root, this.json.privileges); this.resolveArray(root, this.json.roles); this.resolveArray(root, this.json.users); this.imports.forEach(i => i.resolve(root)); } resolveThing(root, val, forbiden) { if ( typeof val === 'string' ) { return this.resolveString(root, val, forbiden); } else if ( val instanceof Array ) { return this.resolveArray(root, val); } else if ( val instanceof Object ) { return this.resolveObject(root, val); } else { return val; } } resolveArray(root, array) { if ( ! array ) { return; } if ( ! array instanceof Array ) { throw new Error('Value not an array: ' + JSON.stringify(array)); } for ( var i = 0; i < array.length; ++i ) { array[i] = this.resolveThing(root, array[i]); } return array; } resolveObject(root, obj, forbid) { if ( ! obj ) { return; } if ( ! obj instanceof Object ) { throw new Error('Value not an object: ' + JSON.stringify(obj)); } for ( var p in obj ) { let name = this.resolveString(root, p); obj[name] = this.resolveThing(root, obj[p], forbid ? p : undefined); if ( p !== name ) { delete obj[p]; } } return obj; } resolveString(root, val, forbiden) { if ( ! val instanceof String ) { throw new Error('Value not a string: ' + JSON.stringify(val)); } val = this.resolveVars(root, val, forbiden, '@', '@'); val = this.resolveVars(root, val, forbiden, '$', ''); return val; } resolveVars(root, str, forbiden, ch, prefix) { var at = str.indexOf(ch + '{'); // no more to escape if ( at < 0 ) { return str; } // first "}" after the @{ or ${ var close = str.indexOf('}', at); // invalid ref if ( close < 0 ) { throw new Error('Invalid ' + ch + ' reference, } is missing: ' + str); } var name = str.slice(at + 2, close); // cannot use a param in its own value if ( name === forbiden ) { throw new Error('Invalid ' + ch + ' reference, value references itself: ' + name); } // name must be alphanumeric, with "-" separator if ( name.search(/^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/) < 0 ) { throw new Error('Invalid ' + ch + ' reference, invalid name: ' + name); } var val = root.param(prefix + name); if ( ! val ) { throw new Error('No value for parameter: ' + ch + name); } var resolved = str.slice(0, at) + val + str.slice(close + 1); return this.resolveVars(root, resolved, forbiden); } // compile databases and servers (resolving import priority) and source sets // // `root` can be the root module, or the environ itself // this function sets the _hosts, _databases, _servers, _sources, _mimetypes, // _execPrivileges, _uriPrivileges, _roles and _users on it compile(root) { // start by resolving the param references (could it be done on the // fly whilst compiling?) this.resolve(root); // resolve the connection infos // TODO: Shouldn't it be done before this.resolve(), right above? [ 'host', 'user', 'password' ].forEach(name => { var val = this.param('@' + name); if ( ! val ) { if ( this.proj && this.proj.connect && this.proj.connect[name] ) { this.param('@' + name, this.proj.connect[name]); } else if ( this.ctxt.connect && this.ctxt.connect[name] ) { this.param('@' + name, this.ctxt.connect[name]); } } }); // merge database and server JSON objects var cache = { href : "@root", hosts : [], hostNames : {}, dbs : [], dbIds : {}, dbNames : {}, srvs : [], srvIds : {}, srvNames : {}, srcs : [], srcNames : {}, mimes : [], mimeNames : {}, execPrivs : [], execPrivNames : {}, uriPrivs : [], uriPrivNames : {}, roles : [], roleNames : {}, users : [], userNames : {} }; this.compileImpl(cache); // compile apis this.compileApis(root, cache); // if no host declared, fetch cluster topology (except in case of command `init`) if ( ! cache.hosts.length && this.ctxt.fetchTopology ) { // include fetching ML version as well, and maybe other values? root._topology = fetchTopology(this.ctxt); root._topology.forEach(t => { const host = { name: t, group: 'Default' }; cache.hosts.push(host); cache.hostNames[t] = host; }); } // instantiate all mime types now root._mimetypes = cache.mimes.map(m => { return new cmp.MimeType(m); }); // instantiate all users now root._users = cache.users.map(u => { return new cmp.User(u); }); // instantiate all sources now let dfltSrc = cache.srcs.find(s => s.name === '@default'); let dflt = dfltSrc && new cmp.SourceSet(dfltSrc); root._sources = cache.srcs.filter(s => s.name !== '@default').map(s => { return new cmp.SourceSet(s, root, dflt); }); // instantiate all hosts now root._hosts = cache.hosts.map(h => { return new cmp.Host(h); }); // compile privileges and roles (the order of root._execPrivileges, // root._uriPrivileges and root._roles is in such a way that all // dependencies are resolved if the privileges and roles are created // in that order) this.compilePrivsRoles(root, cache); // compile databases and servers (the order of both root._servers and // root._databases is in such a way that all dependencies are resolved // if the components are created in that order) this.compileDbsSrvs(root, cache, root.source('src')); // privilege actions (in references to privileges) must be resolved // after we parsed all privileges (declarations) props.privilege.resolve(this.ctxt); } compileApis(root, cache) { // "flatten" the import graph in a single array // from highest priority at index 0, to lowest priority at the end var imports = []; var flatten = mod => { if ( ! imports.includes(mod) ) { imports.push(mod); mod.imports.forEach(i => flatten(i)); } }; flatten(this); // overrides lhs props with those in rhs, if any var collapse = (lhs, rhs) => { if ( rhs ) { Object.keys(rhs).forEach(k => lhs[k] = rhs[k]); } }; root._overridenApis = {}; root._apis = {}; // loop over all known apis Object.keys(DEFAULT_APIS).forEach(name => { // start with nothing root._overridenApis[name] = {}; root._apis[name] = {}; // walk the flatten import graph for ( let i = 0; i < imports.length; ++i ) { let apis = imports[i].json.apis; collapse(root._overridenApis[name], apis && apis[name]); } Object.keys(DEFAULT_APIS[name]).forEach(p => { root._apis[name][p] = root._overridenApis[name][p] || DEFAULT_APIS[name][p]; }); }); } // FIXME: An exec and a uri priv can both have the same name! // The "privs" map here make the assumption they can't. // Fix this and check other potential places where this mistake is done. compilePrivsRoles(root, cache) { // the maps of dependencies const execs = {}; const uris = {}; const roles = {}; // find the next priv, or role, with no more dep (remove it from // execs, uris, or roles, and return it) const nextZero = () => { const none = val => ! (val && val.length); const zero = (map, kind) => { for ( const k in map ) { const item = map[k]; if ( none(item.depExecOn) && none(item.depUriOn) && none(item.depRoleOn) ) { delete map[k]; return { kind: kind, stuff: item }; } } }; return zero(execs, 'exec') || zero(uris, 'uri') || zero(roles, 'role'); }; // init 1. - "empty" privs and roles (all slots, no dependencies yet) // all exec privs cache.execPrivs.forEach(p => execs[p.name] = { name: p.name, depRoleOn: [], depRoleBy: [] }); // all uri privs cache.uriPrivs.forEach(p => uris[p.name] = { name: p.name, depRoleOn: [], depRoleBy: [] }); // all roles cache.roles.forEach(r => roles[r.name] = { name: r.name, depExecOn: [], depExecBy: [], depUriOn: [], depUriBy: [], depRoleOn: [], depRoleBy: [] }); // init 2. - add dependencies // add "exec priv -> role" dependencies cache.execPrivs.forEach(p => { if ( p.roles ) { const priv = execs[p.name]; p.roles.forEach(d => { const dep = roles[d]; if ( dep ) { priv.depRoleOn.push(d); dep.depExecBy.push(p.name); } }); } }); // add "uri priv -> role" dependencies cache.uriPrivs.forEach(p => { if ( p.roles ) { const priv = uris[p.name]; p.roles.forEach(d => { const dep = roles[d]; if ( dep ) { priv.depRoleOn.push(d); dep.depUriBy.push(p.name); } }); } }); // add "role -> role", "role -> exec priv" and "role -> uri priv" deps cache.roles.forEach(r => { const role = roles[r.name]; if ( r.roles ) { r.roles.forEach(d => { const dep = roles[d]; if ( dep ) { role.depRoleOn.push(d); dep.depRoleBy.push(r.name); } }); } if ( r.privileges ) { if ( r.privileges.execute ) { r.privileges.execute.forEach(d => { const dep = execs[d]; if ( dep ) { role.depExecOn.push(d); dep.depRoleBy.push(r.name); } }); } if ( r.privileges.uri ) { r.privileges.uri.forEach(d => { const dep = uris[d]; if ( dep ) { role.depUriOn.push(d); dep.depRoleBy.push(r.name); } }); } } }); // init 3. - the final lists of privileges and roles are empty root._execPrivileges = []; root._uriPrivileges = []; root._roles = []; const rem = (str, list) => { list.splice(list.indexOf(str), 1); }; // as long as there are roles with no more (unresolved) dep, pick one, add it // to the final list, and remove it from the list of dependencies of each role // that depends on it let zero; while ( zero = nextZero() ) { if ( zero.kind === 'exec' ) { const priv = zero.stuff; const found = cache.execPrivs.find(p => p.name === priv.name); root._execPrivileges.push(new cmp.Privilege(found, 'execute', this.ctxt)); priv.depRoleBy.forEach(d => rem(priv.name, roles[d].depExecOn)); } else if ( zero.kind === 'uri' ) { const priv = zero.stuff; const found = cache.uriPrivs.find(p => p.name === priv.name); root._uriPrivileges.push(new cmp.Privilege(found, 'uri', this.ctxt)); priv.depRoleBy.forEach(d => rem(priv.name, roles[d].depUriOn)); } else if ( zero.kind === 'role' ) { const role = zero.stuff; const found = cache.roles.find(r => r.name === role.name); root._roles.push(new cmp.Role(found, this.ctxt)); role.depExecBy.forEach(d => rem(role.name, execs[d].depRoleOn)); role.depUriBy .forEach(d => rem(role.name, uris[d] .depRoleOn)); role.depRoleBy.forEach(d => rem(role.name, roles[d].depRoleOn)); } else { throw new Error(`Internal, cannot happen, not exec|uri|role: ${zero.kind}!`); } } // check whether there are still privileges or roles with unresolved deps const unresolved = [].concat( Object.keys(execs), Object.keys(uris), Object.keys(roles)); if ( unresolved.length ) { throw new Error('Cannot resolve dependencies for all privileges and roles, ' + 'these must have cyclic dependencies: ' + unresolved); } } compileDbsSrvs(root, cache, src) { // build the array of database and server objects // the order of the database array guarantees there is no broken dependency var res = { list : [], ids : {}, names : {} }; // instantiate a database object from its JSON object, and resolve // its schema, security and triggers database if any to objects // already instantiated var instantiate = (json, res) => { // is it a system db name? if ( json.sysref ) { var db = new cmp.SysDatabase(json.sysref); res.list.push(db); res.names[json.sysref] = db; return db; } // resolve a schema, security or triggers DB from the current result list var resolve = db => { if ( ! db ) { return; } var end = ( db.name && res.names[db.name] ) || ( db.nameref && res.names[db.nameref] ) || ( db.id && res.ids[db.id] ) || ( db.idref && res.ids[db.idref] ) || ( db.sysref && res.names[db.sysref] ); if ( end ) { return end; } // is it self-referencing by ID? if ( db.idref && db.idref === json.id ) { return 'self'; } // is it self-referencing by name? if ( db.nameref && db.nameref === json.name ) { return 'self'; } // is it a system db name? if ( db.sysref ) { return res.names[db.sysref] = new cmp.SysDatabase(db.sysref); } }; var schema = resolve(json.schema); var security = resolve(json.security); var triggers = resolve(json.triggers) var db = new cmp.Database(json, schema, security, triggers, this.ctxt); res.list.push(db); if ( json.id ) { res.ids[json.id] = db; } if ( json.name ) { res.names[json.name] = db; } return db; }; // return true if a database does not need instantiation anymore (if // it is already instantiated or if it is undefined) var done = (db, res) => { if ( ! db ) { // no dependency return true; } else if ( db.id && res.ids[db.id] ) { // has an ID and has been done return true; } else if ( db.name && res.names[db.name] ) { // has a name and has been done return true; } else if ( db.idref && res.ids[db.idref] ) { // is a reference to an ID that has been done return true; } else if ( db.nameref && res.names[db.nameref] ) { // is a reference to a name that has been done return true; } else if ( db.sysref && res.names[db.sysref] ) { // is a reference to a name that has been done return true; } else { return false; } }; // return true if `child` references its `parent` var selfRef = (parent, child) => { if ( ! child ) { return false; } else if ( parent.id && parent.id === child.idref ) { return true; } else if ( parent.name && parent.name === child.nameref ) { return true; } else { return false; } }; // return true if `db` is a reference to another DB (by ID or name) var isRef = db => { if ( ! db ) { return false; } else if ( db.idref || db.nameref || db.sysref ) { return true; } else { return false; } }; // starting at one DB (a "standalone" DB or a server's content or // modules DB), return all the DB (itself or embedded, at any level) // that can be instantiated (meaning: with all referrenced schema, // security and triggers DB already instantiated, with the exception // of self-referrencing DB which can be instantiated as well) var candidates = (db, res) => { if ( done(db, res) ) { // if already instantiated, do nothing return []; } else if ( isRef(db) ) { // if the referrenced DB is instantiated, then return it if ( db.idref && res.ids[db.idref] ) { return [ db ]; } else if ( db.nameref && res.names[db.nameref] ) { return [ db ]; } else if ( db.sysref ) { return [ db ]; } else { return []; } } else { // if both referrenced DB are instantiated, or self-refs, then return it var sch = selfRef(db, db.schema) || done(db.schema, res); var sec = selfRef(db, db.security) || done(db.security, res); var trg = selfRef(db, db.triggers) || done(db.triggers, res); if ( sch && sec && trg ) { return [ db ]; } // if not, recurse else { return candidates(db.schema, res) .concat(candidates(db.security, res)) .concat(candidates(db.triggers, res)); } } } // return all candidates (like `candidate`, but using all "roots") var allCandidates = (cache, res) => { var all = []; cache.dbs.forEach(db => { all = all.concat(candidates(db, res)); }); cache.srvs.forEach(srv => { all = all.concat(candidates(srv.content, res)); all = all.concat(candidates(srv.modules, res)); }); return all; } // return all databases and servers for which there is some unmet dependency var unsolved = (cache, res) => { var impl = (db) => { if ( done(db, res) ) { return []; } else { return [ db ] .concat(impl(db.schema)) .concat(impl(db.security)) .concat(impl(db.triggers)); } }; var all = []; var srvs = []; cache.dbs.forEach(db => { all = all.concat(impl(db)); }); cache.srvs.forEach(srv => { var lhs = impl(srv.content); var rhs = impl(srv.modules); if ( lhs.length || rhs.length ) { srvs.push(srv); } all = all.concat(lhs).concat(rhs); }); return all.concat(srvs); } // as long as we find candidates, instantiate them var cands; while ( ( cands = allCandidates(cache, res) ).length ) { cands.forEach(db => instantiate(db, res)); } root._databases = res.list; // ensure we have instantiated all databases var leftover = unsolved(cache, res); if ( leftover.length ) { var disp = leftover.map(c => { if ( c.content || c.modules ) { return '{srv ' + (c.name || '') + '}'; } else if ( c.id || c.name ) { return '{db ' + (c.id || '') + '|' + (c.name || '') + '}'; } else { return '{dbref ' + (c.idref || '') + '|' + (c.nameref || '') + '|' + (c.sysref || '') + '}'; } }); throw new Error('Some components have unsolved database dependencies: ' + disp); } // instantiate all servers now root._servers = cache.srvs.map(srv => { var resolve = db => { if ( ! db ) { return; } var cmp = ( db.name && res.names[db.name] ) || ( db.nameref && res.names[db.nameref] ) || ( db.id && res.ids[db.id] ) || ( db.idref && res.ids[db.idref] ) || ( db.sysref && res.names[db.sysref] ); if ( cmp ) { return cmp; } if ( db.sysref ) { return res.names[db.sysref] = new cmp.SysDatabase(db.sysref); } }; return new cmp.Server(srv, resolve(srv.content), resolve(srv.modules), src, this.ctxt.platform); }); } // recursive implementation of compile(), caching hosts, databases, // servers, users, etc. compileImpl(cache) { // small helper to format info and error messages var _ = (c) => { return 'id=' + (c.id || '') + '|name=' + (c.name || ''); }; // the common implementation for all components var impl = (comp, cache, ids, names, kind) => { // at least one of ID and name mandatory if ( ! comp.name && ! comp.id ) { throw new Error('No ID and no name on ' + kind.kind + ' in ' + cache.href); } // default value for compose if ( ! comp.compose ) { comp.compose = 'merge'; } // does it exist yet? var derived = ( comp.id && ids[comp.id] ) || ( comp.name && names[comp.name] ); // if it does, perform the "compose" action.. if ( derived ) { if ( derived.compose !== comp.compose ) { throw new Error('Different compose actions for ' + kind.kind + 's: derived:' + _(derived) + '|compose=' + derived.compose + ' and base:' + _(comp) + '|compose=' + comp.compose); } else if ( derived.compose === 'merge' ) { this.ctxt.display.info('Merge ' + kind.kind + 's derived:' + _(derived) + ' and base:' + _(comp)); var overriden = Object.keys(derived); for ( var p in comp ) { if ( overriden.indexOf(p) === -1 ) { derived[p] = comp[p]; if ( p === 'id' ) { ids[derived.id] = derived; } else if ( p === 'name' ) { names[derived.name] = derived; } } else { derived[p] = kind.merge(p, derived[p], comp[p]); } } } else if ( derived.compose === 'hide' ) { this.ctxt.platform.info('Hide ' + kind.kind + ' base:' + _(comp) + ' by derived:' + _(derived)); } else { throw new Error('Unknown compose on ' + kind.kind + ': ' + _(derived) + '|compose=' + derived.compose); } } // ...if it does not, just add it else { cache.push(comp); if ( comp.id ) { ids[comp.id] = comp; } if ( comp.name ) { names[comp.name] = comp; } } }; // compile hosts if ( this.json.hosts ) { this.json.hosts.forEach(host => { impl(host, cache.hosts, null, cache.hostNames, cmp.Host); }); } // compile databases if ( this.json.databases ) { this.json.databases.forEach(db => { impl(db, cache.dbs, cache.dbIds, cache.dbNames, cmp.Database); }); } // compile servers if ( this.json.servers ) { this.json.servers.forEach(srv => { impl(srv, cache.srvs, cache.srvIds, cache.srvNames, cmp.Server); }); } // compile sources if ( this.json.sources ) { this.json.sources.forEach(src => { impl(src, cache.srcs, null, cache.srcNames, cmp.SourceSet); }); } // compile mime types if ( this.json['mime-types'] ) { this.json['mime-types'].forEach(mime => { impl(mime, cache.mimes, null, cache.mimeNames, cmp.MimeType); }); } // compile privileges if ( this.json.privileges ) { const each = (privs, cache, names) => { if ( privs ) { privs.forEach(priv => { impl(priv, cache, null, names, cmp.Privilege); }); } }; each(this.json.privileges.execute, cache.execPrivs, cache.execPrivNames); each(this.json.privileges.uri, cache.uriPrivs, cache.uriPrivNames); } // compile roles if ( this.json.roles ) { this.json.roles.forEach(role => { impl(role, cache.roles, null, cache.roleNames, cmp.Role); }); } // compile users if ( this.json.users ) { this.json.users.forEach(user => { impl(user, cache.users, null, cache.userNames, cmp.User); }); } // recurse on imports this.imports.forEach(i => { cache.href = i.path; i.compileImpl(cache); }); } } const DEFAULT_APIS = { manage: { root : '/manage/v2', port : 8002, ssl : false }, rest: { root : '/v1', //root : '/v1/rest-apis', port : 8002, ssl : false }, admin: { root : '/admin/v1', port : 8001, ssl : false }, client: { root : '/v1', port : 8000, ssl : false }, xdbc: { root : '', port : 8000, ssl : false } }; function fetchTopology(ctxt) { try { const resp = new act.HostList().retrieve(ctxt); const items = resp['host-default-list']['list-items']['list-item']; return items.map(o => o.nameref).sort(); } catch (err) { if ( err.code === 'missing-value' && ['@host', '@user', '@password'].includes(err.value) ) { // return empty list if on an abstract environ return []; } else { throw err; } } } function checkHosts(ctxt, configured, topology) { if ( configured.length ) { const declared = configured.map(o => o.name || o.host).sort(); let same = declared.length === topology.length; if ( same ) { declared.forEach((v, i) => { if ( same ) {