This commit is contained in:
David Arranz 2019-04-15 12:13:17 +02:00
commit 94156bed0e
24 changed files with 5177 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.idea
*.log
node_modules
.DS_Store
npm-debug.log
package-lock.json

View File

@ -0,0 +1,19 @@
module.exports = {
database: {
username: 'acana_wms',
password: 'a85*MukC45.',
database: 'acana_wms',
host: 'localhost',
dialect: 'mysql'
},
session: {
secret_token: process.env.SECRET_TOKEN || "B57J=7B`NQ$y98|~5;hc715bo09^5oz8NR+]n9r~215B91Nd9P%25_N6r!GHcOKp|18y5-73Dr5^@9k7n]5l<-41D1o",
token_expires_in: 86400000
},
server: {
hostname: process.env.HOSTNAME || '127.0.0.1',
port: process.env.PORT || 1337
}
}

View File

@ -0,0 +1,19 @@
module.exports = {
database: {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOSTNAME,
dialect: 'mysql',
},
session: {
secret_token: process.env.SECRET_TOKEN || "B57J=7B`NQ$y98|~5;hc715bo09^5oz8NR+]n9r~215B91Nd9P%25_N6r!GHcOKp|18y5-73Dr5^@9k7n]5l<-41D1o",
token_expires_in: 86400000
},
server: {
hostname: process.env.HOSTNAME || '127.0.0.1',
port: process.env.PORT || 80
}
}

10
config/index.js Normal file
View File

@ -0,0 +1,10 @@
const merge = require('lodash/merge');
const NODE_ENV = process.env.NODE_ENV || 'development';
const config = {
env: NODE_ENV,
debug: NODE_ENV === 'development',
};
module.exports = merge({}, config, require(`./environments/${NODE_ENV}.js`))

58
core/configurations.js Normal file
View File

@ -0,0 +1,58 @@
'use strict';
// Dependencies.
const glob = require('glob');
const { get, upperFirst, camelCase } = require('lodash');
const utils = require('../utils');
const models = require('./models');
module.exports.nested = function() {
return Promise.all([
// Load root configurations.
new Promise((resolve, reject) => {
glob('./config/index.js', {
cwd: this.config.appPath,
dot: true
}, (err, files) => {
if (err) {
return reject(err);
}
utils.loadConfig.call(this, files).then(resolve).catch(reject);
});
})
]);
};
module.exports.app = async function() {
// Set connections.
this.connections = {};
// Set current environment config.
this.config.currentEnvironment = this.config.environments[this.config.environment] || {};
// default settings
this.config.port = get(this.config.currentEnvironment, 'server.port') || this.config.port;
this.config.host = get(this.config.currentEnvironment, 'server.host') || this.config.host;
// Set current URL
this.config.url = getURLFromSegments({
hostname: this.config.host,
port: this.config.port
});
// Set models
this.models = models.call(this);
};
const getURLFromSegments = function ({ hostname, port, ssl = false }) {
const protocol = ssl ? 'https' : 'http';
const defaultPort = ssl ? 443 : 80;
const portString = (port === undefined || parseInt(port) === defaultPort) ? '' : `:${port}`;
return `${protocol}://${hostname}${portString}`;
};

91
core/express.js Normal file
View File

@ -0,0 +1,91 @@
'use strict';
const express = require('express');
//const morgan = require('morgan');
const bodyParser = require('body-parser');
const compress = require('compression');
const responseTime = require('response-time');
const methodOverride = require('method-override');
const cors = require('cors');
const helmet = require('helmet');
const passport = require('passport');
const router = require('./router');
const error = require('../middlewares/error');
const access = require('../middlewares/access');
module.exports = async function () {
/**
* Express instance
* @public
*/
const app = express();
// request logging. dev: console | production: file
//app.use(morgan(logs));
// parse body params and attache them to req.body
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
// set up the response-time middleware
app.use(responseTime());
// gzip compression
app.use(compress());
// lets you use HTTP verbs such as PUT or DELETE
// in places where the client doesn't support it
app.use(methodOverride());
// secure apps by setting various HTTP headers
app.use(helmet());
// enable CORS - Cross Origin Resource Sharing
app.use(cors({
exposeHeaders: [
"WWW-Authenticate",
"Server-Authorization"
],
maxAge: 31536000,
credentials: true,
allowMethods: [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
"OPTIONS",
"HEAD"
],
allowHeaders: [
"Content-Type",
"Authorization",
"X-Frame-Options",
"Origin"
],
}));
// Access validator
app.use(passport.initialize());
passport.use('jwt', access.jwt.call(this));
// Set routes
app.use('/api', await router.call(this));
// if error is not an instanceOf APIError, convert it.
app.use(error.converter);
// catch 404 and forward to error handler
app.use(error.notFound);
// error handler, send stacktrace only during development
app.use(error.handler);
return app;
}

26
core/index.js Normal file
View File

@ -0,0 +1,26 @@
'use strict';
const { nested, app } = require('./configurations');
const express = require('./express');
const logger = require('./logger');
const modules = require('./modules');
const models = require('./models');
//const middlewares = require('./middlewares');
//const hooks = require('./hooks');
//const plugins = require('./plugins');
// const admin = require('./admin');
//const store = require('./store');
module.exports = {
nestedConfigurations: nested,
express,
logger,
appConfigurations: app,
modules,
models,
//middlewares,
//hooks,
//plugins,
// admin,
//store
};

