aws-lager
Version:
AWS Lambda / API Gateway / Endpoint Router
351 lines (317 loc) • 11.9 kB
JavaScript
;
const file = require('file');
const path = require('path');
const util = require('util');
const lager = require('lager/lib/lager');
const Promise = lager.getPromise();
const fs = Promise.promisifyAll(require('fs'));
const _ = lager.getLodash();
// Add plugin commands to lager cli
require('./bin/create-api');
require('./bin/create-endpoint');
require('./bin/inspect-api');
require('./bin/inspect-endpoint');
require('./bin/deploy-apis');
const Api = require('./api');
const Endpoint = require('./endpoint');
/**
* Load all API specifications
* @return {Promise<[Api]>}
*/
function loadApis() {
let apiSpecsPath = path.join(process.cwd(), 'apis');
// This event allows to inject code before loading all APIs
return lager.fire('beforeApisLoad')
.then(() => {
// Retrieve configuration path of all API specifications
return fs.readdirAsync(apiSpecsPath);
})
.then(subdirs => {
// Load all the API specifications
let apiPromises = [];
_.forEach(subdirs, (subdir) => {
let apiSpecPath = path.join(apiSpecsPath, subdir, 'spec');
// subdir is the identifier of the API, so we pass it as the second argument
apiPromises.push(loadApi(apiSpecPath, subdir));
});
return Promise.all(apiPromises);
})
.then(apis => {
// This event allows to inject code to add or delete or alter API specifications
return lager.fire('afterApisLoad', apis);
})
.spread(apis => {
return Promise.resolve(apis);
});
}
/**
* Load an API specification
* @param {string} apiSpecPath - the full path to the specification file
* @param {string} OPTIONAL identifier - a human readable identifier, eventually
* configured in the specification file itself
* @return {Promise<Api>}
*/
function loadApi(apiSpecPath, identifier) {
return lager.fire('beforeApiLoad', apiSpecPath, identifier)
.spread((apiSpecPath, identifier) => {
// Because we use require() to get the spec, it could either be a JSON file
// or the content exported by a node module
let apiSpec = require(apiSpecPath);
apiSpec['x-lager'] = apiSpec['x-lager'] || {};
apiSpec['x-lager'].identifier = apiSpec['x-lager'].identifier || identifier;
let api = new Api(apiSpec);
// This event allows to inject code to alter the API specification
return lager.fire('afterApiLoad', api);
})
.spread(api => {
return Promise.resolve(api);
});
}
/**
* Load all Endpoint specifications
* @return {Promise<[Endpoints]>}
*/
function loadEndpoints() {
let endpointsDirectory = 'endpoints';
let endpointSpecsPath = path.join(process.cwd(), endpointsDirectory);
return lager.fire('beforeEndpointsLoad')
.spread(() => {
let endpointPromises = [];
file.walkSync(endpointSpecsPath, (dirPath, dirs, files) => {
// We are looking for directories that have the name of an HTTP method
let subPath = dirPath.substr(endpointSpecsPath.length);
let resourcePathParts = subPath.split(path.sep);
let method = resourcePathParts.pop();
if (['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].indexOf(method)) return;
// We construct the path to the resource (url style, not filesystem)
let resourcePath = resourcePathParts.join('/');
endpointPromises.push(loadEndpoint(endpointSpecsPath, resourcePath, method));
});
return Promise.all(endpointPromises);
})
.then(endpoints => {
return lager.fire('afterEndpointsLoad', endpoints);
})
.spread(endpoints => {
return Promise.resolve(endpoints);
})
.catch(e => {
if (e.code === 'ENOENT' && path.basename(e.path) === endpointsDirectory) {
return Promise.resolve([]);
}
throw e;
});
}
/**
* Load an Endpoint specification
*
* From the `endpointSpecRootPath` directory, lager will look for a specification in
* each subdirectory following the structure `path/to/the/resource/HTTP_METHOD/`
* @param {string} endpointSpecRootPath - the root directory of the endpoint configuration
* @param {string} resourcePath - the URL path to the endpoint resource
* @param {string} method - the HTTP method of the endpoint
* @return {Promise<Endpoint>}
*/
function loadEndpoint(endpointSpecRootPath, resourcePath, method) {
// @TODO throw error if the endpoint does not exists
method = method.toUpperCase();
return lager.fire('beforeEndpointLoad')
.spread(() => {
let parts = resourcePath.split('/');
let subPath = parts.join(path.sep) + path.sep + method;
let spec = mergeSpecsFiles(endpointSpecRootPath, subPath);
let endpoint = new Endpoint(spec, resourcePath, method);
// This event allows to inject code to alter the endpoint specification
return lager.fire('afterEndpointLoad', endpoint);
})
.spread((endpoint) => {
return Promise.resolve(endpoint);
});
}
/**
* Integration load and deployment is performed other plugins
* @return {[IntegrationObject]} [description]
*/
function loadIntegrations(region, stage, environment) {
// The `deployIntegrations` hook takes two arguments
// A object containing the region, stage and environment of the deployment
// and nn array that will receive integration results
return lager.fire('loadIntegrations', {region, stage, environment}, [])
.spread((config, integrationDataInjectors) => {
return Promise.resolve(integrationDataInjectors);
});
}
/**
* Update the configuration of endpoints with data returned by integration
* This data can come from the deployment of a lambda function, the configuration
* of an HTTP proxy, the generation of a mock etc ...
* @param {[Endpoint]} - a list of Endpoints
* @param {[IntegrationDataInjector]} - a list of integration data injectors
* an integration data injector is able to recognize
* if it applies to an endpoint and update its specification
* @return {[Endpoint]}
*/
function addIntegrationDataToEndpoints(endpoints, integrationDataInjectors) {
return lager.fire('beforeAddIntegrationDataToEndpoints', endpoints, integrationDataInjectors)
.spread((endpoints, integrationDataInjectors) => {
return Promise.map(integrationDataInjectors, (integrationDataInjector) => {
return Promise.map(endpoints, (endpoint) => {
return integrationDataInjector.applyToEndpoint(endpoint);
});
});
})
.then(() => {
return lager.fire('afterAddIntegrationDataToEndpoints', endpoints, integrationDataInjectors);
})
.spread((endpoints, integrationDataInjectors) => {
return Promise.resolve(endpoints);
});
}
/**
* [function description]
* @param {[Api]} apis
* @param {[Endpoint]} endpoints
* @return {Promise<[Api]>}
*/
function addEndpointsToApis(apis, endpoints) {
return lager.fire('beforeAddEndpointsToApis', apis, endpoints)
.spread((apis, endpoints) => {
return Promise.map(apis, (api) => {
return Promise.map(endpoints, (endpoint) => {
if (api.doesExposeEndpoint(endpoint)) {
return api.addEndpoint(endpoint);
}
});
});
})
.then(() => {
return lager.fire('afterAddEndpointsToApis', apis, endpoints);
})
.spread((apis, endpoints) => {
return Promise.resolve(apis);
});
}
/**
* [function description]
* @param {[type]} apis [description]
* @return {[type]} [description]
*/
function publishAllApis(apis, region, stage, environment) {
return lager.fire('beforePublishAllApis', apis)
.spread((apis) => {
return Promise.map(apis, (api) => {
return api.publish(region, stage, environment)
.then(() => {
return lager.fire('afterPublishAllApis', apis);
});
});
})
.spread((apis) => {
return Promise.resolve(apis);
});
}
/**
* Build all API specifications with their endpoints
* @return {[Object]}
*/
function buildSpecs() {
return Promise.all([loadApis(), loadEndpoints()])
.spread((apis, endpoints) => {
return Promise.map(apis, (api) => {
return Promise.map(endpoints, (endpoint) => {
return api.addEndpoint(endpoint);
})
.then(() => {
return Promise.resolve(api);
});
});
});
}
/**
*
* @param {string} region - AWS where we want to deploy APIs
* @param {string} stage - the stage to apply to the deployment (typically, the version)
* @param {string} environment - the environment prefixes the API name in API Gateway
* @return {[type]}
*/
function deploy(region, stage, environment) {
// First load API and endpoint specifications
console.log('Load APIs and Endpoints');
return Promise.all([loadApis(), loadEndpoints()])
.spread((apis, endpoints) => {
console.log('Load integrations');
// The load of API and endpoint specifications succeeded, we can deploy the integrations
// Typically, il is lambda functions, but it could be anything published by a plugin
return Promise.all([loadIntegrations(region, stage, environment), apis, endpoints]);
})
.spread((integrationsDataInjectors, apis, endpoints) => {
console.log('Add integrations to endpoints');
// Once the integrations have been deployed we can update the endpoints with integration data
return Promise.all([apis, addIntegrationDataToEndpoints(endpoints, integrationsDataInjectors)]);
})
.spread((apis, endpoints) => {
console.log('Add endpoints to APIs');
// Once the endpoints are up-to-date with the integrations, we can add them to the APIs
return Promise.all([addEndpointsToApis(apis, endpoints), endpoints]);
})
.spread((apis, endpoints) => {
// Now that we have complete API specifications, we can publish them in API Gateway
return publishAllApis(apis, region, stage, environment);
});
}
function getApiSpec(identifier, colors) {
let apiSpecPath = path.join(process.cwd(), 'apis', identifier, 'spec');
return Promise.all([loadApi(apiSpecPath, identifier), loadEndpoints()])
.spread((api, endpoints) => {
return Promise.all([addEndpointsToApis([api], endpoints), endpoints]);
})
.spread((apis, endpoints) => {
// @TODO add syntax highlighting with "-c" option
return console.log(JSON.stringify(apis[0].genSpec('doc'), null, 2));
// return console.log(util.inspect(apis[0].genSpec('doc'), { colors, depth: null }));
});
}
function getEndpointSpec(method, resourcePath, colors) {
let endpointSpecRootPath = path.join(process.cwd(), 'endpoints');
return loadEndpoint(endpointSpecRootPath, resourcePath, method)
.then(endpoint => {
// @TODO add syntax highlighting with "-c" option
return console.log(JSON.stringify(endpoint.getSpec(), null, 2));
});
}
module.exports = {
name: 'api-gateway',
hooks: {},
helpers: {},
commands: {},
outputApiSpec: getApiSpec,
outputEndpointSpec: getEndpointSpec,
deploy: deploy
};
/**
* Function that aggregates the specifications found in all spec.json|js files in a path
* @param {string} beginPath - path from which the function will look for swagger.json|js files
* @param {string} subPath - path until which the function will look for swagger.json|js files
* @return {Object} - aggregation of specifications that have been found
*/
function mergeSpecsFiles(beginPath, subPath) {
// Initialise specification
let spec = {};
// List all directories where we have to look for specifications
let subDirs = subPath.split(path.sep);
// Initialize the directory path for the do/while statement
let searchSpecDir = beginPath;
do {
let subSpec = {};
let subDir = subDirs.shift();
searchSpecDir = path.join(searchSpecDir, subDir);
try {
// Try to load the definition and silently ignore the error if it does not exist
subSpec = require(searchSpecDir + path.sep + 'spec');
} catch (e) {}
// Merge the spec eventually found
_.merge(spec, subSpec);
} while (subDirs.length);
// return the result of the merges
return spec;
}