Linking services together
When using multiple services (or multiple copies of the same service) in thesame database, sometimes you may want to share collections or methods betweenthose services. Typical examples are:
- collections or APIs for managing shared data(e.g. application users or session data)
- common middleware that requires someconfiguration that would be identicalfor multiple services
- reusable routers that provide the same APIfor different services
For scenarios like these, Foxx provides a way to link services together andallow them to export JS APIs other services can use.In Foxx these JS APIs are called dependencies,the services implementing them are called providers,the services using them are called consumers.
This chapter is about Foxx dependencies as described above. In JavaScript theterm dependencies can also refer tobundled node modules, which are an unrelated concept.
Declaring dependencies
Foxx dependencies can be declared in theservice manifestusing the provides
and dependencies
fields:
provides
lists the dependencies a given service provides,i.e. which APIs it claims to be compatible withdependencies
lists the dependencies a given service consumes,i.e. which APIs its dependencies need to be compatible with
Explicitly naming your dependencies helps improving tooling support formanaging service dependencies in ArangoDB but is not strictly necessary.It is possible to omit the provides
field even if your service provides aJS API and the dependencies
field can be used without explicitly specifyingdependency names.
A dependency name should be an alphanumeric identifier, optionally using anamespace prefix (i.e. dependency-name
or @namespace/dependency-name
).For example, services maintained by the ArangoDB Foxx team typically usethe @foxx
namespace whereas the @arangodb
namespaceis reserved for internal use.
There is no official registry for dependency names but we recommend ensuringthe dependency names you use are unambiguous and meaningfulto other developers using your services.
A provides
definition maps each provided dependency’s nameto the provided version:
"provides": {
"@example/auth": "1.0.0"
}
A dependencies
definition maps the local alias of each consumed dependencyagainst a short definition that includes the name and version range:
"dependencies": {
"myAuth": {
"name": "@example/auth",
"version": "^1.0.0",
"description": "This description is entirely optional.",
"required": false,
"multiple": false
}
}
The local alias should be a valid JavaScript identifier(e.g. a valid variable name). When a dependency has been assigned,its JS API will be exposed in a corresponding property of theservice context,e.g. module.context.dependencies.myAuth
.
Assigning dependencies
Like configuration,dependencies can be assigned usingthe web interface,the Foxx CLI orthe Foxx HTTP API.
The value for each dependency should be the database-relative mount path ofthe service (including the leading slash). Both services need to be mounted inthe same database. The same service can be used to provide a dependencyfor multiple services.
Also as with configuration, a service that declares required dependencies whichhave not been assigned will not be mounted by Foxx until all requireddependencies have been assigned. Instead any attempt to access the service’sHTTP API will result in an error code.
Exporting a JS API
In order to provide a JS API other services can consume as a dependency,the service’s main file needs to export something other services can use.You can do this by assigning a value to the module.exports
or propertiesof the exports
object as with any other module export:
module.exports = "Hello world";
This also includes collections. In the following example, the collectionexported by the provider will use the provider’scollection prefix rather than the consumer’s,allowing both services to share the same collection:
module.exports = module.context.collection("shared_documents");
Let’s imagine we have a service managing our application’s users.Rather than allowing any consuming service to access the collection directly,we can provide a number of methods to manipulate it:
const auth = require("./util/auth");
const users = module.context.collection("users");
exports.login = (username, password) => {
const user = users.firstExample({ username });
if (!user) throw new Error("Wrong username");
const valid = auth.verify(user.authData, password);
if (!valid) throw new Error("Wrong password");
return user;
};
exports.setPassword = (user, password) => {
const authData = auth.create(password);
users.update(user, { authData });
return user;
};
Or you could even export a factory function to create an API that uses acustom error type provided by the consumer rather than the producer:
const auth = require("./util/auth");
const users = module.context.collection("users");
module.exports = (BadCredentialsError = Error) => {
return {
login(username, password) {
const user = users.firstExample({ username });
if (!user) throw new BadCredentialsError("Wrong username");
const valid = auth.verify(user.authData, password);
if (!valid) throw new BadCredentialsError("Wrong password");
return user;
},
setPassword(user, password) {
const authData = auth.create(password);
users.update(user, { authData });
return user;
}
};
};
Example usage (the consumer uses the local alias usersApi
):
"use strict";
const createRouter = require("@arangodb/foxx/router");
const joi = require("joi");
// Using the dependency with arguments
const AuthFailureError = require("./errors/auth-failure");
const createUsersApi = module.context.dependencies.usersApi;
const users = createUsersApi(AuthFailureError);
const router = createRouter();
module.context.use(router);
router.use((req, res, next) => {
try {
next();
} catch (e) {
if (e instanceof AuthFailureError) {
res.status(401);
res.json({
error: true,
message: e.message
});
} else {
console.error(e.stack);
res.status(500);
res.json({
error: true,
message: "Something went wrong."
});
}
}
});
router
.post("/login", (req, res) => {
const { username, password } = req.body;
const user = users.login(username, password);
// handle login success
res.json({ welcome: username });
})
.body(
joi.object().keys({
username: joi.string().required(),
password: joi.string().required()
})
);