368 lines
9.7 KiB
JavaScript
Raw Normal View History

2015-10-01 00:30:16 +02:00
'use strict';
2017-07-24 19:58:03 +02:00
// Dependencies.
2016-11-04 16:00:19 +01:00
const Koa = require('koa');
2017-07-24 19:58:03 +02:00
const utils = require('./utils');
2017-07-25 17:12:18 +02:00
const http = require('http');
const path = require('path');
2017-08-02 13:17:40 +02:00
const cluster = require('cluster');
const { includes, get, assign, forEach, cloneDeep, toLower } = require('lodash');
2017-09-29 14:26:28 +02:00
const { logger, models } = require('strapi-utils');
2018-02-06 11:42:16 +01:00
const { nestedConfigurations, appConfigurations, apis, middlewares, hooks, plugins, admin, store } = require('./core');
2017-07-24 19:58:03 +02:00
const initializeMiddlewares = require('./middlewares');
const initializeHooks = require('./hooks');
const { EventEmitter } = require('events');
const stackTrace = require('stack-trace');
2015-10-01 00:30:16 +02:00
/**
* Construct an Strapi instance.
*
* @constructor
*/
2016-07-26 11:57:50 +02:00
class Strapi extends EventEmitter {
2016-07-06 15:51:52 +02:00
constructor() {
super();
this.setMaxListeners(100);
2016-07-06 15:51:52 +02:00
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();
2016-07-06 15:51:52 +02:00
// Mount the HTTP server.
2017-07-31 11:35:57 +02:00
this.server = http.createServer(this.app.callback());
2017-08-10 15:38:08 +02:00
// Logger.
this.log = logger;
2017-09-12 17:58:31 +02:00
// Utils.
this.utils = {
models
};
2017-07-31 11:35:57 +02:00
// Exclude EventEmitter, Koa and HTTP server to be freezed.
this.propertiesToNotFreeze = Object.keys(this);
// Expose `admin`.
this.admin = {};
// Expose `plugin`.
this.plugins = {};
2017-07-24 19:58:03 +02:00
// Default configurations.
this.config = {
launchedAt: Date.now(),
2017-07-24 19:58:03 +02:00
appPath: process.cwd(),
host: process.env.HOST || process.env.HOSTNAME || 'localhost',
port: process.env.PORT || 1337,
environment: toLower(process.env.NODE_ENV) || 'development',
2017-07-25 17:12:18 +02:00
environments: {},
2017-07-24 19:58:03 +02:00
paths: {
2017-07-25 17:12:18 +02:00
admin: 'admin',
2017-07-24 19:58:03 +02:00
api: 'api',
2017-07-25 17:12:18 +02:00
config: 'config',
2017-07-24 19:58:03 +02:00
controllers: 'controllers',
models: 'models',
plugins: 'plugins',
2017-07-25 17:12:18 +02:00
policies: 'policies',
tmp: '.tmp',
services: 'services',
static: 'public',
2017-07-24 19:58:03 +02:00
validators: 'validators',
2017-07-25 17:12:18 +02:00
views: 'views'
},
2017-07-26 18:53:48 +02:00
middleware: {},
hook: {},
functions: {},
routes: {}
2017-07-24 19:58:03 +02:00
};
// Bind context functions.
this.loadFile = utils.loadFile.bind(this);
2016-07-06 15:51:52 +02:00
}
async start(config = {}, cb) {
2017-07-24 19:58:03 +02:00
try {
this.config = assign(this.config, config)
2018-03-16 11:42:17 +01:00
// Emit starting event.
this.emit('server:starting');
2017-07-25 17:12:18 +02:00
// Enhance app.
await this.enhancer();
// Load the app.
2017-07-24 19:58:03 +02:00
await this.load();
2017-07-25 17:12:18 +02:00
// Run bootstrap function.
await this.bootstrap();
2017-07-31 11:35:57 +02:00
// Freeze object.
await this.freeze();
// Update source admin.
await admin.call(this);
2017-07-25 17:12:18 +02:00
// Launch server.
this.server.listen(this.config.port, err => {
2017-07-24 19:58:03 +02:00
if (err) {
2018-03-16 11:42:17 +01:00
this.log.debug(`Server wasn't able to start properly.`);
console.error(err);
return this.stop();
2017-07-24 19:58:03 +02:00
}
this.log.info('Server started in ' + this.config.appPath);
this.log.info('Your server is running at ' + this.config.url);
this.log.debug('Time: ' + new Date());
this.log.debug('Launched in: ' + (Date.now() - this.config.launchedAt) + ' ms');
this.log.debug('Environment: ' + this.config.environment);
this.log.debug('Process PID: ' + process.pid);
this.log.debug(`Version: ${this.config.info.strapi} (node v${this.config.info.node})`);
this.log.info('To shut down your server, press <CTRL> + C at any time');
2017-07-25 17:12:18 +02:00
2018-03-16 11:42:17 +01:00
// Emit started event.
this.emit('server:started');
2017-07-25 17:12:18 +02:00
if (cb && typeof cb === 'function') {
cb();
}
2017-07-24 19:58:03 +02:00
});
2018-03-16 11:42:17 +01:00
} catch (err) {
2017-07-25 17:12:18 +02:00
this.log.debug(`Server wasn't able to start properly.`);
2018-03-16 11:42:17 +01:00
console.error(err);
2017-07-25 17:12:18 +02:00
this.stop();
2017-07-24 19:58:03 +02:00
}
2016-07-26 11:57:50 +02:00
}
2017-07-25 17:12:18 +02:00
async enhancer() {
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
2017-07-25 17:12:18 +02:00
conn.on('close', function() {
delete connections[key];
});
});
2016-07-26 11:57:50 +02:00
2018-03-16 11:42:17 +01:00
this.server.on('error', err => {
if (err.code === 'EADDRINUSE') {
this.log.debug(`Server wasn't able to start properly.`);
this.log.error(`The port ${err.port} is already used by another application.`);
this.stop();
return;
}
console.error(err);
});
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();
};
};
2016-07-26 11:57:50 +02:00
}
stop() {
2017-07-25 17:12:18 +02:00
// Destroy server and available connections.
2017-07-24 19:58:03 +02:00
this.server.destroy();
2017-09-04 15:38:29 +02:00
2018-01-04 16:03:34 +01:00
if (cluster.isWorker && this.config.environment === 'development' && get(this.config, 'currentEnvironment.server.autoReload.enabled', true) === true) {
process.send('stop');
}
2017-09-04 15:38:29 +02:00
2017-07-25 17:12:18 +02:00
// Kill process.
process.exit(0);
2016-07-26 11:57:50 +02:00
}
2017-07-24 19:58:03 +02:00
async load() {
this.app.use(async (ctx, next) => {
2017-09-07 16:35:12 +02:00
if (ctx.request.url === '/_health' && ctx.request.method === 'HEAD') {
ctx.set('strapi', 'You are so French!');
ctx.status = 204;
2017-09-06 11:06:18 +02:00
} else {
await next();
}
});
2017-07-24 19:58:03 +02:00
// Create AST.
await Promise.all([
nestedConfigurations.call(this),
apis.call(this),
middlewares.call(this),
hooks.call(this)
]);
2017-07-25 17:12:18 +02:00
// Populate AST with configurations.
2017-07-24 19:58:03 +02:00
await appConfigurations.call(this);
// Usage.
await utils.usage.call(this);
2018-02-06 11:42:16 +01:00
// Init core store manager
await store.pre.call(this);
2017-07-25 17:12:18 +02:00
// Initialize hooks and middlewares.
2017-07-24 19:58:03 +02:00
await Promise.all([
initializeMiddlewares.call(this),
initializeHooks.call(this)
]);
2018-02-06 11:42:16 +01:00
// Core store post middleware and hooks init validation.
await store.post.call(this);
// Harmonize plugins configuration.
await plugins.call(this);
2016-07-26 11:57:50 +02:00
}
2017-08-02 11:25:18 +02:00
reload() {
2018-03-28 20:13:09 +02:00
const state = {
shouldReload: true
};
2018-01-04 16:03:34 +01:00
const reload = function () {
2018-03-28 20:13:09 +02:00
if (state.shouldReload === false) {
return;
}
2018-01-04 16:03:34 +01:00
if (cluster.isWorker && this.config.environment === 'development' && get(this.config, 'currentEnvironment.server.autoReload.enabled', true) === true) {
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,
set: (value) => {
// Special state when the reloader is disabled temporarly (see GraphQL plugin example).
state.shouldReload = !(state.isWatching === false && value === true);
state.isWatching = value;
return value;
}
});
2017-08-02 11:25:18 +02:00
reload.isReloading = false;
reload.isWatching = true;
return reload;
2016-07-26 11:57:50 +02:00
}
2017-07-25 17:12:18 +02:00
async bootstrap() {
2017-11-20 14:35:24 +01:00
const execBootstrap = (fn) => !fn ? Promise.resolve() : new Promise((resolve, reject) => {
const timeoutMs = this.config.bootstrapTimeout || 3500;
const timer = setTimeout(() => {
this.log.warn(`Bootstrap is taking unusually long to execute its callback ${timeoutMs} miliseconds).`);
this.log.warn('Perhaps you forgot to call it?');
}, timeoutMs);
2017-11-17 11:17:20 +01:00
2017-11-20 14:35:24 +01:00
let ranBootstrapFn = false;
2017-11-17 11:17:20 +01:00
2017-11-20 14:35:24 +01:00
try {
fn(err => {
2017-07-25 17:12:18 +02:00
if (ranBootstrapFn) {
2017-11-20 14:35:24 +01:00
this.log.error('You called the callback in `strapi.config.boostrap` more than once!');
2017-07-25 17:12:18 +02:00
2017-11-20 14:35:24 +01:00
return reject();
2017-07-25 17:12:18 +02:00
}
ranBootstrapFn = true;
clearTimeout(timer);
2017-11-20 14:35:24 +01:00
return resolve(err);
});
} catch (e) {
if (ranBootstrapFn) {
this.log.error('The bootstrap function threw an error after its callback was called.');
return reject(e);
2017-07-25 17:12:18 +02:00
}
2017-11-20 14:35:24 +01:00
ranBootstrapFn = true;
clearTimeout(timer);
2017-07-25 17:12:18 +02:00
2017-11-20 14:35:24 +01:00
return resolve(e);
2017-07-25 17:12:18 +02:00
}
});
2017-11-17 11:17:20 +01:00
2017-11-20 14:35:24 +01:00
return Promise.all(
Object.values(this.plugins)
.map(x => execBootstrap(get(x, 'config.functions.bootstrap')))
).then(() => execBootstrap(this.config.functions.bootstrap));
2016-07-26 11:57:50 +02:00
}
2017-07-31 11:35:57 +02:00
async freeze() {
const propertiesToNotFreeze = this.propertiesToNotFreeze || [];
// Remove object from tree.
delete this.propertiesToNotFreeze;
return Object.keys(this).filter(x => !includes(propertiesToNotFreeze, x)).forEach(key => {
Object.freeze(this[key]);
});
2016-07-26 11:57:50 +02:00
}
2017-11-15 16:59:12 +01:00
query(entity, plugin) {
if (!entity) {
return this.log.error(`You can't call the query method without passing the model's name as a first argument.`);
}
const model = entity.toLowerCase();
2017-11-16 14:12:03 +01:00
const Model = get(strapi.plugins, [plugin, 'models', model]) || get(strapi, ['models', model]) || undefined;
2017-11-15 16:59:12 +01:00
if (!Model) {
return this.log.error(`The model ${model} can't be found.`);
}
2017-11-15 16:59:12 +01:00
const connector = Model.orm;
if (!connector) {
return this.log.error(`Impossible to determine the use ORM for the model ${model}.`);
}
// Get stack trace.
const stack = stackTrace.get()[1];
const file = stack.getFileName();
const method = stack.getFunctionName();
// Extract plugin path.
2017-10-16 15:09:43 +02:00
let pluginPath = undefined;
if (file.indexOf('strapi-plugin-') !== -1) {
pluginPath = file.split(path.sep).filter(x => x.indexOf('strapi-plugin-') !== -1)[0];
2017-10-23 11:30:36 +02:00
} else if (file.indexOf(path.sep + 'plugins' + path.sep) !== -1) {
2017-10-16 15:09:43 +02:00
const pathTerms = file.split(path.sep);
const index = pathTerms.indexOf('plugins');
if (index !== -1) {
pluginPath = pathTerms[index + 1];
}
}
if (!pluginPath) {
return this.log.error('Impossible to find the plugin where `strapi.query` has been called.');
}
// Get plugin name.
const pluginName = pluginPath.replace('strapi-plugin-', '').toLowerCase();
const queries = get(this.plugins, `${pluginName}.config.queries.${connector}`);
if (!queries) {
return this.log.error(`There is no query available for the model ${model}.`);
}
// Bind queries with the current model to allow the use of `this`.
const bindQueries = Object.keys(queries).reduce((acc, current) => {
2017-11-15 16:59:12 +01:00
return acc[current] = queries[current].bind(Model), acc;
2018-02-27 16:53:06 +01:00
}, {
orm: connector,
primaryKey: Model.primaryKey
});
return bindQueries;
}
2016-07-26 11:57:50 +02:00
}
module.exports = new Strapi();