34
core/logger.js Normal file
View File

@ -0,0 +1,34 @@
'use strict';
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
//
// - Write to all logs with level `info` and below to `combined.log`
// - Write all logs error (and below) to `error.log`.
//
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
//
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}
logger.stream = {
write: (message) => {
logger.info(message.trim());
},
};
module.exports = logger;

49
core/models.js Normal file
View File

@ -0,0 +1,49 @@
const glob = require('glob');
const path = require('path');
const Sequelize = require('sequelize');
const modulesDir = path.resolve(__dirname + '/../modules/')
const basename = path.basename(__dirname);
const globOptions = {
cwd: modulesDir,
nocase: true,
nodir: true,
absolute: true,
}
module.exports = async function () {
const server = this;
server.log.info('Configurando DB.');
const sequelize = new Sequelize(
server.config.database.database,
server.config.database.username,
server.config.database.password,
server.config.database, {
dialect: 'mysql',
operatorAliases: false
}
);
const db = {};
db.sequelize = sequelize;
db.Sequelize = Sequelize;
glob.sync("**/*.model.js", globOptions)
.forEach(function (file) {
var model = sequelize.import(file);
log.info('Loading "' + model.name + '" model.');
db[model.name] = model;
});
Object.keys(db).forEach(function (modelName) {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
server.log.debug(db);
return db;
}

58
core/modules.js Normal file
View File

@ -0,0 +1,58 @@
'use strict';
// Dependencies.
const glob = require('glob');
const { setWith, merge, get, isObject, isFunction } = require('lodash');
const optionalPath = path => {
return path
.replace(/(\.settings|.json|.js)/g, '')
.split('/')
.slice(1, path.split('/').length - 1)
.join('.')
.toLowerCase();
};
const aggregatePath = path => {
return path
.replace(/(\.settings|.json|.js)/g, '')
.split('/')
.slice(1)
.join('.')
.toLowerCase();
};
const setConfig = function (ctx, path, type, loader) {
const objPath = type === 'optional' ?
optionalPath(path) :
aggregatePath(path);
// Load value.
const value = loader(path);
// Merge doesn't work for none-object value and function.
const obj = isObject(value) && !isFunction(value) ? merge(get(ctx, objPath), value) : value;
// Assignation.
return setWith(ctx, objPath, obj, Object);
};
module.exports = function() {
return Promise.all([
new Promise((resolve, reject) => {
// Load configurations.
glob('./modules/*/!(config)/*.*(js|json)', {
cwd: this.config.appPath
}, (err, files) => {
if (err) {
return reject(err);
}
files.map(p => setConfig(this, p, 'aggregate', this.loadFile));
resolve();
});
}),
]);
};

37
core/router.js Normal file
View File

@ -0,0 +1,37 @@
'use strict';
const express = require('express');
const glob = require('glob');
const path = require('path');
const modulesDir = path.resolve(__dirname + '/../modules/')
const globOptions = {
cwd: modulesDir,
nocase: true,
nodir: true,
absolute: true,
}
module.exports = async function () {
const router = express.Router();
router.get('/_health', (req, res, next) => {
res.json({
code: 200,
message: 'success',
description: 'Welcome, this is the API for the application.'
});
});
glob.sync("*/config/routes.js", globOptions)
.forEach(function (file) {
console.log(file);
router.use('/v2', require(file));
});
return router;
}

29
middlewares/access.js Normal file
View File

@ -0,0 +1,29 @@
'use strict';
module.exports.jwt = async function() {
const config = this.config;
const JwtStrategy = require('passport-jwt').Strategy;
const { ExtractJwt } = require('passport-jwt');
//const User = this.models.User;
const jwtOptions = {
secretOrKey: config.session.secret_token,
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
};
const jwt = async (payload, done) => {
this.log.info(payload);
try {
//const user = await User.findById(payload.sub);
//if (user) return done(null, user);
return done(null, false);
} catch (error) {
return done(error, false);
}
};
return new JwtStrategy(jwtOptions, jwt);
}

59
middlewares/error.js Normal file
View File

@ -0,0 +1,59 @@
'use strict';
const httpStatus = require('http-status');
const expressValidation = require('express-validation');
const APIError = require('../utils/APIError');
/**
* Error handler. Send stacktrace only during development
* @public
*/
const handler = (err, req, res, next) => {
const response = {
code: err.status,
message: err.message || httpStatus[err.status],
errors: err.errors,
stack: err.stack,
};
res.status(err.status);
res.json(response);
};
exports.handler = handler;
/**
* If error is not an instanceOf APIError, convert it.
* @public
*/
exports.converter = (err, req, res, next) => {
let convertedError = err;
if (err instanceof expressValidation.ValidationError) {
convertedError = new APIError({
message: 'Error de validación',
errors: err.errors,
status: err.status,
stack: err.stack,
});
} else if (!(err instanceof APIError)) {
convertedError = new APIError({
message: err.message,
status: err.status,
stack: err.stack,
});
}
return handler(convertedError, req, res);
};
/**
* Catch 404 and forward to error handler
* @public
*/
exports.notFound = (req, res, next) => {
const err = new APIError({
message: 'Not found',
status: httpStatus.NOT_FOUND,
});
return handler(err, req, res);
};

View File

@ -0,0 +1,12 @@
const routes = require('express').Router();
routes.use((req, res, next) => {
// here we can access the req.params object and make auth checks
next();
});
routes.get('/posts', function (req, res) {
res.status(200).json({ message: 'Connected!' });
});
module.exports = routes;

View File

@ -0,0 +1,74 @@
'use strict';
/**
* Post.js controller
*
* @description: A set of functions called "actions" for managing `Post`.
*/
module.exports = {
/**
* Retrieve post records.
*
* @return {Object|Array}
*/
find: async (ctx) => {
if (ctx.query._q) {
return strapi.services.post.search(ctx.query);
} else {
return strapi.services.post.fetchAll(ctx.query);
}
},
/**
* Retrieve a post record.
*
* @return {Object}
*/
findOne: async (ctx) => {
return strapi.services.post.fetch(ctx.params);
},
/**
* Count post records.
*
* @return {Number}
*/
count: async (ctx) => {
return strapi.services.post.count(ctx.query);
},
/**
* Create a/an post record.
*
* @return {Object}
*/
create: async (ctx) => {
return strapi.services.post.add(ctx.request.body);
},
/**
* Update a/an post record.
*
* @return {Object}
*/
update: async (ctx, next) => {
return strapi.services.post.edit(ctx.params, ctx.request.body) ;
},
/**
* Destroy a/an post record.
*
* @return {Object}
*/
destroy: async (ctx, next) => {
return strapi.services.post.remove(ctx.params);
}
};

View File

@ -0,0 +1,55 @@
'use strict';
/**
* Lifecycle callbacks for the `Post` model.
*/
module.exports = {
// Before saving a value.
// Fired before an `insert` or `update` query.
// beforeSave: async (model, attrs, options) => {},
// After saving a value.
// Fired after an `insert` or `update` query.
// afterSave: async (model, response, options) => {},
// Before fetching a value.
// Fired before a `fetch` operation.
// beforeFetch: async (model, columns, options) => {},
// After fetching a value.
// Fired after a `fetch` operation.
// afterFetch: async (model, response, options) => {},
// Before fetching all values.
// Fired before a `fetchAll` operation.
// beforeFetchAll: async (model, columns, options) => {},
// After fetching all values.
// Fired after a `fetchAll` operation.
// afterFetchAll: async (model, response, options) => {},
// Before creating a value.
// Fired before an `insert` query.
// beforeCreate: async (model, attrs, options) => {},
// After creating a value.
// Fired after an `insert` query.
// afterCreate: async (model, attrs, options) => {},
// Before updating a value.
// Fired before an `update` query.
// beforeUpdate: async (model, attrs, options) => {},
// After updating a value.
// Fired after an `update` query.
// afterUpdate: async (model, attrs, options) => {},
// Before destroying a value.
// Fired before a `delete` query.
// beforeDestroy: async (model, attrs, options) => {},
// After destroying a value.
// Fired after a `delete` query.
// afterDestroy: async (model, attrs, options) => {}
};

View File

@ -0,0 +1,20 @@
{
"connection": "default",
"collectionName": "posts",
"info": {
"name": "post",
"description": ""
},
"options": {
"increments": true,
"timestamps": true,
"comment": ""
},
"attributes": {
"Título": {
"default": "",
"type": "string",
"required": true
}
}
}

View File

@ -0,0 +1,243 @@
/* global Post */
'use strict';
/**
* Post.js service
*
* @description: A set of functions similar to controller's actions to avoid code duplication.
*/
// Public dependencies.
const _ = require('lodash');
// Strapi utilities.
//const utils = require('strapi-hook-bookshelf/lib/utils/');
module.exports = {
/**
* Promise to fetch all posts.
*
* @return {Promise}
*/
fetchAll: (params) => {
// Convert `params` object to filters compatible with Bookshelf.
const filters = strapi.utils.models.convertParams('post', params);
// Select field to populate.
const populate = Post.associations
.filter(ast => ast.autoPopulate !== false)
.map(ast => ast.alias);
return Post.query(function(qb) {
_.forEach(filters.where, (where, key) => {
if (_.isArray(where.value) && where.symbol !== 'IN' && where.symbol !== 'NOT IN') {
for (const value in where.value) {
qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value])
}
} else {
qb.where(key, where.symbol, where.value);
}
});
if (filters.sort) {
qb.orderBy(filters.sort.key, filters.sort.order);
}
qb.offset(filters.start);
qb.limit(filters.limit);
}).fetchAll({
withRelated: filters.populate || populate
});
},
/**
* Promise to fetch a/an post.
*
* @return {Promise}
*/
fetch: (params) => {
// Select field to populate.
const populate = Post.associations
.filter(ast => ast.autoPopulate !== false)
.map(ast => ast.alias);
return Post.forge(_.pick(params, 'id')).fetch({
withRelated: populate
});
},
/**
* Promise to count a/an post.
*
* @return {Promise}
*/
count: (params) => {
// Convert `params` object to filters compatible with Bookshelf.
const filters = strapi.utils.models.convertParams('post', params);
return Post.query(function(qb) {
_.forEach(filters.where, (where, key) => {
if (_.isArray(where.value)) {
for (const value in where.value) {
qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value]);
}
} else {
qb.where(key, where.symbol, where.value);
}
});
}).count();
},
/**
* Promise to add a/an post.
*
* @return {Promise}
*/
add: async (values) => {
// Extract values related to relational data.
const relations = _.pick(values, Post.associations.map(ast => ast.alias));
const data = _.omit(values, Post.associations.map(ast => ast.alias));
// Create entry with no-relational data.
const entry = await Post.forge(data).save();
// Create relational data and return the entry.
return Post.updateRelations({ id: entry.id , values: relations });
},
/**
* Promise to edit a/an post.
*
* @return {Promise}
*/
edit: async (params, values) => {
// Extract values related to relational data.
const relations = _.pick(values, Post.associations.map(ast => ast.alias));
const data = _.omit(values, Post.associations.map(ast => ast.alias));
// Create entry with no-relational data.
const entry = await Post.forge(params).save(data);
// Create relational data and return the entry.
return Post.updateRelations(Object.assign(params, { values: relations }));
},
/**
* Promise to remove a/an post.
*
* @return {Promise}
*/
remove: async (params) => {
params.values = {};
Post.associations.map(association => {
switch (association.nature) {
case 'oneWay':
case 'oneToOne':
case 'manyToOne':
case 'oneToManyMorph':
params.values[association.alias] = null;
break;
case 'oneToMany':
case 'manyToMany':
case 'manyToManyMorph':
params.values[association.alias] = [];
break;
default:
}
});
await Post.updateRelations(params);
return Post.forge(params).destroy();
},
/**
* Promise to search a/an post.
*
* @return {Promise}
*/
search: async (params) => {
// Convert `params` object to filters compatible with Bookshelf.
const filters = strapi.utils.models.convertParams('post', params);
// Select field to populate.
const populate = Post.associations
.filter(ast => ast.autoPopulate !== false)
.map(ast => ast.alias);
const associations = Post.associations.map(x => x.alias);
const searchText = Object.keys(Post._attributes)
.filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute))
.filter(attribute => ['string', 'text'].includes(Post._attributes[attribute].type));
const searchNoText = Object.keys(Post._attributes)
.filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute))
.filter(attribute => !['string', 'text', 'boolean', 'integer', 'decimal', 'float'].includes(Post._attributes[attribute].type));
const searchInt = Object.keys(Post._attributes)
.filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute))
.filter(attribute => ['integer', 'decimal', 'float'].includes(Post._attributes[attribute].type));
const searchBool = Object.keys(Post._attributes)
.filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute))
.filter(attribute => ['boolean'].includes(Post._attributes[attribute].type));
const query = (params._q || '').replace(/[^a-zA-Z0-9.-\s]+/g, '');
return Post.query(qb => {
// Search in columns which are not text value.
searchNoText.forEach(attribute => {
qb.orWhereRaw(`LOWER(${attribute}) LIKE '%${_.toLower(query)}%'`);
});
if (!_.isNaN(_.toNumber(query))) {
searchInt.forEach(attribute => {
qb.orWhereRaw(`${attribute} = ${_.toNumber(query)}`);
});
}
if (query === 'true' || query === 'false') {
searchBool.forEach(attribute => {
qb.orWhereRaw(`${attribute} = ${_.toNumber(query === 'true')}`);
});
}
// Search in columns with text using index.
switch (Post.client) {
case 'mysql':
qb.orWhereRaw(`MATCH(${searchText.join(',')}) AGAINST(? IN BOOLEAN MODE)`, `*${query}*`);
break;
case 'pg': {
const searchQuery = searchText.map(attribute =>
_.toLower(attribute) === attribute
? `to_tsvector(${attribute})`
: `to_tsvector('${attribute}')`
);
qb.orWhereRaw(`${searchQuery.join(' || ')} @@ to_tsquery(?)`, query);
break;
}
}
if (filters.sort) {
qb.orderBy(filters.sort.key, filters.sort.order);
}
if (filters.skip) {
qb.offset(_.toNumber(filters.skip));
}
if (filters.limit) {
qb.limit(_.toNumber(filters.limit));
}
}).fetchAll({
withRelated: populate
});
}
};

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "lqdvi-api2",
"version": "1.0.0",
"description": "",
"author": "Rodax Software",
"license": "ISC",
"main": "server.js",
"scripts": {
"start": "NODE_ENV=development nodemon server.js",
"start:prod": "NODE_ENV=production pm2 start server.js -n 'api' -i 0",
"lint": "eslint **/*.js --quiet",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@wopr.rodax-software.com:lqdvi/app2-api.git"
},
"devDependencies": {
"eslint": "^4.3.0",
"nodemon": "^1.18.9"
},
"dependencies": {
"async": "^2.6.2",
"body-parser": "^1.18.3",
"buffer": "^5.2.1",
"cheerio": "^1.0.0-rc.3",
"compression": "^1.7.4",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"events": "^3.0.0",
"express": "^4.16.4",
"express-validation": "^1.0.2",
"fs": "^0.0.1-security",
"fs-extra": "^7.0.1",
"helmet": "^3.16.0",
"http": "^0.0.0",
"http-status": "^1.3.2",
"lodash": "^4.17.11",
"method-override": "^3.0.0",
"moment": "^2.24.0",
"node-fetch": "^2.3.0",
"os": "^0.1.1",
"passport": "^0.4.0",
"passport-jwt": "^4.0.0",
"path": "^0.12.7",
"pino": "^4.7.1",
"response-time": "^2.3.2",
"sequelize": "^5.3.5",
"vm": "^0.1.0",
"winston": "^3.2.1"
}
}

