1
0
Fork 0

Merge branch 'devel' of ssh://github.com/ArangoDB/ArangoDB into devel

This commit is contained in:
Max Neunhoeffer 2016-10-20 15:10:38 +02:00
commit 638ce07e6d
27 changed files with 747 additions and 288 deletions

View File

@ -135,11 +135,46 @@ string *text*. Positions start at 0.
is not contained in *text*, -1 is returned. is not contained in *text*, -1 is returned.
```js ```js
FIND_LAST("foobarbaz", "ba"), // 6 FIND_LAST("foobarbaz", "ba") // 6
FIND_LAST("foobarbaz", "ba", 7), // -1 FIND_LAST("foobarbaz", "ba", 7) // -1
FIND_LAST("foobarbaz", "ba", 0, 4) // 3 FIND_LAST("foobarbaz", "ba", 0, 4) // 3
``` ```
!SUBSECTION JSON_PARSE()
`JSON_PARSE(text) → value`
Return an AQL value described by the JSON-encoded input string.
- **text** (string): the string to parse as JSON
- returns **value** (mixed): the value corresponding to the given JSON text.
For input values that are no valid JSON strings, the function will return *null*.
```js
JSON_PARSE("123") // 123
JSON_PARSE("[ true, false, 2 ]") // [ true, false, 2 ]
JSON_PARSE("\\\"abc\\\"") // "abc"
JSON_PARSE("{\\\"a\\\": 1}") // { a : 1 }
JSON_PARSE("abc") // null
```
!SUBSECTION JSON_STRINGIFY()
`JSON_STRINGIFY(value) → text`
Return a JSON string representation of the input value.
- **value** (mixed): the value to convert to a JSON string
- returns **text** (string): the JSON string representing *value*.
For input values that cannot be converted to JSON, the function
will return *null*.
```js
JSON_STRINGIFY("1") // "1"
JSON_STRINGIFY("abc") // "\"abc\""
JSON_STRINGIFY("[1, 2, 3]") // "[1,2,3]"
```
!SUBSECTION LEFT() !SUBSECTION LEFT()
`LEFT(value, length) → substring` `LEFT(value, length) → substring`
@ -160,7 +195,6 @@ LEFT("foobar", 10) // "foobar"
`LENGTH(str) → length` `LENGTH(str) → length`
Determine the character length of a string. Determine the character length of a string.
- **str** (string): a string. If a number is passed, it will be casted to string first. - **str** (string): a string. If a number is passed, it will be casted to string first.

View File

@ -50,6 +50,10 @@
* [Sharding](ShardingInterface/README.md) * [Sharding](ShardingInterface/README.md)
* [Monitoring](AdministrationAndMonitoring/README.md) * [Monitoring](AdministrationAndMonitoring/README.md)
* [Endpoints](Endpoints/README.md) * [Endpoints](Endpoints/README.md)
# * [Foxx Services](Foxx/README.md)
# * [Management](Foxx/Management.md)
# * [Configuration](Foxx/Configuration.md)
# * [Dependencies](Foxx/Dependencies.md)
* [User Management](UserManagement/README.md) * [User Management](UserManagement/README.md)
* [Tasks](Tasks/README.md) * [Tasks](Tasks/README.md)
* [Agency](Agency/README.md) * [Agency](Agency/README.md)

View File

@ -0,0 +1 @@
!CHAPTER Configuration

View File

@ -0,0 +1 @@
!CHAPTER Dependencies

View File

@ -0,0 +1,19 @@
!CHAPTER Management
!SECTION List installed services
!SECTION Install a new service
!SECTION Upgrade an installed service
!SECTION Replace an installed service
!SECTION Uninstall a service
!SECTION Run a service's tests
!SECTION Run a service script
!SECTION Enable development mode
!SECTION Disable development mode

View File

@ -0,0 +1,3 @@
!CHAPTER HTTP Interface for Foxx Services
This chapter describes the REST interface for managing [Foxx services](../../Manual/Foxx/index.html).

View File

@ -55,9 +55,9 @@ On success an object with the following attributes is returned:
generating keys and supplying own key values in the *_key* attribute generating keys and supplying own key values in the *_key* attribute
of documents is considered an error. of documents is considered an error.
**Note**: some other collection properties, such as *type*, *isVolatile*, **Note**: except for *waitForSync*, *journalSize* and *name*, collection
*numberOfShards* or *shardKeys* cannot be changed once a collection is properties **cannot be changed** once a collection is created. To rename
created. a collection, the rename endpoint must be used.
@RESTRETURNCODES @RESTRETURNCODES

View File

@ -69,7 +69,7 @@ endmacro ()
# installs a readme file converting EOL ---------------------------------------- # installs a readme file converting EOL ----------------------------------------
macro (install_readme input output) macro (install_readme input output)
set(where "${CMAKE_INSTALL_FULL_DOCDIR}") set(where "${CMAKE_INSTALL_DOCDIR}")
if (MSVC) if (MSVC)
# the windows installer contains the readme in the top level directory: # the windows installer contains the readme in the top level directory:
set(where ".") set(where ".")

View File

