This commit is contained in:
David Arranz 2019-04-15 17:58:58 +02:00
parent 94156bed0e
commit 81d214b50a
23 changed files with 643 additions and 778 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ node_modules
.DS_Store .DS_Store
npm-debug.log npm-debug.log
package-lock.json package-lock.json
yarn.lock

View File

@ -1,10 +1,10 @@
module.exports = { module.exports = {
database: { database: {
username: 'acana_wms', username: 'lqdvi',
password: 'a85*MukC45.', password: 'Z286y386*a',
database: 'acana_wms', database: 'lqdvi_v2',
host: 'localhost', host: 'localhost',
dialect: 'mysql' dialect: 'mysql',
}, },
session: { session: {

View File

@ -1,58 +0,0 @@
'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}`;
};

View File

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

View File

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

View File

@ -2,6 +2,10 @@ const glob = require('glob');
const path = require('path'); const path = require('path');
const Sequelize = require('sequelize'); const Sequelize = require('sequelize');
const config = require('../config');
const log = require('./logger');
const modulesDir = path.resolve(__dirname + '/../modules/') const modulesDir = path.resolve(__dirname + '/../modules/')
const basename = path.basename(__dirname); const basename = path.basename(__dirname);
const globOptions = { const globOptions = {
@ -11,39 +15,33 @@ const globOptions = {
absolute: true, absolute: true,
} }
module.exports = async function () { log.info('Configurando DB.');
const server = this;
server.log.info('Configurando DB.'); const sequelize = new Sequelize(
config.database.database,
config.database.username,
config.database.password,
config.database, {
dialect: 'mysql',
operatorAliases: false
}
);
const sequelize = new Sequelize( const db = {};
server.config.database.database, db.sequelize = sequelize;
server.config.database.username, db.Sequelize = Sequelize;
server.config.database.password,
server.config.database, {
dialect: 'mysql',
operatorAliases: false
}
);
const db = {}; glob.sync("**/*.model.js", globOptions)
db.sequelize = sequelize; .forEach(function (file) {
db.Sequelize = Sequelize; var model = sequelize.import(file);
log.info('Loading "' + model.name + '" model.');
glob.sync("**/*.model.js", globOptions) db[model.name] = model;
.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); Object.keys(db).forEach(function (modelName) {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
return db; module.exports = db;
}

View File

@ -1,58 +0,0 @@
'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();
});
}),
]);
};

View File

@ -12,8 +12,7 @@ const globOptions = {
absolute: true, absolute: true,
} }
module.exports = function () {
module.exports = async function () {
const router = express.Router(); const router = express.Router();
router.get('/_health', (req, res, next) => { router.get('/_health', (req, res, next) => {
@ -24,9 +23,8 @@ module.exports = async function () {
}); });
}); });
glob.sync("*/config/routes.js", globOptions) glob.sync("**/*.routes.js", globOptions)
.forEach(function (file) { .forEach(function (file) {
console.log(file);
router.use('/v2', require(file)); router.use('/v2', require(file));
}); });

View File

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

View File

@ -0,0 +1,16 @@
const routes = require('express').Router();
const models = require('../../core/models');
const postService = require('./post.service')(models.Post);
const postController = require('./post.controller')(postService);
routes.use((req, res, next) => {
// here we can access the req.params object and make auth checks
next();
});
routes.get('/posts', function (req, res) {
data = postController.find(req)
res.status(200).json(data);
});
module.exports = routes;

36
modules/blog/category.model.js Executable file
View File

@ -0,0 +1,36 @@
module.exports = function (sequelize, DataTypes) {
const Category = sequelize.define('Category', {
id: {
type: DataTypes.INTEGER,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
sort: {
// Se cambia el nombre del campo de 'order' a 'sort' porque 'order' es una palabra reservada SQL y da problemas al generar las consultas SQL.
field: 'order', // <- Chapuza!! El nombre del campo en MySQL es una palabra reservada en SQL.
type: DataTypes.INTEGER,
defaultValue: 0,
allowNull: false,
}
}, {
tableName: 'category',
freezeTableName: true,
timestamps: false
});
Category.beforeCreate((category) => {
category.dataValues.id = Math.floor(Math.random() * (999 - 8)) + 8;
})
Category.associate = function (models) {
Category.Posts = Category.belongsToMany(models.Post, {
through: models.PostCategory,
foreignKey: 'categoryId'
});
};
return Category;
};

View File

@ -0,0 +1,20 @@
module.exports = function (sequelize, DataTypes) {
const PostCategory = sequelize.define('PostCategory', {
postId: {
type: DataTypes.UUID,
primaryKey: true,
foreignKey: true
},
categoryId: {
type: DataTypes.INTEGER,
primaryKey: true,
foreignKey: true
}
}, {
tableName: 'post-category',
freezeTableName: true,
timestamps: false
});
return PostCategory;
};

View File

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

View File

@ -0,0 +1,51 @@
'use strict';
module.exports = function (sequelize, DataTypes) {
const Post = sequelize.define('Post', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
/*userId: {
type: DataTypes.UUID,
allowNull: true,
primaryKey: false,
unique: false,
foreignKey: true
},*/
date: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
image: {
type: DataTypes.STRING,
defaultValue: ""
},
title: {
type: DataTypes.STRING,
allowNull: false
},
content: {
type: DataTypes.TEXT,
allowNull: false
},
}, {
tableName: 'post',
freezeTableName: true,
timestamps: true,
updatedAt: false,
});
Post.associate = function (models) {
Post.Categories = Post.belongsToMany(models.Category, {
through: models.PostCategory,
foreignKey: 'postId'
});
//Post.Comments = Post.hasMany(models.PostComment, { foreignKey: 'postId' });
//Post.Reactions = Post.hasMany(models.PostReaction, { foreignKey: 'postId' });
//Post.User = Post.belongsTo(models.User, { foreignKey: 'userId' });
};
return Post;
};