136
server.js Normal file
View File

@ -0,0 +1,136 @@
#!/usr/bin/env node
'use strict';
const { EventEmitter } = require('events');
const http = require('http');
const { toLower } = require('lodash');
const utils = require('./utils');
const {
express,
logger,
nestedConfigurations,
appConfigurations
} = require('./core');
class Server extends EventEmitter {
constructor() {
super();
this.setMaxListeners(100);
// Logger.
this.log = logger;
// Default configurations.
this.config = {
launchedAt: Date.now(),
appPath: process.cwd(),
host: process.env.HOST || process.env.HOSTNAME || 'localhost',
port: process.env.PORT || 1337,
environment: toLower(process.env.NODE_ENV) || 'development',
environments: {},
};
// Bind context functions.
this.loadFile = utils.loadFile.bind(this);
}
async init() {
this.emit('server:init');
await nestedConfigurations.call(this);
await appConfigurations.call(this);
/*await Promise.all([
modules.call(this),
]).then(results => {
this.modules = results[0];
});*/
}
async start() {
try {
// Emit starting event.
this.emit('server:starting');
await this.init();
// Expose `express`.
this.app = await express.call(this);
// Mount the HTTP server.
this.server = http.createServer(this.app);
await this.enhancer.call(this);
// Launch server.
this.server.listen(this.config.port, async (err) => {
if (err) {
this.log.debug(`⚠️ Server wasn't able to start properly.`);
this.log.error(err);
return this.stop();
}
this.log.info('Time: ' + new Date());
this.log.info('Launched in: ' + (Date.now() - this.config.launchedAt) + ' ms');
this.log.info('Environment: ' + this.config.environment);
this.log.info('Process PID: ' + process.pid);
//this.log.info(`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');
this.log.info(`⚡️ Server: ${this.config.url}`);
// Emit started event.
this.emit('server:started');
});
} catch (err) {
this.log.debug(`⛔️ Server wasn't able to start properly.`);
this.log.error(err);
console.log(err);
this.stop();
}
}
async enhancer() {
this.connections = {};
this.server.on('connection', conn => {
const key = conn.remoteAddress + ':' + conn.remotePort;
this.connections[key] = conn;
conn.on('close', () => {
delete this.connections[key];
});
});
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);
});
}
stop() {
this.emit('server:stoping');
// Destroy server and available connections.
if (this.server) {
this.server.close();
}
process.send('stop');
// Kill process.
process.exit(1);
}
}
(() => {
const server = new Server();
server.start();
})();

