473 lines
12 KiB
JavaScript
Raw Normal View History

2015-10-01 00:30:16 +02:00
'use strict';
2017-07-25 17:12:18 +02:00
const http = require('http');
const path = require('path');
const fse = require('fs-extra');
2018-05-04 17:02:27 +02:00
const Koa = require('koa');
2019-08-21 11:05:33 +02:00
const Router = require('koa-router');
2019-04-05 16:11:09 +02:00
const _ = require('lodash');
const chalk = require('chalk');
const CLITable = require('cli-table3');
const { logger, models, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('strapi-utils');
const { createDatabaseManager } = require('strapi-database');
const loadConfiguration = require('./core/app-configuration');
2018-05-04 17:02:27 +02:00
const utils = require('./utils');
const loadModules = require('./core/load-modules');
const bootstrap = require('./core/bootstrap');
2017-07-24 19:58:03 +02:00
const initializeMiddlewares = require('./middlewares');
const initializeHooks = require('./hooks');
const createStrapiFs = require('./core/fs');
2019-12-17 10:35:04 +01:00
const createEventHub = require('./services/event-hub');
2019-12-17 11:24:14 +01:00
const createWebhookRunner = require('./services/webhook-runner');
const { webhookModel, createWebhookStore } = require('./services/webhook-store');
2019-12-17 11:24:14 +01:00
const { createCoreStore, coreStoreModel } = require('./services/core-store');
const createEntityService = require('./services/entity-service');
const entityValidator = require('./services/entity-validator');
const createTelemetry = require('./services/metrics');
const createUpdateNotifier = require('./utils/update-notifier');
const ee = require('./utils/ee');
2019-12-17 10:35:04 +01:00
2015-10-01 00:30:16 +02:00
/**
* Construct an Strapi instance.
*
* @constructor
*/
class Strapi {
2020-01-10 12:25:41 +01:00
constructor(opts = {}) {
2017-08-02 13:17:40 +02:00
this.reload = this.reload();
2016-07-06 15:51:52 +02:00
// Expose `koa`.
2016-11-04 16:00:19 +01:00
this.app = new Koa();
2019-08-21 11:05:33 +02:00
this.router = new Router();
2016-07-06 15:51:52 +02:00
this.initServer();
2017-08-10 15:38:08 +02:00
// Logger.
this.log = logger;
2017-09-12 17:58:31 +02:00
// Utils.
this.utils = {
2018-05-18 14:22:24 +02:00
models,
2017-09-12 17:58:31 +02:00
};
2019-12-17 10:35:04 +01:00
this.dir = opts.dir || process.cwd();
this.admin = {};
this.plugins = {};
this.config = loadConfiguration(this.dir, opts);
this.app.proxy = this.config.get('server.proxy');
this.isLoaded = false;
2019-12-17 10:35:04 +01:00
// internal services.
this.fs = createStrapiFs(this);
this.eventHub = createEventHub();
this.requireProjectBootstrap();
createUpdateNotifier(this).notify();
2019-12-17 10:35:04 +01:00
}
get EE() {
return ee({ dir: this.dir, logger });
}
handleRequest(req, res) {
if (!this.requestHandler) {
this.requestHandler = this.app.callback();
}
return this.requestHandler(req, res);
}
requireProjectBootstrap() {
const bootstrapPath = path.resolve(this.dir, 'config/functions/bootstrap.js');
if (fse.existsSync(bootstrapPath)) {
require(bootstrapPath);
}
}
logStats() {
const columns = Math.min(process.stderr.columns, 80) - 2;
console.log();
console.log(chalk.black.bgWhite(_.padEnd(' Project information', columns)));
console.log();
const infoTable = new CLITable({
colWidths: [20, 50],
chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
});
const isEE = strapi.EE === true && ee.isEE === true;
infoTable.push(
[chalk.blue('Time'), `${new Date()}`],
[chalk.blue('Launched in'), Date.now() - this.config.launchedAt + ' ms'],
[chalk.blue('Environment'), this.config.environment],
[chalk.blue('Process PID'), process.pid],
[chalk.blue('Version'), `${this.config.info.strapi} (node ${process.version})`],
[chalk.blue('Edition'), isEE ? 'Enterprise' : 'Community']
);
console.log(infoTable.toString());
console.log();
console.log(chalk.black.bgWhite(_.padEnd(' Actions available', columns)));
console.log();
}
logFirstStartupMessage() {
this.logStats();
console.log(chalk.bold('One more thing...'));
console.log(
chalk.grey('Create your first administrator 💻 by going to the administration panel at:')
);
console.log();
const addressTable = new CLITable();
const adminUrl = getAbsoluteAdminUrl(strapi.config);
addressTable.push([chalk.bold(adminUrl)]);
console.log(`${addressTable.toString()}`);
console.log();
}
logStartupMessage() {
this.logStats();
console.log(chalk.bold('Welcome back!'));
if (this.config.serveAdminPanel === true) {
console.log(chalk.grey('To manage your project 🚀, go to the administration panel at:'));
const adminUrl = getAbsoluteAdminUrl(strapi.config);
console.log(chalk.bold(adminUrl));
console.log();
}
console.log(chalk.grey('To access the server ⚡️, go to:'));
const serverUrl = getAbsoluteServerUrl(strapi.config);
console.log(chalk.bold(serverUrl));
console.log();
}
initServer() {
this.server = http.createServer(this.handleRequest.bind(this));
2019-04-05 16:11:09 +02:00
// handle port in use cleanly
this.server.on('error', err => {
if (err.code === 'EADDRINUSE') {
return this.stopWithError(`The port ${err.port} is already used by another application.`);
2019-04-05 16:11:09 +02:00
}
this.log.error(err);
});
// Close current connections to fully destroy the server
2017-07-25 17:12:18 +02:00
const connections = {};
2017-07-25 17:12:18 +02:00
this.server.on('connection', conn => {
const key = conn.remoteAddress + ':' + conn.remotePort;
connections[key] = conn;
2016-07-26 11:57:50 +02:00
conn.on('close', function() {
2018-05-04 17:02:27 +02:00
delete connections[key];
2017-07-25 17:12:18 +02:00
});
});
2016-07-26 11:57:50 +02:00
2017-07-25 17:12:18 +02:00
this.server.destroy = cb => {
this.server.close(cb);
2016-07-26 11:57:50 +02:00
2017-07-25 17:12:18 +02:00
for (let key in connections) {
connections[key].destroy();
2018-05-04 17:02:27 +02:00
}
2017-07-25 17:12:18 +02:00
};
}
async start(cb) {
try {
if (!this.isLoaded) {
await this.load();
}
this.app.use(this.router.routes()).use(this.router.allowedMethods());
// Launch server.
this.listen(cb);
} catch (err) {
this.stopWithError(err);
}
}
async destroy() {
if (_.has(this, 'server.destroy')) {
this.server.destroy();
}
2020-12-29 17:44:14 +01:00
await Promise.all(
Object.values(this.plugins).map(plugin => {
if (_.has(plugin, 'destroy') && typeof plugin.destroy === 'function') {
return plugin.destroy();
}
})
);
if (_.has(this, 'admin')) {
await this.admin.destroy();
}
this.eventHub.removeAllListeners();
if (_.has(this, 'db')) {
await this.db.destroy();
}
delete global.strapi;
}
/**
* Add behaviors to the server
*/
async listen(cb) {
const onListen = async err => {
if (err) return this.stopWithError(err);
// Is the project initialised?
const isInitialised = await utils.isInitialised(this);
// Should the startup message be displayed?
const hideStartupMessage = process.env.STRAPI_HIDE_STARTUP_MESSAGE
? process.env.STRAPI_HIDE_STARTUP_MESSAGE === 'true'
: false;
if (hideStartupMessage === false) {
if (!isInitialised) {
this.logFirstStartupMessage();
} else {
this.logStartupMessage();
}
}
// Emit started event.
const databaseClients = _.map(this.config.get('connections'), _.property('settings.client'));
await this.telemetry.send('didStartServer', { database: databaseClients });
if (cb && typeof cb === 'function') {
cb();
}
if (
(this.config.environment === 'development' &&
this.config.get('server.admin.autoOpen', true) !== false) ||
!isInitialised
) {
await utils.openBrowser.call(this);
}
};
const listenSocket = this.config.get('server.socket');
const listenErrHandler = err => onListen(err).catch(err => this.stopWithError(err));
if (listenSocket) {
this.server.listen(listenSocket, listenErrHandler);
} else {
this.server.listen(
this.config.get('server.port'),
this.config.get('server.host'),
listenErrHandler
);
}
2016-07-26 11:57:50 +02:00
}
stopWithError(err, customMessage) {
2019-04-05 16:11:09 +02:00
this.log.debug(`⛔️ Server wasn't able to start properly.`);
if (customMessage) {
this.log.error(customMessage);
}
2019-04-05 16:11:09 +02:00
this.log.error(err);
return this.stop();
}
stop(exitCode = 1) {
2017-07-25 17:12:18 +02:00
// Destroy server and available connections.
if (_.has(this, 'server.destroy')) {
this.server.destroy();
}
2017-09-04 15:38:29 +02:00
2019-05-13 17:32:52 +02:00
if (this.config.autoReload) {
process.send('stop');
}
2017-09-04 15:38:29 +02:00
2017-07-25 17:12:18 +02:00
// Kill process.
process.exit(exitCode);
2016-07-26 11:57:50 +02:00
}
2017-07-24 19:58:03 +02:00
async load() {
this.app.use(async (ctx, next) => {
if (ctx.request.url === '/_health' && ['HEAD', 'GET'].includes(ctx.request.method)) {
ctx.set('strapi', 'You are so French!');
ctx.status = 204;
2017-09-06 11:06:18 +02:00
} else {
await next();
}
});
const modules = await loadModules(this);
this.api = modules.api;
this.admin = modules.admin;
this.components = modules.components;
this.plugins = modules.plugins;
this.middleware = modules.middlewares;
this.hook = modules.hook;
2019-04-11 09:32:16 +02:00
await bootstrap(this);
2020-01-10 12:42:57 +01:00
// init webhook runner
this.webhookRunner = createWebhookRunner({
eventHub: this.eventHub,
logger: this.log,
configuration: this.config.get('server.webhooks', {}),
2020-01-10 12:42:57 +01:00
});
2018-04-24 12:30:43 +02:00
// Init core store
Fix/#3184/fix server crashs on database change (#5703) * Don't set connection field on create/edit operation on content-types & components Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Make sure that every component has a valid connection attribute Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Remove connection check on components load Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Remove default connections from core & webhook stores but make sure it's defined in the application lifecycle Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Fix component's associations that can be undefined instead of empty in populateBareAssociations Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Remove "default" connection from plugins' models Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Remove connection attribute from generated models (cli) Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Mutate each component instead of reassign Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Build core_store and webhook model based on the current config Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Add connection to templates conditionally (based on args) Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Set default value for description to undefined instead of empty string Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Remove unnecessary complexity Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Update getStarted models Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Fix attributes parsing for generate:model Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Removed tpl option from generate:model/api Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Remove tpl option from cli Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>
2020-04-07 16:31:44 +02:00
this.models['core_store'] = coreStoreModel(this.config);
this.models['strapi_webhooks'] = webhookModel(this.config);
2019-09-20 12:44:24 +02:00
this.db = createDatabaseManager(this);
await this.db.initialize();
2019-12-17 11:24:14 +01:00
this.store = createCoreStore({
environment: this.config.environment,
db: this.db,
});
this.webhookStore = createWebhookStore({ db: this.db });
2019-12-17 20:59:57 +01:00
await this.startWebhooks();
this.entityValidator = entityValidator;
this.entityService = createEntityService({
db: this.db,
eventHub: this.eventHub,
entityValidator: this.entityValidator,
});
this.telemetry = createTelemetry(this);
2017-07-25 17:12:18 +02:00
// Initialize hooks and middlewares.
2019-08-21 11:05:33 +02:00
await initializeMiddlewares.call(this);
await initializeHooks.call(this);
await this.runBootstrapFunctions();
await this.freeze();
this.isLoaded = true;
return this;
2016-07-26 11:57:50 +02:00
}
2019-12-17 20:59:57 +01:00
async startWebhooks() {
const webhooks = await this.webhookStore.findWebhooks();
webhooks.forEach(webhook => this.webhookRunner.add(webhook));
2019-12-17 20:59:57 +01:00
}
2017-08-02 11:25:18 +02:00
reload() {
2018-03-28 20:13:09 +02:00
const state = {
shouldReload: 0,
2018-03-28 20:13:09 +02:00
};
const reload = function() {
2018-08-31 13:47:10 +02:00
if (state.shouldReload > 0) {
// Reset the reloading state
2018-08-31 13:47:10 +02:00
state.shouldReload -= 1;
reload.isReloading = false;
2018-03-28 20:13:09 +02:00
return;
}
2019-05-13 17:32:52 +02:00
if (this.config.autoReload) {
2019-03-11 10:42:43 +01:00
this.server.close();
2018-01-04 16:03:34 +01:00
process.send('reload');
}
2017-08-02 11:25:18 +02:00
};
2018-03-28 20:13:09 +02:00
Object.defineProperty(reload, 'isWatching', {
configurable: true,
enumerable: true,
2018-05-18 14:22:24 +02:00
set: value => {
2018-03-28 20:13:09 +02:00
// Special state when the reloader is disabled temporarly (see GraphQL plugin example).
2018-08-31 13:47:10 +02:00
if (state.isWatching === false && value === true) {
state.shouldReload += 1;
}
2018-03-28 20:13:09 +02:00
state.isWatching = value;
},
get: () => {
return state.isWatching;
2018-05-18 14:22:24 +02:00
},
2018-03-28 20:13:09 +02:00
});
2017-08-02 11:25:18 +02:00
reload.isReloading = false;
reload.isWatching = true;
return reload;
2016-07-26 11:57:50 +02:00
}
2019-04-09 15:29:17 +02:00
async runBootstrapFunctions() {
const execBootstrap = async fn => {
2019-08-12 15:35:40 +02:00
if (!fn) return;
return fn();
};
2019-08-12 15:35:40 +02:00
// plugins bootstrap
2019-08-12 15:35:40 +02:00
const pluginBoostraps = Object.keys(this.plugins).map(plugin => {
return execBootstrap(_.get(this.plugins[plugin], 'config.functions.bootstrap')).catch(err => {
2019-08-12 15:35:40 +02:00
strapi.log.error(`Bootstrap function in plugin "${plugin}" failed`);
strapi.log.error(err);
strapi.stop();
2019-04-05 16:11:09 +02:00
});
2019-08-12 15:35:40 +02:00
});
await Promise.all(pluginBoostraps);
2019-04-05 16:11:09 +02:00
// user bootstrap
await execBootstrap(_.get(this.config, ['functions', 'bootstrap']));
// admin bootstrap : should always run after the others
const adminBootstrap = _.get(this.admin.config, 'functions.bootstrap');
return execBootstrap(adminBootstrap).catch(err => {
strapi.log.error(`Bootstrap function in admin failed`);
strapi.log.error(err);
strapi.stop();
});
2016-07-26 11:57:50 +02:00
}
2017-07-31 11:35:57 +02:00
async freeze() {
Object.freeze(this.config);
Object.freeze(this.dir);
Object.freeze(this.admin);
Object.freeze(this.plugins);
Object.freeze(this.api);
2016-07-26 11:57:50 +02:00
}
getModel(modelKey, plugin) {
2019-10-24 17:24:14 +02:00
return this.db.getModel(modelKey, plugin);
}
2019-05-16 21:37:45 +02:00
/**
* Binds queries with a specific model
* @param {string} entity - entity name
* @param {string} plugin - plugin name or null
*/
2019-07-15 15:33:42 +02:00
query(entity, plugin) {
2019-09-20 12:44:24 +02:00
return this.db.query(entity, plugin);
}
2016-07-26 11:57:50 +02:00
}
2019-04-11 09:32:16 +02:00
module.exports = options => {
const strapi = new Strapi(options);
global.strapi = strapi;
return strapi;
};