View File

@ -0,0 +1,252 @@
/* 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');
module.exports = function (Post) {
return {
/**
* 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
});*/
const findOptions = {}
return Post.findAll(findOptions);
},
/**
* 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
});
}
};
}

View File

@ -1,12 +0,0 @@
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

@ -1,74 +0,0 @@
'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

@ -1,55 +0,0 @@
'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

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

View File

@ -1,243 +0,0 @@
/* 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
});
}
};

View File

@ -20,6 +20,7 @@
"nodemon": "^1.18.9" "nodemon": "^1.18.9"
}, },
"dependencies": { "dependencies": {
"args-list": "^0.3.3",
"async": "^2.6.2", "async": "^2.6.2",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"buffer": "^5.2.1", "buffer": "^5.2.1",
@ -27,7 +28,6 @@
"compression": "^1.7.4", "compression": "^1.7.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"events": "^3.0.0",
"express": "^4.16.4", "express": "^4.16.4",
"express-validation": "^1.0.2", "express-validation": "^1.0.2",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",

188
server.js
View File

@ -1,136 +1,76 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict'; 'use strict';
const { EventEmitter } = require('events');
const http = require('http'); const http = require('http');
const { toLower } = require('lodash'); const { assign, toLower } = require('lodash');
const utils = require('./utils');
const { const config = require('./config');
express, const { express, logger, models } = require('./core');
logger,
nestedConfigurations,
appConfigurations
} = require('./core');
class Server extends EventEmitter { const currentState = assign({
constructor() { launchedAt: Date.now(),
super(); appPath: process.cwd(),
host: process.env.HOST || process.env.HOSTNAME || 'localhost',
port: process.env.PORT || 1337,
environment: toLower(process.env.NODE_ENV) || 'development',
connections: {}
}, config);
this.setMaxListeners(100); function stop(server) {
// Destroy server and available connections.
// Logger. if (server) {
this.log = logger; server.close();
// 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() { process.send('stop');
this.emit('server:init'); // Kill process.
await nestedConfigurations.call(this); process.exit(1);
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 = http.createServer(express);
server.on('connection', conn => {
const key = conn.remoteAddress + ':' + conn.remotePort;
currentState.connections[key] = conn;
(() => { conn.on('close', () => {
const server = new Server(); delete currentState.connections[key];
server.start(); });
})(); });
server.on('error', err => {
if (err.code === 'EADDRINUSE') {
logger.debug(`⛔️ Server wasn't able to start properly.`);
logger.error(`The port ${err.port} is already used by another application.`);
stop(server);
return;
}
console.error(err);
});
try {
models.sequelize.sync().then(() => {
// Launch server.
server.listen(currentState.port, (err) => {
if (err) {
logger.debug(`⚠️ Server wasn't able to start properly.`);
logger.error(err);
return stop(server);
}
logger.info('Time: ' + new Date());
logger.info('Launched in: ' + (Date.now() - currentState.launchedAt) + ' ms');
logger.info('Environment: ' + currentState.environment);
logger.info('Process PID: ' + process.pid);
//logger.info(`Version: ${this.config.info.strapi} (node v${this.config.info.node})`);
logger.info('To shut down your server, press <CTRL> + C at any time');
logger.info(`⚡️ Server: http://${currentState.host}:${currentState.port}`);
});
});
} catch (err) {
logger.debug(`⛔️ Server wasn't able to start properly.`);
logger.error(err);
console.error(err);
stop(server);
}

View File

@ -119,6 +119,11 @@ argparse@^1.0.7:
dependencies: dependencies:
sprintf-js "~1.0.2" sprintf-js "~1.0.2"
args-list@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/args-list/-/args-list-0.3.3.tgz#d3fa5b7fe49ec96efb1e99adf20e7cfacc179956"
integrity sha1-0/pbf+SeyW77Hpmt8g58+swXmVY=
arr-diff@^4.0.0: arr-diff@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@ -971,11 +976,6 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
events@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88"
integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==
execa@^0.7.0: execa@^0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"