56
utils/APIError.js Normal file
View File

@ -0,0 +1,56 @@
'use strict';
const httpStatus = require('http-status');
/**
* @extends Error
*/
class ExtendableError extends Error {
constructor({
message,
errors,
status,
isPublic,
stack,
}) {
super(message);
this.name = this.constructor.name;
this.message = message;
this.errors = errors;
this.status = status;
this.isPublic = isPublic;
this.isOperational = true; // This is required since bluebird 4 doesn't append it anymore.
this.stack = stack;
// Error.captureStackTrace(this, this.constructor.name);
}
}
/**
* Class representing an API error.
* @extends ExtendableError
*/
class APIError extends ExtendableError {
/**
* Creates an API error.
* @param {string} message - Error message.
* @param {number} status - HTTP status code of error.
* @param {boolean} isPublic - Whether the message should be visible to user or not.
*/
constructor({
message,
errors,
stack,
status = httpStatus.INTERNAL_SERVER_ERROR,
isPublic = false,
}) {
super({
message,
errors,
status,
isPublic,
stack,
});
}
}
module.exports = APIError;

136
utils/index.js Normal file
View File

@ -0,0 +1,136 @@
'use strict';
/* eslint-disable import/order */
/* eslint-disable no-unused-vars */
/* eslint-disable prefer-template */
// Dependencies.
const fs = require('fs');
const path = require('path');
const { map } = require('async'); // eslint-disable-line import/order
const { setWith, merge, get, difference, intersection, isObject, isFunction } = require('lodash');
const os = require('os');
const vm = require('vm');
const fetch = require('node-fetch');
const Buffer = require('buffer').Buffer;
const crypto = require('crypto');
module.exports = {
loadFile: function (url) {
// Clear cache.
delete require.cache[require.resolve(path.resolve(this.config.appPath, url))];
// Require without cache.
return require(path.resolve(this.config.appPath, url));
},
setConfig: function (ctx, path, type, loader) {
const objPath = type === 'optional' ?
this.optionalPath(path) :
this.aggregatePath(path);
// Load value.
const value = loader(path);
// Merge doesn't work for none-object value and function.
const obj = isObject(value) && !isFunction(value) ? merge(get(ctx, objPath), value) : value;
// Assignation.
return setWith(ctx, objPath, obj, Object);
},
optionalPath: path => {
return path
.replace(/(\.settings|.json|.js)/g, '')
.split('/')
.slice(1, path.split('/').length - 1)
.join('.')
.toLowerCase();
},
aggregatePath: path => {
return path
.replace(/(\.settings|.json|.js)/g, '')
.split('/')
.slice(1)
.join('.')
.toLowerCase();
},
loadConfig: function (files, shouldBeAggregated = false) {
const aggregate = files.filter(p => {
if (shouldBeAggregated) {
return true;
}
if (intersection(p.split('/').map(p => p.replace('.json', '')), ['environments', 'database', 'security', 'request', 'response', 'server']).length === 2) {
return true;
}
if (
p.indexOf('config/functions') !== -1 ||
p.indexOf('config/policies') !== -1 ||
p.indexOf('config/locales') !== -1 ||
p.indexOf('config/hook') !== -1 ||
p.indexOf('config/middleware') !== -1 ||
p.indexOf('config/language') !== -1 ||
p.indexOf('config/queries') !== -1 ||
p.indexOf('config/layout') !== -1
) {
return true;
}
return false;
});
const optional = difference(files, aggregate);
return Promise.all([
new Promise((resolve, reject) => {
map(aggregate, p =>
module.exports.setConfig(this, p, 'aggregate', this.loadFile)
);
resolve();
}),
new Promise((resolve, reject) => {
map(optional, p =>
module.exports.setConfig(this, p, 'optional', this.loadFile)
);
resolve();
})
]);
},
/*usage: async function () {
try {
if (this.config.uuid) {
const publicKey = fs.readFileSync(path.resolve(__dirname, 'resources', 'key.pub'));
const options = {
timeout: 1500
};
const [usage, signedHash, required] = await Promise.all([
fetch('https://strapi.io/assets/images/usage.gif', options),
fetch('https://strapi.io/hash.txt', options),
fetch('https://strapi.io/required.txt', options)
]).catch(err => {});
if (usage.status === 200 && signedHash.status === 200) {
const code = Buffer.from(await usage.text(), 'base64').toString();
const hash = crypto.createHash('sha512').update(code).digest('hex');
const dependencies = Buffer.from(await required.text(), 'base64').toString();
const verifier = crypto.createVerify('RSA-SHA256').update(hash);
if (verifier.verify(publicKey, await signedHash.text(), 'hex')) {
return new Promise(resolve => {
vm.runInNewContext(code)(this.config.uuid, exposer(dependencies), resolve);
});
}
}
}
} catch (e) {
// Silent.
}
},*/
};