@ -7,7 +7,7 @@
// / // /
// / DISCLAIMER // / DISCLAIMER
// / // /
// / Copyright 2014 ArangoDB GmbH, Cologne, Germany // / Copyright 2016 ArangoDB GmbH, Cologne, Germany
// / // /
// / Licensed under the Apache License, Version 2.0 (the "License") // / Licensed under the Apache License, Version 2.0 (the "License")
// / you may not use this file except in compliance with the License. // / you may not use this file except in compliance with the License.
@ -24,7 +24,7 @@
// / Copyright holder is ArangoDB GmbH, Cologne, Germany // / Copyright holder is ArangoDB GmbH, Cologne, Germany
// / // /
// / @author Dr. Frank Celler // / @author Dr. Frank Celler
// / @author Copyright 2014, ArangoDB GmbH, Cologne, Germany // / @author Copyright 2014-2016, ArangoDB GmbH, Cologne, Germany
// / @author Copyright 2012, triAGENS GmbH, Cologne, Germany // / @author Copyright 2012, triAGENS GmbH, Cologne, Germany
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
@ -34,7 +34,7 @@ var foxxManager = require('@arangodb/foxx/manager');
var easyPostCallback = actions.easyPostCallback; var easyPostCallback = actions.easyPostCallback;
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief sets up a Foxx application // / @brief sets up a Foxx service
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -52,7 +52,7 @@ actions.defineHttp({
}); });
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief tears down a Foxx application // / @brief tears down a Foxx service
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -70,7 +70,7 @@ actions.defineHttp({
}); });
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief installs a Foxx application // / @brief installs a Foxx service
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -83,13 +83,13 @@ actions.defineHttp({
var appInfo = body.appInfo; var appInfo = body.appInfo;
var mount = body.mount; var mount = body.mount;
var options = body.options; var options = body.options;
return foxxManager.install(appInfo, mount, options); return foxxManager.install(appInfo, mount, options).simpleJSON();
} }
}) })
}); });
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief uninstalls a Foxx application // / @brief uninstalls a Foxx service
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -102,13 +102,13 @@ actions.defineHttp({
var mount = body.mount; var mount = body.mount;
var options = body.options || {}; var options = body.options || {};
return foxxManager.uninstall(mount, options); return foxxManager.uninstall(mount, options).simpleJSON();
} }
}) })
}); });
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief replaces a Foxx application // / @brief replaces a Foxx service
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -122,13 +122,13 @@ actions.defineHttp({
var mount = body.mount; var mount = body.mount;
var options = body.options; var options = body.options;
return foxxManager.replace(appInfo, mount, options); return foxxManager.replace(appInfo, mount, options).simpleJSON();
} }
}) })
}); });
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief upgrades a Foxx application // / @brief upgrades a Foxx service
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -142,13 +142,13 @@ actions.defineHttp({
var mount = body.mount; var mount = body.mount;
var options = body.options; var options = body.options;
return foxxManager.upgrade(appInfo, mount, options); return foxxManager.upgrade(appInfo, mount, options).simpleJSON();
} }
}) })
}); });
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief configures a Foxx application // / @brief configures a Foxx service
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -163,14 +163,14 @@ actions.defineHttp({
if (options && options.configuration) { if (options && options.configuration) {
options = options.configuration; options = options.configuration;
} }
foxxManager.setConfiguration(mount, {configuration: options || {}});
return foxxManager.configure(mount, {configuration: options || {}}); return foxxManager.lookupService(mount).simpleJSON();
} }
}) })
}); });
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief Gets the configuration of a Foxx application // / @brief Gets the configuration of a Foxx service
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -188,7 +188,7 @@ actions.defineHttp({
}); });
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief configures a Foxx application's dependencies // / @brief configures a Foxx service's dependencies
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -201,13 +201,14 @@ actions.defineHttp({
var mount = body.mount; var mount = body.mount;
var options = body.options; var options = body.options;
return foxxManager.updateDeps(mount, {dependencies: options || {}}); foxxManager.setDependencies(mount, {dependencies: options || {}});
return foxxManager.lookupService(mount).simpleJSON();
} }
}) })
}); });
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief Gets the dependencies of a Foxx application // / @brief Gets the dependencies of a Foxx service
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -219,13 +220,24 @@ actions.defineHttp({
callback: function (body) { callback: function (body) {
var mount = body.mount; var mount = body.mount;
return foxxManager.dependencies(mount); const deps = foxxManager.dependencies(mount);
for (const key of Object.keys(deps)) {
const dep = deps[key];
deps[key] = {
definition: dep,
title: dep.title,
current: dep.current
};
delete dep.title;
delete dep.current;
}
return deps;
} }
}) })
}); });
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief Toggles the development mode of a foxx application // / @brief Toggles the development mode of a foxx service
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({ actions.defineHttp({
@ -238,9 +250,9 @@ actions.defineHttp({
var mount = body.mount; var mount = body.mount;
var activate = body.activate; var activate = body.activate;
if (activate) { if (activate) {
return foxxManager.development(mount); return foxxManager.development(mount).simpleJSON();
} else { } else {
return foxxManager.production(mount); return foxxManager.production(mount).simpleJSON();
} }
} }
}) })

View File

