forest-express
Version:
Official package for all Forest Express Lianas
490 lines (481 loc) • 23.7 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
var _ = require('lodash');
var crypto = require('crypto');
var express = require('express');
var cors = require('cors');
var bodyParser = require('body-parser');
var _require = require('express-jwt'),
jwt = _require.expressjwt;
var url = require('url');
var requireAll = require('require-all');
var _require2 = require('@forestadmin/context'),
init = _require2.init,
inject = _require2.inject;
var serviceBuilder = require('./context/service-builder');
init(serviceBuilder);
var auth = require('./services/auth');
var ResourcesRoutes = require('./routes/resources');
var ActionsRoutes = require('./routes/actions');
var AssociationsRoutes = require('./routes/associations');
var StatRoutes = require('./routes/stats');
var ForestRoutes = require('./routes/forest');
var HealthCheckRoute = require('./routes/healthcheck');
var initScopeRoutes = require('./routes/scopes');
var Schemas = require('./generators/schemas');
var SchemaSerializer = require('./serializers/schema');
var Integrator = require('./integrations');
var _require3 = require('./config/jwt'),
getJWTConfiguration = _require3.getJWTConfiguration;
var initAuthenticationRoutes = require('./routes/authentication');
var _inject = inject(),
logger = _inject.logger,
path = _inject.path,
pathService = _inject.pathService,
errorHandler = _inject.errorHandler,
ipWhitelist = _inject.ipWhitelist,
apimapFieldsFormater = _inject.apimapFieldsFormater,
apimapSender = _inject.apimapSender,
schemaFileUpdater = _inject.schemaFileUpdater,
configStore = _inject.configStore,
modelsManager = _inject.modelsManager,
fs = _inject.fs,
tokenService = _inject.tokenService,
scopeManager = _inject.scopeManager,
smartActionFieldValidator = _inject.smartActionFieldValidator;
var PUBLIC_ROUTES = ['/', '/healthcheck'].concat((0, _toConsumableArray2["default"])(initAuthenticationRoutes.PUBLIC_ROUTES));
var ENVIRONMENT_DEVELOPMENT = !process.env.NODE_ENV || ['dev', 'development'].includes(process.env.NODE_ENV);
var DISABLE_AUTO_SCHEMA_APPLY = process.env.FOREST_DISABLE_AUTO_SCHEMA_APPLY && JSON.parse(process.env.FOREST_DISABLE_AUTO_SCHEMA_APPLY);
var jwtAuthenticator;
var app = null;
function loadCollections(collectionsDir) {
var isJavascriptOrTypescriptFileName = function isJavascriptOrTypescriptFileName(fileName) {
return fileName.endsWith('.js') || fileName.endsWith('.ts') && !fileName.endsWith('.d.ts');
};
// NOTICE: Ends with `.spec.js`, `.spec.ts`, `.test.js` or `.test.ts`.
var isTestFileName = function isTestFileName(fileName) {
return fileName.match(/(?:\.test|\.spec)\.(?:js||ts)$/g);
};
requireAll({
dirname: collectionsDir,
excludeDirs: /^__tests__$/,
filter: function filter(fileName) {
return isJavascriptOrTypescriptFileName(fileName) && !isTestFileName(fileName);
},
recursive: true
});
}
function buildSchema() {
return _buildSchema.apply(this, arguments);
}
function _buildSchema() {
_buildSchema = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2() {
var lianaOptions, Implementation, models;
return _regenerator["default"].wrap(function _callee2$(_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
lianaOptions = configStore.lianaOptions, Implementation = configStore.Implementation;
models = Object.values(modelsManager.getModels());
configStore.integrator = new Integrator(lianaOptions, Implementation);
_context2.next = 5;
return Schemas.perform(Implementation, configStore.integrator, models, lianaOptions);
case 5:
return _context2.abrupt("return", models);
case 6:
case "end":
return _context2.stop();
}
}, _callee2);
}));
return _buildSchema.apply(this, arguments);
}
exports.Schemas = Schemas;
exports.logger = logger;
exports.scopeManager = scopeManager;
exports.ResourcesRoute = {};
/**
* @param {import('express').Request} request
* @param {import('express').Response} response
* @param {import('express').NextFunction} next
*/
exports.ensureAuthenticated = function (request, response, next) {
var parsedUrl = url.parse(request.originalUrl);
var forestPublicRoutes = PUBLIC_ROUTES.map(function (route) {
return "/forest".concat(route);
});
if (forestPublicRoutes.includes(parsedUrl.pathname)) {
next();
return;
}
auth.authenticate(request, response, next, jwtAuthenticator);
};
function generateAndSendSchema(_x) {
return _generateAndSendSchema.apply(this, arguments);
}
function _generateAndSendSchema() {
_generateAndSendSchema = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee3(envSecret) {
var collections, schemaSerializer, serializerOptions, collectionsSent, metaSent, pathSchemaFile, meta, content, _content, contentParsed, schemaSent, hash, schemaFileHash;
return _regenerator["default"].wrap(function _callee3$(_context3) {
while (1) switch (_context3.prev = _context3.next) {
case 0:
collections = _.values(Schemas.schemas);
configStore.integrator.defineCollections(collections);
collections.filter(function (collection) {
return collection.actions && collection.actions.length;
})
// NOTICE: Check each Smart Action declaration to detect configuration errors.
.forEach(function (collection) {
collection.actions.forEach(function (action) {
if (!action.name) {
logger.warn("An unnamed Smart Action of collection \"".concat(collection.name, "\" has been ignored."));
} else {
try {
smartActionFieldValidator.validateSmartActionFields(action, collection.name);
} catch (error) {
logger.error(error.message);
}
}
});
// NOTICE: Ignore actions without a name.
collection.actions = collection.actions.filter(function (action) {
return action.name;
});
});
schemaSerializer = new SchemaSerializer();
serializerOptions = schemaSerializer.options;
pathSchemaFile = path.join(configStore.schemaDir, '.forestadmin-schema.json');
if (!ENVIRONMENT_DEVELOPMENT) {
_context3.next = 13;
break;
}
meta = {
liana: configStore.Implementation.getLianaName(),
liana_version: configStore.Implementation.getLianaVersion(),
stack: {
database_type: configStore.Implementation.getDatabaseType(),
engine: 'nodejs',
engine_version: process.versions && process.versions.node,
orm_version: configStore.Implementation.getOrmVersion()
}
};
content = schemaFileUpdater.update(pathSchemaFile, collections, meta, serializerOptions);
collectionsSent = content.collections;
metaSent = content.meta;
_context3.next = 31;
break;
case 13:
logger.warn('NODE_ENV is not set to "development", the schema file will not be updated.');
logger.info('Loading the current version of .forestadmin-schema.json file…');
_context3.prev = 15;
_content = fs.readFileSync(pathSchemaFile);
if (_content) {
_context3.next = 21;
break;
}
logger.error('The .forestadmin-schema.json file is empty.');
logger.error('The schema cannot be synchronized with Forest Admin servers.');
return _context3.abrupt("return", null);
case 21:
contentParsed = JSON.parse(_content.toString());
collectionsSent = contentParsed.collections;
metaSent = contentParsed.meta;
_context3.next = 31;
break;
case 26:
_context3.prev = 26;
_context3.t0 = _context3["catch"](15);
if (_context3.t0.code === 'ENOENT') {
logger.error('The .forestadmin-schema.json file does not exist.');
} else {
logger.error('The content of .forestadmin-schema.json file is not a correct JSON.');
}
logger.error('The schema cannot be synchronized with Forest Admin servers.');
return _context3.abrupt("return", null);
case 31:
if (!DISABLE_AUTO_SCHEMA_APPLY) {
_context3.next = 34;
break;
}
logger.warn('FOREST_DISABLE_AUTO_SCHEMA_APPLY is set to true, the schema file will not be sent to Forest.');
return _context3.abrupt("return", Promise.resolve());
case 34:
schemaSent = schemaSerializer.perform(collectionsSent, metaSent);
hash = crypto.createHash('sha1');
schemaFileHash = hash.update(JSON.stringify(schemaSent)).digest('hex');
schemaSent.meta.schemaFileHash = schemaFileHash;
logger.info('Checking need for apimap update...');
return _context3.abrupt("return", apimapSender.checkHash(envSecret, schemaFileHash).then(function (_ref2) {
var body = _ref2.body;
if (body.sendSchema) {
logger.info('Sending schema file to Forest...');
return apimapSender.send(envSecret, schemaSent).then(function (result) {
logger.info('Schema file sent.');
return result;
});
}
logger.info('No change in apimap, nothing sent to Forest.');
return Promise.resolve(null);
}));
case 40:
case "end":
return _context3.stop();
}
}, _callee3, null, [[15, 26]]);
}));
return _generateAndSendSchema.apply(this, arguments);
}
var reportSchemaComputeError = function reportSchemaComputeError(error) {
logger.error('An error occurred while computing the Forest schema. Your application schema cannot be synchronized with Forest. Your admin panel might not reflect your application models definition. ', error);
};
exports.init = /*#__PURE__*/function () {
var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(Implementation) {
var opts, options, pathMounted, allowedOrigins, oneDayInSeconds, corsOptions, pathsPublic, _configStore$Implemen, models, collections, generateAndSendSchemaPromise;
return _regenerator["default"].wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
opts = Implementation.opts;
configStore.Implementation = Implementation;
configStore.lianaOptions = opts;
// Trick to update ForestAdminClient options at runtime
options = inject().forestAdminClient.options;
if (configStore.lianaOptions.envSecret) {
options.envSecret = configStore.lianaOptions.envSecret;
}
if (!app) {
_context.next = 8;
break;
}
logger.warn('Forest init function called more than once. Only the first call has been processed.');
return _context.abrupt("return", app);
case 8:
app = express();
_context.prev = 9;
configStore.validateOptions();
_context.next = 17;
break;
case 13:
_context.prev = 13;
_context.t0 = _context["catch"](9);
logger.error(_context.t0.message);
return _context.abrupt("return", Promise.resolve(app));
case 17:
pathMounted = pathService.generateForInit('*', configStore.lianaOptions);
auth.initAuth(configStore.lianaOptions);
// CORS
allowedOrigins = ['localhost:4200', /\.forestadmin\.com$/];
oneDayInSeconds = 86400;
if (process.env.CORS_ORIGINS) {
allowedOrigins = allowedOrigins.concat(process.env.CORS_ORIGINS.split(','));
}
corsOptions = {
origin: allowedOrigins,
maxAge: oneDayInSeconds,
credentials: true,
preflightContinue: true
}; // Support for request-private-network as the `cors` package
// doesn't support it by default
// See: https://github.com/expressjs/cors/issues/236
app.use(pathMounted, function (req, res, next) {
if (req.headers['access-control-request-private-network']) {
res.setHeader('access-control-allow-private-network', 'true');
}
next(null);
});
app.use(pathService.generate(initAuthenticationRoutes.CALLBACK_ROUTE, opts), cors(_objectSpread(_objectSpread({}, corsOptions), {}, {
// this route needs to be called after a redirection
// in this situation, the origin sent by the browser is "null"
origin: ['null'].concat((0, _toConsumableArray2["default"])(corsOptions.origin))
})));
app.use(pathMounted, cors(corsOptions));
// Mime type
app.use(pathMounted, bodyParser.json());
// Authentication
if (configStore.lianaOptions.authSecret) {
jwtAuthenticator = jwt(getJWTConfiguration({
secret: configStore.lianaOptions.authSecret,
getToken: function getToken(request) {
if (request.headers) {
if (request.headers.authorization && request.headers.authorization.split(' ')[0] === 'Bearer') {
return request.headers.authorization.split(' ')[1];
}
// NOTICE: Necessary for downloads authentication.
if (request.headers.cookie) {
var forestSessionToken = tokenService.extractForestSessionToken(request.headers.cookie);
if (forestSessionToken) {
return forestSessionToken;
}
}
}
return null;
},
requestProperty: 'user'
}));
}
if (jwtAuthenticator) {
pathsPublic = [/^\/forest\/authentication$/, /^\/forest\/authentication\/.*$/];
app.use(pathMounted, jwtAuthenticator.unless({
path: pathsPublic
}));
}
new HealthCheckRoute(app, configStore.lianaOptions).perform();
initScopeRoutes(app, inject());
initAuthenticationRoutes(app, configStore.lianaOptions, inject());
// Init
_context.prev = 32;
_context.next = 35;
return buildSchema();
case 35:
models = _context.sent;
if (configStore.doesConfigDirExist()) {
loadCollections(configStore.configDir);
}
if (configStore !== null && configStore !== void 0 && (_configStore$Implemen = configStore.Implementation) !== null && _configStore$Implemen !== void 0 && _configStore$Implemen.Flattener) {
app.use(configStore.Implementation.Flattener.requestUnflattener);
}
models.forEach(function (model) {
var modelName = configStore.Implementation.getModelName(model);
configStore.integrator.defineRoutes(app, model, configStore.Implementation);
var resourcesRoute = new ResourcesRoutes(app, model);
resourcesRoute.perform();
exports.ResourcesRoute[modelName] = resourcesRoute;
new AssociationsRoutes(app, model, configStore.Implementation, configStore.integrator, configStore.lianaOptions).perform();
new StatRoutes(app, model, configStore.Implementation, configStore.lianaOptions).perform();
});
collections = _.values(Schemas.schemas);
collections.forEach(function (collection) {
var retrievedModel = models.find(function (model) {
return configStore.Implementation.getModelName(model) === collection.name;
});
new ActionsRoutes().perform(app, collection, retrievedModel, configStore.Implementation, configStore.lianaOptions, auth);
});
new ForestRoutes(app, configStore.lianaOptions).perform();
app.use(pathMounted, errorHandler({
logger: logger
}));
generateAndSendSchemaPromise = generateAndSendSchema(configStore.lianaOptions.envSecret)["catch"](function (error) {
reportSchemaComputeError(error);
}); // NOTICE: Hide promise for testing purpose. Waiting here in production
// will change app behaviour.
if (process.env.NODE_ENV === 'test') {
app._generateAndSendSchemaPromise = generateAndSendSchemaPromise;
}
_context.prev = 45;
_context.next = 48;
return ipWhitelist.retrieve(configStore.lianaOptions.envSecret);
case 48:
_context.next = 52;
break;
case 50:
_context.prev = 50;
_context.t1 = _context["catch"](45);
case 52:
if (configStore.lianaOptions.expressParentApp) {
configStore.lianaOptions.expressParentApp.use('/forest', app);
}
return _context.abrupt("return", app);
case 56:
_context.prev = 56;
_context.t2 = _context["catch"](32);
reportSchemaComputeError(_context.t2);
throw _context.t2;
case 60:
case "end":
return _context.stop();
}
}, _callee, null, [[9, 13], [32, 56], [45, 50]]);
}));
return function (_x2) {
return _ref.apply(this, arguments);
};
}();
function getSmartActionsUsingHTTPMethod(actions) {
return actions.filter(function (action) {
return Object.hasOwnProperty.call(action, 'httpMethod');
});
}
exports.collection = function (name, opts) {
if (_.isEmpty(Schemas.schemas) && opts.modelsDir) {
logger.error("Cannot customize your collection named \"".concat(name, "\" properly. Did you call the \"collection\" method in the /forest directory?"));
return;
}
var collection = _.find(Schemas.schemas, {
name: name
});
if (!collection) {
collection = _.find(Schemas.schemas, {
nameOld: name
});
if (collection) {
name = collection.name;
logger.warn("DEPRECATION WARNING: Collection names are now based on the models names. Please rename the collection \"".concat(collection.nameOld, "\" of your Forest customisation in \"").concat(collection.name, "\"."));
}
}
if (collection) {
var _configStore$Implemen2;
if (!Schemas.schemas[name].actions) {
Schemas.schemas[name].actions = [];
}
if (!Schemas.schemas[name].segments) {
Schemas.schemas[name].segments = [];
}
Schemas.schemas[name].actions = _.union(opts.actions, Schemas.schemas[name].actions);
Schemas.schemas[name].segments = _.union(opts.segments, Schemas.schemas[name].segments);
// NOTICE: `httpMethod` on smart actions will be removed in the future.
getSmartActionsUsingHTTPMethod(Schemas.schemas[name].actions).forEach(function (action) {
var removeHttpMethodMessage = 'Please update your smart action route to use the POST verb instead, and remove the "httpMethod" property in your forest file.';
if (['get', 'head'].includes(action.httpMethod.toLowerCase())) {
logger.error("The \"httpMethod\" ".concat(action.httpMethod, " of your smart action \"").concat(action.name, "\" is not supported. ").concat(removeHttpMethodMessage));
} else {
logger.warn("DEPRECATION WARNING: The \"httpMethod\" property of your smart action \"".concat(action.name, "\" is now deprecated. ").concat(removeHttpMethodMessage));
}
});
// NOTICE: Smart Field definition case
opts.fields = apimapFieldsFormater.formatFieldsByCollectionName(opts.fields, name);
Schemas.schemas[name].fields = _.concat(opts.fields, Schemas.schemas[name].fields);
if (configStore !== null && configStore !== void 0 && (_configStore$Implemen2 = configStore.Implementation) !== null && _configStore$Implemen2 !== void 0 && _configStore$Implemen2.Flattener) {
var Flattener = new configStore.Implementation.Flattener(Schemas.schemas[name], opts.fieldsToFlatten, modelsManager.getModelByName(name), configStore.lianaOptions);
Flattener.flattenFields();
}
if (opts.searchFields) {
Schemas.schemas[name].searchFields = opts.searchFields;
}
} else if (opts.fields && opts.fields.length) {
// NOTICE: Smart Collection definition case
opts.name = name;
opts.idField = 'id';
opts.isVirtual = true;
opts.isSearchable = !!opts.isSearchable;
opts.fields = apimapFieldsFormater.formatFieldsByCollectionName(opts.fields, name);
Schemas.schemas[name] = opts;
}
};
exports.SchemaSerializer = SchemaSerializer;
exports.StatSerializer = require('./serializers/stat');
exports.ResourceSerializer = require('./serializers/resource');
exports.ResourceDeserializer = require('./deserializers/resource');
exports.BaseFiltersParser = require('./services/base-filters-parser');
exports.BaseOperatorDateParser = require('./services/base-operator-date-parser');
exports.SchemaUtils = require('./utils/schema');
exports.RecordsGetter = require('./services/exposed/records-getter');
exports.RecordsCounter = require('./services/exposed/records-counter')["default"];
exports.RecordsExporter = require('./services/exposed/records-exporter');
exports.RecordGetter = require('./services/exposed/record-getter');
exports.RecordUpdater = require('./services/exposed/record-updater');
exports.RecordCreator = require('./services/exposed/record-creator');
exports.RecordRemover = require('./services/exposed/record-remover');
exports.RecordsRemover = require('./services/exposed/records-remover');
exports.RecordSerializer = require('./services/exposed/record-serializer');
exports.ScopeManager = require('./services/scope-manager');
exports.PermissionMiddlewareCreator = require('./middlewares/permissions');
exports.deactivateCountMiddleware = require('./middlewares/count');
exports.errorHandler = errorHandler;
exports.PUBLIC_ROUTES = PUBLIC_ROUTES;
if (process.env.NODE_ENV === 'test') {
exports.generateAndSendSchema = generateAndSendSchema;
}