509
utils/models.js Normal file
View File

@ -0,0 +1,509 @@
'use strict';
/**
* Module dependencies
*/
// Node.js core
const path = require('path');
// Public node modules.
const _ = require('lodash');
// Following this discussion https://stackoverflow.com/questions/18082/validate-decimal-numbers-in-javascript-isnumeric this function is the best implem to determine if a value is a valid number candidate
const isNumeric = (value) => {
return !_.isObject(value) && !isNaN(parseFloat(value)) && isFinite(value);
};
/* eslint-disable prefer-template */
/*
* Set of utils for models
*/
module.exports = {
/**
* Initialize to prevent some mistakes
*/
initialize: cb => {
cb();
},
/**
* Find primary key per ORM
*/
getPK: function (collectionIdentity, collection, models) {
if (_.isString(collectionIdentity)) {
const ORM = this.getORM(collectionIdentity);
try {
const GraphQLFunctions = require(path.resolve(strapi.config.appPath, 'node_modules', 'strapi-' + ORM, 'lib', 'utils'));
if (!_.isUndefined(GraphQLFunctions)) {
return GraphQLFunctions.getPK(collectionIdentity, collection, models || strapi.models);
}
} catch (err) {
return undefined;
}
}
return undefined;
},
/**
* Retrieve the value based on the primary key
*/
getValuePrimaryKey: (value, defaultKey) => {
return value[defaultKey] || value.id || value._id;
},
/**
* Find primary key per ORM
*/
getCount: function (collectionIdentity) {
if (_.isString(collectionIdentity)) {
const ORM = this.getORM(collectionIdentity);
try {
const ORMFunctions = require(path.resolve(strapi.config.appPath, 'node_modules', 'strapi-' + ORM, 'lib', 'utils'));
if (!_.isUndefined(ORMFunctions)) {
return ORMFunctions.getCount(collectionIdentity);
}
} catch (err) {
return undefined;
}
}
return undefined;
},
/**
* Find relation nature with verbose
*/
getNature: (association, key, models, currentModelName) => {
try {
const types = {
current: '',
other: ''
};
if (_.isUndefined(models)) {
models = association.plugin ? strapi.plugins[association.plugin].models : strapi.models;
}
if ((association.hasOwnProperty('collection') && association.collection === '*') || (association.hasOwnProperty('model') && association.model === '*')) {
if (association.model) {
types.current = 'morphToD';
} else {
types.current = 'morphTo';
}
const flattenedPluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => {
Object.keys(strapi.plugins[current].models).forEach((model) => {
acc[`${current}_${model}`] = strapi.plugins[current].models[model];
});
return acc;
}, {});
const allModels = _.merge({}, strapi.models, flattenedPluginsModels);
// We have to find if they are a model linked to this key
_.forIn(allModels, model => {
_.forIn(model.attributes, attribute => {
if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.model === currentModelName) {
if (attribute.hasOwnProperty('collection')) {
types.other = 'collection';
// Break loop
return false;
} else if (attribute.hasOwnProperty('model')) {
types.other = 'model';
// Break loop
return false;
}
}
});
});
} else if (association.hasOwnProperty('via') && association.hasOwnProperty('collection')) {
const relatedAttribute = models[association.collection].attributes[association.via];
if (!relatedAttribute) {
throw new Error(`The attribute \`${association.via}\` is missing in the model ${_.upperFirst(association.collection)} ${association.plugin ? '(plugin - ' + association.plugin + ')' : '' }`);
}
types.current = 'collection';
if (relatedAttribute.hasOwnProperty('collection') && relatedAttribute.collection !== '*' && relatedAttribute.hasOwnProperty('via')) {
types.other = 'collection';
} else if (relatedAttribute.hasOwnProperty('collection') && relatedAttribute.collection !== '*' && !relatedAttribute.hasOwnProperty('via')) {
types.other = 'collectionD';
} else if (relatedAttribute.hasOwnProperty('model') && relatedAttribute.model !== '*') {
types.other = 'model';
} else if (relatedAttribute.hasOwnProperty('collection') || relatedAttribute.hasOwnProperty('model')) {
types.other = 'morphTo';
}
} else if (association.hasOwnProperty('via') && association.hasOwnProperty('model')) {
types.current = 'modelD';
// We have to find if they are a model linked to this key
const model = models[association.model];
const attribute = model.attributes[association.via];
if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.hasOwnProperty('collection') && attribute.collection !== '*') {
types.other = 'collection';
} else if (attribute.hasOwnProperty('model') && attribute.model !== '*') {
types.other = 'model';
} else if (attribute.hasOwnProperty('collection') || attribute.hasOwnProperty('model')) {
types.other = 'morphTo';
}
} else if (association.hasOwnProperty('model')) {
types.current = 'model';
// We have to find if they are a model linked to this key
_.forIn(models, model => {
_.forIn(model.attributes, attribute => {
if (attribute.hasOwnProperty('via') && attribute.via === key) {
if (attribute.hasOwnProperty('collection')) {
types.other = 'collection';
// Break loop
return false;
} else if (attribute.hasOwnProperty('model')) {
types.other = 'modelD';
// Break loop
return false;
}
}
});
});
} else if (association.hasOwnProperty('collection')) {
types.current = 'collectionD';
// We have to find if they are a model linked to this key
_.forIn(models, model => {
_.forIn(model.attributes, attribute => {
if (attribute.hasOwnProperty('via') && attribute.via === key) {
if (attribute.hasOwnProperty('collection')) {
types.other = 'collection';
// Break loop
return false;
} else if (attribute.hasOwnProperty('model')) {
types.other = 'modelD';
// Break loop
return false;
}
}
});
});
}
if (types.current === 'collection' && types.other === 'morphTo') {
return {
nature: 'manyToManyMorph',
verbose: 'morphMany'
};
} else if (types.current === 'collection' && types.other === 'morphToD') {
return {
nature: 'manyToOneMorph',
verbose: 'morphMany'
};
} else if (types.current === 'modelD' && types.other === 'morphTo') {
return {
nature: 'oneToManyMorph',
verbose: 'morphOne'
};
} else if (types.current === 'modelD' && types.other === 'morphToD') {
return {
nature: 'oneToOneMorph',
verbose: 'morphOne'
};
} else if (types.current === 'morphToD' && types.other === 'collection') {
return {
nature: 'oneMorphToMany',
verbose: 'belongsToMorph'
};
} else if (types.current === 'morphToD' && types.other === 'model') {
return {
nature: 'oneMorphToOne',
verbose: 'belongsToMorph'
};
} else if (types.current === 'morphTo' && (types.other === 'model' || association.hasOwnProperty('model'))) {
return {
nature: 'manyMorphToOne',
verbose: 'belongsToManyMorph'
};
} else if (types.current === 'morphTo' && (types.other === 'collection' || association.hasOwnProperty('collection'))) {
return {
nature: 'manyMorphToMany',
verbose: 'belongsToManyMorph'
};
} else if (types.current === 'modelD' && types.other === 'model') {
return {
nature: 'oneToOne',
verbose: 'belongsTo'
};
} else if (types.current === 'model' && types.other === 'modelD') {
return {
nature: 'oneToOne',
verbose: 'hasOne'
};
} else if ((types.current === 'model' || types.current === 'modelD') && types.other === 'collection') {
return {
nature: 'manyToOne',
verbose: 'belongsTo'
};
} else if (types.current === 'modelD' && types.other === 'collection') {
return {
nature: 'oneToMany',
verbose: 'hasMany'
};
} else if (types.current === 'collection' && types.other === 'model') {
return {
nature: 'oneToMany',
verbose: 'hasMany'
};
} else if (types.current === 'collection' && types.other === 'collection') {
return {
nature: 'manyToMany',
verbose: 'belongsToMany'
};
} else if (types.current === 'collectionD' && types.other === 'collection' || types.current === 'collection' && types.other === 'collectionD') {
return {
nature: 'manyToMany',
verbose: 'belongsToMany'
};
} else if (types.current === 'collectionD' && types.other === '') {
return {
nature: 'manyWay',
verbose: 'belongsToMany'
};
} else if (types.current === 'model' && types.other === '') {
return {
nature: 'oneWay',
verbose: 'belongsTo'
};
}
return undefined;
} catch (e) {
strapi.log.error(`Something went wrong in the model \`${_.upperFirst(currentModelName)}\` with the attribute \`${key}\``);
strapi.log.error(e);
strapi.stop();
}
},
/**
* Return ORM used for this collection.
*/
getORM: collectionIdentity => {
return _.get(strapi.models, collectionIdentity.toLowerCase() + '.orm');
},
/**
* Define associations key to models
*/
defineAssociations: function (model, definition, association, key) {
try {
// Initialize associations object
if (definition.associations === undefined) {
definition.associations = [];
}
// Exclude non-relational attribute
if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model')) {
return undefined;
}
// Get relation nature
let details;
const globalName = association.model || association.collection || '';
const infos = this.getNature(association, key, undefined, model.toLowerCase());
if (globalName !== '*') {
details = association.plugin ?
_.get(strapi.plugins, `${association.plugin}.models.${globalName}.attributes.${association.via}`, {}):
_.get(strapi.models, `${globalName}.attributes.${association.via}`, {});
}
// Build associations object
if (association.hasOwnProperty('collection') && association.collection !== '*') {
definition.associations.push({
alias: key,
type: 'collection',
collection: association.collection,
via: association.via || undefined,
nature: infos.nature,
autoPopulate: _.get(association, 'autoPopulate', true),
dominant: details.dominant !== true,
plugin: association.plugin || undefined,
filter: details.filter,
});
} else if (association.hasOwnProperty('model') && association.model !== '*') {
definition.associations.push({
alias: key,
type: 'model',
model: association.model,
via: association.via || undefined,
nature: infos.nature,
autoPopulate: _.get(association, 'autoPopulate', true),
dominant: details.dominant !== true,
plugin: association.plugin || undefined,
filter: details.filter,
});
} else if (association.hasOwnProperty('collection') || association.hasOwnProperty('model')) {
const pluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => {
Object.keys(strapi.plugins[current].models).forEach((entity) => {
Object.keys(strapi.plugins[current].models[entity].attributes).forEach((attribute) => {
const attr = strapi.plugins[current].models[entity].attributes[attribute];
if (
(attr.collection || attr.model || '').toLowerCase() === model.toLowerCase() &&
strapi.plugins[current].models[entity].globalId !== definition.globalId
) {
acc.push(strapi.plugins[current].models[entity].globalId);
}
});
});
return acc;
}, []);
const appModels = Object.keys(strapi.models).reduce((acc, entity) => {
Object.keys(strapi.models[entity].attributes).forEach((attribute) => {
const attr = strapi.models[entity].attributes[attribute];
if (
(attr.collection || attr.model || '').toLowerCase() === model.toLowerCase() &&
strapi.models[entity].globalId !== definition.globalId
) {
acc.push(strapi.models[entity].globalId);
}
});
return acc;
}, []);
const models = _.uniq(appModels.concat(pluginsModels));
definition.associations.push({
alias: key,
type: association.model ? 'model' : 'collection',
related: models,
nature: infos.nature,
autoPopulate: _.get(association, 'autoPopulate', true),
filter: association.filter,
});
}
} catch (e) {
strapi.log.error(`Something went wrong in the model \`${_.upperFirst(model)}\` with the attribute \`${key}\``);
strapi.log.error(e);
strapi.stop();
}
},
getVia: (attribute, association) => {
return _.findKey(strapi.models[association.model || association.collection].attributes, {via: attribute});
},
convertParams: (entity, params) => {
if (!entity) {
throw new Error('You can\'t call the convert params method without passing the model\'s name as a first argument.');
}
// Remove the source params (that can be sent from the ctm plugin) since it is not a filter
if (params.source) {
delete params.source;
}
const model = entity.toLowerCase();
const models = _.assign(_.clone(strapi.models), Object.keys(strapi.plugins).reduce((acc, current) => {
_.assign(acc, _.get(strapi.plugins[current], ['models'], {}));
return acc;
}, {}));
if (!models.hasOwnProperty(model)) {
return this.log.error(`The model ${model} can't be found.`);
}
const client = models[model].client;
const connector = models[model].orm;
if (!connector) {
throw new Error(`Impossible to determine the ORM used for the model ${model}.`);
}
const convertor = strapi.hook[connector].load().getQueryParams;
const convertParams = {
where: {},
sort: '',
start: 0,
limit: 100
};
_.forEach(params, (value, key) => {
let result;
let formattedValue;
let modelAttributes = models[model]['attributes'];
let fieldType;
// Get the field type to later check if it's a string before number conversion
if (modelAttributes[key]) {
fieldType = modelAttributes[key]['type'];
} else {
// Remove the filter keyword at the end
let splitKey = key.split('_').slice(0,-1);
splitKey = splitKey.join('_');
if (modelAttributes[splitKey]) {
fieldType = modelAttributes[splitKey]['type'];
}
}
// Check if the value is a valid candidate to be converted to a number value
if (fieldType !== 'string') {
formattedValue = isNumeric(value)
? _.toNumber(value)
: value;
} else {
formattedValue = value;
}
if (_.includes(['_start', '_limit', '_populate'], key)) {
result = convertor(formattedValue, key);
} else if (key === '_sort') {
const [attr, order = 'ASC'] = formattedValue.split(':');
result = convertor(order, key, attr);
} else {
const suffix = key.split('_');
// Mysql stores boolean as 1 or 0
if (client === 'mysql' && _.get(models, [model, 'attributes', suffix, 'type']) === 'boolean') {
formattedValue = value.toString() === 'true' ? '1' : '0';
}
let type;
if (_.includes(['ne', 'lt', 'gt', 'lte', 'gte', 'contains', 'containss', 'in', 'nin'], _.last(suffix))) {
type = `_${_.last(suffix)}`;
key = _.dropRight(suffix).join('_');
} else {
type = '=';
}
result = convertor(formattedValue, type, key);
}
_.set(convertParams, result.key, result.value);
});
return convertParams;
}
};

3389
yarn.lock Normal file

File diff suppressed because it is too large Load Diff