From 11c7db437576ddf147fc0fd2d879f868b51af4a4 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Mon, 2 May 2016 15:24:50 +0200 Subject: [PATCH] Quality of life improvements in Foxx --- js/server/modules/@arangodb/foxx/context.js | 14 +- js/server/modules/@arangodb/foxx/manager.js | 229 +++++++++++++------- js/server/modules/@arangodb/foxx/service.js | 41 ++-- 3 files changed, 174 insertions(+), 110 deletions(-) diff --git a/js/server/modules/@arangodb/foxx/context.js b/js/server/modules/@arangodb/foxx/context.js index 88f0877bab..ba97cfe1b7 100644 --- a/js/server/modules/@arangodb/foxx/context.js +++ b/js/server/modules/@arangodb/foxx/context.js @@ -42,7 +42,7 @@ module.exports = class FoxxContext { } use(path, router, name) { - this.service.router.use(path, router, name); + return this.service.router.use(path, router, name); } registerType(type, def) { @@ -85,17 +85,13 @@ module.exports = class FoxxContext { } fileName(filename) { - return fs.safeJoin(this.basePath, filename); + return path.join(this.basePath, filename); } file(filename, encoding) { return fs.readFileSync(this.fileName(filename), encoding); } - path(name) { - return path.join(this.basePath, name); - } - collectionName(name) { const fqn = ( this.collectionPrefix + name @@ -116,7 +112,7 @@ module.exports = class FoxxContext { } get baseUrl() { - return `/_db/${encodeURIComponent(internal.db._name())}/${this.service.mount.slice(1)}`; + return `/_db/${encodeURIComponent(internal.db._name())}${this.service.mount}`; } get collectionPrefix() { @@ -139,10 +135,6 @@ module.exports = class FoxxContext { return !this.isDevelopment; } - get options() { - return this.service.options; - } - get configuration() { return this.service.configuration; } diff --git a/js/server/modules/@arangodb/foxx/manager.js b/js/server/modules/@arangodb/foxx/manager.js index 76be5f3ff0..7f5fe3011d 100644 --- a/js/server/modules/@arangodb/foxx/manager.js +++ b/js/server/modules/@arangodb/foxx/manager.js @@ -1,4 +1,3 @@ -/*jshint esnext: true */ /*global ArangoServerState, ArangoClusterInfo, ArangoClusterComm */ 'use strict'; @@ -35,6 +34,7 @@ const fs = require('fs'); const joi = require('joi'); const util = require('util'); const semver = require('semver'); +const dd = require('dedent'); const il = require('@arangodb/util').inline; const utils = require('@arangodb/foxx/manager-utils'); const store = require('@arangodb/foxx/store'); @@ -93,7 +93,7 @@ const manifestSchema = { ), lib: joi.string().default('.'), - main: joi.string().optional(), + main: joi.string().default('index.js'), configuration: ( 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: ( joi.object().optional() .pattern(RE_EMPTY, joi.forbidden()) @@ -215,7 +226,10 @@ function lookupService(mount) { } throw new ArangoError({ 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]; @@ -288,86 +302,136 @@ function checkMountedSystemService(dbname) { function checkManifest(filename, inputManifest, mount) { const serverVersion = plainServerVersion(); - const validationErrors = []; + const errors = []; + const warnings = []; + const notices = []; + const manifest = {}; let legacy = false; Object.keys(manifestSchema).forEach(function (key) { - let schema = manifestSchema[key]; - let value = manifest[key]; - let result = joi.validate(value, schema); - if (result.value !== undefined) { + const schema = manifestSchema[key]; + const value = inputManifest[key]; + const result = joi.validate(value, schema); + if (result.error) { + const error = result.error.message.replace(/^"value"/, `Field "${key}"`); + errors.push(`${error} (was "${util.format(value)}").`); + } else { 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 (semver.gtr('3.0.0', manifest.engines.arangodb)) { legacy = true; - console.warn( - `Manifest "${filename}" for service "${mount}":` - + ` Service expects version ${manifest.engines.arangodb}` - + ` and will run in legacy compatibility mode.` - ); + notices.push(il` + Service expects version ${manifest.engines.arangodb} + and will run in legacy compatibility mode. + `); } else if (!semver.satisfies(serverVersion, manifest.engines.arangodb)) { - console.warn( - `Manifest "${filename}" for service "${mount}":` - + ` ArangoDB version ${serverVersion} probably not compatible` - + ` with expected version ${manifest.engines.arangodb}.` - ); + warnings.push(il` + ArangoDB version ${serverVersion} probably not compatible + with expected version ${manifest.engines.arangodb}. + `); } } - Object.keys(manifest).forEach(function (key) { - if (!manifestSchema[key] && (!legacy || legacyManifestFields.indexOf(key) === -1)) { - console.warn(`Manifest "${filename}" for service "${mount}": unknown attribute "${key}"`); + for (const key of Object.keys(inputManifest)) { + if (manifestSchema[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({ 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'; } if (typeof manifest.controllers === 'string') { 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) { - var mf, msg; + let mf; if (!fs.exists(filename)) { - msg = `Cannot find manifest file "${filename}"`; - throwFileNotFound(msg); + throwFileNotFound(`Cannot find manifest file "${filename}"`); } try { mf = JSON.parse(fs.read(filename)); } catch (e) { - const error = new ArangoError({ - errorNum: errors.ERROR_MALFORMED_MANIFEST_FILE.code, - errorMessage: errors.ERROR_MALFORMED_MANIFEST_FILE.message - + '\nFile: ' + filename - + '\nCause: ' + e.stack - }); - error.cause = e; - throw error; + throw Object.assign( + new ArangoError({ + errorNum: errors.ERROR_MALFORMED_MANIFEST_FILE.code, + errorMessage: dd` + ${errors.ERROR_MALFORMED_MANIFEST_FILE.message} + File: ${filename} + Cause: ${e.stack} + ` + }), {cause: e} + ); } try { - checkManifest(filename, mf); + mf = checkManifest(filename, mf, mount); } catch (e) { - const error = new ArangoError({ - errorNum: errors.ERROR_INVALID_APPLICATION_MANIFEST.code, - errorMessage: errors.ERROR_INVALID_APPLICATION_MANIFEST.message - + '\nFile: ' + filename - + '\nCause: ' + e.stack - }); - error.cause = e; - throw error; + throw Object.assign( + new ArangoError({ + errorNum: errors.ERROR_INVALID_APPLICATION_MANIFEST.code, + errorMessage: dd` + ${errors.ERROR_INVALID_APPLICATION_MANIFEST.message} + File: ${filename} + Cause: ${e.stack} + ` + }), {cause: e} + ); } return mf; } @@ -559,10 +626,6 @@ function uploadToPeerCoordinators(serviceInfo, coordinators) { function installServiceFromGenerator(targetPath, options) { var invalidOptions = []; // 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.edgeCollections = options.edgeCollections || []; if (typeof options.name !== 'string') { @@ -586,8 +649,10 @@ function installServiceFromGenerator(targetPath, options) { if (invalidOptions.length > 0) { throw new ArangoError({ errorNum: errors.ERROR_INVALID_FOXX_OPTIONS.code, - errorMessage: errors.ERROR_INVALID_FOXX_OPTIONS.message - + '\nOptions: ' + JSON.stringify(invalidOptions, undefined, 2) + errorMessage: dd` + ${errors.ERROR_INVALID_FOXX_OPTIONS.message} + Options: ${JSON.stringify(invalidOptions, undefined, 2)} + ` }); } var cfg = generator.generate(options); diff --git a/js/server/modules/@arangodb/foxx/service.js b/js/server/modules/@arangodb/foxx/service.js index b65f150551..62056132b2 100644 --- a/js/server/modules/@arangodb/foxx/service.js +++ b/js/server/modules/@arangodb/foxx/service.js @@ -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 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 { constructor(data) { @@ -97,8 +108,8 @@ module.exports = class FoxxService { this.legacy = range ? semver.gtr('3.0.0', range) : false; if (this.legacy) { console.debugLines(dd` - Running "${this.mount}" in 2.x 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". `); } @@ -380,17 +391,7 @@ module.exports = class FoxxService { this.main.filename = path.resolve(moduleRoot, '.foxx'); this.main[$_MODULE_ROOT] = moduleRoot; this.main[$_MODULE_CONTEXT].console = foxxConsole; - this.main.require.aliases = new Map(this.legacy ? [ - ['@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.aliases = new Map(this.legacy ? LEGACY_ALIASES : []); this.main.require.cache = this.requireCache; this.main.context = new FoxxContext(this); this.router = new Router(); @@ -401,9 +402,15 @@ module.exports = class FoxxService { }; 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.version = this.version = this.manifest.version; 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) { const config = {}; - Object.keys(definitions).forEach(function (name) { + for (const name of Object.keys(definitions)) { const def = definitions[name]; if (def.default !== undefined) { config[name] = def.default; } - }); + } return config; } function createDependencies(definitions, options) { const deps = {}; - Object.keys(definitions).forEach(function (name) { + for (const name of Object.keys(definitions)) { Object.defineProperty(deps, name, { configurable: true, enumerable: true, @@ -483,6 +490,6 @@ function createDependencies(definitions, options) { return FoxxManager.requireService('/' + mount.replace(/(^\/+|\/+$)/, '')); } }); - }); + } return deps; }