1
0
Fork 0

Quality of life improvements in Foxx

This commit is contained in:
Alan Plum 2016-05-02 15:24:50 +02:00
parent e1d384813e
commit 11c7db4375
No known key found for this signature in database
GPG Key ID: 8ED72A9A323B6EFD
3 changed files with 174 additions and 110 deletions

View File

@ -42,7 +42,7 @@ module.exports = class FoxxContext {
} }
use(path, router, name) { use(path, router, name) {
this.service.router.use(path, router, name); return this.service.router.use(path, router, name);
} }
registerType(type, def) { registerType(type, def) {
@ -85,17 +85,13 @@ module.exports = class FoxxContext {
} }
fileName(filename) { fileName(filename) {
return fs.safeJoin(this.basePath, filename); return path.join(this.basePath, filename);
} }
file(filename, encoding) { file(filename, encoding) {
return fs.readFileSync(this.fileName(filename), encoding); return fs.readFileSync(this.fileName(filename), encoding);
} }
path(name) {
return path.join(this.basePath, name);
}
collectionName(name) { collectionName(name) {
const fqn = ( const fqn = (
this.collectionPrefix + name this.collectionPrefix + name
@ -116,7 +112,7 @@ module.exports = class FoxxContext {
} }
get baseUrl() { get baseUrl() {
return `/_db/${encodeURIComponent(internal.db._name())}/${this.service.mount.slice(1)}`; return `/_db/${encodeURIComponent(internal.db._name())}${this.service.mount}`;
} }
get collectionPrefix() { get collectionPrefix() {
@ -139,10 +135,6 @@ module.exports = class FoxxContext {
return !this.isDevelopment; return !this.isDevelopment;
} }
get options() {
return this.service.options;
}
get configuration() { get configuration() {
return this.service.configuration; return this.service.configuration;
} }

View File

@ -1,4 +1,3 @@
/*jshint esnext: true */
/*global ArangoServerState, ArangoClusterInfo, ArangoClusterComm */ /*global ArangoServerState, ArangoClusterInfo, ArangoClusterComm */
'use strict'; 'use strict';
@ -35,6 +34,7 @@ const fs = require('fs');
const joi = require('joi'); const joi = require('joi');
const util = require('util'); const util = require('util');
const semver = require('semver'); const semver = require('semver');
const dd = require('dedent');
const il = require('@arangodb/util').inline; const il = require('@arangodb/util').inline;
const utils = require('@arangodb/foxx/manager-utils'); const utils = require('@arangodb/foxx/manager-utils');
const store = require('@arangodb/foxx/store'); const store = require('@arangodb/foxx/store');
@ -93,7 +93,7 @@ const manifestSchema = {
), ),
lib: joi.string().default('.'), lib: joi.string().default('.'),
main: joi.string().optional(), main: joi.string().default('index.js'),
configuration: ( configuration: (
joi.object().optional() joi.object().optional()
@ -126,6 +126,17 @@ const manifestSchema = {
)) ))
), ),
provides: (
joi.alternatives().try(
joi.string().optional(),
joi.array().optional()
.items(joi.string().required()),
joi.object().optional()
.pattern(RE_EMPTY, joi.forbidden())
.pattern(RE_NOT_EMPTY, joi.string().required())
)
),
files: ( files: (
joi.object().optional() joi.object().optional()
.pattern(RE_EMPTY, joi.forbidden()) .pattern(RE_EMPTY, joi.forbidden())
@ -215,7 +226,10 @@ function lookupService(mount) {
} }
throw new ArangoError({ throw new ArangoError({
errorNum: errors.ERROR_APP_NOT_FOUND.code, errorNum: errors.ERROR_APP_NOT_FOUND.code,
errorMessage: 'Service not found at: ' + mount errorMessage: dd`
${errors.ERROR_APP_NOT_FOUND.message}
Service not found at "${mount}".
`
}); });
} }
return serviceCache[dbname][mount]; return serviceCache[dbname][mount];
@ -288,86 +302,136 @@ function checkMountedSystemService(dbname) {
function checkManifest(filename, inputManifest, mount) { function checkManifest(filename, inputManifest, mount) {
const serverVersion = plainServerVersion(); const serverVersion = plainServerVersion();
const validationErrors = []; const errors = [];
const warnings = [];
const notices = [];
const manifest = {};
let legacy = false; let legacy = false;
Object.keys(manifestSchema).forEach(function (key) { Object.keys(manifestSchema).forEach(function (key) {
let schema = manifestSchema[key]; const schema = manifestSchema[key];
let value = manifest[key]; const value = inputManifest[key];
let result = joi.validate(value, schema); const result = joi.validate(value, schema);
if (result.value !== undefined) { if (result.error) {
const error = result.error.message.replace(/^"value"/, `Field "${key}"`);
errors.push(`${error} (was "${util.format(value)}").`);
} else {
manifest[key] = result.value; manifest[key] = result.value;
} }
if (result.error) {
let error = result.error.message.replace(/^"value"/, `"${key}"`);
let message = `Manifest "${mount}": attribute ${error} (was "${util.format(value)}").`;
validationErrors.push(message);
console.error(message);
}
}); });
if (!manifest.engines && manifest.engine) {
console.warn(il`
Found unexpected "engine" field in manifest "${filename}" for service "${mount}".
Did you mean "engines"?
`);
}
if (manifest.engines && manifest.engines.arangodb) { if (manifest.engines && manifest.engines.arangodb) {
if (semver.gtr('3.0.0', manifest.engines.arangodb)) { if (semver.gtr('3.0.0', manifest.engines.arangodb)) {
legacy = true; legacy = true;
console.warn( notices.push(il`
`Manifest "${filename}" for service "${mount}":` Service expects version ${manifest.engines.arangodb}
+ ` Service expects version ${manifest.engines.arangodb}` and will run in legacy compatibility mode.
+ ` and will run in legacy compatibility mode.` `);
);
} else if (!semver.satisfies(serverVersion, manifest.engines.arangodb)) { } else if (!semver.satisfies(serverVersion, manifest.engines.arangodb)) {
console.warn( warnings.push(il`
`Manifest "${filename}" for service "${mount}":` ArangoDB version ${serverVersion} probably not compatible
+ ` ArangoDB version ${serverVersion} probably not compatible` with expected version ${manifest.engines.arangodb}.
+ ` with expected version ${manifest.engines.arangodb}.` `);
);
} }
} }
Object.keys(manifest).forEach(function (key) { for (const key of Object.keys(inputManifest)) {
if (!manifestSchema[key] && (!legacy || legacyManifestFields.indexOf(key) === -1)) { if (manifestSchema[key]) {
console.warn(`Manifest "${filename}" for service "${mount}": unknown attribute "${key}"`); continue;
} }
}); manifest[key] = inputManifest[key];
if (key === 'engine' && !inputManifest.engines) {
warnings.push('Unknown field "engine". Did you mean "engines"?');
} else if (!legacy || legacyManifestFields.indexOf(key) === -1) {
warnings.push(`Unknown field "${key}".`);
}
}
if (validationErrors.length) { if (manifest.version && !semver.valid(manifest.version)) {
warnings.push(`Not a valid version: "${manifest.verison}"`);
}
if (manifest.provides) {
if (typeof manifest.provides === 'string') {
manifest.provides = [manifest.provides];
}
if (Array.isArray(manifest.provides)) {
const provides = manifest.provides;
manifest.provides = {};
for (const provided of provides) {
const tokens = provided.split(':');
manifest.provides[tokens[0]] = tokens[1] || '*';
}
}
for (const name of Object.keys(manifest.provides)) {
const version = manifest.provides[name];
if (!semver.valid(version)) {
errors.push(`Provided "${name}" invalid version: "${version}".`);
}
}
}
if (manifest.dependencies) {
for (const key of Object.keys(manifest.dependencies)) {
if (typeof manifest.dependencies[key] === 'string') {
const tokens = manifest.dependencies[key].split(':');
manifest.dependencies[key] = {
name: tokens[0] || '*',
version: tokens[1] || '*',
required: true
};
}
const version = manifest.dependencies[key].version;
if (!semver.validRange(version)) {
errors.push(`Dependency "${key}" invalid version: "${version}".`);
}
}
}
if (notices.length) {
console.infoLines(dd`
Manifest for service at "${mount}":
${notices.join('\n')}
`);
}
if (warnings.length) {
console.warnLines(dd`
Manifest for service at "${mount}":
${warnings.join('\n')}
`);
}
if (errors.length) {
console.errorLines(dd`
Manifest for service at "${mount}":
${errors.join('\n')}
`);
throw new ArangoError({ throw new ArangoError({
errorNum: errors.ERROR_INVALID_APPLICATION_MANIFEST.code, errorNum: errors.ERROR_INVALID_APPLICATION_MANIFEST.code,
errorMessage: validationErrors.join('\n') errorMessage: dd`
${errors.ERROR_INVALID_APPLICATION_MANIFEST.message}
Manifest for service at "${mount}":
${errors.join('\n')}
`
}); });
} else { }
if (manifest.dependencies) {
Object.keys(manifest.dependencies).forEach(function (key) {
const dependency = manifest.dependencies[key];
if (typeof dependency === 'string') {
const tokens = dependency.split(':');
manifest.dependencies[key] = {
name: tokens[0] || '*',
version: tokens[1] || '*',
required: true
};
}
});
}
if (legacy && manifest.defaultDocument === undefined) { if (legacy) {
if (manifest.defaultDocument === undefined) {
manifest.defaultDocument = 'index.html'; manifest.defaultDocument = 'index.html';
} }
if (typeof manifest.controllers === 'string') { if (typeof manifest.controllers === 'string') {
manifest.controllers = {'/': manifest.controllers}; manifest.controllers = {'/': manifest.controllers};
} }
if (typeof manifest.tests === 'string') {
manifest.tests = [manifest.tests];
}
} }
if (typeof manifest.tests === 'string') {
manifest.tests = [manifest.tests];
}
return manifest;
} }
@ -378,34 +442,37 @@ function checkManifest(filename, inputManifest, mount) {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
function validateManifestFile(filename, mount) { function validateManifestFile(filename, mount) {
var mf, msg; let mf;
if (!fs.exists(filename)) { if (!fs.exists(filename)) {
msg = `Cannot find manifest file "${filename}"`; throwFileNotFound(`Cannot find manifest file "${filename}"`);
throwFileNotFound(msg);
} }
try { try {
mf = JSON.parse(fs.read(filename)); mf = JSON.parse(fs.read(filename));
} catch (e) { } catch (e) {
const error = new ArangoError({ throw Object.assign(
errorNum: errors.ERROR_MALFORMED_MANIFEST_FILE.code, new ArangoError({
errorMessage: errors.ERROR_MALFORMED_MANIFEST_FILE.message errorNum: errors.ERROR_MALFORMED_MANIFEST_FILE.code,
+ '\nFile: ' + filename errorMessage: dd`
+ '\nCause: ' + e.stack ${errors.ERROR_MALFORMED_MANIFEST_FILE.message}
}); File: ${filename}
error.cause = e; Cause: ${e.stack}
throw error; `
}), {cause: e}
);
} }
try { try {
checkManifest(filename, mf); mf = checkManifest(filename, mf, mount);
} catch (e) { } catch (e) {
const error = new ArangoError({ throw Object.assign(
errorNum: errors.ERROR_INVALID_APPLICATION_MANIFEST.code, new ArangoError({
errorMessage: errors.ERROR_INVALID_APPLICATION_MANIFEST.message errorNum: errors.ERROR_INVALID_APPLICATION_MANIFEST.code,
+ '\nFile: ' + filename errorMessage: dd`
+ '\nCause: ' + e.stack ${errors.ERROR_INVALID_APPLICATION_MANIFEST.message}
}); File: ${filename}
error.cause = e; Cause: ${e.stack}
throw error; `
}), {cause: e}
);
} }
return mf; return mf;
} }
@ -559,10 +626,6 @@ function uploadToPeerCoordinators(serviceInfo, coordinators) {
function installServiceFromGenerator(targetPath, options) { function installServiceFromGenerator(targetPath, options) {
var invalidOptions = []; var invalidOptions = [];
// Set default values: // Set default values:
options.name = options.name || 'MyService';
options.author = options.author || 'Author';
options.description = options.description || '';
options.license = options.license || 'Apache 2';
options.documentCollections = options.documentCollections || []; options.documentCollections = options.documentCollections || [];
options.edgeCollections = options.edgeCollections || []; options.edgeCollections = options.edgeCollections || [];
if (typeof options.name !== 'string') { if (typeof options.name !== 'string') {
@ -586,8 +649,10 @@ function installServiceFromGenerator(targetPath, options) {
if (invalidOptions.length > 0) { if (invalidOptions.length > 0) {
throw new ArangoError({ throw new ArangoError({
errorNum: errors.ERROR_INVALID_FOXX_OPTIONS.code, errorNum: errors.ERROR_INVALID_FOXX_OPTIONS.code,
errorMessage: errors.ERROR_INVALID_FOXX_OPTIONS.message errorMessage: dd`
+ '\nOptions: ' + JSON.stringify(invalidOptions, undefined, 2) ${errors.ERROR_INVALID_FOXX_OPTIONS.message}
Options: ${JSON.stringify(invalidOptions, undefined, 2)}
`
}); });
} }
var cfg = generator.generate(options); var cfg = generator.generate(options);

View File

@ -45,6 +45,17 @@ const APP_PATH = internal.appPath ? path.resolve(internal.appPath) : undefined;
const STARTUP_PATH = internal.startupPath ? path.resolve(internal.startupPath) : undefined; const STARTUP_PATH = internal.startupPath ? path.resolve(internal.startupPath) : undefined;
const DEV_APP_PATH = internal.devAppPath ? path.resolve(internal.devAppPath) : undefined; const DEV_APP_PATH = internal.devAppPath ? path.resolve(internal.devAppPath) : undefined;
const LEGACY_ALIASES = [
['@arangodb/foxx/authentication', '@arangodb/foxx/legacy/authentication'],
['@arangodb/foxx/controller', '@arangodb/foxx/legacy/controller'],
['@arangodb/foxx/model', '@arangodb/foxx/legacy/model'],
['@arangodb/foxx/query', '@arangodb/foxx/legacy/query'],
['@arangodb/foxx/repository', '@arangodb/foxx/legacy/repository'],
['@arangodb/foxx/schema', '@arangodb/foxx/legacy/schema'],
['@arangodb/foxx/sessions', '@arangodb/foxx/legacy/sessions'],
['@arangodb/foxx/template_middleware', '@arangodb/foxx/legacy/template_middleware'],
['@arangodb/foxx', '@arangodb/foxx/legacy']
];
module.exports = class FoxxService { module.exports = class FoxxService {
constructor(data) { constructor(data) {
@ -97,8 +108,8 @@ module.exports = class FoxxService {
this.legacy = range ? semver.gtr('3.0.0', range) : false; this.legacy = range ? semver.gtr('3.0.0', range) : false;
if (this.legacy) { if (this.legacy) {
console.debugLines(dd` console.debugLines(dd`
Running "${this.mount}" in 2.x compatibility mode. Service "${this.mount}" is running in legacy compatibility mode.
Requested version ${range} is lower than 3.0.0. Requested version "${range}" is lower than "3.0.0".
`); `);
} }
@ -380,17 +391,7 @@ module.exports = class FoxxService {
this.main.filename = path.resolve(moduleRoot, '.foxx'); this.main.filename = path.resolve(moduleRoot, '.foxx');
this.main[$_MODULE_ROOT] = moduleRoot; this.main[$_MODULE_ROOT] = moduleRoot;
this.main[$_MODULE_CONTEXT].console = foxxConsole; this.main[$_MODULE_CONTEXT].console = foxxConsole;
this.main.require.aliases = new Map(this.legacy ? [ this.main.require.aliases = new Map(this.legacy ? LEGACY_ALIASES : []);
['@arangodb/foxx/authentication', '@arangodb/foxx/legacy/authentication'],
['@arangodb/foxx/controller', '@arangodb/foxx/legacy/controller'],
['@arangodb/foxx/model', '@arangodb/foxx/legacy/model'],
['@arangodb/foxx/query', '@arangodb/foxx/legacy/query'],
['@arangodb/foxx/repository', '@arangodb/foxx/legacy/repository'],
['@arangodb/foxx/schema', '@arangodb/foxx/legacy/schema'],
['@arangodb/foxx/sessions', '@arangodb/foxx/legacy/sessions'],
['@arangodb/foxx/template_middleware', '@arangodb/foxx/legacy/template_middleware'],
['@arangodb/foxx', '@arangodb/foxx/legacy']
] : []);
this.main.require.cache = this.requireCache; this.main.require.cache = this.requireCache;
this.main.context = new FoxxContext(this); this.main.context = new FoxxContext(this);
this.router = new Router(); this.router = new Router();
@ -401,9 +402,15 @@ module.exports = class FoxxService {
}; };
if (this.legacy) { if (this.legacy) {
this.main.context.path = this.main.context.fileName;
this.main.context.fileName = (filename) => fs.safeJoin(this.basePath, filename);
this.main.context.foxxFilename = this.main.context.fileName; this.main.context.foxxFilename = this.main.context.fileName;
this.main.context.version = this.version = this.manifest.version; this.main.context.version = this.version = this.manifest.version;
this.main.context.name = this.name = this.manifest.name; this.main.context.name = this.name = this.manifest.name;
this.main.context.options = this.options;
this.main.context.use = undefined;
this.main.context.apiDocumentation = undefined;
this.main.context.registerType = undefined;
} }
} }
@ -458,19 +465,19 @@ module.exports = class FoxxService {
function createConfiguration(definitions) { function createConfiguration(definitions) {
const config = {}; const config = {};
Object.keys(definitions).forEach(function (name) { for (const name of Object.keys(definitions)) {
const def = definitions[name]; const def = definitions[name];
if (def.default !== undefined) { if (def.default !== undefined) {
config[name] = def.default; config[name] = def.default;
} }
}); }
return config; return config;
} }
function createDependencies(definitions, options) { function createDependencies(definitions, options) {
const deps = {}; const deps = {};
Object.keys(definitions).forEach(function (name) { for (const name of Object.keys(definitions)) {
Object.defineProperty(deps, name, { Object.defineProperty(deps, name, {
configurable: true, configurable: true,
enumerable: true, enumerable: true,
@ -483,6 +490,6 @@ function createDependencies(definitions, options) {
return FoxxManager.requireService('/' + mount.replace(/(^\/+|\/+$)/, '')); return FoxxManager.requireService('/' + mount.replace(/(^\/+|\/+$)/, ''));
} }
}); });
}); }
return deps; return deps;
} }