UNPKG

mlproj-core

Version:

Project management for MarkLogic, core implementation

1,229 lines (1,173 loc) 69.6 kB
"use strict"; (function() { const act = require('./action'); const props = require('./properties'); // const trace = require('debug')('mlproj:core:trace'); /*~ * Interface of a component. */ class Component { show(display) { throw new Error('Component.show is abstract'); } setup(actions, display) { throw new Error('Component.setup is abstract'); } } /*~ * A system database. */ class SysDatabase extends Component { constructor(name) { super(); this.name = name; } show(display) { display.sysDatabase(this.name); } setup(actions, display) { display.check(0, 'the database', this.name); const body = new act.DatabaseProps(this).retrieve(actions.ctxt); // if DB does not exist if ( ! body ) { display.remove(0, 'be created', 'outside', this.name); } } } function handleForestsNumber(forests, existing, hosts, db, ctxt) { if ( forests < 0 ) { throw new Error(`Negative number of forests (${forests}) on id:${db.id}|name:${db.name}`); } if ( forests > 100 ) { throw new Error(`Number of forests greater than 100 (${forests}) on id:${db.id}|name:${db.name}`); } const fmt = n => n.toLocaleString('en-IN', { minimumIntegerDigits: 3 }); const name = (i, j) => `${db.name}-${fmt(i)}-${fmt(j)}`; hosts.forEach((h, i) => { for ( let j = 0; j < forests; ++j ) { const n = name(i + 1, j + 1); const f = new Forest(db, h, { name: n }); if ( existing.includes(n) ) { const props = new act.ForestProps(n).retrieve(ctxt); if ( h.name !== props.host ) { throw new Error(`Host do not match for forest ${n}: ${h.name} vs. ${props.host}`); } f.exists = true; } db.forests[n] = f; } }); } function handleForestsList(forests, existing, hosts, db, ctxt) { const map = {}; hosts.forEach(h => map[h.name] = h); const hasHost = forests.find(f => f.host); const noHost = forests.find(f => ! f.host); if ( hasHost && noHost ) { throw new Error(`Forests with both host and no host not allowed in the same array`); } const dflt = (noHost && hosts.length === 1) ? hosts[0] : null; if ( noHost && ! dflt ) { throw new Error(`Forests have no explicit host and there is not exactly one host`); } forests.forEach(decl => { const h = noHost ? dflt : map[decl.host]; if ( ! h ) { throw new Error(`No such host for forest ${decl.name}: ${decl.host}`); } const f = new Forest(db, h, decl); if ( existing.includes(decl.name) ) { const props = new act.ForestProps(decl.name).retrieve(ctxt); if ( h.name !== props.host ) { throw new Error(`Host do not match for forest ${decl.name}: ${h.name} vs. ${props.host}`); } f.exists = true; } db.forests[decl.name] = f; }); } function handleForestsObject(forests, existing, hosts, db, ctxtx) { // TODO: ... throw new Error('TODO: Implement the "object" case for forests'); } /*~ * A database. */ class Database extends Component { constructor(json, schema, security, triggers, ctxt) { super(); this.id = json.id; this.name = json.name; this.properties = json.properties; this.forests = {}; this.schema = schema === 'self' ? this : schema; this.security = security === 'self' ? this : security; this.triggers = triggers === 'self' ? this : triggers; this.sources = json.sources; // extract the configured properties this.props = props.database.parse(json); // the forests // // TODO: Improve forest support. They can be configured using // different ways in the environ files: // // - nothing: there is one forest per host // - number: there is N forests per host // - array: explicit forest list, each being an object // - object: several config options (per host, name template, etc.) // (TODO: make it another property, like `forest-config`?) // // Short-term: support the following: // // - assign a number of forests per host (`number` above) // - create explicit forests, supporting replication (`array` above) // // Support also replica forests. This can become even harder to // generate "meaningful" names for the forests accros hosts, with // replication... // normalize forests value, at the end must be an array of Forest objects let forests = json.forests; if ( forests === null || forests === undefined ) { forests = 1; } // do not do forests if 0 or false if ( forests ) { // is there any reason not to have an environ, except during unit tests? const hosts = ctxt.platform.environ ? ctxt.platform.environ.hosts() : []; // disable forest computation if host list not available if ( hosts.length ) { // the forests actually existing on the cluster const existing = new act.ForestList() .retrieve(ctxt) ['forest-default-list']['list-items']['list-item'] .map(o => o.nameref); // support several types of data for forests // // TODO: Accept also a function? Is there any incentive for that, // or is it just possible to simply generate, say, the forest array // using a piece of code in a *.js environment? That is, is there // any benefit to call a function from here? (like passing some // parameters, calling it only for some specific (sub-)parts, etc.) let handler; if ( Number.isInteger(forests) ) { handler = handleForestsNumber; } else if ( Array.isArray(forests) ) { handler = handleForestsList; } else if ( typeof forests === 'object' ) { handler = handleForestsObject; } else { throw new Error(`Unsupported data type for forests: ${typeof forests}`); } handler(forests, existing, hosts, this, ctxt); } } } show(display) { display.database( this.name, this.id, this.schema, this.security, this.triggers, Object.keys(this.forests).sort(), this.props); } setup(actions, display) { display.check(0, 'the database', this.name); const body = new act.DatabaseProps(this).retrieve(actions.ctxt); let names; if ( Object.keys(this.forests).length ) { const forests = new act.ForestList().retrieve(actions.ctxt); const items = forests['forest-default-list']['list-items']['list-item']; names = items.map(o => o.nameref); } // if DB does not exist yet if ( ! body ) { this.create(actions, display, names); } // if DB already exists else { this.update(actions, display, body, names); } } create(actions, display, forests) { display.add(0, 'create', 'database', this.name); // the base database object const obj = { "database-name": this.name }; // its schema, security and triggers DB this.schema && ( obj['schema-database'] = this.schema.name ); this.security && ( obj['security-database'] = this.security.name ); this.triggers && ( obj['triggers-database'] = this.triggers.name ); // its properties Object.keys(this.props).forEach(p => { this.props[p].create(obj); }); if ( this.properties ) { Object.keys(this.properties).forEach(p => { if ( obj[p] ) { throw new Error('Explicit property already set on database: name=' + this.name + ',id=' + this.id + ' - ' + p); } obj[p] = this.properties[p]; }); } // enqueue the "create db" action actions.add(new act.DatabaseCreate(this, obj)); const fkeys = Object.keys(this.forests); if ( ! fkeys.length ) { display.check(1, 'forests - disabled'); } else { display.check(1, 'forests'); // check the forests fkeys.forEach(f => { this.forests[f].create(actions, display, forests); }); } } update(actions, display, body, forests) { // check databases this.updateDb(actions, display, this.schema, body, 'schema-database', 'Schemas'); this.updateDb(actions, display, this.security, body, 'security-database', 'Security'); this.updateDb(actions, display, this.triggers, body, 'triggers-database', null); // check forests const desired = Object.keys(this.forests); if ( ! desired.length ) { display.check(1, 'forests - disabled'); } else { display.check(1, 'forests'); const actual = body.forest || []; // forests to remove: those in `actual` but not in `desired` actual .filter(name => ! desired.includes(name)) .forEach(name => { new Forest(this, null, { name: name }).remove(actions, display); }); // forests to add: those in `desired` but not in `actual` desired .filter(name => ! actual.includes(name)) .forEach(name => { this.forests[name].create(actions, display, forests); }); // forests to (potentially) update: those in both `desired` and `actual` desired .filter(name => actual.includes(name)) .forEach(name => { this.forests[name].update(actions, display, forests); }); } // check properties display.check(1, 'properties'); const updateProp = key => { let res = this.props[key]; // TODO: Rather fix the "_type" setting mechanism, AKA the "root cause"... if ( ! res.prop._type ) { res.prop._type = 'database'; } res.update(actions, display, body, this); }; Object.keys(this.props) .filter(key => key !== 'range-field-index') .forEach(updateProp); // done after all the others, to avoid dependency on `field` property if ( this.props['range-field-index'] ) { updateProp('range-field-index'); } if ( this.properties ) { Object.keys(this.properties).forEach(p => { if ( this.properties[p] !== body[p] ) { actions.add(new act.DatabaseUpdate(this, p, this.properties[p])); } }); } } updateDb(actions, display, db, body, prop, dflt) { const actual = body[prop]; var newName; // do not exist, not desired if ( ! actual && ! db || (actual === dflt && ! db) ) { // nothing } // does exist, to remove else if ( ! db ) { newName = dflt || null; } // do not exist, to create, or does exist, to chamge else if ( ! actual || (actual !== db.name) ) { newName = db.name; } // already set to the right db else { // nothing } // enqueue the action if necessary if ( newName !== undefined ) { display.add(0, 'update', prop); actions.add(new act.DatabaseUpdate(this, prop, newName)); } } } Database.kind = 'database'; Database.merge = (name, derived, base) => { if ( name === 'indexes' ) { // TODO: Implement and document merging of indexes... Only 'ranges' // are supported for now... The equality function between two range // index takes several params into account (its type first, // depending on the properties set, path or parent or nothing): type // + path/name (incl. ns) + parent name if any (incl. ns.) //throw new Error('Merging of indexes is not implemented yet!'); // check derived index properties if ( ! derived.ranges ) { throw new Error('No range index in property indexes in derived object'); } if ( Object.keys(derived).length !== 1 ) { throw new Error('Unknown properties on indexes in derived object: ' + Object.keys(derived).filter(k => k !== 'ranges')); } // check base index properties if ( ! base.ranges ) { throw new Error('No range index in property indexes in base object'); } if ( Object.keys(base).length !== 1 ) { throw new Error('Unknown properties on indexes in base object: ' + Object.keys(base).filter(k => k !== 'ranges')); } // copy a range, with a given name const copy = (name, range) => { let r = { name: name }; for ( let p in range ) { if ( p !== 'name' ) { r[p] = range[p]; } } return r; } // provision result array with range indexes from derived let res = []; derived.ranges.forEach(range => { if ( Array.isArray(range.name) ) { range.name.forEach(n => { res.push(copy(n, range)); }); } else { res.push(range); } }); // add the range indexes from base not already in res base.ranges.forEach(range => { const handle = r => { let existing = res.find(b => { // namespaces must be both not there, or both there and equal const nsDiff = (lhs, rhs) => { if ( lhs ) { if ( ! rhs || lhs !== rhs ) { return true; } } else if ( rhs ) { return true; } }; // for all, if type differ... if ( r.type !== b.type ) { return false; } // at least one is a path range if ( r.path || b.path ) { if ( ! r.path || ! b.path || r.path !== b.path ) { return false; } } else { // at least one is an attribute range if ( r.parent || b.parent ) { if ( ! r.parent || ! b.parent || r.parent.name !== b.parent.name ) { return false; } if ( nsDiff(r.parent.namespace, b.parent.namespace) ) { return false; } } // for both attribute and element ranges if ( r.name !== b.name ) { return false; } if ( nsDiff(r.namespace, b.namespace) ) { return false; } } return true; }); if ( ! existing ) { res.push(r); } }; if ( Array.isArray(range.name) ) { range.name.forEach(n => { handle(copy(n, range)); }); } else { handle(range); } }); return { ranges: res }; } else { // by default, the value in the derived object overrides the one from // the base object return derived; } }; /*~ * A forest. */ class Forest extends Component { constructor(db, host, json) { if ( ! json.name ) { throw new Error(`Forest has no name: ${JSON.stringify(json)}`); } super(); this.db = db; this.host = host; this.name = json.name; this.properties = json.properties; this.props = props.forest.parse(json); this.replicas = []; // TODO: "Parse" (and validate) the replica objects themselves? if ( json.replica && json.replicas ) { throw new Error(`Both replica and replicas set for forest ${json.name}`); } else if ( json.replica ) { if ( Array.isArray(json.replica) ) { throw new Error(`Forest.replica cannot be an array, use forest.replicas instead (plural)`); } this.replicas.push(json.replica); } else if ( json.replicas ) { json.replicas.forEach(r => this.replicas.push(r)); } // sort by name this.replicas.sort((a, b) => { return a.name < b.name ? -1 : a.name === b.name ? 0 : 1; }); // transform to API payload this.replicas = this.replicas.map(decl => { const def = { "replica-name": decl.name, "host": decl.host }; decl['dir'] && ( def['data-directory'] = decl['dir'] ); decl['large-dir'] && ( def['large-data-directory'] = decl['large-dir'] ); decl['fast-dir'] && ( def['fast-data-directory'] = decl['fast-dir'] ); return def; }); } create(actions, display, forests) { // if already exists, attach it instead of creating it if ( forests.includes(this.name) ) { display.add(1, 'attach', 'forest', this.name); actions.add(new act.ForestAttach(this)); // check whether needs to update properties this.update(actions, display); } else { display.add(1, 'create', 'forest', this.name); // the base forest object let obj = { "forest-name": this.name, "database": this.db.name, "host": this.host.name }; // replicas if any if ( this.replicas.length ) { obj['forest-replica'] = this.replicas; } // its properties Object.keys(this.props).forEach(p => { this.props[p].create(obj); }); if ( this.properties ) { Object.keys(this.properties).forEach(p => { if ( obj[p] ) { throw new Error('Explicit property already set on forest: name=' + this.name + ' - ' + p); } obj[p] = this.properties[p]; }); } actions.add(new act.ForestCreate(this, obj)); } } // Check properties, and update accordingly, if necessary. update(actions, display) { const body = new act.ForestProps(this).retrieve(actions.ctxt); display.check(2, 'properties'); // replicas const replicas = body['forest-replica']; if ( this.replicas.length !== (replicas ? replicas.length : 0) ) { actions.add(new act.ForestUpdate(this, 'forest-replica', this.replicas)); } else if ( this.replicas.length ) { const compare = (i, lhs, rhs) => { if ( i >= lhs.length ) { return true; } const eq = p => { // values are the same return lhs[i][p] === rhs[i][p] // or are both unset (undefined, empty string...) || (! lhs[i][p] && ! rhs[i][p]); }; if ( ! (eq('replica-name') && eq('host') && eq('data-directory') && eq('large-data-directory') && eq('fast-data-directory')) ) { return false; } return compare(i + 1, lhs, rhs); }; if ( ! compare(0, this.replicas, replicas) ) { actions.add(new act.ForestUpdate(this, 'forest-replica', this.replicas)); } } // properties Object.keys(this.props).forEach(p => { let res = this.props[p]; res.update(actions, display, body, this); }); if ( this.properties ) { Object.keys(this.properties).forEach(p => { if ( this.properties[p] !== body[p] ) { actions.add(new act.ForestUpdate(this, p, this.properties[p])); } }); } } remove(actions, display) { display.remove(1, 'detach', 'forest', this.name); // just detach it, not delete it for real actions.add(new act.ForestDetach(this)); } } /*~ * A server. */ class Server extends Component { constructor(json, content, modules, src, platform) { super(); this.type = json.type; this.group = json.group || 'Default'; this.id = json.id; this.name = json.name; this.properties = json.properties; this.content = content; this.modules = modules; // extract the configured properties this.props = props.server.parse(json); // some validation const error = msg => { throw new Error(msg + ': ' + this.type + ' - ' + this.id + '/' + this.name); }; if ( ! content ) { error('App server with no content database'); } // validation specific to REST servers if ( json.type === 'rest' ) { this.rest = json['rest-config']; if ( ! modules ) { error('REST server has no modules database'); } if ( json.root ) { error('REST server has root (' + json.root + ')'); } if ( json.rewriter ) { error('REST server has rewriter (' + json.rewriter + ')'); } if ( json.properties && json.properties['rewrite-resolves-globally'] ) { error('REST server has rewrite-resolves-globally (' + json.properties['rewrite-resolves-globally'] + ')'); } } // for plain HTTP servers else { if ( json['rest-config'] ) { error('REST config on a non-REST server'); } // use a source set as filesystem modules if no modules DB and no root if ( ! this.modules && ! this.props.root ) { // TODO: For now, only try the default `src`. Once // implmented the links from databses and servers to source // sets, check if there is one on this server then. if ( ! src ) { throw new Error( 'The app server has no modules db, no root, and there is no default src: ', this.name); } if ( ! src.props.dir ) { throw new Error( 'The app server has no modules db, no root, and default src has no dir: ', this.name); } const dir = platform.resolve(src.props.dir.value) + '/'; this.props.root = new props.Result(props.server.props.root, dir); } } } show(display) { display.server( this.name, this.id, this.type, this.group, this.content, this.modules, this.props); } setup(actions, display) { display.check(0, 'the ' + this.type + ' server', this.name); const body = new act.ServerProps(this).retrieve(actions.ctxt); // if AS does not exist yet if ( ! body ) { if ( this.type === 'http' ) { this.createHttp(actions, display); } else if ( this.type === 'xdbc' ) { this.createHttp(actions, display); } else if ( this.type === 'rest' ) { this.createRest(actions, display); } else { throw new Error('Unknown app server type: ' + this.type); } } // if AS already exists else { if ( this.type === 'rest' ) { this.updateRest(actions, display, body); } this.updateHttp(actions, display, body); } } createAddProps(obj) { Object.keys(this.props).forEach(p => { this.props[p].create(obj); }); if ( this.properties ) { Object.keys(this.properties).forEach(p => { if ( obj[p] ) { throw new Error('Explicit property already set on server: name=' + this.name + ',id=' + this.id + ' - ' + p); } obj[p] = this.properties[p]; }); } } createHttp(actions, display) { display.add(0, 'create', this.type + ' server', this.name); // the base server object const obj = { "server-name": this.name, "server-type": this.type, "content-database": this.content.name }; // its modules DB this.modules && ( obj['modules-database'] = this.modules.name ); // its properties this.createAddProps(obj); // enqueue the "create server" action actions.add(new act.ServerCreate(this, obj)); } createRest(actions, display) { display.add(0, 'create', 'rest server', this.name); let obj = { "name": this.name, "group": this.group, "database": this.content.name, "modules-database": this.modules.name }; this.props.port.create(obj); if ( this.rest ) { if ( this.rest['error-format'] ) { obj['error-format'] = this.rest['error-format']; } if ( this.rest['xdbc'] ) { obj['xdbc-enabled'] = this.rest['xdbc']; } } // enqueue the "create rest server" action actions.add(new act.ServerRestCreate(this, { "rest-api": obj })); // its other properties let extra = {}; this.createAddProps(extra); // port is handled at creation delete extra['port']; delete extra['server-type']; if ( Object.keys(extra).length ) { // enqueue the "update server" action actions.add(new act.ServerUpdate(this, extra)); } if ( this.rest ) { let keys = Object.keys(this.rest).filter(k => { return k !== 'error-format' && k !== 'xdbc'; }); if ( keys.length ) { const map = { "debug": 'debug', "tranform-all": 'document-transform-all', "tranform-out": 'document-transform-out', "update-policy": 'update-policy', "validate-options": 'validate-options', "validate-queries": 'validate-queries' }; let props = {}; keys.forEach(k => { let p = map[k]; if ( ! p ) { throw new Error('Unknown property on server.rest: ' + k); } props[p] = this.rest[k]; }); // enqueue the "update rest server props" action actions.add(new act.ServerRestUpdate(this, props, this.props.port.value)); } } } // TODO: It should not be hard to make it possible to add more and more // property/value pairs to the server update action, and send them all // in one request. That would have an impact on displaying the action // though, as we would probably want to keep multiple lines for multiple // properties, as it is clearer. // // This is actually required for scenarii where on property depends on // another, like path range index on path namespaces... // updateHttp(actions, display, actual) { let type = this.type === 'rest' ? 'http' : this.type; if ( type !== actual['server-type'] ) { throw new Error('Server type cannot change, from ' + actual['server-type'] + ' to ' + this.type); } // the content and modules databases if ( this.content.name !== actual['content-database'] ) { display.add(0, 'update', 'content-database'); actions.add(new act.ServerUpdate(this, 'content-database', this.content.name)); } if ( ( ! this.modules && actual['modules-database'] ) || ( this.modules && ! actual['modules-database'] ) || ( this.modules && this.modules.name !== actual['modules-database'] ) ) { const mods = this.modules ? this.modules.name : 0; display.add(0, 'update', 'modules-database'); actions.add(new act.ServerUpdate(this, 'modules-database', mods)); } // check properties display.check(1, 'properties'); Object.keys(this.props) .filter(p => p !== 'server-type') .forEach(p => { this.props[p].update(actions, display, actual, this); }); if ( this.properties ) { Object.keys(this.properties).forEach(p => { if ( this.properties[p] !== actual[p] ) { actions.add(new act.ServerUpdate(this, p, this.properties[p])); } }); } } /*~ * For a REST server, check REST-specific config items (its "creation * properties", the values passed to the endpoint when creating the REST * server), like `xdbc-enabled`. These properties are to be retrieved * from `:8002/v1/rest-apis/[name]`. * * There seems to be no way to change the value of such a creation * property (they are used at creation only). * * In addition, there are properties specific to REST servers (not for * HTTP), like `debug` and `update-policy`. These properties are to be * retrieved from `:[port]/v1/config/properties`. * * 1) retrieve creation properties, if anything differs, -> error * 2) retrieve properties, and update them, as for any component */ updateRest(actions, display, actual) { // 1) check creation properties for any difference const check = (name, old, current) => { if ( old !== current ) { throw new Error('Cannot update REST server ' + name + ', from ' + old + ' to ' + current); } }; const bool = val => { const type = typeof val; if ( 'boolean' === type ) { return val; } else if ( 'string' === type ) { if ( 'false' === val ) { return false; } else if ( 'true' === val ) { return true; } else { throw new Error('Invalid boolean value: ' + val); } } else { throw new Error('Boolean value neither a string or a boolean: ' + type); } }; const cprops = new act.ServerRestCreationProps(this).retrieve(actions.ctxt); check('name', cprops.name, this.name); check('group', cprops.group, this.group); check('database', cprops.database, this.content && this.content.name); check('modules-database', cprops['modules-database'], this.modules && this.modules.name); check('port', parseInt(cprops.port, 10), this.props.port && this.props.port.value); check('error-format', cprops['error-format'], this.rest && this.rest['error-format']); check('xdbc-enabled', bool(cprops['xdbc-enabled']), bool(this.rest && this.rest.xdbc)); // 2) update all properties with different value let obj = {}; const update = (name, old, current, dflt) => { if ( old !== (current === undefined ? dflt : current) ) { obj[name] = current; } }; const props = new act.ServerRestProps(this, this.props.port.value).retrieve(actions.ctxt); update('debug', bool(props['debug']), this.rest && this.rest['debug'], false); update('document-transform-all', bool(props['document-transform-all']), this.rest && this.rest['transform-all'], true); update('document-transform-out', props['document-transform-out'], this.rest && this.rest['transform-out'] || '', ''); update('update-policy', props['update-policy'], this.rest && this.rest['update-policy'], 'merge-metadata'); update('validate-options', bool(props['validate-options']), this.rest && this.rest['validate-options'], true); update('validate-queries', bool(props['validate-queries']), this.rest && this.rest['validate-queries'], false); if ( Object.keys(obj).length ) { actions.add(new act.ServerRestUpdate(this, obj, this.props.port.value)); } } } Server.kind = 'server'; Server.merge = (name, derived, base) => { return derived; }; /*~ * A named source set. * * TODO: Should we refactor to have different subclasses for different * source set types? */ class SourceSet extends Component { constructor(json, environ, dflt) { super(); this.dflt = dflt; this.name = json && json.name; this.filter = json && json.filter; // extract the configured properties this.props = json ? props.source.parse(json) : {}; this.type = this.props.type && this.props.type.value; this.environ = environ; } // Return the dbs this source set is a source set of. This cannot be applied to servers, // as it does not make sense to have sources of a srv, without saying explicitely whether // it is the sources of its content, or modules, or schemas database. // // Maybe these mappings (both `sourcesOf` and `targets`) should be provided by the environ // object, so they can be accessed from the outside of `SourceSet` (e.g. if the entry point // is rather a db.) sourcesOf() { // resolution cannot be done in ctor, as environ is still being constructed then if ( ! this._sourcesof ) { this._sourcesof = []; this.environ.databases() .filter(db => db.sources === this.name) .forEach(db => this._sourcesof.push(db)); } return this._sourcesof; } targets() { // resolution cannot be done in ctor, as environ is still being constructed then if ( ! this._targets ) { this._targets = []; // resolve targets (dbs and srvs) if ( this.props.target ) { if ( ! this.environ ) { const msg = 'Source set has target(s) but no environ provided for resolving: '; throw new Error(msg + this.name); } this.props.target.value.forEach(t => { this._targets.push(this.environ.database(t) || this.environ.server(t)); }); } } return this._targets; } restTarget() { if ( this.type === 'rest-src' ) { let rests = this.targets().filter(t => { return t instanceof Server && t.type === 'rest'; }); if ( ! rests.length ) { rests = this.environ.servers().filter(s => s.type === 'rest'); } if ( rests.length > 1 ) { throw new Error('More than one REST servers for resolving the REST source set ' + this.name + ': ' + rests.map(s => s.id + '/' + s.name)); } if ( ! rests.length ) { throw new Error('No REST server for resolving the REST source set: ' + this.name); } return rests[0]; } } show(display) { // TODO: What about this.dflt...? display.source( this.name, this.props); } prop(name) { let v = this.props[name]; if ( ! v && this.dflt ) { v = this.dflt.props[name]; } if ( v ) { return v.value; } if ( name === 'garbage' ) { // .gitignore/, .idea/, #*, etc. return [ 'TODO: Set the default default garbage value...', '*~' ]; } } // TODO: Resolve the `db` here, to take targets into account? Or at least // take them into account where `db` is resolved (in LoadCommand...) // load(actions, db, srv, display) { let meta = { body: {} }; this.props.collection && this.props.collection.create(meta.body); // TODO: Not the same structure for the Client API than for the // Management API (permissions vs. permission, etc.) //this.props.permission && this.props.permission.create(meta.body); if ( this.props.permission ) { meta.body.permissions = []; this.props.permission.value.forEach(p => { let role = p['role-name'].value; let cap = p.capability.value; let perm = meta.body.permissions.find(p => p['role-name'] === role); if ( ! perm ) { perm = { "role-name": role, capabilities: [] }; meta.body.permissions.push(perm); } perm.capabilities.push(cap); }); } let matches = [ meta ]; matches.count = 0; matches.flush = function() { if ( this.count ) { actions.add( new act.MultiDocInsert(db, this)); // empty the array this.splice(0); this.push(meta); this.count = 0; } }; matches.add = function(item) { this.push(item); ++ this.count; if ( this.count >= INSERT_LENGTH ) { this.flush(); } }; if ( ! this.type || this.type === 'plain' ) { this.loadPlain(actions.ctxt, display, matches); } else if ( this.type === 'rest-src' ) { const port = (srv || this.restTarget()).props.port.value; this.loadRestSrc(actions, port, display, matches); } else if ( this.type === 'tde' ) { this.loadTde(actions, db, display); } else { throw new Error('Unknown source set type: ' + this.type); } } loadRestSrc(actions, port, display, matches) { const pf = actions.ctxt.platform; const dir = this.prop('dir'); // check there is nothing outside of `root/`, `services/` and `transforms/` const children = pf.dirChildren(dir); let count = 0; const filter = (name) => { let match = children.find(c => c.name === name); if ( ! match ) { // nothing } else if ( ! match.isdir ) { throw new Error('REST source child not a dir: ' + name); } else { ++ count; } }; filter('root'); filter('services'); filter('transforms'); if ( count !== children.length ) { let unknown = children.map(c => c.name).filter(n => { return n !== 'root' && n !== 'services' && n !== 'transforms'; }); throw new Error('Unknown children in REST source: ' + unknown); } // deploy `root/*` const root = dir + '/root'; if ( pf.exists(root) ) { this.loadPlain(actions.ctxt, display, matches, root); } else if ( display.verbose ) { display.check(0, 'dir, not exist', root); } // install `services/*` const services = dir + '/services'; if ( pf.exists(services) ) { this.walk(actions.ctxt, display, (path, uri) => { actions.add( this.installRestThing(port, 'resources', uri, path)); }, services); } else if ( display.verbose ) { display.check(0, 'dir, not exist', services); } // install `transforms/*` const transforms = dir + '/transforms'; if ( pf.exists(transforms) ) { this.walk(actions.ctxt, display, (path, uri) => { actions.add( this.installRestThing(port, 'transforms', uri, path)); }, transforms); } else if ( display.verbose ) { display.check(0, 'dir, not exist', transforms); } } basenameAndMime(path) { // extract mime type from extension const type = (ext) => { if ( ext === 'xqy' ) { return 'application/xquery'; } else if ( ext === 'sjs' ) { return 'application/javascript'; } else if ( ext === 'xml' ) { return 'application/xml'; } else if ( ext === 'json' ) { return 'application/json'; } else { console.log('Extension is neither xqy, sjs, xml or json: ' + ext); } }; // the basename and extension const slash = path.lastIndexOf('/'); const filename = slash > -1 ? path.slice(slash + 1) : path; const dot = filename.lastIndexOf('.'); if ( dot < 0 ) { throw new Error('No dot in filename to get the extension: ' + filename); } const basename = filename.slice(0, dot); const ext = filename.slice(dot + 1); // return the values return [ basename, type(ext) ]; } installRestThing(port, kind, uri, path) { const [ name, mime ] = this.basenameAndMime(uri); if ( mime ) { return new act.ServerRestDeploy(kind, name, path, mime, port); } } loadTde(actions, db, display) { const dir = this.prop('dir'); this.walk(actions.ctxt, display, (path, uri, meta) => { display.check(0, 'tde template', uri); const [ name, mime ] = this.basenameAndMime(path); if ( mime ) { actions.add(new act.TdeInstall(db, uri, path, mime)); } }, dir); } loadPlain(ctxt, display, matches, dir) { this.walk(ctxt, display, (path, uri, meta) => { if ( meta ) { // metadata, if any, must be before the doc content matches.push({ uri: uri, body: meta }); } matches.add({ uri: uri, path: path }); }, dir); matches.flush(); } walk(ctxt, display, onMatch, dir) { // from one array of strings, return two arrays: // - first one with all strings ending with '/', removed // - second one with all strings not ending with '/' const dirNotDir = pats => { let dir = []; let notdir = []; if ( pats ) { pats.forEach(p => {