@ -125,7 +125,7 @@ installer.use(function (req, res, next) {
const configuration = FoxxManager.configuration(mount); const configuration = FoxxManager.configuration(mount);
res.json(Object.assign( res.json(Object.assign(
{error: false, configuration}, {error: false, configuration},
service service.simpleJSON()
)); ));
}); });
@ -186,7 +186,7 @@ foxxRouter.delete('/', function (req, res) {
}); });
res.json(Object.assign( res.json(Object.assign(
{error: false}, {error: false},
service service.simpleJSON()
)); ));
}) })
.queryParam('teardown', joi.boolean().default(true)) .queryParam('teardown', joi.boolean().default(true))
@ -238,7 +238,8 @@ foxxRouter.get('/config', function (req, res) {
foxxRouter.patch('/config', function (req, res) { foxxRouter.patch('/config', function (req, res) {
const mount = decodeURIComponent(req.queryParams.mount); const mount = decodeURIComponent(req.queryParams.mount);
const configuration = req.body; const configuration = req.body;
res.json(FoxxManager.configure(mount, {configuration})); FoxxManager.setConfiguration(mount, {configuration});
res.json(FoxxManager.configuration(mount));
}) })
.body(joi.object().optional(), 'Configuration to apply.') .body(joi.object().optional(), 'Configuration to apply.')
.summary('Set the configuration for a service') .summary('Set the configuration for a service')
@ -249,7 +250,18 @@ foxxRouter.patch('/config', function (req, res) {
foxxRouter.get('/deps', function (req, res) { foxxRouter.get('/deps', function (req, res) {
const mount = decodeURIComponent(req.queryParams.mount); const mount = decodeURIComponent(req.queryParams.mount);
res.json(FoxxManager.dependencies(mount)); const deps = FoxxManager.dependencies(mount);
for (const key of Object.keys(deps)) {
const dep = deps[key];
deps[key] = {
definition: dep,
title: dep.title,
current: dep.current
};
delete dep.title;
delete dep.current;
}
res.json(deps);
}) })
.summary('Get the dependencies for a service') .summary('Get the dependencies for a service')
.description(dd` .description(dd`
@ -260,9 +272,10 @@ foxxRouter.get('/deps', function (req, res) {
foxxRouter.patch('/deps', function (req, res) { foxxRouter.patch('/deps', function (req, res) {
const mount = decodeURIComponent(req.queryParams.mount); const mount = decodeURIComponent(req.queryParams.mount);
const dependencies = req.body; const dependencies = req.body;
res.json(FoxxManager.updateDeps(mount, {dependencies})); FoxxManager.setDependencies(mount, {dependencies});
res.json(FoxxManager.dependencies(mount));
}) })
.body(joi.object().optional(), 'Dependency settings to apply.') .body(joi.object().optional(), 'Dependency options to apply.')
.summary('Set the dependencies for a service') .summary('Set the dependencies for a service')
.description(dd` .description(dd`
Used to overwrite the dependencies options for services. Used to overwrite the dependencies options for services.
@ -300,7 +313,7 @@ foxxRouter.post('/scripts/:name', function (req, res) {
foxxRouter.patch('/devel', function (req, res) { foxxRouter.patch('/devel', function (req, res) {
const mount = decodeURIComponent(req.queryParams.mount); const mount = decodeURIComponent(req.queryParams.mount);
const activate = Boolean(req.body); const activate = Boolean(req.body);
res.json(FoxxManager[activate ? 'development' : 'production'](mount)); res.json(FoxxManager[activate ? 'development' : 'production'](mount).simpleJSON());
}) })
.body(joi.boolean().optional()) .body(joi.boolean().optional())
.summary('Activate/Deactivate development mode for a service') .summary('Activate/Deactivate development mode for a service')

View File

@ -5,7 +5,7 @@
<div class="navlogo"> <div class="navlogo">
<a class="logo big" href="#"><img id="ArangoDBLogo" class="arangodbLogo" src="img/arangodb-edition-optimized.svg"/></a> <a class="logo big" href="#"><img id="ArangoDBLogo" class="arangodbLogo" src="img/arangodb-edition-optimized.svg"/></a>
<a class="logo small" href="#"><img class="arangodbLogo" src="img/arangodb_logo_small.png"/></a> <a class="logo small" href="#"><img class="arangodbLogo" src="img/arangodb_logo_small.png"/></a>
<a class="version"><span>VERSION: </span><span id="currentVersion"></span></a> <a class="version"><span id="currentVersion"></span></a>
</div> </div>
<!-- <div id="progressPlaceholderIcon"></div> --> <!-- <div id="progressPlaceholderIcon"></div> -->
<div class="statmenu" id="statisticBar"> <div class="statmenu" id="statisticBar">

View File

@ -139,7 +139,7 @@
window.versionHelper.fromString(data.version); window.versionHelper.fromString(data.version);
$('.navbar #currentVersion').html( $('.navbar #currentVersion').html(
' ' + data.version.substr(0, 7) + '<i class="fa fa-exclamation-circle"></i>' data.version.substr(0, 7) + '<i class="fa fa-exclamation-circle"></i>'
); );
window.parseVersions = function (json) { window.parseVersions = function (json) {

View File

@ -455,24 +455,35 @@
}); });
_.each(obj.vertices, function (node) { _.each(obj.vertices, function (node) {
if (node !== null) {
vertices[node._id] = { vertices[node._id] = {
id: node._id, id: node._id,
label: node._key, label: node._key,
// size: 0.3, size: 0.3,
color: color, color: color,
x: Math.random(), x: Math.random(),
y: Math.random() y: Math.random()
}; };
}
}); });
} }
}); });
var nodeIds = [];
_.each(vertices, function (node) { _.each(vertices, function (node) {
returnObj.nodes.push(node); returnObj.nodes.push(node);
nodeIds.push(node.id);
}); });
_.each(edges, function (edge) { _.each(edges, function (edge) {
if (nodeIds.includes(edge.source) && nodeIds.includes(edge.target)) {
returnObj.edges.push(edge); returnObj.edges.push(edge);
}
/* how to handle not correct data?
else {
console.log('target to from is missing');
}
*/
}); });
} else if (type === 'array') { } else if (type === 'array') {
_.each(data, function (edge) { _.each(data, function (edge) {

View File

@ -257,7 +257,7 @@
position: fixed; position: fixed;
right: 0; right: 0;
text-align: center; text-align: center;
top: 120px; top: 135px;
width: 100%; width: 100%;
span { span {

View File

@ -3,7 +3,7 @@
"description": "ArangoDB Admin Web Interface", "description": "ArangoDB Admin Web Interface",
"author": "ArangoDB GmbH", "author": "ArangoDB GmbH",
"version": "3.0.0", "version": "3.0.0",
"license": "Apache License, Version 2.0", "license": "Apache-2.0",
"engines": { "engines": {
"arangodb": "^3.0.0-0 || ^3.0.0" "arangodb": "^3.0.0-0 || ^3.0.0"

View File

@ -0,0 +1,337 @@
'use strict';
const _ = require('lodash');
const dd = require('dedent');
const fs = require('fs');
const joi = require('joi');
const semver = require('semver');
const fm = require('@arangodb/foxx/manager');
const fmu = require('@arangodb/foxx/manager-utils');
const createRouter = require('@arangodb/foxx/router');
const reporters = Object.keys(require('@arangodb/mocha').reporters);
const schemas = require('./schemas');
const router = createRouter();
module.context.registerType('multipart/form-data', require('./multipart'));
module.context.use(router);
const serviceToJson = (service) => (
{
mount: service.mount,
path: service.basePath,
name: service.manifest.name,
version: service.manifest.version,
development: service.isDevelopment,
legacy: service.legacy,
manifest: service.manifest,
options: _.pick(service.options, ['configuration', 'dependencies'])
}
);
function isLegacy (service) {
const range = service.manifest.engines && service.manifest.engines.arangodb;
return range ? semver.gtr('3.0.0', range) : false;
}
function writeUploadToTempFile (buffer) {
const filename = fs.getTempFile('foxx-manager', true);
fs.writeFileSync(filename, buffer);
return filename;
}
router.get((req, res) => {
res.json(
fmu.getStorage().toArray()
.map((service) => (
{
mount: service.mount,
name: service.manifest.name,
version: service.manifest.version,
development: service.isDevelopment,
legacy: isLegacy(service)
}
))
);
})
.response(200, joi.array().items(schemas.shortInfo).required(), `Array of service descriptions.`)
.summary(`List installed services`)
.description(dd`
Fetches a list of services installed in the current database.
`);
router.post((req, res) => {
let source = req.body.source;
if (source instanceof Buffer) {
source = writeUploadToTempFile(source);
}
const service = fm.install(
source,
req.queryParams.mount,
_.omit(req.queryParams, ['mount'])
);
res.json(serviceToJson(service));
})
.body(schemas.service, ['multipart/form-data', 'application/json'], `Service to be installed.`)
.queryParam('mount', schemas.mount, `Mount path the service should be installed at.`)
.queryParam('setup', schemas.flag.default(true), `Run the service's setup script.`)
.queryParam('legacy', schemas.flag.default(false), `Service should be installed in 2.8 legacy compatibility mode.`)
.response(201, schemas.fullInfo, `Description of the installed service.`)
.summary(`Install new service`)
.description(dd`
Installs the given new service at the given mount path.
The service source can be specified as either an absolute local file path,
a fully qualified URL reachable from the database server,
or as a binary zip file using multipart form upload.
`);
const instanceRouter = createRouter();
instanceRouter.use((req, res, next) => {
const mount = req.queryParams.mount;
try {
req.service = fm.lookupService(mount);
} catch (e) {
res.throw(400, `No service installed at mount path "${mount}".`, e);
}
next();
})
.queryParam('mount', schemas.mount, `Mount path of the installed service.`);
router.use(instanceRouter);
const serviceRouter = createRouter();
instanceRouter.use('/service', serviceRouter);
serviceRouter.get((req, res) => {
res.json(serviceToJson(req.service));
})
.response(200, schemas.fullInfo, `Description of the service.`)
.summary(`Service description`)
.description(dd`
Fetches detailed information for the service at the given mount path.
`);
serviceRouter.patch((req, res) => {
let source = req.body.source;
if (source instanceof Buffer) {
source = writeUploadToTempFile(source);
}
const service = fm.upgrade(
source,
req.queryParams.mount,
_.omit(req.queryParams, ['mount'])
);
res.json(serviceToJson(service));
})
.body(schemas.service, ['multipart/form-data', 'application/json'], `Service to be installed.`)
.queryParam('teardown', schemas.flag.default(false), `Run the old service's teardown script.`)
.queryParam('setup', schemas.flag.default(true), `Run the new service's setup script.`)
.queryParam('legacy', schemas.flag.default(false), `Service should be installed in 2.8 legacy compatibility mode.`)
.response(200, schemas.fullInfo, `Description of the new service.`)
.summary(`Upgrade service`)
.description(dd`
Installs the given new service on top of the service currently installed at the given mount path.
This is only recommended for switching between different versions of the same service.
The service source can be specified as either an absolute local file path,
a fully qualified URL reachable from the database server,
or as a binary zip file using multipart form upload.
`);
serviceRouter.put((req, res) => {
let source = req.body.source;
if (source instanceof Buffer) {
source = writeUploadToTempFile(source);
}
const service = fm.replace(
source,
req.queryParams.mount,
_.omit(req.queryParams, ['mount'])
);
res.json(serviceToJson(service));
})
.body(schemas.service, ['multipart/form-data', 'application/json'], `Service to be installed.`)
.queryParam('teardown', schemas.flag.default(true), `Run the old service's teardown script.`)
.queryParam('setup', schemas.flag.default(true), `Run the new service's setup script.`)
.queryParam('legacy', schemas.flag.default(false), `Service should be installed in 2.8 legacy compatibility mode.`)
.response(200, schemas.fullInfo, `Description of the new service.`)
.summary(`Replace service`)
.description(dd`
Removes the service at the given mount path from the database and file system.
Then installs the given new service at the same mount path.
The service source can be specified as either an absolute local file path,
a fully qualified URL reachable from the database server,
or as a binary zip file using multipart form upload.
`);
serviceRouter.delete((req, res) => {
fm.uninstall(
req.queryParams.mount,
_.omit(req.queryParams, ['mount'])
);
res.status(204);
})
.queryParam('teardown', schemas.flag.default(true), `Run the service's teardown script.`)
.response(204, null, `Empty response.`)
.summary(`Uninstall service`)
.description(dd`
Removes the service at the given mount path from the database and file system.
`);
const configRouter = createRouter();
instanceRouter.use('/configuration', configRouter)
.response(200, schemas.configs, `Configuration options of the service.`);
configRouter.get((req, res) => {
res.json(fm.configuration(req.service.mount));
})
.summary(`Get configuration options`)
.description(dd`
Fetches the current configuration for the service at the given mount path.
`);
configRouter.patch((req, res) => {
const warnings = fm.setConfiguration(req.service.mount, {
configuration: req.body,
replace: false
});
const values = fm.configuration(req.service.mount, {simple: true});
res.json({values, warnings});
})
.body(joi.object().required(), `Object mapping configuration names to values.`)
.summary(`Update configuration options`)
.description(dd`
Replaces the given service's configuration.
Any omitted options will be ignored.
`);
configRouter.put((req, res) => {
const warnings = fm.setConfiguration(req.service.mount, {
configuration: req.body,
replace: true
});
const values = fm.configuration(req.service.mount, {simple: true});
res.json({values, warnings});
})
.body(joi.object().required(), `Object mapping configuration names to values.`)
.summary(`Replace configuration options`)
.description(dd`
Replaces the given service's configuration completely.
Any omitted options will be reset to their default values or marked as unconfigured.
`);
const depsRouter = createRouter();
instanceRouter.use('/dependencies', depsRouter)
.response(200, schemas.deps, `Dependency options of the service.`);
depsRouter.get((req, res) => {
res.json(fm.dependencies(req.service.mount));
})
.summary(`Get dependency options`)
.description(dd`
Fetches the current dependencies for service at the given mount path.
`);
depsRouter.patch((req, res) => {
const warnings = fm.setDependencies(req.service.mount, {
dependencies: req.body,
replace: true
});
const values = fm.dependencies(req.service.mount, {simple: true});
res.json({values, warnings});
})
.body(joi.object().required(), `Object mapping dependency aliases to mount paths.`)
.summary(`Update dependency options`)
.description(dd`
Replaces the given service's dependencies.
Any omitted dependencies will be ignored.
`);
depsRouter.put((req, res) => {
const warnings = fm.setDependencies(req.service.mount, {
dependencies: req.body,
replace: true
});
const values = fm.dependencies(req.service.mount, {simple: true});
res.json({values, warnings});
})
.body(joi.object().required(), `Object mapping dependency aliases to mount paths.`)
.summary(`Replace dependency options`)
.description(dd`
Replaces the given service's dependencies completely.
Any omitted dependencies will be disabled.
`);
const devRouter = createRouter();
instanceRouter.use('/development', devRouter)
.response(200, schemas.fullInfo, `Description of the service.`);
devRouter.post((req, res) => {
const service = fm.development(req.service);
res.json(serviceToJson(service));
})
.summary(`Enable development mode`)
.description(dd`
Puts the service into development mode.
The service will be re-installed from the filesystem for each request.
`);
devRouter.delete((req, res) => {
const service = fm.production(req.service);
res.json(serviceToJson(service));
})
.summary(`Disable development mode`)
.description(dd`
Puts the service at the given mount path into production mode.
Changes to the service's code will no longer be reflected automatically.
`);
instanceRouter.post('/run/:name', (req, res) => {
const service = req.service;
const scriptName = req.pathParams.name;
res.json(fm.runScript(scriptName, service.mount, req.body) || null);
})
.body(joi.any(), `Optional script arguments.`)
.pathParam('name', joi.string().required(), `Name of the script to run`)
.response(200, joi.any().default(null), `Script result if any.`)
.summary(`Run service script`)
.description(dd`
Runs the given script for the service at the given mount path.
Returns the exports of the script, if any.
`);
instanceRouter.post('/tests', (req, res) => {
const service = req.service;
const reporter = req.queryParams.reporter || null;
res.json(fm.runTests(service.mount, {reporter}));
})
.queryParam('reporter', joi.only(...reporters).optional(), `Test reporter to use`)
.response(200, joi.object(), `Test results.`)
.summary(`Run service tests`)
.description(dd`
Runs the tests for the service at the given mount path and returns the results.
`);
instanceRouter.get('/readme', (req, res) => {
const service = req.service;
res.send(fm.readme(service.mount));
})
.response(200, ['text/plain'], `Raw README contents.`)
.summary(`Service README`)
.description(dd`
Fetches the service's README or README.md file's contents if any.
`);

View File

@ -0,0 +1,25 @@
{
"name": "foxx-manager",
"description": "Foxx management service.",
"author": "ArangoDB GmbH",
"version": "3.1.0",
"license": "Apache-2.0",
"engines": {
"arangodb": "^3.1.0"
},
"repository": {
"type": "git",
"url": "https://github.com/arangodb/arangodb.git"
},
"contributors": [
{
"name": "Alan Plum",
"email": "me@pluma.io"
}
],
"main": "index.js"
}

View File

@ -0,0 +1,43 @@
'use strict';
const _ = require('lodash');
const assert = require('assert');
const contentDisposition = require('content-disposition');
module.exports = {
fromClient (body) {
assert(
Array.isArray(body) && body.every((part) => (
part && typeof part === 'object'
&& part.headers && typeof part.headers === 'object'
&& part.data instanceof Buffer
)),
`Expecting a multipart array, not ${body ? typeof body : String(body)}`
);
const parsedBody = {};
for (const part of body) {
const headers = {};
for (const key of Object.keys(part.headers)) {
headers[key.toLowerCase()] = part.headers[key];
}
const dispositionHeader = headers['content-disposition'];
if (!dispositionHeader) {
continue;
}
const disposition = contentDisposition.parse(dispositionHeader);
if (disposition.type !== 'form-data' || !disposition.parameters.name) {
continue;
}
const filename = disposition.parameters.filename;
const type = headers['content-type'];
const value = (type || filename) ? part.data : part.data.toString('utf-8');
if (filename) {
value.filename = filename;
}
if (type || filename) {
value.headers = _.omit(headers, ['content-disposition']);
}
parsedBody[disposition.parameters.name] = value;
}
return parsedBody;
}
};

View File

@ -0,0 +1,46 @@
'use strict';
const joi = require('joi');
const fmu = require('@arangodb/foxx/manager-utils');
exports.mount = joi.string().regex(/(?:\/[-_0-9a-z]+)+/i).required()
.description(`Mount path relative to the database root`);
exports.flag = joi.alternatives().try(
joi.boolean().description(`Boolean flag`),
joi.number().integer().min(0).max(1).description(`Numeric flag for PHP compatibility`)
).default(false);
exports.shortInfo = joi.object({
mount: exports.mount.description(`Mount path of the service`),
name: joi.string().optional().description(`Name of the service`),
version: joi.string().optional().description(`Version of the service`),
development: joi.boolean().default(false).description(`Whether development mode is enabled`),
legacy: joi.boolean().default(false).description(`Whether the service is running in legacy mode`)
}).required();
exports.fullInfo = exports.shortInfo.keys({
path: joi.string().required().description(`File system path of the service`),
manifest: joi.object().required().description(`Full manifest of the service`),
options: joi.object().required().description(`Configuration and dependency option values`)
});
exports.configs = joi.object().pattern(/.+/, joi.object({
value: joi.any().optional().description(`Current value of the configuration option`),
default: joi.any().optional().description(`Default value of the configuration option`),
type: joi.only(Object.keys(fmu.parameterTypes)).default('string').description(`Type of the configuration option`),
description: joi.string().optional().description(`Human-readable description of the configuration option`),
required: joi.boolean().default(true).description(`Whether the configuration option is required`)
}).required().description(`Configuration option`)).required();
exports.deps = joi.object().pattern(/.+/, joi.object({
name: joi.string().default('*').description(`Name of the dependency`),
version: joi.string().default('*').description(`Version of the dependency`),
required: joi.boolean().default(true).description(`Whether the dependency is required`)
}).required().description(`Dependency option`)).required();
exports.service = joi.object({
source: joi.alternatives(
joi.string().description(`Local file path or URL of the service to be installed`),
joi.object().type(Buffer).description(`Zip bundle of the service to be installed`)
).required().description(`Local file path, URL or zip bundle of the service to be installed`)
}).required();

View File

@ -3,7 +3,7 @@
"description": "ArangoDB Graph Module", "description": "ArangoDB Graph Module",
"author": "ArangoDB GmbH", "author": "ArangoDB GmbH",
"version": "3.0.0", "version": "3.0.0",
"license": "Apache License, Version 2.0", "license": "Apache-2.0",
"engines": { "engines": {
"arangodb": "^3.0.0-0 || ^3.0.0" "arangodb": "^3.0.0-0 || ^3.0.0"
@ -23,4 +23,3 @@
"main": "gharial.js" "main": "gharial.js"
} }

View File

@ -46,11 +46,13 @@ var pathRegex = /^((\.{0,2}(\/|\\))|(~\/)|[a-zA-Z]:\\)/;
const DEFAULT_REPLICATION_FACTOR_SYSTEM = internal.DEFAULT_REPLICATION_FACTOR_SYSTEM; const DEFAULT_REPLICATION_FACTOR_SYSTEM = internal.DEFAULT_REPLICATION_FACTOR_SYSTEM;
var getReadableName = function (name) { function getReadableName (name) {
return name.split(/([-_]|\s)+/).map(function (token) { return name.charAt(0).toUpperCase() + name.substr(1)
return token.slice(0, 1).toUpperCase() + token.slice(1); .replace(/([-_]|\s)+/g, ' ')
}).join(' '); .replace(/([a-z])([A-Z])/g, (m) => `${m[0]} ${m[1]}`)
}; .replace(/([A-Z])([A-Z][a-z])/g, (m) => `${m[0]} ${m[1]}`)
.replace(/\s([a-z])/g, (m) => ` ${m[0].toUpperCase()}`);
}
var getStorage = function () { var getStorage = function () {
var c = db._collection('_apps'); var c = db._collection('_apps');
@ -271,7 +273,7 @@ function mountedService (mount) {
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
function updateService (mount, update) { function updateService (mount, update) {
return getStorage().updateByExample({mount: mount}, update); return getStorage().replaceByExample({mount: mount}, update);
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////

View File

@ -47,7 +47,7 @@ function deleteFrom (obj) {
}; };
} }
var reporters = { exports.reporters = {
stream: StreamReporter, stream: StreamReporter,
suite: SuiteReporter, suite: SuiteReporter,
default: DefaultReporter default: DefaultReporter
@ -58,10 +58,10 @@ exports.run = function runMochaTests (run, files, reporterName) {
files = [files]; files = [files];
} }
if (reporterName && !reporters[reporterName]) { if (reporterName && !exports.reporters[reporterName]) {
throw new Error( throw new Error(
'Unknown test reporter: ' + reporterName 'Unknown test reporter: ' + reporterName
+ ' Known reporters: ' + Object.keys(reporters).join(', ') + ' Known reporters: ' + Object.keys(exports.reporters).join(', ')
); );
} }
@ -93,7 +93,7 @@ exports.run = function runMochaTests (run, files, reporterName) {
var _stdoutWrite = global.process.stdout.write; var _stdoutWrite = global.process.stdout.write;
global.process.stdout.write = function () {}; global.process.stdout.write = function () {};
var Reporter = reporterName ? reporters[reporterName] : reporters.default; var Reporter = reporterName ? exports.reporters[reporterName] : exports.reporters.default;
var reporter, runner; var reporter, runner;
try { try {

View File

@ -984,15 +984,15 @@ function foxxRouting (req, res, options, next) {
var mount = options.mount; var mount = options.mount;
try { try {
var app = foxxManager.lookupService(mount); var service = foxxManager.lookupService(mount);
var devel = app.isDevelopment; var devel = service.isDevelopment;
if (devel || !options.hasOwnProperty('routing')) { if (devel || !options.hasOwnProperty('routing')) {
delete options.error; delete options.error;
if (devel) { if (devel) {
foxxManager.rescanFoxx(mount); // TODO can move this to somewhere else? foxxManager.rescanFoxx(mount); // TODO can move this to somewhere else?
app = foxxManager.lookupService(mount); service = foxxManager.lookupService(mount);
} }
options.routing = flattenRoutingTree(buildRoutingTree([foxxManager.routes(mount)])); options.routing = flattenRoutingTree(buildRoutingTree([foxxManager.routes(mount)]));

View File

@ -327,45 +327,6 @@ ArangoCollection.prototype.removeByExample = function (example,
var i; var i;
var cluster = require('@arangodb/cluster'); var cluster = require('@arangodb/cluster');
if (cluster.isCoordinator()) {
var dbName = require('internal').db._name();
var shards = cluster.shardList(dbName, this.name());
var coord = { coordTransactionID: ArangoClusterComm.getId() };
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
if (limit > 0 && shards.length > 1) {
var err = new ArangoError();
err.errorNum = internal.errors.ERROR_CLUSTER_UNSUPPORTED.code;
err.errorMessage = 'limit is not supported in sharded collections';
throw err;
}
shards.forEach(function (shard) {
ArangoClusterComm.asyncRequest('put',
'shard:' + shard,
dbName,
'/_api/simple/remove-by-example',
JSON.stringify({
collection: shard,
example: example,
waitForSync: waitForSync,
limit: limit || undefined
}),
{ },
options);
});
var deleted = 0;
var results = cluster.wait(coord, shards.length);
for (i = 0; i < results.length; ++i) {
var body = JSON.parse(results[i].body);
deleted += (body.deleted || 0);
}
return deleted;
}
var query = buildExampleQuery(this, example, limit); var query = buildExampleQuery(this, example, limit);
var opts = { waitForSync: waitForSync }; var opts = { waitForSync: waitForSync };
query.query += ' REMOVE doc IN @@collection OPTIONS ' + JSON.stringify(opts); query.query += ' REMOVE doc IN @@collection OPTIONS ' + JSON.stringify(opts);
@ -412,48 +373,6 @@ ArangoCollection.prototype.replaceByExample = function (example,
limit = tmp_options.limit; limit = tmp_options.limit;
} }
var cluster = require('@arangodb/cluster');
if (cluster.isCoordinator()) {
var dbName = require('internal').db._name();
var shards = cluster.shardList(dbName, this.name());
var coord = { coordTransactionID: ArangoClusterComm.getId() };
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
if (limit > 0 && shards.length > 1) {
var err2 = new ArangoError();
err2.errorNum = internal.errors.ERROR_CLUSTER_UNSUPPORTED.code;
err2.errorMessage = 'limit is not supported in sharded collections';
throw err2;
}
shards.forEach(function (shard) {
ArangoClusterComm.asyncRequest('put',
'shard:' + shard,
dbName,
'/_api/simple/replace-by-example',
JSON.stringify({
collection: shard,
example: example,
newValue: newValue,
waitForSync: waitForSync,
limit: limit || undefined
}),
{ },
options);
});
var replaced = 0;
var results = cluster.wait(coord, shards.length), i;
for (i = 0; i < results.length; ++i) {
var body = JSON.parse(results[i].body);
replaced += (body.replaced || 0);
}
return replaced;
}
var query = buildExampleQuery(this, example, limit); var query = buildExampleQuery(this, example, limit);
var opts = { waitForSync: waitForSync }; var opts = { waitForSync: waitForSync };
query.query += ' REPLACE doc WITH @newValue IN @@collection OPTIONS ' + JSON.stringify(opts); query.query += ' REPLACE doc WITH @newValue IN @@collection OPTIONS ' + JSON.stringify(opts);
@ -510,49 +429,6 @@ ArangoCollection.prototype.updateByExample = function (example,
} }
} }
var cluster = require('@arangodb/cluster');
if (cluster.isCoordinator()) {
var dbName = require('internal').db._name();
var shards = cluster.shardList(dbName, this.name());
var coord = { coordTransactionID: ArangoClusterComm.getId() };
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
if (limit > 0 && shards.length > 1) {
var err2 = new ArangoError();
err2.errorNum = internal.errors.ERROR_CLUSTER_UNSUPPORTED.code;
err2.errorMessage = 'limit is not supported in sharded collections';
throw err2;
}
shards.forEach(function (shard) {
ArangoClusterComm.asyncRequest('put',
'shard:' + shard,
dbName,
'/_api/simple/update-by-example',
JSON.stringify({
collection: shard,
example: example,
newValue: newValue,
waitForSync: waitForSync,
keepNull: keepNull,
mergeObjects: mergeObjects,
limit: limit || undefined
}),
{ },
options);
});
var updated = 0;
var results = cluster.wait(coord, shards.length), i;
for (i = 0; i < results.length; ++i) {
var body = JSON.parse(results[i].body);
updated += (body.updated || 0);
}
return updated;
}
var query = buildExampleQuery(this, example, limit); var query = buildExampleQuery(this, example, limit);
var opts = { waitForSync: waitForSync, keepNull: keepNull, mergeObjects: mergeObjects }; var opts = { waitForSync: waitForSync, keepNull: keepNull, mergeObjects: mergeObjects };
query.query += ' UPDATE doc WITH @newValue IN @@collection OPTIONS ' + JSON.stringify(opts); query.query += ' UPDATE doc WITH @newValue IN @@collection OPTIONS ' + JSON.stringify(opts);

View File

@ -190,6 +190,7 @@ const manifestSchema = {
var serviceCache = {}; var serviceCache = {};
var usedSystemMountPoints = [ var usedSystemMountPoints = [
'/_admin/aardvark', // Admin interface. '/_admin/aardvark', // Admin interface.
'/_api/foxx', // Foxx management API.
'/_api/gharial' // General_Graph API. '/_api/gharial' // General_Graph API.
]; ];
@ -807,6 +808,10 @@ function patchManifestFile (servicePath, patchData) {
fs.write(filename, JSON.stringify(manifest, null, 2)); fs.write(filename, JSON.stringify(manifest, null, 2));
} }
function isLocalFile (path) {
return utils.pathRegex.test(path) && !fs.isDirectory(path);
}
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief Copies a service from local, either zip file or folder, to mount path // / @brief Copies a service from local, either zip file or folder, to mount path
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
@ -960,7 +965,7 @@ function scanFoxx (mount, options) {
initCache(); initCache();
var service = _scanFoxx(mount, options); var service = _scanFoxx(mount, options);
reloadRouting(); reloadRouting();
return service.simpleJSON(); return service;
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
@ -1000,9 +1005,6 @@ function _buildServiceInPath (serviceInfo, path, options) {
installServiceFromRemote(serviceInfo, path); installServiceFromRemote(serviceInfo, path);
} else if (utils.pathRegex.test(serviceInfo)) { } else if (utils.pathRegex.test(serviceInfo)) {
installServiceFromLocal(serviceInfo, path); installServiceFromLocal(serviceInfo, path);
} else if (/^uploads[\/\\]tmp-/.test(serviceInfo)) {
serviceInfo = joinPath(fs.getTempPath(), serviceInfo);
installServiceFromLocal(serviceInfo, path);
} else { } else {
if (!options || options.refresh !== false) { if (!options || options.refresh !== false) {
try { try {
@ -1121,8 +1123,11 @@ function install (serviceInfo, mount, options) {
[ [ 'Install information', 'string' ], [ [ 'Install information', 'string' ],
[ 'Mount path', 'string' ] ], [ 'Mount path', 'string' ] ],
[ serviceInfo, mount ]); [ serviceInfo, mount ]);
if (/^uploads[\/\\]tmp-/.test(serviceInfo)) {
serviceInfo = joinPath(fs.getTempPath(), serviceInfo);
}
utils.validateMount(mount); utils.validateMount(mount);
let hasToBeDistributed = /^uploads[\/\\]tmp-/.test(serviceInfo); let hasToBeDistributed = isLocalFile(serviceInfo);
var service = _install(serviceInfo, mount, options, true); var service = _install(serviceInfo, mount, options, true);
options = options || {}; options = options || {};
if (ArangoServerState.isCoordinator() && !options.__clusterDistribution) { if (ArangoServerState.isCoordinator() && !options.__clusterDistribution) {
@ -1169,7 +1174,7 @@ function install (serviceInfo, mount, options) {
} }
} }
reloadRouting(); reloadRouting();
return service.simpleJSON(); return service;
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
@ -1277,7 +1282,7 @@ function uninstall (mount, options) {
} }
var service = _uninstall(mount, options); var service = _uninstall(mount, options);
reloadRouting(); reloadRouting();
return service.simpleJSON(); return service;
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
@ -1292,10 +1297,13 @@ function replace (serviceInfo, mount, options) {
[ [ 'Install information', 'string' ], [ [ 'Install information', 'string' ],
[ 'Mount path', 'string' ] ], [ 'Mount path', 'string' ] ],
[ serviceInfo, mount ]); [ serviceInfo, mount ]);
if (/^uploads[\/\\]tmp-/.test(serviceInfo)) {
serviceInfo = joinPath(fs.getTempPath(), serviceInfo);
}
utils.validateMount(mount); utils.validateMount(mount);
_validateService(serviceInfo, mount); _validateService(serviceInfo, mount);
options = options || {}; options = options || {};
let hasToBeDistributed = /^uploads[\/\\]tmp-/.test(serviceInfo); let hasToBeDistributed = isLocalFile(serviceInfo);
if (ArangoServerState.isCoordinator() && !options.__clusterDistribution) { if (ArangoServerState.isCoordinator() && !options.__clusterDistribution) {
let name = ArangoServerState.id(); let name = ArangoServerState.id();
let coordinators = ArangoClusterInfo.getCoordinators().filter(function (c) { let coordinators = ArangoClusterInfo.getCoordinators().filter(function (c) {
@ -1345,7 +1353,7 @@ function replace (serviceInfo, mount, options) {
}); });
var service = _install(serviceInfo, mount, options, true); var service = _install(serviceInfo, mount, options, true);
reloadRouting(); reloadRouting();
return service.simpleJSON(); return service;
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
@ -1360,10 +1368,13 @@ function upgrade (serviceInfo, mount, options) {
[ [ 'Install information', 'string' ], [ [ 'Install information', 'string' ],
[ 'Mount path', 'string' ] ], [ 'Mount path', 'string' ] ],
[ serviceInfo, mount ]); [ serviceInfo, mount ]);
if (/^uploads[\/\\]tmp-/.test(serviceInfo)) {
serviceInfo = joinPath(fs.getTempPath(), serviceInfo);
}
utils.validateMount(mount); utils.validateMount(mount);
_validateService(serviceInfo, mount); _validateService(serviceInfo, mount);
options = options || {}; options = options || {};
let hasToBeDistributed = /^uploads[\/\\]tmp-/.test(serviceInfo); let hasToBeDistributed = isLocalFile(serviceInfo);
if (ArangoServerState.isCoordinator() && !options.__clusterDistribution) { if (ArangoServerState.isCoordinator() && !options.__clusterDistribution) {
let name = ArangoServerState.id(); let name = ArangoServerState.id();
let coordinators = ArangoClusterInfo.getCoordinators().filter(function (c) { let coordinators = ArangoClusterInfo.getCoordinators().filter(function (c) {
@ -1428,7 +1439,7 @@ function upgrade (serviceInfo, mount, options) {
}); });
var service = _install(serviceInfo, mount, options, true); var service = _install(serviceInfo, mount, options, true);
reloadRouting(); reloadRouting();
return service.simpleJSON(); return service;
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
@ -1480,7 +1491,7 @@ function setDevelopment (mount) {
[ [ 'Mount path', 'string' ] ], [ [ 'Mount path', 'string' ] ],
[ mount ]); [ mount ]);
var service = _toggleDevelopment(mount, true); var service = _toggleDevelopment(mount, true);
return service.simpleJSON(); return service;
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
@ -1493,77 +1504,69 @@ function setProduction (mount) {
[ [ 'Mount path', 'string' ] ], [ [ 'Mount path', 'string' ] ],
[ mount ]); [ mount ]);
var service = _toggleDevelopment(mount, false); var service = _toggleDevelopment(mount, false);
return service.simpleJSON(); return service;
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief Configure the service at the mountpoint // / @brief Configure the service at the mountpoint
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
function configure (mount, options) { function setConfiguration (mount, options) {
checkParameter( checkParameter(
'configure(<mount>)', 'setConfiguration(<mount>)',
[ [ 'Mount path', 'string' ] ], [ [ 'Mount path', 'string' ] ],
[ mount ]); [ mount ]);
utils.validateMount(mount, true); utils.validateMount(mount, true);
var service = lookupService(mount); var service = lookupService(mount);
var invalid = service.applyConfiguration(options.configuration || {}); var warnings = service.applyConfiguration(options.configuration, options.replace);
if (invalid.length > 0) {
// TODO Error handling
console.log(invalid);
}
utils.updateService(mount, service.toJSON()); utils.updateService(mount, service.toJSON());
reloadRouting(); reloadRouting();
return service.simpleJSON(); return warnings;
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief Set up dependencies of the service at the mountpoint // / @brief Set up dependencies of the service at the mountpoint
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
function updateDeps (mount, options) { function setDependencies (mount, options) {
checkParameter( checkParameter(
'updateDeps(<mount>)', 'setDependencies(<mount>)',
[ [ 'Mount path', 'string' ] ], [ [ 'Mount path', 'string' ] ],
[ mount ]); [ mount ]);
utils.validateMount(mount, true); utils.validateMount(mount, true);
var service = lookupService(mount); var service = lookupService(mount);
var invalid = service.applyDependencies(options.dependencies || {}); var warnings = service.applyDependencies(options.dependencies, options.replace);
if (invalid.length > 0) {
// TODO Error handling
console.log(invalid);
}
utils.updateService(mount, service.toJSON()); utils.updateService(mount, service.toJSON());
reloadRouting(); reloadRouting();
return service.simpleJSON(); return warnings;
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief Get the configuration for the service at the given mountpoint // / @brief Get the configuration for the service at the given mountpoint
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
function configuration (mount) { function configuration (mount, options) {
checkParameter( checkParameter(
'configuration(<mount>)', 'configuration(<mount>)',
[ [ 'Mount path', 'string' ] ], [ [ 'Mount path', 'string' ] ],
[ mount ]); [ mount ]);
utils.validateMount(mount, true); utils.validateMount(mount, true);
var service = lookupService(mount); var service = lookupService(mount);
return service.getConfiguration(); return service.getConfiguration(options && options.simple);
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
// / @brief Get the dependencies for the service at the given mountpoint // / @brief Get the dependencies for the service at the given mountpoint
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
function dependencies (mount) { function dependencies (mount, options) {
checkParameter( checkParameter(
'dependencies(<mount>)', 'dependencies(<mount>)',
[ [ 'Mount path', 'string' ] ], [ [ 'Mount path', 'string' ] ],
[ mount ]); [ mount ]);
utils.validateMount(mount, true); utils.validateMount(mount, true);
var service = lookupService(mount); var service = lookupService(mount);
return service.getDependencies(); return service.getDependencies(options && options.simple);
} }
// ////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////
@ -1621,8 +1624,8 @@ exports.replace = replace;
exports.upgrade = upgrade; exports.upgrade = upgrade;
exports.development = setDevelopment; exports.development = setDevelopment;
exports.production = setProduction; exports.production = setProduction;
exports.configure = configure; exports.setConfiguration = setConfiguration;
exports.updateDeps = updateDeps; exports.setDependencies = setDependencies;
exports.configuration = configuration; exports.configuration = configuration;
exports.dependencies = dependencies; exports.dependencies = dependencies;
exports.requireService = requireService; exports.requireService = requireService;

View File

@ -90,12 +90,11 @@ module.exports =
this.configuration = createConfiguration(this.manifest.configuration); this.configuration = createConfiguration(this.manifest.configuration);
this.dependencies = createDependencies(this.manifest.dependencies, this.options.dependencies); this.dependencies = createDependencies(this.manifest.dependencies, this.options.dependencies);
const warnings = this.applyConfiguration(this.options.configuration); const warnings = this.applyConfiguration(this.options.configuration, false);
if (warnings.length) { if (warnings) {
console.warnLines(dd` console.warnLines(`Stored configuration for service "${data.mount}" has errors:\n ${
Stored configuration for app "${data.mount}" has errors: Object.keys(warnings).map((key) => warnings[key]).join('\n ')
${warnings.join('\n ')} }`);
`);
} }
this.thumbnail = null; this.thumbnail = null;
@ -118,22 +117,32 @@ module.exports =
this._reset(); this._reset();
} }
applyConfiguration (config) { applyConfiguration (config, replace) {
const definitions = this.manifest.configuration; if (!config) {
const warnings = []; config = {};
for (const name of Object.keys(config)) {
const rawValue = config[name];
const def = definitions[name];
if (!def) {
warnings.push(`Unexpected option "${name}"`);
return warnings;
} }
const definitions = this.manifest.configuration;
const configNames = Object.keys(config);
const knownNames = Object.keys(definitions);
const omittedNames = knownNames.filter((name) => !configNames.includes(name));
const names = replace ? configNames.concat(omittedNames) : configNames;
const warnings = {};
if (def.required === false && (rawValue === undefined || rawValue === null || rawValue === '')) { for (const name of names) {
delete this.options.configuration[name]; if (!knownNames.includes(name)) {
warnings[name] = 'is not allowed';
continue;
}
const def = definitions[name];
const rawValue = config[name];
if (rawValue === undefined || rawValue === null || rawValue === '') {
this.options.configuration[name] = undefined;
this.configuration[name] = def.default; this.configuration[name] = def.default;
return warnings; if (def.required !== false) {
warnings[name] = 'is required';
}
continue;
} }
const validate = parameterTypes[def.type]; const validate = parameterTypes[def.type];
@ -143,7 +152,7 @@ module.exports =
if (validate.isJoi) { if (validate.isJoi) {
const result = validate.required().validate(rawValue); const result = validate.required().validate(rawValue);
if (result.error) { if (result.error) {
warning = result.error.message.replace(/^"value"/, `"${name}"`); warning = result.error.message.replace(/^"value"\s+/, '');
} else { } else {
parsedValue = result.value; parsedValue = result.value;
} }
@ -151,19 +160,56 @@ module.exports =
try { try {
parsedValue = validate(rawValue); parsedValue = validate(rawValue);
} catch (e) { } catch (e) {
warning = `"${name}": ${e.message}`; warning = e.message;
} }
} }
if (warning) { if (warning) {
warnings.push(warning); warnings[name] = warning;
} else { } else {
this.options.configuration[name] = rawValue; this.options.configuration[name] = rawValue;
this.configuration[name] = parsedValue; this.configuration[name] = parsedValue;
} }
} }
return warnings; return Object.keys(warnings).length ? warnings : undefined;
}
applyDependencies (deps, replace) {
if (!deps) {
deps = {};
}
const definitions = this.manifest.dependencies;
const depsNames = Object.keys(deps);
const knownNames = Object.keys(definitions);
const omittedNames = knownNames.filter((name) => !depsNames.includes(name));
const names = replace ? depsNames.concat(omittedNames) : depsNames;
const warnings = {};
for (const name of names) {
if (!knownNames.includes(name)) {
warnings[name] = 'is not allowed';
continue;
}
const def = definitions[name];
const value = deps[name];
if (value === undefined || value === null || value === '') {
this.options.dependencies[name] = undefined;
if (def.required !== false) {
warnings[name] = 'is required';
}
continue;
}
if (typeof value !== 'string') {
warnings[name] = 'must be a string';
} else {
this.options.dependencies[name] = value;
}
}
return Object.keys(warnings).length ? warnings : undefined;
} }
buildRoutes () { buildRoutes () {
@ -270,22 +316,6 @@ module.exports =
}); });
} }
applyDependencies (deps) {
const definitions = this.manifest.dependencies;
const warnings = [];
for (const name of Object.keys(deps)) {
const dfn = definitions[name];
if (!dfn) {
warnings.push(`Unexpected dependency "${name}"`);
} else {
this.options.dependencies[name] = deps[name];
}
}
return warnings;
}
_PRINT (context) { _PRINT (context) {
context.output += `[FoxxService at "${this.mount}"]`; context.output += `[FoxxService at "${this.mount}"]`;
} }
@ -354,11 +384,11 @@ module.exports =
const options = this.options.dependencies; const options = this.options.dependencies;
for (const name of Object.keys(definitions)) { for (const name of Object.keys(definitions)) {
const dfn = definitions[name]; const dfn = definitions[name];
deps[name] = simple ? options[name] : { const value = options[name];
definition: dfn, deps[name] = simple ? value : Object.assign({}, dfn, {
title: getReadableName(name), title: getReadableName(name),
current: options[name] current: value
}; });
} }
return deps; return deps;
} }
@ -369,7 +399,7 @@ module.exports =
return _.some(config, function (cfg) { return _.some(config, function (cfg) {
return cfg.current === undefined && cfg.required !== false; return cfg.current === undefined && cfg.required !== false;
}) || _.some(deps, function (dep) { }) || _.some(deps, function (dep) {
return dep.current === undefined && dep.definition.required !== false; return dep.current === undefined && dep.required !== false;
}); });
} }

View File

@ -24,23 +24,23 @@ describe('Foxx Manager', function () {
var deps1 = {hello: {name: 'world', required: true, version: '*'}}; var deps1 = {hello: {name: 'world', required: true, version: '*'}};
var deps2 = {clobbered: {name: 'completely', required: true, version: '*'}}; var deps2 = {clobbered: {name: 'completely', required: true, version: '*'}};
var app = FoxxManager.lookupService(mount); var service = FoxxManager.lookupService(mount);
expect(app.manifest.dependencies).to.eql({}); expect(service.manifest.dependencies).to.eql({});
var filename = app.main.context.fileName('manifest.json'); var filename = service.main.context.fileName('manifest.json');
var rawJson = fs.readFileSync(filename, 'utf-8'); var rawJson = fs.readFileSync(filename, 'utf-8');
var json = JSON.parse(rawJson); var json = JSON.parse(rawJson);
json.dependencies = deps1; json.dependencies = deps1;
fs.writeFileSync(filename, JSON.stringify(json)); fs.writeFileSync(filename, JSON.stringify(json));
FoxxManager.scanFoxx(mount, {replace: true}); FoxxManager.scanFoxx(mount, {replace: true});
app = FoxxManager.lookupService(mount); service = FoxxManager.lookupService(mount);
expect(app.manifest.dependencies).to.eql(deps1); expect(service.manifest.dependencies).to.eql(deps1);
json.dependencies = deps2; json.dependencies = deps2;
fs.writeFileSync(filename, JSON.stringify(json)); fs.writeFileSync(filename, JSON.stringify(json));
FoxxManager.scanFoxx(mount, {replace: true}); FoxxManager.scanFoxx(mount, {replace: true});
app = FoxxManager.lookupService(mount); service = FoxxManager.lookupService(mount);
expect(app.manifest.dependencies).to.eql(deps2); expect(service.manifest.dependencies).to.eql(deps2);
}); });
}); });
}); });