UNPKG

forest-express

Version:

Official package for all Forest Express Lianas

490 lines (481 loc) 23.7 kB
"use strict"; 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; }