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.
```js
FIND_LAST("foobarbaz", "ba"), // 6
FIND_LAST("foobarbaz", "ba", 7), // -1
FIND_LAST("foobarbaz", "ba") // 6
FIND_LAST("foobarbaz", "ba", 7) // -1
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()
`LEFT(value, length) → substring`
@ -160,7 +195,6 @@ LEFT("foobar", 10) // "foobar"
`LENGTH(str) → length`
Determine the character length of a string.
- **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)
* [Monitoring](AdministrationAndMonitoring/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)
* [Tasks](Tasks/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
of documents is considered an error.
**Note**: some other collection properties, such as *type*, *isVolatile*,
*numberOfShards* or *shardKeys* cannot be changed once a collection is
created.
**Note**: except for *waitForSync*, *journalSize* and *name*, collection
properties **cannot be changed** once a collection is created. To rename
a collection, the rename endpoint must be used.
@RESTRETURNCODES

View File

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

View File

@ -7,7 +7,7 @@
// /
// / DISCLAIMER
// /
// / Copyright 2014 ArangoDB GmbH, Cologne, Germany
// / Copyright 2016 ArangoDB GmbH, Cologne, Germany
// /
// / Licensed under the Apache License, Version 2.0 (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
// /
// / @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
// //////////////////////////////////////////////////////////////////////////////
@ -34,7 +34,7 @@ var foxxManager = require('@arangodb/foxx/manager');
var easyPostCallback = actions.easyPostCallback;
// //////////////////////////////////////////////////////////////////////////////
// / @brief sets up a Foxx application
// / @brief sets up a Foxx service
// //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({
@ -52,7 +52,7 @@ actions.defineHttp({
});
// //////////////////////////////////////////////////////////////////////////////
// / @brief tears down a Foxx application
// / @brief tears down a Foxx service
// //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({
@ -70,7 +70,7 @@ actions.defineHttp({
});
// //////////////////////////////////////////////////////////////////////////////
// / @brief installs a Foxx application
// / @brief installs a Foxx service
// //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({
@ -83,13 +83,13 @@ actions.defineHttp({
var appInfo = body.appInfo;
var mount = body.mount;
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({
@ -102,13 +102,13 @@ actions.defineHttp({
var mount = body.mount;
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({
@ -122,13 +122,13 @@ actions.defineHttp({
var mount = body.mount;
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({
@ -142,13 +142,13 @@ actions.defineHttp({
var mount = body.mount;
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({
@ -163,14 +163,14 @@ actions.defineHttp({
if (options && options.configuration) {
options = options.configuration;
}
return foxxManager.configure(mount, {configuration: options || {}});
foxxManager.setConfiguration(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({
@ -188,7 +188,7 @@ actions.defineHttp({
});
// //////////////////////////////////////////////////////////////////////////////
// / @brief configures a Foxx application's dependencies
// / @brief configures a Foxx service's dependencies
// //////////////////////////////////////////////////////////////////////////////
actions.defineHttp({
@ -201,13 +201,14 @@ actions.defineHttp({
var mount = body.mount;
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({
@ -219,13 +220,24 @@ actions.defineHttp({
callback: function (body) {
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({
@ -238,9 +250,9 @@ actions.defineHttp({
var mount = body.mount;
var activate = body.activate;
if (activate) {
return foxxManager.development(mount);
return foxxManager.development(mount).simpleJSON();
} 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);
res.json(Object.assign(
{error: false, configuration},
service
service.simpleJSON()
));
});
@ -186,7 +186,7 @@ foxxRouter.delete('/', function (req, res) {
});
res.json(Object.assign(
{error: false},
service
service.simpleJSON()
));
})
.queryParam('teardown', joi.boolean().default(true))
@ -238,7 +238,8 @@ foxxRouter.get('/config', function (req, res) {
foxxRouter.patch('/config', function (req, res) {
const mount = decodeURIComponent(req.queryParams.mount);
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.')
.summary('Set the configuration for a service')
@ -249,7 +250,18 @@ foxxRouter.patch('/config', function (req, res) {
foxxRouter.get('/deps', function (req, res) {
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')
.description(dd`
@ -260,9 +272,10 @@ foxxRouter.get('/deps', function (req, res) {
foxxRouter.patch('/deps', function (req, res) {
const mount = decodeURIComponent(req.queryParams.mount);
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')
.description(dd`
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) {
const mount = decodeURIComponent(req.queryParams.mount);
const activate = Boolean(req.body);
res.json(FoxxManager[activate ? 'development' : 'production'](mount));
res.json(FoxxManager[activate ? 'development' : 'production'](mount).simpleJSON());
})
.body(joi.boolean().optional())
.summary('Activate/Deactivate development mode for a service')

View File

@ -5,7 +5,7 @@
<div class="navlogo">
<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="version"><span>VERSION: </span><span id="currentVersion"></span></a>
<a class="version"><span id="currentVersion"></span></a>
</div>
<!-- <div id="progressPlaceholderIcon"></div> -->
<div class="statmenu" id="statisticBar">

View File

@ -139,7 +139,7 @@
window.versionHelper.fromString(data.version);
$('.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) {

View File

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

View File

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

View File

@ -3,7 +3,7 @@
"description": "ArangoDB Admin Web Interface",
"author": "ArangoDB GmbH",
"version": "3.0.0",
"license": "Apache License, Version 2.0",
"license": "Apache-2.0",
"engines": {
"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",
"author": "ArangoDB GmbH",
"version": "3.0.0",
"license": "Apache License, Version 2.0",
"license": "Apache-2.0",
"engines": {
"arangodb": "^3.0.0-0 || ^3.0.0"
@ -23,4 +23,3 @@
"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;
var getReadableName = function (name) {
return name.split(/([-_]|\s)+/).map(function (token) {
return token.slice(0, 1).toUpperCase() + token.slice(1);
}).join(' ');
};
function getReadableName (name) {
return name.charAt(0).toUpperCase() + name.substr(1)
.replace(/([-_]|\s)+/g, ' ')
.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 c = db._collection('_apps');
@ -271,7 +273,7 @@ function mountedService (mount) {
// //////////////////////////////////////////////////////////////////////////////
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,
suite: SuiteReporter,
default: DefaultReporter
@ -58,10 +58,10 @@ exports.run = function runMochaTests (run, files, reporterName) {
files = [files];
}
if (reporterName && !reporters[reporterName]) {
if (reporterName && !exports.reporters[reporterName]) {
throw new Error(
'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;
global.process.stdout.write = function () {};
var Reporter = reporterName ? reporters[reporterName] : reporters.default;
var Reporter = reporterName ? exports.reporters[reporterName] : exports.reporters.default;
var reporter, runner;
try {

View File

@ -984,15 +984,15 @@ function foxxRouting (req, res, options, next) {
var mount = options.mount;
try {
var app = foxxManager.lookupService(mount);
var devel = app.isDevelopment;
var service = foxxManager.lookupService(mount);
var devel = service.isDevelopment;
if (devel || !options.hasOwnProperty('routing')) {
delete options.error;
if (devel) {
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)]));

View File

@ -327,45 +327,6 @@ ArangoCollection.prototype.removeByExample = function (example,
var i;
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 opts = { waitForSync: waitForSync };
query.query += ' REMOVE doc IN @@collection OPTIONS ' + JSON.stringify(opts);
@ -412,48 +373,6 @@ ArangoCollection.prototype.replaceByExample = function (example,
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 opts = { waitForSync: waitForSync };
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 opts = { waitForSync: waitForSync, keepNull: keepNull, mergeObjects: mergeObjects };
query.query += ' UPDATE doc WITH @newValue IN @@collection OPTIONS ' + JSON.stringify(opts);

View File

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

View File

@ -90,12 +90,11 @@ module.exports =
this.configuration = createConfiguration(this.manifest.configuration);
this.dependencies = createDependencies(this.manifest.dependencies, this.options.dependencies);
const warnings = this.applyConfiguration(this.options.configuration);
if (warnings.length) {
console.warnLines(dd`
Stored configuration for app "${data.mount}" has errors:
${warnings.join('\n ')}
`);
const warnings = this.applyConfiguration(this.options.configuration, false);
if (warnings) {
console.warnLines(`Stored configuration for service "${data.mount}" has errors:\n ${
Object.keys(warnings).map((key) => warnings[key]).join('\n ')
}`);
}
this.thumbnail = null;
@ -110,30 +109,40 @@ module.exports =
this.legacy = range ? semver.gtr('3.0.0', range) : false;
if (this.legacy) {
console.debugLines(dd`
Service "${this.mount}" is running in legacy compatibility mode.
Requested version "${range}" is lower than "3.0.0".
`);
Service "${this.mount}" is running in legacy compatibility mode.
Requested version "${range}" is lower than "3.0.0".
`);
}
this._reset();
}
applyConfiguration (config) {
applyConfiguration (config, replace) {
if (!config) {
config = {};
}
const definitions = this.manifest.configuration;
const warnings = [];
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 = {};
for (const name of Object.keys(config)) {
const rawValue = config[name];
const def = definitions[name];
if (!def) {
warnings.push(`Unexpected option "${name}"`);
return warnings;
for (const name of names) {
if (!knownNames.includes(name)) {
warnings[name] = 'is not allowed';
continue;
}
const def = definitions[name];
const rawValue = config[name];
if (def.required === false && (rawValue === undefined || rawValue === null || rawValue === '')) {
delete this.options.configuration[name];
if (rawValue === undefined || rawValue === null || rawValue === '') {
this.options.configuration[name] = undefined;
this.configuration[name] = def.default;
return warnings;
if (def.required !== false) {
warnings[name] = 'is required';
}
continue;
}
const validate = parameterTypes[def.type];
@ -143,7 +152,7 @@ module.exports =
if (validate.isJoi) {
const result = validate.required().validate(rawValue);
if (result.error) {
warning = result.error.message.replace(/^"value"/, `"${name}"`);
warning = result.error.message.replace(/^"value"\s+/, '');
} else {
parsedValue = result.value;
}
@ -151,19 +160,56 @@ module.exports =
try {
parsedValue = validate(rawValue);
} catch (e) {
warning = `"${name}": ${e.message}`;
warning = e.message;
}
}
if (warning) {
warnings.push(warning);
warnings[name] = warning;
} else {
this.options.configuration[name] = rawValue;
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 () {
@ -180,10 +226,10 @@ module.exports =
err = err.cause;
}
console.warnLines(dd`
Failed to build API documentation for "${this.mount}"!
This is likely a bug in your Foxx service.
Check the route methods you are using to document your API.
`);
Failed to build API documentation for "${this.mount}"!
This is likely a bug in your Foxx service.
Check the route methods you are using to document your API.
`);
}
this.docs = {
swagger: '2.0',
@ -220,14 +266,14 @@ module.exports =
}
if (logLevel) {
console[logLevel](`Service "${
service.mount
}" encountered error ${
e.statusCode || 500
} while handling ${
req.requestType
} ${
req.absoluteUrl()
}`);
service.mount
}" encountered error ${
e.statusCode || 500
} while handling ${
req.requestType
} ${
req.absoluteUrl()
}`);
console[`${logLevel}Lines`](e.stack);
let err = e.cause;
while (err && err.stack) {
@ -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) {
context.output += `[FoxxService at "${this.mount}"]`;
}
@ -354,11 +384,11 @@ module.exports =
const options = this.options.dependencies;
for (const name of Object.keys(definitions)) {
const dfn = definitions[name];
deps[name] = simple ? options[name] : {
definition: dfn,
const value = options[name];
deps[name] = simple ? value : Object.assign({}, dfn, {
title: getReadableName(name),
current: options[name]
};
current: value
});
}
return deps;
}
@ -369,7 +399,7 @@ module.exports =
return _.some(config, function (cfg) {
return cfg.current === undefined && cfg.required !== false;
}) || _.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 deps2 = {clobbered: {name: 'completely', required: true, version: '*'}};
var app = FoxxManager.lookupService(mount);
expect(app.manifest.dependencies).to.eql({});
var filename = app.main.context.fileName('manifest.json');
var service = FoxxManager.lookupService(mount);
expect(service.manifest.dependencies).to.eql({});
var filename = service.main.context.fileName('manifest.json');
var rawJson = fs.readFileSync(filename, 'utf-8');
var json = JSON.parse(rawJson);
json.dependencies = deps1;
fs.writeFileSync(filename, JSON.stringify(json));
FoxxManager.scanFoxx(mount, {replace: true});
app = FoxxManager.lookupService(mount);
expect(app.manifest.dependencies).to.eql(deps1);
service = FoxxManager.lookupService(mount);
expect(service.manifest.dependencies).to.eql(deps1);
json.dependencies = deps2;
fs.writeFileSync(filename, JSON.stringify(json));
FoxxManager.scanFoxx(mount, {replace: true});
app = FoxxManager.lookupService(mount);
expect(app.manifest.dependencies).to.eql(deps2);
service = FoxxManager.lookupService(mount);
expect(service.manifest.dependencies).to.eql(deps2);
});
});
});