├── .bowerrc ├── files ├── robots.txt └── public │ ├── core-favicon.ico │ └── images │ ├── login.png │ ├── logo.png │ └── logo-small.png ├── .mocharc.json ├── src ├── Hooks │ └── index.js ├── responses │ ├── parsers │ │ └── index.js │ ├── formaters │ │ └── index.js │ ├── index.js │ ├── JSONF.js │ ├── JSONApi.js │ └── methods │ │ └── index.js ├── unhandledErrorCatcher.js ├── Router │ ├── haveAndAcceptsHtmlResponse.js │ ├── uploader.js │ ├── responseType.js │ └── search.js ├── localization │ ├── index.js │ └── i18n.js ├── PluginManager │ ├── isPlugin.js │ └── index.js ├── getEnv.js ├── cron │ └── index.js ├── express │ ├── sessionStore.js │ ├── publicFolders.js │ └── index.js ├── staticConfig │ ├── getAppBootstrapConfig.js │ └── index.js ├── utils │ ├── mkdirp.js │ └── index.js ├── log │ └── index.js ├── messages │ └── index.js ├── Sanitizer │ └── index.js ├── class │ ├── Theme.js │ ├── Controller.js │ └── Plugin.js └── bootstrapFunctions.js ├── test ├── testData │ ├── invalidLogConfig │ │ └── config │ │ │ └── log.js │ ├── we-plugin-post │ │ ├── package.json │ │ ├── server │ │ │ ├── models │ │ │ │ ├── instanceMethods │ │ │ │ │ └── returnModelId.js │ │ │ │ ├── classMethods │ │ │ │ │ └── returnModelName.js │ │ │ │ ├── hooks │ │ │ │ │ └── setWananingoValue.js │ │ │ │ ├── tag.json │ │ │ │ ├── user.js │ │ │ │ ├── post.js │ │ │ │ └── hero.json │ │ │ ├── search │ │ │ │ ├── parsers │ │ │ │ │ └── orWithComaParser.js │ │ │ │ └── targets │ │ │ │ │ └── inTitleAndText.js │ │ │ └── controllers │ │ │ │ └── post.js │ │ └── plugin.js │ ├── we-plugin-fastload │ │ ├── package.json │ │ └── plugin.js │ ├── config │ │ └── log.js │ └── models │ │ └── pstub.js ├── tests │ ├── modules │ │ ├── modelClassMethods.test.js │ │ ├── lib.sanitizer.test.js │ │ ├── lib.log.test.js │ │ ├── lib.staticConfig.getAppBootstrapConfig.test.js │ │ ├── modelHooks.test.js │ │ ├── database.syncAllModels.test.js │ │ ├── modelInstanceMethods.test.js │ │ ├── plugin.fastload.test.js │ │ ├── lib.index.test.js │ │ ├── pluginManager.test.js │ │ ├── lib.staticConfig.test.js │ │ ├── lib.env.test.js │ │ ├── lib.utils.test.js │ │ ├── database.defaultModelDefinitionConfigs.test.js │ │ └── lib.messages.test.js │ └── requests │ │ ├── routes.test.js │ │ ├── plugin.fastload.test.js │ │ ├── resource.test.js │ │ ├── pluralized-resource.test.js │ │ └── resource_jsonAPI.test.js └── bootstrap.js ├── .nycrc ├── .jshintignore ├── .npmignore ├── .travis.yml ├── .jshintrc ├── .gitignore ├── LICENSE.md ├── .jscsrc ├── README.md ├── CHANGELOG.md ├── package.json ├── locales ├── pt-br.json └── en-us.json ├── CONTRIBUTING.md └── install.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { "directory": "files/public/libs" } -------------------------------------------------------------------------------- /files/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /admin -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 15000, 3 | "exit": true 4 | } -------------------------------------------------------------------------------- /src/Hooks/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('simple-hooks-callback'); -------------------------------------------------------------------------------- /test/testData/invalidLogConfig/config/log.js: -------------------------------------------------------------------------------- 1 | this will whrow error -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ "lcov" ], 3 | "include": [ "src" ] 4 | } 5 | 6 | -------------------------------------------------------------------------------- /files/public/core-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wejs/we-core/HEAD/files/public/core-favicon.ico -------------------------------------------------------------------------------- /files/public/images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wejs/we-core/HEAD/files/public/images/login.png -------------------------------------------------------------------------------- /files/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wejs/we-core/HEAD/files/public/images/logo.png -------------------------------------------------------------------------------- /files/public/images/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wejs/we-core/HEAD/files/public/images/logo-small.png -------------------------------------------------------------------------------- /test/testData/we-plugin-post/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "we-plugin-post", 3 | "keywords": [ 4 | "wejs-plugin" 5 | ] 6 | } -------------------------------------------------------------------------------- /test/testData/we-plugin-fastload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "we-plugin-fastload", 3 | "keywords": [ 4 | "wejs-plugin" 5 | ] 6 | } -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | config 5 | test 6 | files 7 | docs 8 | client/shared/libs 9 | client/shared/beforeAll -------------------------------------------------------------------------------- /test/testData/we-plugin-post/server/models/instanceMethods/returnModelId.js: -------------------------------------------------------------------------------- 1 | module.exports = function returnModelId () { 2 | return this.id; 3 | }; -------------------------------------------------------------------------------- /test/testData/we-plugin-post/server/models/classMethods/returnModelName.js: -------------------------------------------------------------------------------- 1 | module.exports = function returnModelName () { 2 | return String(this); 3 | }; -------------------------------------------------------------------------------- /src/responses/parsers/index.js: -------------------------------------------------------------------------------- 1 | const { jsonAPIParser } = require('../JSONApi.js'); 2 | 3 | module.exports = { 4 | 'application/vnd.api+json': jsonAPIParser 5 | }; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .jscsrc 3 | .jshintignore 4 | .jshintrc 5 | .travis.yml 6 | .gitignore 7 | .nycrc 8 | .nyc_output 9 | config 10 | coverage 11 | *.log 12 | *.sqlite 13 | coverage 14 | files/public/images -------------------------------------------------------------------------------- /test/testData/config/log.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | log: { 3 | level: 'warn' , 4 | colorize: true, 5 | timestamp: true, 6 | prettyPrint: true, 7 | depth: 6, 8 | showLevel: true 9 | } 10 | } -------------------------------------------------------------------------------- /test/testData/we-plugin-post/server/models/hooks/setWananingoValue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple function to test modelHooks feature 3 | */ 4 | module.exports = function setWananingoValue(record) { 5 | record.wananingo = true; 6 | }; -------------------------------------------------------------------------------- /src/unhandledErrorCatcher.js: -------------------------------------------------------------------------------- 1 | module.exports = function unhandledErrorCatcher(we) { 2 | process.on('unhandledRejection', error => { 3 | we.log.warn('unhandledRejection catch', { 4 | error: error 5 | }); 6 | }); 7 | }; -------------------------------------------------------------------------------- /test/testData/we-plugin-post/server/search/parsers/orWithComaParser.js: -------------------------------------------------------------------------------- 1 | module.exports = function orWithComaParser(searchName, field, value, w) { 2 | // = [] is same of or in sequelize 3 | return w[field] = { 4 | [this.we.db.Sequelize.Op.or]: value.split(',') 5 | }; 6 | }; -------------------------------------------------------------------------------- /src/responses/formaters/index.js: -------------------------------------------------------------------------------- 1 | const { jsonAPIFormater } = require('../JSONApi'), 2 | { jsonFormater } = require('../JSONF'); 3 | 4 | /** 5 | * We.js core response formatters 6 | */ 7 | module.exports = { 8 | json: jsonFormater, 9 | 'application/json': jsonFormater, 10 | 'application/vnd.api+json': jsonAPIFormater 11 | }; -------------------------------------------------------------------------------- /test/testData/we-plugin-post/server/search/targets/inTitleAndText.js: -------------------------------------------------------------------------------- 1 | module.exports = function inTitleAndText(searchName, field, value, query, req) { 2 | req.we.router.search.parsers[field.parser](searchName, 'title', value, query.where, req); 3 | req.we.router.search.parsers[field.parser](searchName, 'text', value, query.where, req); 4 | } -------------------------------------------------------------------------------- /test/testData/models/pstub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * pstub model 3 | */ 4 | module.exports = function Model(we) { 5 | var model = { 6 | definition: { 7 | name: { 8 | type: we.db.Sequelize.STRING 9 | }, 10 | description: { 11 | type: we.db.Sequelize.STRING 12 | } 13 | } 14 | } 15 | return model; 16 | } 17 | -------------------------------------------------------------------------------- /test/testData/we-plugin-post/server/models/tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": { 3 | "text": { 4 | "type": "string", 5 | "allowNull": false 6 | } 7 | }, 8 | "associations": { 9 | "inPosts": { 10 | "type": "belongsToMany", 11 | "model": "post", 12 | "inverse": "tags", 13 | "through": "posts_tags" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /test/testData/we-plugin-post/server/models/user.js: -------------------------------------------------------------------------------- 1 | module.exports = function (we) { 2 | var model = { 3 | definition: { 4 | displayName: { 5 | type: we.db.Sequelize.STRING, 6 | allowNull: false 7 | } 8 | }, 9 | associations: { 10 | posts: { 11 | type: 'hasMany', model: 'post', inverse: 'creator' 12 | } 13 | } 14 | }; 15 | 16 | return model; 17 | }; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - v8 5 | - v10 6 | 7 | services: 8 | - mysql 9 | 10 | addons: 11 | code_climate: 12 | 13 | env: 14 | NODE_ENV: 'test' 15 | 16 | notifications: 17 | email: 18 | - alberto@wejs.org 19 | 20 | before_script: 21 | - mysql -e 'create database test;' 22 | 23 | after_success: 24 | - npm install coveralls 25 | - npm run coverage 26 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js -------------------------------------------------------------------------------- /src/Router/haveAndAcceptsHtmlResponse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if have or accepts html response, usefull for features like redirect on html response format 3 | */ 4 | module.exports = function haveAndAcceptsHtmlResponse(req, res) { 5 | if ( 6 | req.accepts('html') && 7 | ( res.locals.responseType == 'html' || 8 | ( req.we.config.responseTypes.indexOf('html') > -1 ) 9 | ) 10 | ) { 11 | return true; 12 | } else { 13 | return false; 14 | } 15 | }; -------------------------------------------------------------------------------- /src/localization/index.js: -------------------------------------------------------------------------------- 1 | const i18n = require('./i18n'), 2 | moment = require('moment'); 3 | 4 | var localization = function init (we) { 5 | i18n.configure (we.config.i18n, we); 6 | 7 | we.i18n = i18n; 8 | 9 | // set default moment locale 10 | moment.locale(we.config.i18n.defaultLocale); 11 | 12 | we.events.on('we:after:load:express', function afterLoadExpress(we) { 13 | // set i18n middleware 14 | we.express.use(i18n.init); 15 | }); 16 | }; 17 | 18 | module.exports = localization; -------------------------------------------------------------------------------- /test/testData/we-plugin-post/server/models/post.js: -------------------------------------------------------------------------------- 1 | module.exports = function (we) { 2 | var model = { 3 | definition: { 4 | title: { 5 | type: we.db.Sequelize.STRING, 6 | allowNull: false 7 | }, 8 | text: { 9 | type: we.db.Sequelize.TEXT 10 | }, 11 | }, 12 | associations: { 13 | creator: { type: 'belongsTo', model: 'user' }, 14 | tags: { 15 | type: 'belongsToMany', 16 | model: 'tag', 17 | inverse: 'inPosts', 18 | through: 'posts_tags' 19 | } 20 | } 21 | }; 22 | 23 | return model; 24 | }; -------------------------------------------------------------------------------- /test/tests/modules/modelClassMethods.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var helpers = require('we-test-tools').helpers; 3 | var we; 4 | 5 | describe('modelClassMethods', function () { 6 | before(function (done) { 7 | we = helpers.getWe(); 8 | done(); 9 | }); 10 | 11 | it('Should load model class methods from plugin', function (done) { 12 | assert(we.db.modelClassMethods.returnModelName); 13 | done(); 14 | }); 15 | 16 | it('Should run the model classMethod in right model', function (done) { 17 | 18 | assert(we.db.models.hero.returnModelNameccc(), 'hero'); 19 | 20 | done(); 21 | }); 22 | }); -------------------------------------------------------------------------------- /src/PluginManager/isPlugin.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | /** 4 | * Helper to check if a npm module is a plugin 5 | * The logic is: is plugin if the npm module package.json have the wejs-plugin keyword 6 | * 7 | * @param {String} nodeModulePath 8 | * @return {Boolean} 9 | */ 10 | module.exports = function isPlugin (nodeModulePath) { 11 | let pkg; 12 | try { 13 | pkg = require( resolve(nodeModulePath, 'package.json') ); 14 | } catch(e) { 15 | return false; 16 | } 17 | 18 | if (pkg.keywords && pkg.keywords.includes('wejs-plugin') ) { 19 | return true; 20 | } else { 21 | return false; 22 | } 23 | }; -------------------------------------------------------------------------------- /src/Router/uploader.js: -------------------------------------------------------------------------------- 1 | const multer = require('multer'), 2 | uuid = require('uuid'); 3 | 4 | function defaultFilename (req, file, cb) { 5 | file.name = Date.now() + '_' + uuid.v1() +'.'+ file.originalname.split('.').pop(); 6 | cb(null, file.name); 7 | } 8 | 9 | function getUploader(uploadConfigs) { 10 | return multer({ 11 | storage: multer.diskStorage({ 12 | destination: uploadConfigs.dest || uploadConfigs.destination, 13 | filename: uploadConfigs.filename || defaultFilename, 14 | }), 15 | limits: uploadConfigs.limits, 16 | fileFilter: uploadConfigs.fileFilter 17 | }) 18 | .fields(uploadConfigs.fields); 19 | } 20 | 21 | module.exports = getUploader; -------------------------------------------------------------------------------- /test/testData/we-plugin-post/server/models/hero.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": { 3 | "name": { 4 | "type": "string", 5 | "allowNull": false 6 | }, 7 | "price": { 8 | "type": "DECIMAL", 9 | "size": [10, 2] 10 | }, 11 | "history": { 12 | "type": "text" 13 | } 14 | }, 15 | "associations": { 16 | "creator": { 17 | "type": "belongsTo", 18 | "model": "user", 19 | "required": true 20 | } 21 | }, 22 | "hooks": { 23 | "afterCreate": ["setWananingoValue"] 24 | }, 25 | "classMethods": { 26 | "returnModelNameccc": "returnModelName" 27 | }, 28 | "instanceMethods": { 29 | "returnModelIdiii": "returnModelId" 30 | } 31 | } -------------------------------------------------------------------------------- /test/testData/we-plugin-post/plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = function loadPlugin(projectPath, Plugin) { 2 | var plugin = new Plugin(__dirname); 3 | 4 | plugin.setResource({ 5 | name: 'post', 6 | findAll: { 7 | search: { 8 | title: { 9 | parser: 'equal', 10 | target: { 11 | type: 'field', 12 | field: 'title' 13 | } 14 | }, 15 | text: { 16 | parser: 'contains', 17 | target: { 18 | type: 'field', 19 | field: 'text' 20 | } 21 | }, 22 | q: { 23 | parser: 'orWithComaParser', 24 | target: { 25 | type: 'inTitleAndText' 26 | } 27 | } 28 | } 29 | } 30 | }); 31 | 32 | return plugin; 33 | }; -------------------------------------------------------------------------------- /test/tests/modules/lib.sanitizer.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var helpers = require('we-test-tools').helpers; 3 | var we, sanitizer; 4 | 5 | describe('lib.sanitizer', function () { 6 | 7 | before(function (done) { 8 | we = helpers.getWe(); 9 | sanitizer = we.sanitizer; 10 | done(); 11 | }); 12 | 13 | 14 | describe('sanitizeAllAttr', function(){ 15 | it ('should remove uneed tags and attributes from html', function (done){ 16 | 17 | var dirtyObj = { 18 | title: '
Hello!

' 19 | } 20 | 21 | 22 | var safeObject = sanitizer.sanitizeAllAttr(dirtyObj) 23 | 24 | assert.equal(safeObject.title, '

Hello!

') 25 | 26 | done() 27 | }) 28 | }) 29 | }) -------------------------------------------------------------------------------- /src/getEnv.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Environment variable prod | dev | test 3 | * 4 | * @type {String} 5 | */ 6 | 7 | module.exports = function getEnv() { 8 | let env; 9 | 10 | // check in process.arg 11 | if (process.argv.includes('--dev')) { 12 | env = 'dev'; 13 | } else if (process.argv.includes('--test')) { 14 | env = 'test'; 15 | } else if (process.argv.includes('--prod')) { 16 | env = 'prod'; 17 | } 18 | 19 | if (!env) { 20 | switch (process.env.NODE_ENV) { 21 | case 'production': 22 | case 'prod': 23 | env = 'prod'; 24 | break; 25 | case 'test': 26 | env = 'test'; 27 | break; 28 | case 'development': 29 | case 'dev': 30 | env = 'dev'; 31 | break; 32 | } 33 | } 34 | 35 | return ( env || 'dev' ); 36 | }; -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "boss": true, 4 | "browser": true, 5 | "camelcase": false, 6 | "curly": false, 7 | "devel": true, 8 | "eqeqeq": false, 9 | "eqnull": true, 10 | "esversion": 6, 11 | "evil": false, 12 | "immed": false, 13 | "indent": 2, 14 | "jquery": true, 15 | "latedef": false, 16 | "laxbreak": true, 17 | "laxcomma": true, 18 | "maxcomplexity": 20, 19 | "maxdepth": 4, 20 | "maxlen": 120, 21 | "maxstatements": 30, 22 | "newcap": true, 23 | "node": true, 24 | "noempty": false, 25 | "nonew": true, 26 | "predef": [ 27 | "describe", 28 | "it", 29 | "before", 30 | "beforeEach", 31 | "after", 32 | "afterEach" 33 | ], 34 | "quotmark": "single", 35 | "smarttabs": true, 36 | "strict": false, 37 | "trailing": false, 38 | "undef": true, 39 | "unused": true 40 | } -------------------------------------------------------------------------------- /test/tests/modules/lib.log.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const helpers = require('we-test-tools').helpers; 3 | let getLogger, we; 4 | 5 | describe('lib/log', function () { 6 | before(function (done) { 7 | getLogger = require('../../../src/log'); 8 | we = helpers.getWe(); 9 | 10 | we.config.log.level = 'info'; 11 | 12 | done(); 13 | }); 14 | 15 | it('should throw error if we not are avaible', function (done) { 16 | try { 17 | getLogger(); 18 | } catch (e) { 19 | assert.equal(e.message, 'we instance is required for get logger instance'); 20 | done(); 21 | } 22 | }); 23 | 24 | it('should return logger without config file', function (done) { 25 | const logger = getLogger(we); 26 | 27 | assert.equal(logger.transports[0].level, 'info'); 28 | 29 | done(); 30 | }); 31 | }); -------------------------------------------------------------------------------- /test/testData/we-plugin-post/server/controllers/post.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | find(req, res) { 3 | 4 | res.locals.query.include.push({ 5 | model: req.we.db.models.tag, 6 | as: 'tags' 7 | }); 8 | 9 | return res.locals.Model 10 | .findAll(res.locals.query) 11 | .then(function count(rows) { 12 | 13 | delete res.locals.query.include; 14 | 15 | return res.locals.Model 16 | .count(res.locals.query) 17 | .then(function afterCount(count) { 18 | return { 19 | count: count, 20 | rows: rows 21 | }; 22 | }); 23 | }) 24 | .then(function afterFindAndCount (result) { 25 | res.locals.metadata.count = result.count; 26 | res.locals.data = result.rows; 27 | res.ok(); 28 | return null; 29 | }) 30 | .catch(res.queryError); 31 | } 32 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Logs 3 | logs 4 | *.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 29 | node_modules 30 | 31 | bower_components 32 | 33 | # Debug log from npm 34 | npm-debug.log 35 | 36 | *.sql 37 | *.sqlite 38 | *.tmp 39 | 40 | config 41 | 42 | *.DS_Store 43 | 44 | 45 | # OS X 46 | .DS_Store 47 | 48 | # Linux 49 | *~ 50 | 51 | -------------------------------------------------------------------------------- /test/tests/modules/lib.staticConfig.getAppBootstrapConfig.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var helpers = require('we-test-tools').helpers; 3 | var getAppBootstrapConfig, we; 4 | 5 | describe('lib.staticConfig.getAppBootstrapConfig', function () { 6 | before(function (done) { 7 | getAppBootstrapConfig = require('../../../src/staticConfig/getAppBootstrapConfig.js'); 8 | we = helpers.getWe(); 9 | done(); 10 | }); 11 | 12 | it('should return public configurations', function (done) { 13 | var configs = getAppBootstrapConfig(we); 14 | 15 | assert.equal(configs.version, 2); 16 | assert.equal(configs.env, 'test'); 17 | assert.equal(configs.client.language, 'en-us'); 18 | assert.equal(configs.client.publicVars.dynamicLayout, false); 19 | assert.equal(configs.appName, 'We test'); 20 | assert.equal(configs.locales[0], 'en-us'); 21 | 22 | 23 | done(); 24 | }); 25 | }); -------------------------------------------------------------------------------- /src/cron/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const cron = { 4 | loadAndRunAllTasks(we, cb) { 5 | we.cron.loadTasks(we, (err, tasks)=> { 6 | if (err) return cb(err); 7 | if (!tasks) return cb(); 8 | 9 | we.utils.async.eachSeries(tasks, (t, next)=> { 10 | t(we, next); 11 | }, cb); 12 | }); 13 | }, 14 | /** 15 | * Load all project and plugin tasks 16 | * 17 | * @param {Object} we We.js object 18 | * @param {Function} done callback 19 | */ 20 | loadTasks(we, done) { 21 | let tasks = {}; 22 | we.utils.async.each(we.pluginNames, (name, next)=> { 23 | // try to load the cron.js files 24 | try { 25 | tasks[name] = require(path.resolve(we.plugins[name].pluginPath, 'cron.js')); 26 | } catch(e) { 27 | if (e.code != 'MODULE_NOT_FOUND') { 28 | we.log.error(e); 29 | } 30 | } 31 | next(); 32 | }, (err)=> { 33 | done(err, tasks); 34 | }); 35 | } 36 | }; 37 | 38 | module.exports = cron; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2015 Alberto Souza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test/tests/modules/modelHooks.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var helpers = require('we-test-tools').helpers; 3 | var we; 4 | 5 | describe('modelHooks', function () { 6 | before(function (done) { 7 | we = helpers.getWe(); 8 | done(); 9 | }); 10 | 11 | it('Should load model hooks from plugin', function (done) { 12 | assert(we.db.modelHooks.setWananingoValue); 13 | done(); 14 | }); 15 | 16 | it('Should run the model hook in right model hook', function (done) { 17 | var hs = { 18 | name: 'Iron Man', 19 | history: 'Iron Man (Tony Stark) is a fictional superhero appearing in American comic '+ 20 | 'books published by Marvel Comics,'+ 21 | ' as well as its associated media. The character was created by writer and editor Stan Lee,'+ 22 | ' developed by scripter Larry Lieber, and designed by artists Don Heck and Jack Kirby. '+ 23 | 'He made his first appearance in Tales of Suspense #39 (cover dated March 1963)' 24 | }; 25 | 26 | we.db.models.hero.create(hs) 27 | .then(function(h) { 28 | assert.equal(h.wananingo, true); 29 | 30 | done(); 31 | }) 32 | .catch(done); 33 | }); 34 | }); -------------------------------------------------------------------------------- /test/tests/modules/database.syncAllModels.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var helpers = require('we-test-tools').helpers; 3 | var we, syncAllModels; 4 | 5 | describe('database.syncAllModels', function () { 6 | 7 | before(function (done) { 8 | we = helpers.getWe(); 9 | syncAllModels = we.db.syncAllModels; 10 | done(); 11 | }); 12 | 13 | it('database.syncAllModels should run db.defaultConnection.sync', function (done) { 14 | syncAllModels.bind({ 15 | defaultConnection: { 16 | sync: function() { 17 | return new we.db.Sequelize.Promise(function (resolve) { 18 | resolve(); 19 | }); 20 | } 21 | } 22 | })(done); 23 | }); 24 | 25 | it('database.syncAllModels should run db.defaultConnection.sync with'+ 26 | 'forced reset if cd.resetAllData is set', function (done) { 27 | syncAllModels.bind({ 28 | defaultConnection: { 29 | sync: function(opts) { 30 | assert.equal(opts.force, true); 31 | return new we.db.Sequelize.Promise(function (resolve) { 32 | resolve(); 33 | }); 34 | } 35 | } 36 | })({ resetAllData: true }, done); 37 | }); 38 | }); -------------------------------------------------------------------------------- /src/Router/responseType.js: -------------------------------------------------------------------------------- 1 | const mime = require('mime'); 2 | 3 | /** 4 | * Parse response type middleware 5 | * 6 | * @param {Object} req express.js request 7 | * @param {Object} res express.js response 8 | * @param {Function} next callback 9 | */ 10 | module.exports = function responseType (req, res, next){ 11 | 12 | if (!req.headers) req.headers = {}; 13 | 14 | parseResponseType(req); 15 | 16 | next(); 17 | }; 18 | 19 | /** 20 | * Parse the response type 21 | * 22 | * with order: 1. extension, 2.responseType, 3.Accept header 23 | * 24 | * @param {Object} req express.js request 25 | * @return {String} the response type string 26 | */ 27 | function parseResponseType (req) { 28 | if (req.query && req.query.responseType) { 29 | if (req.query.responseType == 'modal') { 30 | // suport for old we.js contentOnly api 31 | req.query.responseType = req.we.config.defaultResponseType; 32 | req.query.contentOnly = true; 33 | } 34 | 35 | req.headers.accept = mime.getType(req.query.responseType.toLowerCase()); 36 | } 37 | 38 | if (req.accepts(req.we.config.responseTypes)) 39 | return; 40 | 41 | req.headers.accept = req.we.config.defaultResponseType; 42 | } -------------------------------------------------------------------------------- /test/tests/modules/modelInstanceMethods.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var helpers = require('we-test-tools').helpers; 3 | var we; 4 | 5 | describe('modelInstanceMethods', function () { 6 | before(function (done) { 7 | we = helpers.getWe(); 8 | done(); 9 | }); 10 | 11 | it('Should load model instanceMethods from plugin', function (done) { 12 | assert(we.db.modelInstanceMethods.returnModelId); 13 | done(); 14 | }); 15 | 16 | it('Should run the model instanceMethod in right model', function (done) { 17 | var hs = { 18 | name: 'Iron Man', 19 | history: 'Iron Man (Tony Stark) is a fictional superhero appearing in American comic '+ 20 | 'books published by Marvel Comics,'+ 21 | ' as well as its associated media. The character was created by writer and editor Stan Lee,'+ 22 | ' developed by scripter Larry Lieber, and designed by artists Don Heck and Jack Kirby. '+ 23 | 'He made his first appearance in Tales of Suspense #39 (cover dated March 1963)' 24 | }; 25 | 26 | we.db.models.hero.create(hs) 27 | .then(function(h) { 28 | assert.equal(h.returnModelIdiii(), h.id); 29 | 30 | done(); 31 | }) 32 | .catch(done); 33 | }); 34 | }); -------------------------------------------------------------------------------- /test/tests/modules/plugin.fastload.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'), 2 | helpers = require('we-test-tools').helpers; 3 | 4 | let we; 5 | 6 | describe('plugin.fastload.unit', function() { 7 | before(function (done) { 8 | we = helpers.getWe(); 9 | done(); 10 | }); 11 | 12 | it('Should load the orWithMinusParser search parser', function() { 13 | assert(we.router.search.parsers.orWithMinusParser); 14 | }); 15 | 16 | it('Should load the inNameAndDescription search target', function() { 17 | assert(we.router.search.targets.inNameAndDescription); 18 | }); 19 | 20 | it('Should load the dog controller', function() { 21 | assert(we.controllers.dog); 22 | }); 23 | 24 | it('Should load giveVaccine modelHooks', function() { 25 | assert(we.db.modelHooks.giveVaccine); 26 | }); 27 | 28 | it('Should load bark modelInstanceMethod', function() { 29 | assert(we.db.modelInstanceMethods.bark); 30 | }); 31 | 32 | it('Should load jump modelClassMethod', function() { 33 | assert(we.db.modelClassMethods.jump); 34 | }); 35 | 36 | it('Should load dog model config and model', function(){ 37 | assert(we.db.modelsConfigs.dog); 38 | assert(we.db.models.dog); 39 | assert(we.db.models.dog.jump); 40 | }); 41 | }); -------------------------------------------------------------------------------- /test/tests/modules/lib.index.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var We; 3 | 4 | describe('lib/index.js', function () { 5 | before(function (done) { 6 | We = require('../../../src/index.js'); 7 | done(); 8 | }); 9 | 10 | it ('should delete some attrs from request and response in freeResponseMemory', function (done) { 11 | const req = {}; 12 | const res = { 13 | locals: { 14 | req: true, 15 | currentUser: true, 16 | regions: true, 17 | Model: true, 18 | body: true, 19 | layoutHtml: true 20 | } 21 | }; 22 | 23 | We.prototype.freeResponseMemory(req, res); 24 | 25 | assert(res.locals.regions !== true); 26 | assert(res.locals.currentUser!== true); 27 | 28 | done(); 29 | }); 30 | 31 | describe('We instance', function(){ 32 | let we; 33 | 34 | before(function(done){ 35 | we = new We(); 36 | we.bootstrap(done); 37 | }) 38 | 39 | it('should set response type and formater with we.responses.addResponseFormater', function (done) { 40 | we.responses.addResponseFormater('xml', function formater(){}); 41 | 42 | assert(we.config.responseTypes.indexOf('xml') > -1); 43 | assert(we.responses.formaters.xml); 44 | 45 | done(); 46 | }); 47 | }); 48 | 49 | 50 | }); -------------------------------------------------------------------------------- /test/tests/modules/pluginManager.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var path = require('path'); 3 | var helpers = require('we-test-tools').helpers; 4 | var npmf = path.resolve(process.cwd(), 'node_modules'); 5 | var we; 6 | 7 | describe('modulePluginManager', function () { 8 | var pluginManager; 9 | 10 | before(function (done) { 11 | we = helpers.getWe(); 12 | pluginManager = we.pluginManager; 13 | done(); 14 | }); 15 | 16 | it('pluginManager.isPlugin should return false for async and moment', function (done) { 17 | assert.equal(false, pluginManager.isPlugin(npmf+'/async')); 18 | assert.equal(false, pluginManager.isPlugin(npmf+'/moment')); 19 | assert.equal(false, pluginManager.isPlugin('invalid string')); 20 | done(); 21 | }); 22 | 23 | it('pluginManager.isPlugin should return true for we-plugin-post', function (done) { 24 | assert.equal(true, pluginManager.isPlugin(npmf+'/we-plugin-post')); 25 | done(); 26 | }); 27 | 28 | it('pluginManager.getPluginNames should return plugin names list', function (done) { 29 | var names = pluginManager.getPluginNames(); 30 | assert(names.length > 1); 31 | assert(names.indexOf('project')>-1); 32 | assert(names.indexOf('we-plugin-post')>-1) ; 33 | done(); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /src/express/sessionStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Session store loader file 3 | * only load session store modules if session are enabled 4 | */ 5 | module.exports = function sessionStoreLoader(we, weExpress) { 6 | if (we.config.session) { 7 | const session = require('express-session'); 8 | 9 | // - default session storage 10 | // To change the session store change the we.config.session.store 11 | // To disable session set we.config.session to null 12 | if (we.config.session && !we.config.session.store && we.db.activeConnectionConfig.dialect == 'mysql') { 13 | const c = we.db.defaultConnection.connectionManager.config; 14 | 15 | let SessionStore = require('express-mysql-session'); 16 | we.config.session.store = new SessionStore({ 17 | host: c.host || 'localhost', 18 | port: c.port || 3306, 19 | user: c.username, 20 | password: c.password, 21 | database: c.database 22 | }); 23 | we.config.session.resave = true; 24 | we.config.session.saveUninitialized = true; 25 | } 26 | 27 | if (we.config.session) { 28 | // save the instance for reuse in plugins 29 | we.sessionStore = we.config.session.store; 30 | we.session = session(we.config.session); 31 | 32 | weExpress.use(we.session); 33 | } 34 | } 35 | 36 | }; -------------------------------------------------------------------------------- /src/staticConfig/getAppBootstrapConfig.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | /** 4 | * Get App Bootstrap configs 5 | * 6 | * @param {Object} req express request 7 | * @return {Object} configs 8 | */ 9 | module.exports = function getAppBootstrapConfig(we) { 10 | 11 | let configs = {}; 12 | 13 | configs.version = '2'; 14 | 15 | configs.env = we.env; 16 | 17 | configs.client = {}; 18 | configs.appName = we.config.appName; 19 | configs.appLogo = we.config.appLogo; 20 | 21 | configs.client.publicVars = {}; 22 | 23 | // auth configs 24 | configs.auth = { 25 | cookieDomain: we.config.passport.cookieDomain, 26 | cookieName: we.config.passport.cookieName, 27 | cookieSecure: we.config.passport.cookieSecure, 28 | accessTokenTime: we.config.passport.accessTokenTime, 29 | 30 | oauth: { 31 | server: null 32 | } 33 | }; 34 | 35 | // get log config 36 | configs.client.log = we.config.clientside.log; 37 | 38 | // get public vars 39 | if (we.config.clientside.publicVars) { 40 | // clone it to dont change global variable 41 | configs.client.publicVars = _.clone(we.config.clientside.publicVars); 42 | } 43 | 44 | configs.locales = we.config.i18n.locales; 45 | configs.client.language = we.config.i18n.defaultLocale; 46 | 47 | configs.structure = {}; 48 | 49 | return configs; 50 | }; -------------------------------------------------------------------------------- /test/tests/requests/routes.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'), 2 | request = require('supertest'), 3 | helpers = require('we-test-tools').helpers; 4 | 5 | let _, http, we; 6 | 7 | describe('routes', function() { 8 | before(function (done) { 9 | http = helpers.getHttp(); 10 | we = helpers.getWe(); 11 | _ = we.utils._; 12 | return done(); 13 | }); 14 | 15 | describe('json', function() { 16 | describe('GET /', function(){ 17 | it ('should return 200', function (done) { 18 | 19 | request(http) 20 | .get('/') 21 | .set('Accept', 'application/json') 22 | .expect(200) 23 | .end(function (err, res) { 24 | if (err) throw err; 25 | 26 | assert(res.body.messages); 27 | 28 | done(); 29 | }); 30 | 31 | }); 32 | }); 33 | }); 34 | 35 | describe('public folders', function(){ 36 | describe('GET /public/project/images/logo.png', function(){ 37 | it ('should return 200', function (done) { 38 | 39 | request(http) 40 | .get('/public/project/images/logo.png') 41 | .expect(200) 42 | .end(function (err, res) { 43 | if (err) throw err; 44 | 45 | assert(res.body); 46 | assert(!res.text); 47 | assert.equal(res.headers['content-type'], 'image/png'); 48 | 49 | done(); 50 | }); 51 | 52 | }); 53 | }); 54 | }) 55 | }); -------------------------------------------------------------------------------- /src/express/publicFolders.js: -------------------------------------------------------------------------------- 1 | const express = require('express'), 2 | path = require('path'); 3 | 4 | module.exports = function setPublicFolderMiddlewares (we, weExpress) { 5 | const cfg = { maxAge: we.config.cache.maxage }, 6 | publicRouter = express.Router(); 7 | 8 | publicRouter.use((req, res, next)=> { 9 | res.header('Access-Control-Allow-Origin', '*'); 10 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); 11 | next(); 12 | }); 13 | 14 | let plugin; 15 | 16 | if (we.view && we.view.themes) { 17 | // set themes public folder 18 | for (let themeName in we.view.themes) { 19 | publicRouter.use( 20 | '/theme/' + we.view.themes[themeName].name, 21 | express.static(path.join( 22 | we.view.themes[themeName].config.themeFolder, 'files/public' 23 | ), cfg) 24 | ); 25 | } 26 | } 27 | 28 | // set plugins public folder 29 | for (let pluginName in we.plugins) { 30 | plugin = we.plugins[pluginName]; 31 | publicRouter.use( 32 | '/plugin/' + plugin['package.json'].name + '/files', 33 | express.static(path.join( plugin.pluginPath, 'files/public'), cfg) 34 | ); 35 | } 36 | 37 | // public project folder 38 | publicRouter.use('/project', express.static( 39 | path.join(we.projectPath, 'files/public'), cfg 40 | )); 41 | 42 | we.router.publicRouter = publicRouter; 43 | 44 | weExpress.use('/public', publicRouter); 45 | }; -------------------------------------------------------------------------------- /src/responses/index.js: -------------------------------------------------------------------------------- 1 | const formaters = require('./formaters'), 2 | methods = require('./methods'), 3 | parsers = require('./parsers'); 4 | 5 | // set default response formater 6 | formaters.default = formaters.json; 7 | 8 | module.exports = { 9 | formaters: formaters, 10 | methods: methods, 11 | parsers: parsers, 12 | 13 | /** 14 | * Set response formatters in current request 15 | */ 16 | format(format, data, req, res) { 17 | res.format(formaters); 18 | }, 19 | /** 20 | * Set custom responses in res variable 21 | */ 22 | setCustomResponses(req, res, next) { 23 | for (let response in methods) { 24 | res[response] = methods[response].bind({req: req, res: res, next: next}); 25 | } 26 | 27 | return next(); 28 | }, 29 | 30 | /** 31 | * Sort response formatters 32 | * 33 | * @param {Object} we we.js app 34 | */ 35 | sortResponses(we) { 36 | const formats = Object.keys(we.responses.formaters); 37 | we.responses.formatersUnsorted = we.responses.formaters; 38 | 39 | we.responses.formaters = {}; 40 | 41 | let name; 42 | 43 | for (let i = 0; i < we.config.responseTypes.length; i++) { 44 | name = we.config.responseTypes[i]; 45 | we.responses.formaters[name] = we.responses.formatersUnsorted[name]; 46 | } 47 | 48 | formats.forEach( (f)=> { 49 | if (!we.responses.formaters[f]) { 50 | we.responses.formaters[f] = we.responses.formatersUnsorted[f]; 51 | } 52 | }); 53 | } 54 | }; -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { "excludeFiles": 2 | [ "**/node_modules/**" 3 | , "coverage" 4 | , "data" 5 | , "client/shared/beforeAll/**" 6 | , "client/shared/libs/**" 7 | , "files/public/**" 8 | ] 9 | , "requireLineFeedAtFileEnd": true 10 | , "disallowMultipleLineBreaks": true 11 | , "requireMultipleVarDecl": true 12 | , "disallowEmptyBlocks": true 13 | , "disallowSpaceAfterObjectKeys": true 14 | , "disallowTrailingWhitespace": true 15 | , "requireCapitalizedConstructors": true 16 | , "requireSpacesInsideObjectBrackets": "all" 17 | , "requireSpacesInsideArrayBrackets": "all" 18 | , "validateLineBreaks": "LF" 19 | , "requireSpaceBeforeBinaryOperators": [ "+", "-", "/", "*", "=", "==", "===", "!=", "!==" ] 20 | , "requireSpaceAfterBinaryOperators": [ "+", "-", "/", "*", "=", "==", "===", "!=", "!==" ] 21 | , "requireSpaceAfterKeywords": [ "if", "else", "for", "while", "do", "switch", "try", "catch" ] 22 | , "requireSpaceBeforeBinaryOperators": [ "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<=" ] 23 | , "requireSpaceAfterBinaryOperators": [ "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<=" ] 24 | , "requireSpacesInConditionalExpression": true 25 | , "disallowSpaceAfterPrefixUnaryOperators": [ "++", "--", "+", "-", "~", "!" ] 26 | , "disallowSpaceBeforePostfixUnaryOperators": [ "++", "--" ] 27 | , "requireSpaceBeforeBinaryOperators": [ "+", "-", "/", "*", "=", "==", "===", "!=", "!==" ] 28 | , "requireSpaceAfterBinaryOperators": [ "+", "-", "/", "*", "=", "==", "===", "!=", "!==" ] 29 | , "disallowKeywordsOnNewLine": [ "else" ] 30 | , "requireSpacesInFunctionExpression": { "beforeOpeningCurlyBrace": true } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/mkdirp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Code from mkdirp: https://github.com/substack/node-mkdirp 3 | */ 4 | const path = require('path'), 5 | fs = require('fs'), 6 | _0777 = parseInt('0777', 8); 7 | 8 | module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP; 9 | 10 | function mkdirP (p, opts, f, made) { 11 | if (typeof opts === 'function') { 12 | f = opts; 13 | opts = {}; 14 | } 15 | else if (!opts || typeof opts !== 'object') { 16 | opts = { mode: opts }; 17 | } 18 | 19 | let mode = opts.mode; 20 | let xfs = opts.fs || fs; 21 | 22 | if (mode === undefined) { 23 | mode = _0777 & (~process.umask()); 24 | } 25 | if (!made) made = null; 26 | 27 | let cb = f || function () {}; 28 | p = path.resolve(p); 29 | 30 | xfs.mkdir(p, mode, function (er) { 31 | if (!er) { 32 | made = made || p; 33 | return cb(null, made); 34 | } 35 | switch (er.code) { 36 | case 'ENOENT': 37 | mkdirP(path.dirname(p), opts, function (er, made) { 38 | if (er) cb(er, made); 39 | else mkdirP(p, opts, cb, made); 40 | }); 41 | break; 42 | 43 | // In the case of any other error, just see if there's a dir 44 | // there already. If so, then hooray! If not, then something 45 | // is borked. 46 | default: 47 | xfs.stat(p, function (er2, stat) { 48 | // if the stat fails, then that's super weird. 49 | // let the original error be the failure reason. 50 | if (er2 || !stat.isDirectory()) cb(er, made) 51 | else cb(null, made); 52 | }); 53 | break; 54 | } 55 | }); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # We.js core module :green_heart: 2 | 3 | [![Join the chat at https://gitter.im/wejs/we](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/wejs/we?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/wejs/we-core.svg?branch=master)](https://travis-ci.org/wejs/we-core) 4 | 5 | **Main repository: https://github.com/wejs/we ** 6 | 7 | Site: [http://wejs.org](https://wejs.org) 8 | 9 | Status: **maintained** 10 | 11 | [**Changelog**](CHANGELOG.md) 12 | 13 | ## Install for develop we.js core: 14 | 15 | After install npm and node.js 16 | 17 | ```sh 18 | // clone this project 19 | git clone https://github.com/wejs/we-core.git 20 | // enter in cloned folder 21 | cd we-core 22 | // install all dependencies 23 | npm install 24 | // test 25 | npm test 26 | ``` 27 | 28 | ## Development: 29 | 30 | Run `npm run cw` for compile files in src to lib folder 31 | 32 | Only edit code in **src** folder 33 | 34 | ### How to test 35 | 36 | after clone and install npm packages: 37 | 38 | ```sh 39 | npm test 40 | ``` 41 | 42 | ##### For run only 'userFeature' test use: 43 | 44 | ```sh 45 | we test -g 'resourceRequests' 46 | ``` 47 | 48 | ##### For run the code coverage 49 | 50 | ```sh 51 | npm run coverage 52 | ``` 53 | ## V3 migration 54 | 55 | - Breaking changes in sequelize ORM: Updated to v5.x 56 | - Breaking changes in winston logger: Updated to v3.x 57 | - string npm module removed from we.utils.string and dependencies. 58 | 59 | ## V2 migration 60 | 61 | - Breaking changes in ORM: http://docs.sequelizejs.com/manual/tutorial/upgrade-to-v4.html 62 | 63 | ## License 64 | 65 | [the MIT license](LICENSE.md). 66 | 67 | ## Sponsored by 68 | 69 | - Linky: https://linkysystems.com 70 | 71 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log file 2 | 3 | ## Changes: 4 | - v1.8.0: New plugin manager 5 | - to update: run `we update` 6 | - v1.7.0: 7 | - Added suport for jsonapi requests in core and response format improved 8 | - v1.6.0: we-plugin-file api updated. Title, breadcrumb and metatag moved to we-plugin-view, add babel in we-core. 9 | - to update: run `we update` and install we-plugin-file-local 10 | - v1.5.0: Big update in cli and generators, Moved email features to we-plugin-email 11 | - To update run `npm install --save we-plugin-email` 12 | - v1.4.0: Split we-core in small modules and update app project to be ready to APIs 13 | - To update run `npm install --save we-plugin-editor-summernote we-plugin-url-alias we-plugin-user we-plugin-view we-plugin-widget we-plugin-acl we-plugin-auth` 14 | - And update we-core to v1.4.0 `npm install we-core` 15 | - v1.3.0: Widget feature moved to we-plugin-widget 16 | - To update run `npm install we-plugin-widget` 17 | - v1.0.0: we-core now returns one prototype and mysql modules is removed from we-core. For update your project do: 18 | - Update your global we cli and generators: `npm install we generator-wejs -g` 19 | - Update your project app.js to: 20 | ```js 21 | var We = require('we-core'); 22 | // instantiate an new app 23 | var app = new We(); 24 | 25 | app.go(function (err) { 26 | if (err) return console.error(err); 27 | }); 28 | ``` 29 | - Update your project gulpfile to: 30 | ```js 31 | var We = require('we-core'); 32 | var we = new We(); 33 | 34 | var projectFolder = process.cwd(); 35 | var gulp = require('gulp'); 36 | var weGulpTasks = require('we-gulp-tasks-default'); 37 | 38 | weGulpTasks(we, gulp, projectFolder, function doneTask() { 39 | we.exit(function(){ 40 | process.exit(); 41 | }); 42 | }); 43 | 44 | ``` 45 | - If you use mysql then to install **mysql** and **express-mysql-session**:
46 | `npm install --save mysql express-mysql-session` 47 | - v0.3.97: Add suport to url alias 48 | - v0.3.96: Add suport to windows 10 49 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | const projectPath = process.cwd(), 2 | path = require('path'), 3 | deleteDir = require('rimraf'), 4 | async = require('async'), 5 | testTools = require('we-test-tools'), 6 | ncp = require('ncp').ncp, 7 | We = require('../src'); 8 | 9 | let we; 10 | 11 | before(function (callback) { 12 | testTools.copyLocalSQLiteConfigIfNotExists(projectPath, callback); 13 | }); 14 | 15 | // Add the stub plugin in node_modules folder: 16 | before(function (callback) { 17 | const f = path.resolve(__dirname, 'testData/we-plugin-post'), 18 | d = path.resolve(process.cwd(), 'node_modules/we-plugin-post'); 19 | 20 | ncp(f, d, callback); 21 | }); 22 | 23 | // add an seccond plugin with support to fast load 24 | before(function (callback) { 25 | const f = path.resolve(__dirname, 'testData/we-plugin-fastload'), 26 | d = path.resolve(process.cwd(), 'node_modules/we-plugin-fastload'); 27 | ncp(f, d, callback); 28 | }); 29 | 30 | // prepare we.js core and load app features: 31 | before(function (callback) { 32 | this.slow(100); 33 | 34 | we = new We({ bootstrapMode: 'test' }); 35 | 36 | testTools.init({}, we); 37 | 38 | we.bootstrap({ 39 | // disable access log 40 | enableRequestLog: false, 41 | 42 | i18n: { 43 | directory: path.resolve(__dirname, '..', 'config/locales'), 44 | updateFiles: true, 45 | locales: ['en-us'] 46 | }, 47 | themes: {} 48 | }, callback); 49 | }); 50 | 51 | // start the server: 52 | before(function (callback) { 53 | we.startServer(callback); 54 | }); 55 | 56 | // after all tests remove test folders and delete the database: 57 | after(function (callback) { 58 | const tempFolders = [ 59 | path.resolve(process.cwd(), 'node_modules/we-plugin-post'), 60 | projectPath + '/files/config', 61 | projectPath + '/files/uploads', 62 | projectPath + '/database.sqlite', 63 | projectPath + '/files/templatesCacheBuilds.js' 64 | ]; 65 | 66 | async.each(tempFolders, (folder, next)=> { 67 | deleteDir( folder, next); 68 | }, (err)=> { 69 | if (err) throw new Error(err); 70 | we.exit(callback); 71 | }); 72 | }); -------------------------------------------------------------------------------- /test/tests/modules/lib.staticConfig.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var helpers = require('we-test-tools').helpers 3 | var fs = require('fs') 4 | var path = require('path') 5 | var staticConfig, we 6 | 7 | describe('lib.staticConfig', function () { 8 | before(function (done) { 9 | staticConfig = require('../../../src/staticConfig') 10 | we = helpers.getWe() 11 | done() 12 | }); 13 | 14 | it('static configs should throw error if run without project path', function (done) { 15 | try { 16 | staticConfig() 17 | } catch (e) { 18 | 19 | assert.equal(e.message, 'project path is required for load static configs') 20 | 21 | return done() 22 | } 23 | 24 | assert(false, 'should throw error') 25 | }) 26 | 27 | it('staticConfig should return we.config (cache) if staticConfigsIsLoad is true', function (done) { 28 | var w = { 29 | staticConfigsIsLoad: true, 30 | config: { 31 | isMee: true 32 | } 33 | } 34 | 35 | var wr = staticConfig(process.cwd(), w) 36 | 37 | assert.equal(wr.isMee, true) 38 | 39 | done() 40 | }) 41 | 42 | it('loadPluginConfigs should return we.config (cache) if pluginConfigsIsLoad is true', function (done) { 43 | var w = { 44 | pluginConfigsIsLoad: true, 45 | config: { 46 | isMe: true 47 | } 48 | } 49 | 50 | var wr = staticConfig.loadPluginConfigs(w) 51 | 52 | assert.equal(wr.isMe, true) 53 | 54 | done() 55 | }) 56 | 57 | it ('readJsonConfiguration should create the configuration.json file if not exists', function (done) { 58 | this.slow(250) 59 | var projectConfigFolder = path.resolve(process.cwd(), 'config') 60 | var cfgFilePath = path.join(we.projectConfigFolder, 'configuration.json') 61 | 62 | fs.unlink(cfgFilePath, function (err) { 63 | if (err) return done(err) 64 | 65 | var cfg = staticConfig.readJsonConfiguration(projectConfigFolder) 66 | 67 | fs.readFile(cfgFilePath, function (err, result) { 68 | if (err) return done(err) 69 | 70 | assert(typeof cfg == 'object') 71 | assert.equal(result.toString(), '{}') 72 | 73 | done() 74 | }) 75 | 76 | }) 77 | }) 78 | }); -------------------------------------------------------------------------------- /src/log/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { createLogger, transports, format } = require('winston'); 3 | const { combine, timestamp, json, label, errors } = format; 4 | 5 | module.exports = function getTheLogger(we) { 6 | if (!we) throw new Error('we instance is required for get logger instance'); 7 | 8 | const env = we.env, 9 | log = we.config.log, 10 | defaultAll = { 11 | level: 'info', 12 | colorize: true, 13 | timestamp: true, 14 | json: false, 15 | stringify: false, 16 | prettyPrint: true, 17 | depth: 5, 18 | showLevel: true 19 | }; 20 | 21 | let configs; 22 | // if have an specific configuration for this env: 23 | if (log !== undefined && log[env]) { 24 | // Add support to set multiple log configs for diferent envs in same configuration: 25 | configs = _.defaults(log[env], defaultAll); 26 | } else if (log) { 27 | // log config without env log: 28 | configs = _.defaults(log, defaultAll); 29 | } else { 30 | // if configs not is set use the default: 31 | configs = defaultAll; 32 | } 33 | 34 | // allows to set log level with LOG_LV enviroment variable 35 | if (process.env.LOG_LV) configs.level = process.env.LOG_LV; 36 | 37 | let format; 38 | 39 | if (configs.format) { 40 | format = configs.format; 41 | } else { 42 | format = combine( 43 | errors({ stack: true }), 44 | label({label: 'wejs-app'}), 45 | timestamp(), 46 | json() 47 | ); 48 | } 49 | 50 | // start one logger 51 | const logger = createLogger({ 52 | level: configs.level, 53 | format: format, 54 | transports: configs.transports 55 | }); 56 | 57 | if (configs.keepConsoleTransport || !configs.transports || !configs.transports.length) { 58 | // default console logger 59 | logger.add(new transports.Console(configs)); 60 | } 61 | // save close method 62 | logger.closeAllLoggersAndDisconnect = closeAllLoggersAndDisconnect; 63 | 64 | return logger; 65 | }; 66 | 67 | /** 68 | * Method for wait all log writes before disconnect 69 | * 70 | * @param {Object} we 71 | * @param {Function} cb 72 | */ 73 | function closeAllLoggersAndDisconnect(we, cb) { 74 | setTimeout(() => { 75 | cb(); 76 | }, 350); 77 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "we-core", 3 | "version": "3.1.18", 4 | "description": "We.js is a node.js framework for build real time applications, sites or blogs!", 5 | "homepage": "https://wejs.org", 6 | "main": "./src/index.js", 7 | "scripts": { 8 | "test": "NODE_ENV=test LOG_LV=info ./node_modules/.bin/mocha test/bootstrap.js test/**/*.test.js -b ", 9 | "verbose-test": "NODE_ENV=test LOG_LV=verbose ./node_modules/.bin/mocha test/bootstrap.js test/**/*.test.js -b ", 10 | "coverage": "NODE_ENV=test LOG_LV=info nyc mocha test/bootstrap.js test/**/*.test.js -b", 11 | "postversion": "git push" 12 | }, 13 | "keywords": [ 14 | "we.js", 15 | "wejs", 16 | "plugin", 17 | "wejs-plugin", 18 | "web", 19 | "api", 20 | "systems", 21 | "framework", 22 | "web framework" 23 | ], 24 | "repository": "wejs/we-core", 25 | "files": [ 26 | "files", 27 | "src", 28 | "locales", 29 | "server", 30 | "install.js", 31 | "plugin.js" 32 | ], 33 | "author": "Alberto Souza ", 34 | "license": "MIT", 35 | "dependencies": { 36 | "async": "^3.2.0", 37 | "body-parser": "1.19.0", 38 | "compression": "1.7.4", 39 | "connect-flash": "0.1.1", 40 | "cookie-parser": "1.4.5", 41 | "cors": "2.8.5", 42 | "express": "4.17.1", 43 | "express-session": "1.17.1", 44 | "handlebars": "4.7.6", 45 | "lodash": "4.17.20", 46 | "mime": "2.4.7", 47 | "moment": "2.29.1", 48 | "morgan": "1.10.0", 49 | "pluralize": "^8.0.0", 50 | "request": "2.88.2", 51 | "sanitize-html": "1.27.5", 52 | "sequelize": "5.22.3", 53 | "serve-favicon": "~2.5.0", 54 | "simple-hooks-callback": "1.0.0", 55 | "slugify": "^1.4.0", 56 | "uuid": "^8.1.0", 57 | "winston": "^3.2.1" 58 | }, 59 | "devDependencies": { 60 | "chance": "^1.1.5", 61 | "connect-sqlite3": "^0.9.11", 62 | "mocha": "7.2.0", 63 | "mysql2": "^2.1.0", 64 | "ncp": "^2.0.0", 65 | "nyc": "^15.0.1", 66 | "rimraf": "3.0.2", 67 | "sinon": "9.2.2", 68 | "sqlite3": "^4.2.0", 69 | "supertest": "4.0.2", 70 | "we-test-tools": "^1.0.0" 71 | }, 72 | "engines": { 73 | "node": ">=8.0.0" 74 | }, 75 | "wejs": { 76 | "plugins": { 77 | "we-plugin-post": true, 78 | "we-plugin-fastload": true 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/tests/modules/lib.env.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var getEnv; 3 | 4 | describe('getEnv', function () { 5 | before(function (done) { 6 | getEnv = require('../../../src/getEnv.js'); 7 | done(); 8 | }); 9 | 10 | it('should return dev for unknow environment', function (done) { 11 | process.env.NODE_ENV = null; 12 | assert.equal(getEnv(), 'dev'); 13 | process.env.NODE_ENV = 'test'; 14 | done(); 15 | }); 16 | 17 | it('should return dev if --dev command option is set', function (done) { 18 | 19 | process.argv.push('--dev'); 20 | assert.equal(getEnv(), 'dev'); 21 | var index = process.argv.indexOf('--dev'); 22 | if (index > -1) process.argv.splice(index, 1); 23 | 24 | done(); 25 | }); 26 | 27 | it('should return test if --test command option is set', function (done) { 28 | process.argv.push('--test'); 29 | assert.equal(getEnv(), 'test'); 30 | 31 | var index = process.argv.indexOf('--test'); 32 | if (index > -1) process.argv.splice(index, 1); 33 | 34 | done(); 35 | }); 36 | 37 | it('should return prod if --prod command option is set', function (done) { 38 | process.argv.push('--prod'); 39 | assert.equal(getEnv(), 'prod'); 40 | 41 | var index = process.argv.indexOf('--prod'); 42 | if (index > -1) process.argv.splice(index, 1); 43 | 44 | done(); 45 | }); 46 | 47 | it('should return prod if NODE_ENV=production', function (done) { 48 | process.env.NODE_ENV = 'production'; 49 | assert.equal(getEnv(), 'prod'); 50 | 51 | process.env.NODE_ENV = 'test'; 52 | done(); 53 | }); 54 | 55 | it('should return prod if NODE_ENV=prod', function (done) { 56 | process.env.NODE_ENV = 'prod'; 57 | assert.equal(getEnv(), 'prod'); 58 | 59 | process.env.NODE_ENV = 'test'; 60 | 61 | done(); 62 | }); 63 | 64 | it('should return test if NODE_ENV=test', function (done) { 65 | process.env.NODE_ENV = 'test'; 66 | assert.equal(getEnv(), 'test'); 67 | 68 | done(); 69 | }); 70 | 71 | it('should return dev if NODE_ENV=development', function (done) { 72 | process.env.NODE_ENV = 'development'; 73 | assert.equal(getEnv(), 'dev'); 74 | 75 | process.env.NODE_ENV = 'test'; 76 | 77 | done(); 78 | }); 79 | 80 | it('should return dev if NODE_ENV=dev', function (done) { 81 | process.env.NODE_ENV = 'dev'; 82 | assert.equal(getEnv(), 'dev'); 83 | 84 | process.env.NODE_ENV = 'test'; 85 | 86 | done(); 87 | }); 88 | }); -------------------------------------------------------------------------------- /locales/pt-br.json: -------------------------------------------------------------------------------- 1 | { 2 | "widget.delete.confirm.msg": "Vocẽ tem certeza que deseja deletar esse widget?", 3 | "Profile": "Perfil", 4 | "Widget": "Widget", 5 | "Edit": "Editar", 6 | "Delete": "Deletar", 7 | "widget.title": " ", 8 | "widget.add": "Adicionar widget", 9 | "widget.context": "Contexto do widget", 10 | "widget.visibility": "Exibir o widget: ", 11 | "widget.in-context": "No contexto atual", 12 | "widget.in-portal": "Em todas as páginas", 13 | "widget.in-session": "Nessa sessão", 14 | "widget.in-session-record": "Nos conteúdos dessa sessão", 15 | "widget.in-page": "Na página atual", 16 | "layout.select.widget.type": "Selecione o tipo de widget", 17 | "layout.edit.hide.btn": "Editar layout", 18 | "date.to": "a", 19 | "layout.edit.btn": "Editar layout", 20 | "image.remove": "remover imagem", 21 | "image.selector": "Selecionar imagem", 22 | "image.uploader.title": "Enviar imagem", 23 | "image.selector.title": "Selecionar imagem", 24 | "image.upload.selector.btn": "Selecionar para enviar", 25 | "file.image.select.btn": "selecionar imagem", 26 | "paginate.sumary": "Exibindo {{recordsLength}} de {{count}}", 27 | "widget.html.label": "Texto ou HTML customizado", 28 | "permission.link": "Permissões", 29 | "widget.update": "Atualizar widget", 30 | "widget.create": "Criar widget", 31 | "auth.register.spam": "O seu email {{email}} está marcado como spam pelo Google Recaptcha.", 32 | "Home": "Início", 33 | "Male": "Masculino", 34 | "Female": "Feminino", 35 | "updatedAt": "Atualizado em", 36 | "createdAt": "Criado em", 37 | "Create": "Criar", 38 | "widget.invalid.context": "O contexto do widget é inválido", 39 | "response.badRequest.title": "Dados inválidos", 40 | "response.badRequest.description": "Alguma informação enviada para o sistema está inválida", 41 | "response.notFound.title": "Não encontrado", 42 | "response.notFound.description": "O link ou a página que você procurava não foi encontrada no sistema", 43 | "response.forbidden.title": "Acesso restrito!", 44 | "response.forbidden.description": "Você não tem permissão no sistema para acessar essa página", 45 | "response.serveError.title": "Erro no servidor!", 46 | "response.serveError.description": "Aconteceu um erro no sistema", 47 | "auth.register.acceptTerms.required": "Você deve aceitar os nossos termos para criar uma conta no sistema", 48 | "skip.to.content.link.text": "Ir para o conteúdo principal", 49 | "role.delete.confirm.msg": "Tem certeza que deseja deletar esse perfil de usuário?", 50 | "Search": "Buscar" 51 | } -------------------------------------------------------------------------------- /src/Router/search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route search parsers and targets 3 | */ 4 | const moment = require('moment'), 5 | Sequelize = require('sequelize'), 6 | Op = Sequelize.Op; 7 | 8 | /** 9 | * Converts one date string to dateTime 10 | * @param {string} d date 11 | * @return {string} dateTime 12 | */ 13 | function dateToDateTime(d) { 14 | if (d) { 15 | let date = moment(d); 16 | // return null if not is valid 17 | if (!date.isValid()) return null; 18 | // return data in datetime format 19 | return date.format('YYYY-MM-DD HH:mm:ss'); 20 | } else { 21 | return null; 22 | } 23 | } 24 | 25 | module.exports = { 26 | parsers: { 27 | equalBoolean(searchName, field, value, w) { 28 | if (!value || value.toLowerCase() === 'false') { 29 | return w[field.target.field] = false; 30 | } else { 31 | return w[field.target.field] = true; 32 | } 33 | }, 34 | equal(searchName, field, value, w) { 35 | return w[field.target.field] = value; 36 | }, 37 | contains(searchName, field, value, w) { 38 | return w[field.target.field] = { 39 | [Op.like]: '%'+value+'%' 40 | }; 41 | }, 42 | startsWith(searchName, field, value, w) { 43 | return w[field.target.field] = { 44 | [Op.like]: value+'%' 45 | }; 46 | }, 47 | userSearchQuery(searchName, field, value, w) { 48 | return w[Op.or] = { 49 | email: { 50 | [Op.eq]: value 51 | }, 52 | displayName: { 53 | [Op.like]: value+'%' 54 | }, 55 | username: { 56 | [Op.eq]: value 57 | } 58 | }; 59 | }, 60 | since(searchName, field, value, w) { 61 | if (!value) return w; 62 | 63 | return w[field.target.field] = { 64 | [Op.gt]: dateToDateTime(value) 65 | }; 66 | }, 67 | 68 | // if user from :userId is 69 | paramIs(searchName, field, value, w, req) { 70 | return w[field.target.field] = req.params[field.param]; 71 | } 72 | }, 73 | targets: { 74 | field(searchName, field, value, query, req) { 75 | req.we.router.search.parsers[field.parser](searchName, field, value, query.where, req); 76 | }, 77 | association(searchName, field, value, query, req) { 78 | for (let i = 0; i < query.include.length; i++) { 79 | if (query.include[i]) { 80 | if (!query.include[i].where) query.include[i].where = {}; 81 | req.we.router.search.parsers[field.parser](searchName, field, value, query.include[i].where, req); 82 | // required target configuration 83 | if (field.target.required) query.include[i].required = true; 84 | 85 | break; 86 | } 87 | } 88 | } 89 | } 90 | }; -------------------------------------------------------------------------------- /test/testData/we-plugin-fastload/plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = function loadPlugin(projectPath, Plugin) { 2 | const plugin = new Plugin(__dirname); 3 | 4 | plugin.fastLoader = function fastLoader(we, done) { 5 | const Op = we.db.Sequelize.Op; 6 | 7 | // search parsers: 8 | we.router.search.parsers.orWithMinusParser = function orWithMinusParser(searchName, field, value, w) { 9 | // = [] is same of or in sequelize 10 | return w[field] = { [Op.or]: value.split(',') }; 11 | }; 12 | 13 | // search targets: 14 | we.router.search.targets.inNameAndDescription = 15 | function inNameAndDescription(searchName, field, value, query, req) { 16 | req.we.router.search.parsers[field.parser](searchName, 'title', value, query.where, req); 17 | req.we.router.search.parsers[field.parser](searchName, 'text', value, query.where, req); 18 | }; 19 | 20 | // controllers: 21 | we.controllers.dog = new we.class.Controller({ 22 | bark(req, res) { 23 | req.we.db.models.dog 24 | .findById(req.params.id) 25 | .then( (d)=> { 26 | if (!d) return res.notFound('dog.not.found'); 27 | 28 | res.send({ result: d.bark() }); 29 | return null; 30 | }) 31 | .catch(res.queryError); 32 | } 33 | }); 34 | 35 | // model hooks 36 | we.db.modelHooks.giveVaccine = function giveVaccine(record) { 37 | if (record.age > 1) { 38 | record.vaccine = record.vaccine+1; 39 | } 40 | }; 41 | 42 | // model instance methods 43 | we.db.modelInstanceMethods.bark = function bark() { 44 | return 'AuAU'; 45 | }; 46 | 47 | // model class methods 48 | we.db.modelClassMethods.jump = function bark(dog) { 49 | return `${dog.id} jumped!`; 50 | }; 51 | 52 | // JSON model 53 | we.db.modelsConfigs.dog = we.db.defineModelFromJson( { 54 | attributes: { 55 | name: { 56 | type: 'STRING' 57 | }, 58 | vaccine: { 59 | type: 'INTEGER', 60 | dafaultValue: 0 61 | }, 62 | age: { 63 | type: 'INTEGER', 64 | dafaultValue: 1 65 | } 66 | }, 67 | options: { 68 | tableName: 'hotdog' 69 | }, 70 | hooks: { 71 | beforeUpdate: [ 'giveVaccine' ] 72 | }, 73 | instanceMethods: { 74 | bark: 'bark' 75 | }, 76 | classMethods: { 77 | jump: 'jump' 78 | } 79 | }, we); 80 | 81 | done(); 82 | }; 83 | 84 | plugin.setResource({ 85 | name: 'dog' 86 | }); 87 | 88 | plugin.setRoutes({ 89 | 'post /dog/:id/bark': { 90 | controller: 'dog', 91 | model: 'dog', 92 | action: 'bark' 93 | } 94 | }); 95 | 96 | return plugin; 97 | }; -------------------------------------------------------------------------------- /src/messages/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Express request message strategy 3 | * 4 | * This module helps with system messages how will be send to users 5 | */ 6 | 7 | const messenger = { 8 | setFunctionsInResponse(req, res, next) { 9 | if (!next) next = function(){}; 10 | if (!res.locals.messages ) res.locals.messages = []; 11 | 12 | /** 13 | * add one message in res.messages array 14 | * 15 | * @param {String} status success, error, warning, info ... etc 16 | * @param {String} message message text , use one translatable string 17 | * @param {Object} extraData extra data to set in message 18 | */ 19 | res.addMessage = function addMessage(status, message, extraData) { 20 | if (status == 'error') status = 'danger'; 21 | if (status == 'warn') status = 'warning'; 22 | 23 | // suport for localization ... see node-i18n 24 | if (req.__) { 25 | if (typeof message != 'string') { 26 | message = req.__(message.text, message.vars); 27 | } else { 28 | message = req.__(message); 29 | } 30 | } else { 31 | // only set texts if dont have localization configured 32 | if (typeof message != 'string') { 33 | message = message.text; 34 | } else { 35 | message = message; 36 | } 37 | } 38 | 39 | // push messages array 40 | res.locals.messages.push({ 41 | status: status, 42 | message: message, 43 | extraData: (extraData || null) 44 | }); 45 | }; 46 | 47 | /** 48 | * Get all messages 49 | * 50 | * @return {Array} messages array 51 | */ 52 | res.getMessages = function getMessages() { 53 | let messages = []; 54 | 55 | if (this.locals && this.locals.messages) { 56 | messages = this.locals.messages; 57 | } 58 | 59 | // suport to flash messages 60 | if (req.flash && req.we.config.session) { 61 | let flashMessages = this.locals.req.flash('messages'); 62 | if (flashMessages) { 63 | for (let i = 0; i < flashMessages.length; i++) { 64 | messages.push(flashMessages[i]); 65 | } 66 | } 67 | } 68 | 69 | return messages; 70 | }; 71 | 72 | /** 73 | * Move all locals messages to session (flash) 74 | * 75 | * This function is used in http redirects to store messages between page changes 76 | */ 77 | res.moveLocalsMessagesToFlash = function moveLocalsMessagesToFlash() { 78 | if (req.flash) { 79 | const msgs = res.getMessages(); 80 | if (msgs && msgs.length) req.flash('messages', msgs); 81 | } 82 | }; 83 | 84 | next(); 85 | } 86 | }; 87 | 88 | // alias 89 | module.middleware = module.setFunctionsInResponse; 90 | 91 | module.exports = messenger; -------------------------------------------------------------------------------- /src/responses/JSONF.js: -------------------------------------------------------------------------------- 1 | const { isEmpty, isObject } = require('lodash'); 2 | 3 | /** 4 | * JSON response formatter 5 | * @type {Object} 6 | */ 7 | const jsonF = { 8 | /** 9 | * JSON response format 10 | * 11 | * @param {Object} data 12 | * @param {Object} req Express.js request 13 | * @param {Object} res Express.js response 14 | * @return {Object} JS object to send with res.send 15 | */ 16 | jsonFormater(req, res) { 17 | if (!res.locals.model) { 18 | if (!res.locals.data) res.locals.data = {}; 19 | // set messages 20 | res.locals.data.messages = res.locals.messages; 21 | return res.send(res.locals.data); 22 | } 23 | 24 | const response = {}; 25 | 26 | if (req.we.config.sendNestedModels) { 27 | response[res.locals.model] = res.locals.data; 28 | } else { 29 | response[res.locals.model] = parseRecord(req, res, res.locals.data); 30 | } 31 | 32 | // check field privacity access 33 | if (res.locals.data) { 34 | req.we.db.checkRecordsPrivacity(res.locals.data); 35 | } 36 | 37 | response.meta = res.locals.metadata; 38 | 39 | if (!isEmpty( res.locals.messages) ) { 40 | // set messages 41 | response.messages = res.locals.messages; 42 | } 43 | 44 | res.send(response); 45 | } 46 | }; 47 | 48 | 49 | /** 50 | * Parse one record associations for JSON format to change association model object to association model id 51 | * 52 | * @param {Object} req Express.js request 53 | * @param {Object} res Express.js response 54 | * @param {Obejct} record Record to parse 55 | * @return {Object} returns the parsed record 56 | */ 57 | function parseRecord(req, res, record) { 58 | for (var associationName in res.locals.Model.associations) { 59 | if (!record[associationName]) { 60 | if ( record.dataValues[ associationName + 'Id' ] ) { 61 | record.dataValues[ associationName ] = record[ associationName + 'Id' ]; 62 | } 63 | } else { 64 | if (record.dataValues[ associationName + 'Id' ]) { 65 | record.dataValues[ associationName ] = record[ associationName + 'Id' ]; 66 | } else if ( isObject(record[ associationName ] && record[ associationName ].id) ) { 67 | record.dataValues[ associationName ] = record[ associationName ].id; 68 | // if is a NxN association 69 | } else if( req.we.utils.isNNAssoc ( record[ associationName ] ) ) { 70 | record.dataValues[ associationName ] = record[ associationName ].id; 71 | } else { 72 | for (var i = record.dataValues[ associationName ].length - 1; i >= 0; i--) { 73 | record.dataValues[ associationName ][i] = record.dataValues[ associationName ][i].id; 74 | } 75 | } 76 | } 77 | } 78 | return record; 79 | } 80 | 81 | module.exports = jsonF; -------------------------------------------------------------------------------- /src/Sanitizer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We.js sanitizer to sanitize model and text variable data 3 | * 4 | */ 5 | const sanitizeHtml = require('sanitize-html'); 6 | 7 | function Sanitizer (we) { 8 | this.we = we; 9 | let sanitizer = this; 10 | 11 | // after define all models add term field hooks in models how have terms 12 | we.hooks.on('we:models:set:joins', this.setSanitizeModelAttrs); 13 | 14 | /** 15 | * sequelize hook handler to sanitize all text fields with we sanitizer 16 | * 17 | * @param {record} r 18 | * @param {options} opts 19 | * @param {FuncDBBeforeUpdateAndCreateHooktion} done 20 | */ 21 | this.DBBeforeUpdateAndCreateHook = function DBBeforeUpdateAndCreateHook(r) { 22 | sanitizer.sanitizeModelAttrs(r, this.name); 23 | }; 24 | } 25 | 26 | /** 27 | * Set sanitizer in before create and update model hooks 28 | * 29 | * @param {Object} we we.js object 30 | * @param {Function} done callback 31 | */ 32 | Sanitizer.prototype.setSanitizeModelAttrs = function setSanitizeModelAttrs (we, done) { 33 | let models = we.db.models; 34 | let sanitizer = we.sanitizer; 35 | 36 | for (let modelName in models) { 37 | // set sanitizer hook 38 | models[modelName].addHook('beforeCreate', 'sanitizeBeforeSv', sanitizer.DBBeforeUpdateAndCreateHook); 39 | models[modelName].addHook('beforeUpdate', 'sanitizeBeforeUP', sanitizer.DBBeforeUpdateAndCreateHook); 40 | } 41 | 42 | done(); 43 | }; 44 | 45 | /** 46 | * Sanitize one text html 47 | * @param {String} dirty html to sanitize 48 | * @return {String} sanitized html 49 | */ 50 | Sanitizer.prototype.sanitize = function sanitize (dirty) { 51 | return sanitizeHtml(dirty, this.we.config.security.sanitizer); 52 | }; 53 | 54 | /** 55 | * Sanitize all text attrs in one object 56 | * 57 | * @param {Object} obj sanitize obj attrs 58 | * @return {Object} return obj 59 | */ 60 | Sanitizer.prototype.sanitizeAllAttr = function sanitizeAllAttr(obj){ 61 | for (let prop in obj) { 62 | 63 | if (prop !== 'id') { 64 | if (typeof obj[prop] == 'string') { 65 | obj[prop] = this.sanitize(obj[prop]); 66 | } 67 | } 68 | } 69 | return obj; 70 | }; 71 | 72 | /** 73 | * Sanitize all sequelize text record attrs 74 | * 75 | * @param {Object} record sequelize record to sanitize 76 | * @param {String} modelName model name 77 | * @return {Object} return obj 78 | */ 79 | Sanitizer.prototype.sanitizeModelAttrs = function sanitizeModelAttrs (record, modelName) { 80 | let db = this.we.db; 81 | 82 | for (let prop in record.dataValues) { 83 | if (prop !== 'id') { 84 | if (typeof record.dataValues[prop] == 'string') { 85 | // if dont have value 86 | if (!record.getDataValue(prop)) continue; 87 | // check skip cfg, skipSanitizer 88 | if ( 89 | !db.modelsConfigs[modelName] || 90 | !db.modelsConfigs[modelName].definition[prop] || 91 | db.modelsConfigs[modelName].definition[prop].skipSanitizer 92 | ) { 93 | continue; 94 | } 95 | // sanitize this value 96 | record.setDataValue(prop, this.sanitize(record.getDataValue(prop))); 97 | } 98 | } 99 | } 100 | return record; 101 | }; 102 | 103 | module.exports = Sanitizer; -------------------------------------------------------------------------------- /src/staticConfig/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Static configs loader 3 | */ 4 | 5 | const fs = require('fs'), 6 | path = require('path'), 7 | _ = require('lodash'); 8 | 9 | /** 10 | * Get project static configs 11 | * 12 | * @param {String} projectPath project path ( opcional ) 13 | * @return {Object} configs 14 | */ 15 | function staticConfig (projectPath, app) { 16 | if (!projectPath) throw new Error('project path is required for load static configs'); 17 | 18 | // return configs if already is loaded 19 | if (app.staticConfigsIsLoad) return app.config; 20 | 21 | // - load and merge project configs 22 | 23 | let projectConfigFolder = app.projectConfigFolder; 24 | 25 | let files = []; 26 | 27 | try { 28 | files = fs.readdirSync(projectConfigFolder); 29 | } catch(e) { 30 | if (e.code != 'ENOENT') console.error('Error on load project config folder: ', e); 31 | } 32 | 33 | let file; 34 | for (let i = 0; i < files.length; i++) { 35 | if (files[i] == 'local.js') continue; // skip locals.js to load after all 36 | if (!files[i].endsWith('.js')) continue; // only accepts .js config files 37 | 38 | file = path.resolve(projectConfigFolder, files[i]); 39 | // skip dirs 40 | if (fs.lstatSync(file).isDirectory()) continue; 41 | _.merge(app.config, require(file)); 42 | } 43 | 44 | let jsonConfiguration = staticConfig.readJsonConfiguration(projectConfigFolder); 45 | let localConfigFile = staticConfig.readLocalConfigFile(projectConfigFolder); 46 | 47 | // load project local config file 48 | _.merge(app.config, jsonConfiguration, localConfigFile); 49 | 50 | app.staticConfigsIsLoad = true; 51 | 52 | return app.config; 53 | } 54 | 55 | /** 56 | * Read the config/locals.js configuration file 57 | */ 58 | staticConfig.readLocalConfigFile = function (projectConfigFolder) { 59 | try { 60 | // load local.js after others configs 61 | return require( path.resolve(projectConfigFolder, 'local.js') ); 62 | } catch (e) { 63 | if (e.code != 'MODULE_NOT_FOUND' ) { 64 | console.error('Unknow error on load local.js config:', e); 65 | } 66 | return {}; 67 | } 68 | }; 69 | 70 | /** 71 | * Read JSON Configuration file and create it if not exists 72 | */ 73 | staticConfig.readJsonConfiguration = function (projectConfigFolder) { 74 | const dirCFJSON = path.resolve(projectConfigFolder, 'configuration.json'); 75 | 76 | try { 77 | // load configuration.json after others but before local.js 78 | return JSON.parse(fs.readFileSync(dirCFJSON)); 79 | } catch (e) { 80 | if (e.code != 'ENOENT' ) { 81 | console.error('Unknow error on load config/configuration.json config:', e); 82 | } else { 83 | // if not exists create it 84 | fs.writeFileSync(dirCFJSON, '{}'); 85 | console.log('The file configuration.json was created'); 86 | } 87 | return {}; 88 | } 89 | }; 90 | 91 | staticConfig.loadPluginConfigs = function loadPluginConfigs(we) { 92 | if (we.pluginConfigsIsLoad) return we.config; 93 | 94 | const pluginManager = we.pluginManager; 95 | 96 | let pluginConfigs = {}; 97 | 98 | // - load and merge plugin configs 99 | for (let pluginName in pluginManager.plugins) { 100 | _.merge(pluginConfigs, pluginManager.plugins[pluginName].configs); 101 | } 102 | // load project local config file 103 | we.config = _.merge(pluginConfigs, we.config); 104 | 105 | we.pluginConfigsIsLoad = true; 106 | 107 | return we.config; 108 | }; 109 | 110 | module.exports = staticConfig; 111 | -------------------------------------------------------------------------------- /src/express/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We.js express module 3 | */ 4 | 5 | const express = require('express'), 6 | compress = require('compression'), 7 | favicon = require('serve-favicon'), 8 | bodyParser = require('body-parser'), 9 | responseTypeMD = require('../Router/responseType.js'), 10 | messages = require('../messages'), 11 | cors = require('cors'); 12 | 13 | module.exports = function initExpress (we) { 14 | const weExpress = express(); 15 | // express response compression middleware 16 | weExpress.use(compress()); 17 | // remove uneed x-powered-by header 18 | weExpress.disable('x-powered-by'); 19 | // set default vars 20 | weExpress.use(function setDefaultVars (req, res, next) { 21 | // set default req.getWe for suport with this getter 22 | req.getWe = function getWejs() { return we; }; 23 | // set message functions in response 24 | messages.setFunctionsInResponse(req, res); 25 | // save a reference to appName 26 | res.locals.appName = we.config.appName || ''; 27 | // set default app title 28 | res.locals.title = we.config.appName || ''; 29 | // set req to be avaible in template vars 30 | Object.defineProperty(res.locals, 'req', { 31 | get: function getReq() { return req; } 32 | }); 33 | // set metadata var 34 | res.locals.metadata = {}; 35 | // metadata tags to print in html response 36 | res.locals.metatag = ''; 37 | // set user role names array 38 | req.userRoleNames = []; 39 | // save env in locals 40 | res.locals.env = we.env; 41 | // add default is authenticated check 42 | if (!req.isAuthenticated) req.isAuthenticated = we.utils.isAuthenticated.bind(req); 43 | // set response type 44 | return responseTypeMD(req, res, ()=> { 45 | // alias targets redirect for html request 46 | if (we.plugins['we-plugin-url-alias'] && req.haveAlias && req.accepts('html')) { 47 | // is a target how have alias then redirect to it 48 | res.writeHead(307, { 49 | 'Location': req.haveAlias.alias + (req.aliasQuery || ''), 50 | 'Content-Type': 'text/plain', 51 | 'Cache-Control':'public, max-age=345600', 52 | 'Expires': new Date(Date.now() + 345600000).toUTCString() 53 | }); 54 | return res.end(); 55 | } else { 56 | next(); 57 | } 58 | }); 59 | }); 60 | // send set params event 61 | we.events.emit('we:express:set:params', { we: we, express: weExpress }); 62 | // custom we.js responses like: res.ok() and res.forbidden() 63 | weExpress.use(we.responses.setCustomResponses); 64 | // CORS https://github.com/troygoode/node-cors 65 | weExpress.options('*', cors(we.config.security.CORS)); 66 | // favicon config 67 | if (we.config.favicon) weExpress.use(favicon(we.config.favicon)); 68 | 69 | if (we.config.enableRequestLog) { 70 | const logger = require('morgan'); 71 | weExpress.use(logger('dev')); 72 | } 73 | 74 | // robots .txt file middleware 75 | weExpress.get('/robots.txt', function robotsTXTmiddleware(req, res) { 76 | if (req.we.config.robotsTXT) { 77 | res.sendFile(req.we.config.robotsTXT); 78 | } else { 79 | res.notFound(); 80 | } 81 | }); 82 | 83 | weExpress.use(bodyParser.json(we.config.bodyParser)); 84 | weExpress.use(bodyParser.json({ type: 'application/vnd.api+json' })); 85 | weExpress.use(bodyParser.urlencoded({ extended: false })); 86 | 87 | weExpress.use(we.utils.cookieParser()); 88 | // set session store 89 | require('./sessionStore')(we, weExpress); 90 | // add flash middleware if session is avaible 91 | if (we.config.session) { 92 | const flash = require('connect-flash'); 93 | weExpress.use(flash()); 94 | } 95 | 96 | // set public folders 97 | if (!we.config.disablePublicFolder) { 98 | require('./publicFolders')(we, weExpress); 99 | } 100 | return weExpress; 101 | }; 102 | -------------------------------------------------------------------------------- /test/tests/requests/plugin.fastload.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'), 2 | request = require('supertest'), 3 | helpers = require('we-test-tools').helpers, 4 | Chance = require('chance'), 5 | chance = new Chance(); 6 | 7 | let _, http, we; 8 | 9 | function dogStub() { 10 | return { 11 | name: chance.name(), 12 | }; 13 | } 14 | 15 | describe('plugin.fastload.requests', function() { 16 | 17 | before(function (done) { 18 | http = helpers.getHttp(); 19 | we = helpers.getWe(); 20 | _ = we.utils._; 21 | return done(); 22 | }); 23 | 24 | afterEach(function(done) { 25 | // delete old dogs 26 | we.db.models.dog.truncate() 27 | .then( ()=> { 28 | done(); 29 | return null; 30 | }) 31 | .catch(done); 32 | }); 33 | 34 | describe('json', function() { 35 | describe('GET /dog', function(){ 36 | it ('should return 3 dogs', function (done) { 37 | const data = [ 38 | dogStub(), 39 | dogStub(), 40 | dogStub() 41 | ]; 42 | 43 | we.db.models.dog 44 | .bulkCreate(data) 45 | .spread( ()=> { 46 | request(http) 47 | .get('/dog') 48 | .set('Accept', 'application/json') 49 | .expect(200) 50 | .end(function (err, res) { 51 | if (err) { 52 | console.error('res.text>',res.text); 53 | throw err; 54 | } 55 | 56 | for (var i = 0; i < data.length; i++) { 57 | assert(res.body.dog[i].id); 58 | } 59 | 60 | assert.equal(res.body.meta.count, data.length, `Should return 3 dogs`); 61 | 62 | done(); 63 | }); 64 | }) 65 | .catch(done); 66 | }); 67 | }); 68 | 69 | it ('/dog/count should get dogs count', function (done) { 70 | const data = [ 71 | dogStub(), 72 | dogStub(), 73 | dogStub() 74 | ]; 75 | 76 | we.db.models.dog 77 | .bulkCreate(data) 78 | .spread( ()=> { 79 | request(http) 80 | .get('/dog/count') 81 | .set('Accept', 'application/json') 82 | .expect(200) 83 | .end( (err, res)=> { 84 | if (err) { 85 | console.error('res.text>',res.text); 86 | throw err; 87 | } 88 | 89 | assert(res.body.count, `Count should be present in response`); 90 | assert.equal(res.body.count, 3, `Count should be 3`); 91 | 92 | done(); 93 | }); 94 | }) 95 | .catch(done); 96 | }); 97 | 98 | }); 99 | 100 | describe('GET /dog/:id', function(){ 101 | it ('Should get one dog', function (done) { 102 | we.db.models.dog 103 | .create(dogStub()) 104 | .then( (p)=> { 105 | request(http) 106 | .get('/dog/'+p.id) 107 | .set('Accept', 'application/json') 108 | .expect(200) 109 | .end( (err, res)=> { 110 | if (err) throw err; 111 | 112 | assert(res.body.dog.id); 113 | assert.equal(res.body.dog.id, p.id); 114 | assert.equal(res.body.dog.name, p.name); 115 | 116 | done(); 117 | }); 118 | }).catch(done); 119 | }); 120 | }); 121 | 122 | 123 | describe('POST /dog/:id/bark', function(){ 124 | it ('Should return dog bark', function (done) { 125 | we.db.models.dog 126 | .create(dogStub()) 127 | .then( (p)=> { 128 | 129 | request(http) 130 | .post('/dog/'+p.id+'/bark') 131 | .set('Accept', 'application/json') 132 | .expect(200) 133 | .end( (err, res)=> { 134 | if (err) throw err; 135 | 136 | assert(res.body.result); 137 | assert.equal(res.body.result, 'AuAU'); 138 | 139 | done(); 140 | }); 141 | 142 | }) 143 | .catch(done); 144 | }); 145 | }); 146 | 147 | }); 148 | -------------------------------------------------------------------------------- /src/class/Theme.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We.js Theme prototype 3 | */ 4 | 5 | const _ = require('lodash'), 6 | path = require('path'); 7 | 8 | module.exports = function getThemePrototype(we) { 9 | /** 10 | * We.js theme Class constructor 11 | * 12 | * @param {string} name theme npm pakage name 13 | * @param {string} projectPath project path where the theme is instaled 14 | */ 15 | function Theme (name, projectPath, options) { 16 | if (!name || (typeof name !== 'string') ) { 17 | return new Error('Param name is required for instantiate a new Theme object'); 18 | } 19 | if (!options) options = {}; 20 | 21 | this.we = we; 22 | this.hooks = this.we.hooks; 23 | this.events = this.we.events; 24 | 25 | const self = this; 26 | 27 | this.config = {}; 28 | 29 | this.projectPath = projectPath; 30 | 31 | // npm theme path / folder 32 | this.config.themeFolder = options.themeFolder || path.resolve(projectPath, 'node_modules', name); 33 | 34 | this.config.shortThemeFolder = options.themeFolder || 'node_modules' + '/' + name; 35 | 36 | // load theme module 37 | delete require.cache[require.resolve(this.config.themeFolder)]; 38 | const npmModule = require(this.config.themeFolder); 39 | 40 | // always initialize all instance properties 41 | this.name = name; 42 | 43 | const packageJSONPath = this.config.themeFolder+'/package.json'; 44 | delete require.cache[require.resolve(packageJSONPath)]; 45 | this['package.json'] = require(packageJSONPath); 46 | // shortcut for get installed theme version 47 | this.version = this['package.json'].version; 48 | 49 | this.templates = {}; 50 | this.layouts = {}; 51 | this.widgets = {}; 52 | 53 | this.tplsFolder = path.resolve(self.config.themeFolder, 'templates/server'); 54 | 55 | _.merge(this, npmModule); 56 | } 57 | 58 | Theme.prototype.init = function init(cb) { 59 | const self = this; 60 | 61 | // if autoLoadAllTemplates not is set or is true load all template names 62 | if (!we.config.cacheThemeTemplates) { 63 | we.utils.listFilesRecursive(self.tplsFolder, (err, files)=> { 64 | if (err) return cb(err); 65 | 66 | files.filter(function filterTPL(f) { 67 | if (f.endsWith('.hbs')) return true; 68 | return false; 69 | }) 70 | .forEach(function loadTPL(f) { 71 | // remove the base url and the file extension 72 | let name = f.replace(self.tplsFolder + path.sep, '') 73 | .replace('.hbs', ''); 74 | 75 | // ensures that template names always have / slashes 76 | if (path.sep != '/') name = name.split(path.sep).join('/'); 77 | 78 | self.templates[name] = f; 79 | }); 80 | 81 | cb(); 82 | }); 83 | } else { 84 | cb(); 85 | } 86 | }; 87 | /** 88 | * Theme config object 89 | * 90 | * @type {Object} 91 | */ 92 | // Theme.prototype.config = {}; 93 | 94 | /** 95 | * Render one template with variables and template 96 | * 97 | * @param {object} req express js request 98 | * @param {object} res express.js response 99 | * @param {string} template the template name 100 | * @param {object} data data passed to template 101 | */ 102 | Theme.prototype.render = function render(req, res, template, data) { 103 | 104 | if (!res.locals.layout ) { 105 | res.locals.layout = this.config.layoutPath; 106 | } 107 | 108 | res.view( path.resolve(this.config.templatesFolder, template), data ); 109 | }; 110 | 111 | /** 112 | * Get theme layout path 113 | */ 114 | Theme.prototype.getThemeLayout = function getThemeSailsTemplatesFolder() { 115 | return this.config.layoutPath; 116 | }; 117 | 118 | /** 119 | * Get theme email template folder 120 | */ 121 | Theme.prototype.getThemeEmailTemplatesFolder = function getThemeSailsTemplatesFolder(){ 122 | return path.resolve(this.config.themeFolder, this.config.emailTemplates.path); 123 | }; 124 | 125 | return Theme; 126 | }; -------------------------------------------------------------------------------- /test/tests/modules/lib.utils.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var helpers = require('we-test-tools').helpers; 3 | var path = require('path'); 4 | var utils, we; 5 | 6 | describe('lib/utils', function () { 7 | before(function (done) { 8 | utils = require('../../../src/utils'); 9 | we = helpers.getWe(); 10 | done(); 11 | }); 12 | 13 | describe('listFilesRecursive', function() { 14 | it('should list files in we-plugin-post dir', function (done) { 15 | var folder = path.resolve(process.cwd(), 'node_modules', 'we-plugin-post'); 16 | 17 | utils.listFilesRecursive(folder, function(err, files){ 18 | if (err) return done(err); 19 | 20 | assert.equal(files.length, 12); 21 | 22 | assert(files.indexOf(folder+'/package.json') > -1); 23 | assert(files.indexOf(folder+'/plugin.js') > -1); 24 | assert(files.indexOf(folder+'/server/controllers/post.js') > -1); 25 | assert(files.indexOf(folder+'/server/models/hero.json') > -1); 26 | assert(files.indexOf(folder+'/server/models/post.js') > -1); 27 | assert(files.indexOf(folder+'/server/models/user.js') > -1); 28 | 29 | done(); 30 | }); 31 | }); 32 | it('should return a empty list if the dir not is found', function (done) { 33 | var folder = path.resolve(process.cwd(), 'asadasdas'); 34 | 35 | utils.listFilesRecursive(folder, function(err, files){ 36 | if (err) return done(err); 37 | 38 | assert.equal(files.length, 0); 39 | 40 | done(); 41 | }); 42 | }); 43 | it('should a empty list if the dir is one file', function (done) { 44 | var folder = path.resolve(process.cwd(), 'plugin.js'); 45 | 46 | utils.listFilesRecursive(folder, function(err, files) { 47 | assert(!files); 48 | assert.equal(err.code, 'ENOTDIR'); 49 | 50 | done(); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('isNNAssoc', function() { 56 | it ('should return true if are a belongsTo assoc', function(done) { 57 | assert(utils.isNNAssoc({ 58 | associationType: 'belongsTo' 59 | })); 60 | done(); 61 | }); 62 | it ('should return false if are a hasMany assoc', function(done) { 63 | assert(!utils.isNNAssoc({ 64 | associationType: 'hasMany' 65 | })); 66 | done(); 67 | }); 68 | }); 69 | 70 | describe('getRedirectUrl', function() { 71 | it ('should return a valid url from body', function(done) { 72 | var req = { 73 | we: we, 74 | body: { 75 | redirectTo: '/test' 76 | } 77 | }; 78 | 79 | var url = utils.getRedirectUrl(req); 80 | assert.equal(url, '/test'); 81 | 82 | done(); 83 | }); 84 | 85 | it ('should return null to invalid url from body', function(done) { 86 | var req = { 87 | we: we, 88 | body: { 89 | redirectTo: 'http://google.com' 90 | } 91 | }; 92 | 93 | var url = utils.getRedirectUrl(req); 94 | assert.equal(url, null); 95 | 96 | done(); 97 | }); 98 | 99 | it ('should return url from service', function(done) { 100 | we.config.services.dance = { 101 | url: '/dancee' 102 | }; 103 | 104 | var req = { 105 | we: we, 106 | query: { 107 | service: 'dance' 108 | } 109 | }; 110 | 111 | var url = utils.getRedirectUrl(req); 112 | assert.equal(url, '/dancee'); 113 | 114 | delete we.config.services.dance; 115 | 116 | done(); 117 | }); 118 | 119 | it ('should return url from query.redirectTo', function(done) { 120 | 121 | var req = { 122 | we: we, 123 | query: { 124 | redirectTo: '/iooo' 125 | } 126 | }; 127 | 128 | var url = utils.getRedirectUrl(req); 129 | assert.equal(url, '/iooo'); 130 | 131 | done(); 132 | }); 133 | }); 134 | 135 | describe('parseAttributes', function(){ 136 | it ('should return false if are a hasMany assoc', function (done) { 137 | 138 | var str = utils.helper.parseAttributes({ 139 | hash: { 140 | class: 'btn btn-default' 141 | } 142 | }); 143 | 144 | assert(str.indexOf('class="btn btn-default"') > -1); 145 | 146 | done(); 147 | }); 148 | }) 149 | }); -------------------------------------------------------------------------------- /test/tests/modules/database.defaultModelDefinitionConfigs.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var helpers = require('we-test-tools').helpers; 3 | var we, dmdc, contextLoader, instanceMethods; 4 | 5 | describe('database.defaultModelDefinitionConfigs', function () { 6 | 7 | before(function (done) { 8 | we = helpers.getWe(); 9 | dmdc = we.db.defaultModelDefinitionConfigs; 10 | contextLoader = we.db.defaultClassMethods.contextLoader; 11 | instanceMethods = we.db.defaultInstanceMethods; 12 | done(); 13 | }); 14 | 15 | describe('define.classMethods', function() { 16 | 17 | it('classMethods.contextLoader should return done id dont have res.local.id', function (done) { 18 | var req = { 19 | userRoleNames: [], 20 | locals: {} 21 | }; 22 | var res = { 23 | locals: { loadCurrentRecord: true } 24 | }; 25 | 26 | var callback = function callback() { 27 | done(); 28 | }; 29 | 30 | contextLoader(req, res, callback); 31 | }); 32 | 33 | it('classMethods.contextLoader should set owner if creatorId = req.user.id', function (done) { 34 | var creatorId = 2016; 35 | var recordId = 10; 36 | var req = { 37 | isAuthenticated: function() { return true; }, 38 | user: { id: creatorId }, 39 | userRoleNames: [], 40 | locals: {} 41 | }; 42 | var res = { 43 | locals: { 44 | id: recordId, // record ID 45 | loadCurrentRecord: true 46 | }, 47 | }; 48 | 49 | var callback = function callback() { 50 | assert(req.userRoleNames.indexOf('owner') > -1); 51 | done(); 52 | }; 53 | 54 | contextLoader.bind({ 55 | findOne: function(opts) { 56 | assert(opts.where.id, recordId); 57 | 58 | return new we.db.Sequelize.Promise(function(resolve){ 59 | resolve({ 60 | dataValues: { 61 | creatorId: creatorId 62 | }, 63 | isOwner: function(id) { 64 | assert.equal(id, creatorId); 65 | return true; 66 | } 67 | }); 68 | }); 69 | } 70 | })(req, res, callback); 71 | }); 72 | }); 73 | 74 | describe('define.instanceMethods', function() { 75 | describe('isOwner', function() { 76 | it ('isOwner should return true if uid = this.creatorId', function () { 77 | assert(instanceMethods.isOwner.bind({ 78 | creatorId: 2016 79 | })(2016)); 80 | }); 81 | it ('isOwner should return false if uid != this.creatorId', function () { 82 | assert(!instanceMethods.isOwner.bind({ 83 | creatorId: 20 84 | })(2016)); 85 | }); 86 | }); 87 | 88 | describe('getPath', function() { 89 | it ('getPath should throw error without req', function () { 90 | var error = null; 91 | try { 92 | instanceMethods.getPath(); 93 | } catch(e) { 94 | error = e; 95 | } 96 | 97 | assert(error); 98 | }); 99 | it ('getPath should get url from req.we.router.urlTo' 100 | // ,function () { 101 | // var req = { 102 | // we: we, 103 | // paramsArray: [] 104 | // }; 105 | 106 | // var url = instanceMethods.getPath.bind({ 107 | // id: 11, 108 | // '$modelOptions': { 109 | // name: { 110 | // singular: 'user' 111 | // } 112 | // } 113 | // })(req); 114 | 115 | // assert(url); 116 | // assert.equal(url, '/user/11'); 117 | // } 118 | ); 119 | }); 120 | 121 | describe('getLink', function() { 122 | it ('getLink should throw error without req', function () { 123 | var error = null; 124 | try { 125 | instanceMethods.getLink(); 126 | } catch(e) { 127 | error = e; 128 | } 129 | assert(error); 130 | }); 131 | 132 | it ('getLink should return url with hostname', function () { 133 | var req = { we: we }; 134 | 135 | var url = instanceMethods.getLink.bind({ 136 | getPath: function() { 137 | return '/path'; 138 | } 139 | })(req); 140 | assert(url); 141 | assert.equal(url, we.config.hostname+'/path'); 142 | }); 143 | }); 144 | }); 145 | }); -------------------------------------------------------------------------------- /locales/en-us.json: -------------------------------------------------------------------------------- 1 | { 2 | "user.profile.view": "View profile", 3 | "user.profile.edit": "Edit profile", 4 | "we.email.AccontActivationEmail.subject": "User account validation email", 5 | "widget.delete.confirm.msg": "Are you sure you want to delete this widget?", 6 | "Validation isEmail failed": "The email format is invalid", 7 | "Widget": "Widget", 8 | "Edit": "Edit", 9 | "Delete": "Delete", 10 | "widget.title": " ", 11 | "widget.context": "Widget context", 12 | "widget.add": "Add widget", 13 | "widget.sort": "Sort widgets", 14 | "widget.visibility": "Show the widget: ", 15 | "widget.in-portal": "In all pages", 16 | "widget.in-session": "In this session", 17 | "widget.in-session-record": "In contents of this session", 18 | "widget.in-page": "In current page", 19 | "widget.in-context": "In current context", 20 | "layout.select.widget.type": "Select widget type", 21 | "layout.edit.hide.btn": "Save layout", 22 | "layout.edit.btn": "Edit layout", 23 | "image.remove": "remove image", 24 | "image.selector": "Image selector", 25 | "image.uploader.title": "Upload", 26 | "image.selector.title": "Select from salved", 27 | "image.upload.selector.btn": "Select to upload", 28 | "file.image.select.btn": "select image", 29 | "paginate.sumary": "{{recordsLength}} of {{count}}", 30 | "select.option.text": "Select one option ...", 31 | "menu.find": "Menus", 32 | "menu.create": "Create menu", 33 | "form-menu-name": "Name", 34 | "form-placeholder-menu-name": " ", 35 | "form-helper-menu-name": " ", 36 | "form-menu-class": "Class", 37 | "form-placeholder-menu-class": " ", 38 | "form-helper-menu-class": " ", 39 | "link.create": "Criar link", 40 | "form-link-href": "HREF", 41 | "form-placeholder-link-href": " ", 42 | "form-helper-link-href": "Link url", 43 | "form-link-text": "Text", 44 | "form-placeholder-link-text": " ", 45 | "form-helper-link-text": " ", 46 | "form-link-title": "Title", 47 | "form-placeholder-link-title": " ", 48 | "form-helper-link-title": " ", 49 | "form-link-class": "Class", 50 | "form-placeholder-link-class": " ", 51 | "form-helper-link-class": " ", 52 | "form-link-style": "Style", 53 | "form-placeholder-link-style": " ", 54 | "form-helper-link-style": " ", 55 | "form-link-target": "Target", 56 | "form-placeholder-link-target": " ", 57 | "form-helper-link-target": " ", 58 | "form-link-rel": "Rel", 59 | "form-placeholder-link-rel": " ", 60 | "form-helper-link-rel": " ", 61 | "form-link-key": "Key", 62 | "form-placeholder-link-key": " ", 63 | "form-helper-link-key": " ", 64 | "form-link-depth": "Depth", 65 | "form-placeholder-link-depth": " ", 66 | "form-helper-link-depth": " ", 67 | "form-link-weight": "Weight", 68 | "form-placeholder-link-weight": " ", 69 | "form-helper-link-weight": " ", 70 | "form-link-parent": "Parent", 71 | "form-placeholder-link-parent": " ", 72 | "form-helper-link-parent": " ", 73 | "role.create": "Create role", 74 | "role.edit": "Edit role", 75 | "form-role-name": "Name", 76 | "form-placeholder-role-name": " ", 77 | "form-helper-role-name": " ", 78 | "form-role-description": "Description", 79 | "form-placeholder-role-description": " ", 80 | "form-helper-role-description": " ", 81 | "form-role-permissions": "Permissions", 82 | "form-placeholder-role-permissions": " ", 83 | "form-helper-role-permissions": " ", 84 | "widget.html.label": "HTML or text block", 85 | "permission.link": "Permissions", 86 | "widget.update": "Update widget", 87 | "widget.create": "Create widget", 88 | "auth.register.spam": "You email {{email}} is marked as span by Google Recaptcha", 89 | "Home": "Home", 90 | "updatedAt": "Updated at", 91 | "createdAt": "Created at", 92 | "widget.invalid.context": "Invalid widget context", 93 | "response.badRequest.title": "Bad request", 94 | "response.badRequest.description": "Something is wrong with you request or data send to server", 95 | "response.notFound.title": "Not found", 96 | "response.notFound.description": "Page not found in this system", 97 | "response.forbidden.title": "Forbidden!", 98 | "response.forbidden.description": "You dont have access to this page!", 99 | "response.serveError.title": "Server error!", 100 | "response.serveError.description": " ", 101 | "auth.register.acceptTerms.required": "You need to accept your terms to create account", 102 | "skip.to.content.link.text": "Skip to main content", 103 | 104 | "role.delete.confirm.msg": "Are you sure you want to delete this role?", 105 | "Search": "Search" 106 | } -------------------------------------------------------------------------------- /src/class/Controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We.js default controller prototype 3 | * 4 | * All controllers is instance of this Controller prototype and have all actions defined here 5 | */ 6 | const _ = require('lodash'); 7 | 8 | /** 9 | * Constructor 10 | */ 11 | function Controller (attrs) { 12 | for (let attr in attrs) { 13 | if (attrs[attr].bind) { 14 | this[attr] = attrs[attr].bind(this); 15 | } else { 16 | this[attr] = attrs[attr]; 17 | } 18 | } 19 | } 20 | 21 | Controller.prototype = { 22 | /** 23 | * Default find action 24 | * 25 | * @param {Object} req express.js request 26 | * @param {Object} res express.js response 27 | */ 28 | find(req, res) { 29 | return res.locals.Model 30 | .findAndCountAll(res.locals.query) 31 | .then(function afterFindAndCount (record) { 32 | res.locals.metadata.count = record.count; 33 | res.locals.data = record.rows; 34 | res.ok(); 35 | }) 36 | .catch(res.queryError); 37 | }, 38 | 39 | /** 40 | * Default count action 41 | * 42 | * Built for only send count as JSON 43 | * 44 | * @param {Object} req express.js request 45 | * @param {Object} res express.js response 46 | */ 47 | count(req, res) { 48 | return res.locals.Model 49 | .count(res.locals.query) 50 | .then( (count)=> { 51 | res.status(200).send({ count: count }); 52 | }) 53 | .catch(res.queryError); 54 | }, 55 | /** 56 | * Default findOne action 57 | * 58 | * Record is preloaded in context loader by default and is avaible as res.locals.data 59 | * 60 | * @param {Object} req express.js request 61 | * @param {Object} res express.js response 62 | */ 63 | findOne(req, res, next) { 64 | if (!res.locals.data) { 65 | return next(); 66 | } 67 | // by default record is preloaded in context load 68 | res.ok(); 69 | }, 70 | /** 71 | * Create and create page actions 72 | * 73 | * @param {Object} req express.js request 74 | * @param {Object} res express.js response 75 | */ 76 | create(req, res) { 77 | if (!res.locals.template) { 78 | res.locals.template = res.locals.model + '/' + 'create'; 79 | } 80 | 81 | if (!res.locals.data) { 82 | res.locals.data = {}; 83 | } 84 | 85 | if (req.method === 'POST') { 86 | if (req.isAuthenticated && req.isAuthenticated()) { 87 | req.body.creatorId = req.user.id; 88 | } 89 | 90 | _.merge(res.locals.data, req.body); 91 | 92 | return res.locals.Model 93 | .create(req.body) 94 | .then(function afterCreate (record) { 95 | res.locals.data = record; 96 | res.created(); 97 | }) 98 | .catch(res.queryError); 99 | } else { 100 | res.ok(); 101 | } 102 | }, 103 | /** 104 | * Edit, edit page and update action 105 | * 106 | * Record is preloaded in context loader by default and is avaible as res.locals.data 107 | * 108 | * @param {Object} req express.js request 109 | * @param {Object} res express.js response 110 | */ 111 | edit(req, res) { 112 | if (!res.locals.template) { 113 | res.locals.template = res.local.model + '/' + 'edit'; 114 | } 115 | 116 | let record = res.locals.data; 117 | 118 | if (req.we.config.updateMethods.indexOf(req.method) >-1) { 119 | if (!record) { 120 | return res.notFound(); 121 | } 122 | 123 | record.update(req.body) 124 | .then(function reloadAssocs(n) { 125 | return n.reload() 126 | .then(function() { 127 | return n; 128 | }); 129 | }) 130 | .then(function afterUpdate (newRecord) { 131 | res.locals.data = newRecord; 132 | res.updated(); 133 | }) 134 | .catch(res.queryError); 135 | } else { 136 | res.ok(); 137 | } 138 | }, 139 | /** 140 | * Delete and delete action 141 | * 142 | * @param {Object} req express.js request 143 | * @param {Object} res express.js response 144 | */ 145 | delete(req, res) { 146 | if (!res.locals.template) { 147 | res.locals.template = res.local.model + '/' + 'delete'; 148 | } 149 | 150 | let record = res.locals.data; 151 | 152 | if (!record) { 153 | return res.notFound(); 154 | } 155 | 156 | res.locals.deleteMsg = res.locals.model + '.delete.confirm.msg'; 157 | 158 | if (req.method === 'POST' || req.method === 'DELETE') { 159 | record 160 | .destroy() 161 | .then(function afterDestroy () { 162 | res.locals.deleted = true; 163 | res.deleted(); 164 | }) 165 | .catch(res.queryError); 166 | } else { 167 | res.ok(); 168 | } 169 | } 170 | }; 171 | 172 | module.exports = Controller; 173 | -------------------------------------------------------------------------------- /test/tests/modules/lib.messages.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var helpers = require('we-test-tools').helpers; 3 | var messages, we; 4 | 5 | describe('lib/messages', function () { 6 | before(function (done) { 7 | messages = require('../../../src/messages'); 8 | we = helpers.getWe(); 9 | done(); 10 | }); 11 | 12 | it('setFunctionsInResponse should set addMessage, getMessages and moveLocalsMessagesToFlash', function (done) { 13 | var req = {}; 14 | var res = { 15 | locals: {} 16 | }; 17 | 18 | messages.setFunctionsInResponse(req, res); 19 | 20 | assert(res.addMessage); 21 | assert(res.getMessages); 22 | assert(res.moveLocalsMessagesToFlash); 23 | 24 | assert(res.locals.messages); 25 | 26 | done(); 27 | }); 28 | 29 | describe('response with messageMethods', function(){ 30 | var res, req; 31 | before(function (done) { 32 | res = { locals: {} } 33 | req = { __: we.i18n.__ } 34 | 35 | messages.setFunctionsInResponse(req, res) 36 | 37 | done() 38 | }); 39 | describe('res.addMessage', function(){ 40 | it ('should add one error message', function(done) { 41 | res.addMessage('error', 'we.test.message.1'); 42 | 43 | assert.equal(res.locals.messages[0].status, 'danger'); 44 | assert.equal(res.locals.messages[0].message, 'we.test.message.1'); 45 | assert.equal(res.locals.messages[0].extraData, null); 46 | 47 | res.locals.messages = []; 48 | 49 | done(); 50 | }); 51 | 52 | it ('should add one warn message', function(done) { 53 | res.addMessage('warn', { 54 | text: 'we.test.message.2', 55 | vars: { uno: 'dos' } 56 | }, { 57 | field: 'name' 58 | }); 59 | 60 | assert.equal(res.locals.messages[0].status, 'warning'); 61 | assert.equal(res.locals.messages[0].message, 'we.test.message.2'); 62 | assert.equal(res.locals.messages[0].extraData.field, 'name'); 63 | 64 | res.locals.messages = []; 65 | 66 | done(); 67 | }); 68 | 69 | it ('should add one warn message without localization and with text param', function(done) { 70 | var res = { locals: {} }; 71 | var req = {} 72 | 73 | messages.setFunctionsInResponse(req, res); 74 | 75 | res.addMessage('warn', { 76 | text: 'we.test.message.3' 77 | }); 78 | 79 | assert.equal(res.locals.messages[0].status, 'warning'); 80 | assert.equal(res.locals.messages[0].message, 'we.test.message.3'); 81 | 82 | res.locals.messages = []; 83 | 84 | done(); 85 | }); 86 | 87 | it ('should add one warn message without localization', function(done) { 88 | var res = { locals: {} }; 89 | var req = {} 90 | 91 | messages.setFunctionsInResponse(req, res); 92 | 93 | res.addMessage('warn', 'we.test.message.4'); 94 | 95 | assert.equal(res.locals.messages[0].status, 'warning'); 96 | assert.equal(res.locals.messages[0].message, 'we.test.message.4'); 97 | 98 | res.locals.messages = []; 99 | 100 | done(); 101 | }); 102 | }); 103 | describe('res.getMessages', function(){ 104 | it('should get messages from locals and flash', function (done) { 105 | var req = { 106 | we: we, 107 | flash: function() { 108 | return [{ 109 | status: 'error', 110 | message: 'we.test.error.message' 111 | }] 112 | } 113 | } 114 | var res = { 115 | locals: { 116 | req: req, 117 | messages: [{ 118 | status: 'success', 119 | message: 'we.test.message.10' 120 | }] 121 | } 122 | } 123 | 124 | messages.setFunctionsInResponse(req, res) 125 | var msgs = res.getMessages() 126 | 127 | assert(msgs); 128 | assert.equal(msgs[0].status, 'success'); 129 | assert.equal(msgs[0].message, 'we.test.message.10'); 130 | assert.equal(msgs[1].status, 'error'); 131 | assert.equal(msgs[1].message, 'we.test.error.message'); 132 | 133 | done(); 134 | }); 135 | }); 136 | describe('res.moveLocalsMessagesToFlash', function(){ 137 | it('should run res.getMessages and set in req.flash', function (done) { 138 | var req = { 139 | we: we, 140 | flash: function(name, data) { 141 | if (!data) return []; 142 | assert.equal(name, 'messages'); 143 | 144 | assert.equal(data[0].status, 'success'); 145 | assert.equal(data[0].message, 'we.test.message.11'); 146 | 147 | done(); 148 | } 149 | } 150 | var res = { 151 | locals: { 152 | req: req, 153 | messages: [{ 154 | status: 'success', 155 | message: 'we.test.message.11' 156 | }] 157 | } 158 | } 159 | 160 | messages.setFunctionsInResponse(req, res) 161 | 162 | res.moveLocalsMessagesToFlash() 163 | }); 164 | }); 165 | }); 166 | }); -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | Please take a moment to review this document in order to make the contribution 4 | process easy and effective for everyone involved. 5 | 6 | Following these guidelines helps to communicate that you respect the time of 7 | the developers managing and developing this open source project. In return, 8 | they should reciprocate that respect in addressing your issue or assessing 9 | patches and features. 10 | 11 | 12 | ## Using the issue tracker 13 | 14 | The issue tracker is the preferred channel for [bug reports](#bugs), 15 | [features requests](#features) and [submitting pull 16 | requests](#pull-requests), but please respect the following restrictions: 17 | 18 | * Please **do not** use the issue tracker for personal support requests (use 19 | [Stack Overflow](http://stackoverflow.com) or [Gitter](https://gitter.im/wejs/we). 20 | 21 | * Please **do not** derail or troll issues. Keep the discussion on topic and 22 | respect the opinions of others. 23 | 24 | 25 | ## Bug reports 26 | 27 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 28 | Good bug reports are extremely helpful - thank you! 29 | 30 | Guidelines for bug reports: 31 | 32 | 1. **Use the GitHub issue search** — check if the issue has already been 33 | reported. 34 | 35 | 2. **Check if the issue has been fixed** — try to reproduce it using the 36 | latest `master` or development branch in the repository. 37 | 38 | A good bug report shouldn't leave others needing to chase you up for more 39 | information. Please try to be as detailed as possible in your report. What is 40 | your environment? What steps will reproduce the issue? What browser(s) and OS 41 | experience the problem? What would you expect to be the outcome? All these 42 | details will help people to fix any potential bugs. 43 | 44 | Example: 45 | 46 | > Short and descriptive example bug report title 47 | > 48 | > A summary of the issue and the browser/OS environment in which it occurs. If 49 | > suitable, include the steps required to reproduce the bug. 50 | > 51 | > 1. This is the first step 52 | > 2. This is the second step 53 | > 3. Further steps, etc. 54 | > 55 | > Any other information you want to share that is relevant to the issue being 56 | > reported. This might include the lines of code that you have identified as 57 | > causing the bug, and potential solutions (and your opinions on their 58 | > merits). 59 | 60 | 61 | 62 | ## Feature requests 63 | 64 | Feature requests are welcome. But take a moment to find out whether your idea 65 | fits with the scope and aims of the project. It's up to *you* to make a strong 66 | case to convince the project's developers of the merits of this feature. Please 67 | provide as much detail and context as possible. 68 | 69 | 70 | 71 | ## Pull requests 72 | 73 | Good pull requests - patches, improvements, new features - are a fantastic 74 | help. They should remain focused in scope and avoid containing unrelated 75 | commits. 76 | 77 | **Please ask first** before embarking on any significant pull request (e.g. 78 | implementing features, refactoring code, porting to a different language), 79 | otherwise you risk spending a lot of time working on something that someone already is working. 80 | 81 | Please adhere to the coding conventions used throughout a project (indentation, 82 | accurate comments, etc.) and any other requirements (such as test coverage). 83 | 84 | Follow this process if you'd like your work considered for inclusion in the 85 | project: 86 | 87 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 88 | and configure the remotes: 89 | 90 | ```bash 91 | # Clone your fork of the repo into the current directory 92 | git clone https://github.com//we 93 | # Navigate to the newly cloned directory 94 | cd we 95 | # Assign the original repo to a remote called "upstream" 96 | git remote add upstream https://github.com/wejs/we 97 | ``` 98 | 99 | 2. If you cloned a while ago, get the latest changes from upstream: 100 | 101 | ```bash 102 | git checkout 103 | git pull upstream 104 | ``` 105 | 106 | 3. Create a new topic branch (off the main project development branch) to 107 | contain your feature, change, or fix: 108 | 109 | ```bash 110 | git checkout -b 111 | ``` 112 | 113 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 114 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 115 | or your code is unlikely be merged into the main project. Use Git's 116 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 117 | feature to tidy up your commits before making them public. 118 | 119 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 120 | 121 | ```bash 122 | git pull [--rebase] upstream 123 | ``` 124 | 125 | 6. Push your topic branch up to your fork: 126 | 127 | ```bash 128 | git push origin 129 | ``` 130 | 131 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 132 | with a clear title and description. 133 | 134 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to 135 | license your work under the same license as that used by the project. 136 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Libs and functions utils for project development 3 | * 4 | */ 5 | 6 | const fs = require('fs'), 7 | path = require('path'), 8 | _ = require('lodash'), 9 | // single model associations 10 | singleAssociations = ['belongsTo', 'hasOne'], 11 | slugify = require('slugify'); 12 | 13 | const utils = { 14 | listFilesRecursive: walk, 15 | moment: require('moment'), 16 | async: require('async'), 17 | _: _, 18 | mkdirp: require('./mkdirp.js'), 19 | cookieParser: require('cookie-parser'), 20 | mime: require('mime'), 21 | express: require('express'), 22 | slugify: slugify, 23 | 24 | /** 25 | * Strip tags from string 26 | * 27 | * @param {String} string String to cleanup 28 | * @return {String} String without tags 29 | */ 30 | stripTags(string = '') { 31 | return string.replace(/<\/?[^>]+(>|$)/g, ''); 32 | }, 33 | 34 | /** 35 | * Strip tags and truncate 36 | * 37 | * Usage: stripTagsAndTruncate('something big', 5, '......') 38 | * 39 | * @param {String} string String to cleanup and truncate 40 | * @param {Number} length Length to truncate if it is too big 41 | * @param {String} omission default: ... 42 | * @return {String} Clean and truncated string 43 | */ 44 | stripTagsAndTruncate(string, length = 200, omission = '...') { 45 | return _.truncate(utils.stripTags(string), { 46 | length: length, 47 | omission: omission, 48 | }); 49 | }, 50 | 51 | /** 52 | * Slugfy and truncate 53 | * 54 | * Usage: slugifyAndTruncate('something big', 5, '......') 55 | * 56 | * @param {String} string String to cleanup and truncate 57 | * @param {Number} length Length to truncate if it is too big 58 | * @param {String} omission default: ... 59 | * @param {Object} opts slugfy options 60 | * @return {String} Clean and truncated string 61 | */ 62 | slugifyAndTruncate(string, length = 200, omission = '...', opts = {}) { 63 | return _.truncate(slugify(string, { ...opts, lower: true, strict: true, }), { 64 | length: length, 65 | omission: omission, 66 | }); 67 | }, 68 | 69 | /** 70 | * Is authenticated method usable if we-plugin-auth not is installed 71 | * 72 | * @return {Boolean} True 73 | */ 74 | isAuthenticated() { 75 | if (!this.user || !this.user.id) return false; 76 | return true; 77 | }, 78 | /** 79 | * Get redirect url 80 | * @param {Object} req express request 81 | * @param {[type]} res express response 82 | * @return {string|null} url or null 83 | */ 84 | getRedirectUrl(req) { 85 | if (req.query) { 86 | if (req.query.service) { 87 | if (req.we.config.services && req.we.config.services[req.query.service]) { 88 | req.we.log.verbose( 89 | 'Service redirect found for service: ', req.we.config.services[req.query.service] 90 | ); 91 | return req.we.config.services[req.query.service].url; 92 | } 93 | } 94 | 95 | if (req.query.redirectTo) { 96 | if (!req.we.router.isAbsoluteUrl(req.query.redirectTo)) 97 | return req.query.redirectTo; 98 | } 99 | } 100 | 101 | if (req.body && req.body.redirectTo) { 102 | if (!req.we.router.isAbsoluteUrl(req.body.redirectTo)) 103 | return req.body.redirectTo; 104 | } 105 | 106 | return null; 107 | }, 108 | 109 | helper: { 110 | /** 111 | * Parse handlebars helper options and return attributes text 112 | * 113 | * @param {Object} options handlebars helper options 114 | * @return {String} [description] 115 | */ 116 | parseAttributes(options) { 117 | let attributes = []; 118 | // pass helper attributes to link element 119 | for (let attributeName in options.hash) { 120 | if (typeof options.hash[attributeName] == 'string') { 121 | attributes.push(attributeName + '="' + options.hash[attributeName] + '"'); 122 | } 123 | } 124 | 125 | return attributes.join(' '); 126 | } 127 | }, 128 | 129 | /** 130 | * Check if is a NxN association 131 | * 132 | * @param {Object} association The sequelize association object 133 | * @return {Boolean} 134 | */ 135 | isNNAssoc(association) { 136 | if ( singleAssociations.indexOf( association.associationType ) > -1 ) { 137 | return true; 138 | } 139 | 140 | return false; 141 | } 142 | }; 143 | 144 | /** 145 | * List files in dir in parallel 146 | * 147 | * see: http://stackoverflow.com/questions/5827612/node-js-fs-readdir-recursive-directory-search 148 | * 149 | * @param {String} dir 150 | * @param {Function} done run with error, files 151 | */ 152 | function walk(dir, done) { 153 | var results = []; 154 | 155 | fs.readdir(dir, (err, list)=> { 156 | if (err) { 157 | if (err.code === 'ENOENT') { 158 | return done(null, []); 159 | } else { 160 | return done(err); 161 | } 162 | } 163 | let pending = list.length; 164 | if (!pending) return done(null, results); 165 | 166 | list.forEach( (file)=> { 167 | file = path.resolve(dir, file); 168 | fs.stat(file, (err, stat)=> { 169 | if (stat && stat.isDirectory()) { 170 | walk(file, (err, res)=> { 171 | results = results.concat(res); 172 | if (!--pending) done(null, results); 173 | }); 174 | } else { 175 | results.push(file); 176 | if (!--pending) done(null, results); 177 | } 178 | }); 179 | }); 180 | }); 181 | } 182 | 183 | module.exports = utils; 184 | -------------------------------------------------------------------------------- /src/responses/JSONApi.js: -------------------------------------------------------------------------------- 1 | const { isArray } = require('lodash'), 2 | actions = ['edit', 'create']; 3 | 4 | let JSONApi = { 5 | jsonAPIFormater(req, res) { 6 | let response = {}; 7 | 8 | // check field privacity access for users 9 | if (res.locals.model == 'user') { 10 | req.we.db.checkRecordsPrivacity(res.locals.data); 11 | } 12 | 13 | if (res.locals.action == 'find' || isArray(res.locals.data) ) { 14 | response = JSONApi.formatList (req, res); 15 | } else if (res.locals.action == 'delete') { 16 | // dont send data in delete 17 | response = {}; 18 | } else if (res.locals.model) { 19 | response = JSONApi.formatItem (req, res); 20 | } 21 | 22 | response.meta = res.locals.metadata || {}; 23 | 24 | // parse we.js errors to jsonapi errors 25 | JSONApi.parseErrors(response, req, res); 26 | 27 | res.send(response); 28 | }, 29 | 30 | formatList(req, res) { 31 | let data = [], 32 | r = {}; 33 | 34 | // skip if data is empty 35 | if (!res.locals.data) return { data: [] }; 36 | 37 | data = res.locals.data 38 | .map(d => { 39 | if (d.toJSONAPI) { 40 | return d.toJSONAPI(); 41 | } else { 42 | return d; 43 | } 44 | }); 45 | 46 | r.data = data; 47 | 48 | return r; 49 | }, 50 | 51 | formatItem(req, res) { 52 | // skip if data is empty 53 | if (!res.locals.data) return { data: [] }; 54 | 55 | if (res.locals.data.toJSONAPI) { 56 | return { data: res.locals.data.toJSONAPI() }; 57 | } else { 58 | return res.locals.data; 59 | } 60 | }, 61 | 62 | jsonAPIParser(req, res, context) { 63 | // create and update actions 64 | if (actions.includes(context.config.action)) { 65 | // atributes is required 66 | if (!req.body.data || !req.body.data.attributes) return req.body; 67 | // change json api body to default we.js controller body compatible with sequelize 68 | for (let attr in req.body.data.attributes) { 69 | req.body[attr] = req.body.data.attributes[attr]; 70 | } 71 | 72 | if (req.body.data.relationships) { 73 | JSONApi.jsonAPIParseAssoc(req, res, context); 74 | } 75 | } 76 | 77 | return req.body; 78 | }, 79 | 80 | jsonAPIParseAssoc(req, res, context) { 81 | const associations = req.we.db.models[context.config.model].associations; 82 | for(let assocName in associations) { 83 | if ( 84 | req.body.data.relationships[assocName] && 85 | ( 86 | req.body.data.relationships[assocName].data || 87 | req.body.data.relationships[assocName].data == null 88 | ) 89 | ) { 90 | 91 | switch(associations[assocName].associationType) { 92 | case 'BelongsTo': 93 | if (req.body.data.relationships[assocName].data) { 94 | req.body[ assocName+'Id' ] = req.body.data.relationships[assocName].data.id; 95 | } else { 96 | // is null 97 | req.body[ assocName+'Id' ] = null; 98 | } 99 | 100 | break; 101 | case 'HasMany': 102 | case 'BelongsToMany': 103 | if ( 104 | req.body.data.relationships[assocName].data && 105 | req.body.data.relationships[assocName].data.map 106 | ) { 107 | req.body[ assocName ] = req.body.data.relationships[assocName].data.map(function(d) { 108 | return d.id; 109 | }); 110 | } 111 | break; 112 | } 113 | } 114 | } 115 | }, 116 | 117 | parse1xNAssociation(mc, attrN, items, relationships, included, we) { 118 | if (items.toJSON) items = items.toJSON(); 119 | 120 | let iid = mc.associations[attrN].model + '_' + items.id; 121 | 122 | if (we.config.JSONApi.sendSubRecordAttributes && !included[iid]) { 123 | included[iid] = { 124 | type: mc.associations[attrN].model, 125 | id: items.id 126 | }; 127 | 128 | included[iid].attributes = items; 129 | } 130 | 131 | relationships[attrN] = { 132 | data: { 133 | id: items.id, 134 | type: mc.associations[attrN].model 135 | } 136 | }; 137 | }, 138 | 139 | parseNxNAssociation(mc, attrN, items, relationships, included, we) { 140 | let modelName = mc.associations[attrN].model; 141 | 142 | if (!relationships[attrN]) { 143 | relationships[attrN] = { 144 | data: [] 145 | }; 146 | } 147 | 148 | items.forEach(item => { 149 | 150 | relationships[attrN].data.push({ 151 | id: item.id, 152 | type: modelName 153 | }); 154 | 155 | let iid = modelName + '_' + item.id; 156 | // skyp if record already in included 157 | if (we.config.JSONApi.sendSubRecordAttributes && !included[iid]) { 158 | included[iid] = { 159 | type: modelName, 160 | id: item.id 161 | }; 162 | included[iid].attributes = item; 163 | } 164 | }); 165 | }, 166 | 167 | /** 168 | * Format erros from res.locals.messages to jsonapi erros and set it in response 169 | */ 170 | parseErrors(response, req, res) { 171 | if (!response.meta.messages) response.meta.messages = []; 172 | 173 | if (res.locals.messages) { 174 | let errors = []; 175 | 176 | // first get and format all errros: 177 | res.locals.messages.forEach(m => { 178 | if (m.status == 'danger' || m.status == 'error') { 179 | errors.push(req.we.utils._.merge({ 180 | status: res.statusCode, 181 | title: m.message 182 | }, m.extraData)); 183 | } else { 184 | // others messages like success goes in meta object 185 | response.meta.messages.push(m); 186 | } 187 | 188 | }); 189 | 190 | 191 | if (errors && errors.length) { 192 | response.errors = errors; 193 | } 194 | } 195 | } 196 | }; 197 | 198 | module.exports = JSONApi; -------------------------------------------------------------------------------- /src/bootstrapFunctions.js: -------------------------------------------------------------------------------- 1 | const staticConfig = require('./staticConfig'), 2 | localization = require('./localization'), 3 | weExpress = require('./express'); 4 | 5 | module.exports = { 6 | checkDBConnection(we, next) { 7 | we.db.checkDBConnection(we, next); 8 | }, 9 | loadCoreFeatures(we, next) { 10 | we.log.verbose('loadCoreFeatures step'); 11 | 12 | we.db.loadCoreModels( err => { 13 | if(err) return next(err); 14 | 15 | we.pluginManager.loadPluginsSettingsFromDB(we, err => { 16 | if (err) return next(err); 17 | // preload all plugins 18 | we.pluginManager.loadPlugins(we, (err, plugins) => { 19 | if (err) return next(err); 20 | we.plugins = plugins; 21 | next(); 22 | }); 23 | }); 24 | }); 25 | } , 26 | loadPluginFeatures(we, next) { 27 | we.log.verbose('loadPluginFeatures step'); 28 | 29 | we.pluginNames = we.pluginManager.pluginNames; 30 | // load plugin static configs, merge with old we.config and 31 | // override the defalt config 32 | we.config = staticConfig.loadPluginConfigs(we); 33 | // set add ResponseFormat here for use we.js app 34 | we.responses.addResponseFormater = (extension, formater, position)=> { 35 | position = (position === 0 || position)? position: we.config.responseTypes.length; 36 | 37 | we.config.responseTypes.splice(position, 0, extension); 38 | we.responses.formaters[extension] = formater; 39 | }; 40 | 41 | we.hooks.trigger('we:before:load:plugin:features', we, (err)=> { 42 | if (err) return next(err); 43 | 44 | we.utils.async.eachSeries(we.pluginNames, (pluginName, next)=> { 45 | we.plugins[pluginName].loadFeatures(we, next); 46 | }, (err) => { 47 | if (err) return next(err); 48 | 49 | we.events.emit('we:after:load:plugins', we); 50 | we.hooks.trigger('we:after:load:plugins', we, next); 51 | }); 52 | }); 53 | }, 54 | loadTemplateCache(we, next) { 55 | // step to plug we-plugin-view 56 | we.hooks.trigger('we-core:on:load:template:cache', we, next); 57 | }, 58 | instantiateModels(we, next) { 59 | 60 | // step to define all models with sequelize 61 | we.log.verbose('instantiateModels step'); 62 | we.hooks.trigger('we:models:before:instance', we, (err) => { 63 | if (err) return next(err); 64 | 65 | for (let modelName in we.db.modelsConfigs) { 66 | let mc = we.db.modelsConfigs[modelName]; 67 | 68 | // all models have a link permanent 69 | mc.definition.linkPermanent = { 70 | type: we.db.Sequelize.VIRTUAL, 71 | formFieldType: null, 72 | get() { 73 | if (this.cachedLinkPermanent) return this.cachedLinkPermanent; 74 | this.cachedLinkPermanent = this.getUrlPath(); 75 | return this.cachedLinkPermanent; 76 | } 77 | }; 78 | 79 | // set 80 | mc.definition.metadata = { 81 | type: we.db.Sequelize.VIRTUAL, 82 | formFieldType: null 83 | }; 84 | 85 | we.db.setModelClassMethods(); 86 | we.db.setModelInstanceMethods(); 87 | 88 | // save attrs list: 89 | mc.attributeList = Object.keys(mc.definition); 90 | // add created and updated at attrs 91 | mc.attributeList.push('createdAt'); 92 | mc.attributeList.push('updatedAt'); 93 | // save assoc attr names list: 94 | if (mc.associations) 95 | mc.associationNames = Object.keys(mc.associations); 96 | 97 | // define the model 98 | we.db.models[modelName] = we.db.define( 99 | modelName, 100 | mc.definition, 101 | mc.options 102 | ); 103 | } 104 | 105 | // set all associations 106 | we.db.setModelAllJoins(); 107 | we.db.setModelHooks(); 108 | 109 | we.hooks.trigger('we:models:set:joins', we, next); 110 | }); 111 | }, 112 | syncModels(we, next) { 113 | we.db.defaultConnection.sync().nodeify( (err)=> { 114 | if (err) return next(err); 115 | we.hooks.trigger('we:models:ready', we, next); 116 | }); 117 | }, 118 | 119 | loadControllers(we, next) { 120 | we.log.verbose('loadControllers step'); 121 | we.events.emit('we:after:load:controllers', we); 122 | next(); 123 | }, 124 | initI18n(we, next) { 125 | we.log.verbose('initI18n step'); 126 | localization(we); 127 | we.events.emit('we:after:init:i18n', we); 128 | next(); 129 | }, 130 | installAndRegisterPlugins(we, next) { 131 | if (we.config.skipInstall) return next(); 132 | 133 | we.log.verbose('installAndRegisterPluginsIfNeed step'); 134 | // dont have plugins to install 135 | if (!we.pluginManager.pluginsToInstall) return next(); 136 | // get plugins to install names 137 | const names = Object.keys(we.pluginManager.pluginsToInstall); 138 | we.utils.async.eachSeries(names, function onEachPlugin (name, nextPlugin) { 139 | // run install scripts 140 | we.pluginManager.installPlugin(name, function afterInstallOnePlugin (err){ 141 | if (err) return nextPlugin(err); 142 | // register it 143 | we.pluginManager.registerPlugin(name, nextPlugin); 144 | }); 145 | }, function afterInstallAllPlugins (err) { 146 | if (err) return next(err); 147 | next(); 148 | }); 149 | }, 150 | setExpressApp(we, next) { 151 | // load express 152 | we.express = weExpress(we); 153 | we.events.emit('we:after:load:express', we); 154 | next(); 155 | }, 156 | passport(we, next) { 157 | // hook to set authentication. 158 | // if we-plugin-auth is installed, load passport here 159 | we.hooks.trigger('we-core:on:set:passport', we, next); 160 | }, 161 | createDefaultFolders(we, next) { 162 | we.log.verbose('createDefaultFolders step'); 163 | we.hooks.trigger('we:create:default:folders', we, next); 164 | }, 165 | registerAllViewTemplates(we, next) { 166 | // hook to plugin we-plugin-view template register 167 | we.hooks.trigger('we-core:on:register:templates', we, next); 168 | }, 169 | mergeRoutes(we, next) { 170 | we.log.verbose('mergeRoutes step'); 171 | we.routes = {}; 172 | // merge plugin routes 173 | for (let plugin in we.plugins) { 174 | we.utils._.merge(we.routes, we.plugins[plugin].routes); 175 | } 176 | // merge project routes 177 | we.utils._.merge(we.routes, we.config.routes); 178 | next(); 179 | }, 180 | /** 181 | * Bind all resources in App 182 | * 183 | * @param {Object} we 184 | * @param {Function} next 185 | */ 186 | bindResources(we, next) { 187 | we.log.verbose('bindResources step'); 188 | try { 189 | for (let resource in we.router.resources) { 190 | we.router.bindResource(we.router.resources[resource]); 191 | } 192 | next(); 193 | } catch (e) { 194 | next(e); 195 | } 196 | }, 197 | bindRoutes(we, next) { 198 | we.log.verbose('bindRoutes step'); 199 | we.hooks.trigger('we:before:routes:bind', we, function beforeRouteBind(err) { 200 | if (err) return next(err); 201 | 202 | for (let route in we.routes) { 203 | we.router.bindRoute(we, route, we.routes[route] ); 204 | } 205 | 206 | we.hooks.trigger('we:after:routes:bind', we, function afterRouteBind(err) { 207 | if (err) return next(err); 208 | 209 | // bind after router handler for run responseMethod 210 | we.express.use( (req, res, done)=> { 211 | if (res.responseMethod) return res[res.responseMethod](); 212 | done(); 213 | }); 214 | 215 | we.responses.sortResponses(we); 216 | 217 | next(); 218 | }); 219 | }); 220 | } 221 | }; -------------------------------------------------------------------------------- /src/localization/i18n.js: -------------------------------------------------------------------------------- 1 | const path = require('path'), 2 | fs = require('fs'), 3 | hbs = require('handlebars'); 4 | 5 | const i18n = { 6 | we: null, 7 | fileI18nConfig: null, 8 | systemSettingsEnabled: false, 9 | // file locale directory: 10 | directory: null, 11 | locales: [], 12 | defaultLocale: null, 13 | 14 | lastLocaleUpdatedTimestamp: 0, 15 | 16 | // translations methods: 17 | fns: {}, 18 | // translations cache 19 | cache: {}, 20 | // translations with strings only: 21 | strCache: {}, 22 | 23 | configure(c, we) { 24 | this.we = we; 25 | this.fileI18nConfig = c || {}; 26 | 27 | if (c.locales) i18n.setLocales(c.locales); 28 | if (c.defaultLocale) i18n.setLocale(c.defaultLocale); 29 | if (c.directory) i18n.directory = c.directory; 30 | 31 | // system settings is avaible: 32 | if (typeof we.systemSettings == 'object') { 33 | this.systemSettingsEnabled = true; 34 | } 35 | 36 | this.setHooksAndEvents(); 37 | 38 | this.loadFilesIFExists(); 39 | }, 40 | 41 | /** 42 | * Init middleware 43 | * 44 | * @param {Object} req 45 | * @param {Object} res 46 | * @param {Function} next 47 | */ 48 | init(req, res, next) { 49 | req.setLocale = (locale)=> { 50 | req.locale = locale; 51 | res.locale = locale; 52 | res.locals.locale = locale; 53 | }; 54 | 55 | req.getLocale = ()=> { 56 | return req.locale; 57 | }; 58 | 59 | req.setLocale(i18n.getDefaultLocale(req)); 60 | 61 | req.__ = function __(t, params) { 62 | return i18n.__(t, params, req); 63 | }; 64 | 65 | res.__ = req.__; 66 | res.locals.__ = req.__; 67 | 68 | next(); 69 | }, 70 | 71 | setHooksAndEvents() { 72 | const we = this.we; 73 | we.hooks.on('we:models:ready', (we, done)=> { 74 | i18n.loadAllTranslationsFromDB(done); 75 | }); 76 | 77 | // on system settings start: 78 | we.hooks.on('system-settings:started', (we, done)=> { 79 | if (we.systemSettings) { 80 | if (we.systemSettings.defaultLocale) { 81 | this.defaultLocale = we.systemSettings.defaultLocale; 82 | } 83 | 84 | if (we.systemSettings.LLUT) { 85 | i18n.lastLocaleUpdatedTimestamp = Number(we.systemSettings.LLUT); 86 | } 87 | } 88 | 89 | done(); 90 | }); 91 | 92 | // on system settings change: 93 | we.events.on('system-settings:updated:after', (we)=> { 94 | if (we.systemSettings.LLUT) { 95 | if (we.systemSettings.LLUT > i18n.lastLocaleUpdatedTimestamp) { 96 | // should update ... 97 | i18n.loadTranslationChangesInDB(); 98 | // set new timestamp: 99 | i18n.lastLocaleUpdatedTimestamp = Number(we.systemSettings.LLUT); 100 | } 101 | } 102 | }); 103 | 104 | }, 105 | 106 | /** 107 | * Set global locale 108 | * @param {String} locale locale to set as default 109 | */ 110 | setLocale(locale) { 111 | this.defaultLocale = locale; 112 | }, 113 | 114 | getDefaultLocale(req) { 115 | const cookieLocale = i18n.getLocaleFromCookie(req); 116 | if (cookieLocale) return cookieLocale; 117 | 118 | const queryLocale = i18n.getLocaleFromQuery(req); 119 | if (queryLocale) return queryLocale; 120 | 121 | return i18n.defaultLocale; 122 | }, 123 | 124 | getLocaleFromCookie(req) { 125 | const c = i18n.fileI18nConfig.cookie; 126 | 127 | if ( 128 | c && 129 | req && 130 | req.cookies && 131 | req.cookies[c] && 132 | i18n.locales.indexOf(req.cookies[c]) > -1 133 | ) { 134 | return req.cookies[c]; 135 | } 136 | 137 | return null; 138 | }, 139 | 140 | getLocaleFromQuery(req) { 141 | const q = i18n.fileI18nConfig.queryParameter; 142 | 143 | if ( 144 | q && 145 | req && 146 | req.query && 147 | req.query[q] && 148 | i18n.locales.indexOf(req.query[q]) > -1 149 | ) { 150 | return req.query[q]; 151 | } 152 | 153 | return null; 154 | }, 155 | 156 | /** 157 | * Set global locales 158 | * @param {Array} locales array of locales strings 159 | */ 160 | setLocales(locales) { 161 | this.locales = locales; 162 | }, 163 | 164 | __(s, params, req) { 165 | if ( 166 | req && 167 | i18n.fns[req.locale] && 168 | i18n.fns[req.locale][s] 169 | ) { 170 | return i18n.fns[req.locale][s](params); 171 | } 172 | 173 | if ( 174 | i18n.defaultLocale && 175 | i18n.fns[i18n.defaultLocale] && 176 | i18n.fns[i18n.defaultLocale][s] 177 | ) { 178 | return i18n.fns[i18n.defaultLocale][s](params); 179 | } 180 | 181 | return s; 182 | }, 183 | 184 | loadFilesIFExists() { 185 | for (let i = 0; i < this.locales.length; i++) { 186 | let d = i18n.loadFileIfExists(this.locales[i]); 187 | i18n.parseAndImportTraslations(d, this.locales[i]); 188 | } 189 | }, 190 | 191 | loadFileIfExists(locale) { 192 | let file = path.join(this.directory, locale+'.json'); 193 | 194 | try { 195 | let contents = fs.readFileSync(file); 196 | return JSON.parse(contents); 197 | } catch(e) { 198 | i18n.we.log.verbose('Error on parse location file:', { 199 | error: e 200 | }); 201 | } 202 | 203 | return null; 204 | }, 205 | 206 | parseAndImportTraslations(d, locale) { 207 | if (!d) return; 208 | 209 | if (!i18n.fns[locale]) i18n.fns[locale] = {}; 210 | if (!i18n.cache[locale]) i18n.cache[locale] = {}; 211 | if (!i18n.strCache[locale]) i18n.strCache[locale] = {}; 212 | 213 | for (let s in d) { 214 | let t = d[s]; 215 | 216 | i18n.parseAndImportTraslation({ 217 | s: s, 218 | t: t, 219 | l: locale, 220 | isChanged: false 221 | }, locale); 222 | } 223 | }, 224 | 225 | parseAndImportTraslation(r, locale) { 226 | if (!r || !r.s) return; 227 | if (!locale) return; 228 | 229 | if (r.t) { 230 | if (i18n.isTPLString(r.t)) { 231 | i18n.fns[locale][r.s] = hbs.compile(r.t); 232 | } else { 233 | i18n.fns[locale][r.s] = i18n.getSimpleTMethod(r.t); 234 | } 235 | 236 | i18n.strCache[locale][r.s] = r.t; 237 | } else { 238 | i18n.fns[locale][r.s] = i18n.getSimpleTMethod(r.s); 239 | } 240 | 241 | i18n.cache[locale][r.s] = r; 242 | }, 243 | 244 | isTPLString(s) { 245 | // if the msg string contains {{Mustache}} patterns we render it as a mini tempalate 246 | if ((/{{.*}}/).test(s)) return true; 247 | return false; 248 | }, 249 | 250 | getSimpleTMethod(t) { 251 | return function() { 252 | return t; 253 | }; 254 | }, 255 | 256 | loadAllTranslationsFromDB(done) { 257 | const we = this.we; 258 | 259 | let sql = `SELECT s, t, l, isChanged, updatedAt 260 | FROM t 261 | ORDER BY s, updatedAt`; 262 | we.db.defaultConnection.query(sql) 263 | .then( (r)=> { 264 | 265 | if (r && r[0] && r[0].length) { 266 | let ts = r[0]; 267 | 268 | ts.forEach( (tRecord)=> { 269 | i18n.parseAndImportTraslation(tRecord, tRecord.l); 270 | }); 271 | } 272 | 273 | done(); 274 | }) 275 | .catch( (e)=> { 276 | we.log.warn('we-core.i18n:Error on load db translations', { 277 | error: e 278 | }); 279 | 280 | done(); 281 | }); 282 | }, 283 | 284 | loadTranslationChangesInDB(done) { 285 | if (!done) done = function(){}; 286 | 287 | const we = this.we; 288 | const changeTime = we.utils 289 | .moment 290 | .unix( this.lastLocaleUpdatedTimestamp ) 291 | .utc() 292 | .format('YYYY/MM/DD HH:mm:ss'); 293 | 294 | let sql = `SELECT 295 | s, t, l, isChanged, updatedAt 296 | FROM t 297 | WHERE updatedAt >= '${changeTime}' 298 | ORDER BY s, updatedAt`; 299 | we.db.defaultConnection.query(sql) 300 | .then( (r)=> { 301 | if (r && r[0] && r[0].length) { 302 | let ts = r[0]; 303 | ts.forEach( (tRecord)=> { 304 | i18n.parseAndImportTraslation(tRecord, tRecord.l); 305 | }); 306 | } 307 | 308 | done(); 309 | }) 310 | .catch( (e)=> { 311 | we.log.warn('we-core.i18n:Error on load db translations', { 312 | error: e 313 | }); 314 | 315 | done(); 316 | }); 317 | }, 318 | 319 | getCachedTranslations(locale) { 320 | if (locale) { 321 | return i18n.strCache[locale]; 322 | } else { 323 | return i18n.strCache; 324 | } 325 | } 326 | }; 327 | 328 | module.exports = i18n; -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | path = require('path'); 3 | 4 | module.exports = { 5 | requirements(we, done) { 6 | done(); 7 | }, 8 | /** 9 | * Install function run in we.js install. 10 | * 11 | * @param {Object} we we.js object 12 | * @param {Function} done callback 13 | */ 14 | install(we, done) { 15 | done(); 16 | }, 17 | 18 | /** 19 | * Return a list of updates 20 | * 21 | * @param {Object} we we.js object 22 | * @return {Array} a list of update objects 23 | */ 24 | updates() { 25 | return [{ 26 | version: '0.3.69', // your plugin version 27 | update(we, done) { 28 | const sql = 'ALTER TABLE `widgets` ADD '+ 29 | ' COLUMN `inRecord` TINYINT(1) DEFAULT NULL;'; 30 | we.db.defaultConnection 31 | .query(sql) 32 | .then( ()=> { 33 | done(); 34 | return null; 35 | }) 36 | .catch(done); 37 | } 38 | } , { 39 | version: '0.3.120', 40 | update(we, done) { 41 | 42 | we.utils.async.series([ 43 | function changeWidgetTableThemeField(done) { 44 | const sql = 'ALTER TABLE `widgets` '+ 45 | 'CHANGE COLUMN `theme` `theme` VARCHAR(255) NULL ;'; 46 | we.db.defaultConnection 47 | .query(sql) 48 | .then( ()=> { 49 | done(); 50 | return null; 51 | }) 52 | .catch(done); 53 | }, 54 | function addRoleIsSystemField(done) { 55 | const sql = 'ALTER TABLE `roles` ' + 56 | 'ADD COLUMN `isSystemRole` TINYINT(1) NULL DEFAULT 0 AFTER `updatedAt`;'; 57 | we.db.defaultConnection 58 | .query(sql) 59 | .then( ()=> { 60 | done(); 61 | return null; 62 | }) 63 | .catch( (err)=> { 64 | if (err != 'SequelizeDatabaseError') { 65 | return done(err); 66 | } else { 67 | we.log.error(err); 68 | } 69 | 70 | done(); 71 | return null; 72 | }); 73 | } 74 | ], done); 75 | } 76 | }, 77 | { 78 | version: '1.0.2', 79 | update(we, done) { 80 | we.utils.async.series([ 81 | function addWidgetTablURLField(done) { 82 | var sql = 'ALTER TABLE `widgets` '+ 83 | ' ADD COLUMN `path` TEXT NULL; '; 84 | we.db.defaultConnection 85 | .query(sql) 86 | .then( ()=> { 87 | done(); 88 | return null; 89 | }) 90 | .catch( (err)=> { 91 | if (err != 'SequelizeDatabaseError') { 92 | return done(err); 93 | } else { 94 | we.log.error(err); 95 | } 96 | done(); 97 | return null; 98 | }); 99 | } 100 | ], done); 101 | } 102 | }, 103 | { 104 | version: '1.1.4', 105 | update(we, done) { 106 | we.utils.async.series([ 107 | function updateUserTable(done) { 108 | var sql = 'ALTER TABLE `users` '+ 109 | ' ADD COLUMN `roles` TEXT NULL; '; 110 | we.db.defaultConnection 111 | .query(sql) 112 | .then( ()=> { 113 | done(); 114 | }) 115 | .catch( (err)=> { 116 | if (err) { 117 | we.log.error(err); 118 | } 119 | 120 | done(); 121 | return null; 122 | }); 123 | }, 124 | 125 | function migrateRolesField(done) { 126 | var sql = ' SELECT users_roles.userId, roles.name FROM users_roles '+ 127 | ' LEFT JOIN roles ON roles.id=users_roles.roleId;'; 128 | we.db.defaultConnection 129 | .query(sql) 130 | .spread( (results)=> { 131 | var users = {}; 132 | 133 | we.utils.async.eachSeries(results, function onEachResult(r, next){ 134 | if (!users[r.userId]) { 135 | users[r.userId] = { 136 | id: r.userId, 137 | roles: [] 138 | }; 139 | } 140 | 141 | users[r.userId].roles.push(r.name); 142 | next(); 143 | }, function afterEachResult(){ 144 | we.utils.async.eachSeries(users, function onUpdateEachUser(user, next){ 145 | 146 | we.db.models.user 147 | .update(user, { 148 | where: { 149 | id: user.id 150 | } 151 | }) 152 | .then(function afterUpdateAllUsers(r) { 153 | we.log.info('install.js:Update user data:', r); 154 | next(); 155 | return null; 156 | }) 157 | .catch(next); 158 | }, ()=> { 159 | done(); 160 | }); 161 | }); 162 | // done(); 163 | }) 164 | .catch( (err)=> { 165 | if (err) { 166 | we.log.warn(err); 167 | } 168 | done(); 169 | return null; 170 | }); 171 | }, 172 | function exportRoles(done) { 173 | fs.lstat(we.projectPath+'/config/roles.js', function afterCheckRolesFile(err) { 174 | if (err) { 175 | 176 | if (err.code == 'ENOENT') { 177 | const sql = 'SELECT name, permissions FROM roles;'; 178 | we.db.defaultConnection.query(sql) 179 | .spread( (results)=> { 180 | 181 | for (let i = 0; i < results.length; i++) { 182 | if (results[i].permissions) { 183 | 184 | if (!we.acl.roles[results[i].name]) { 185 | // add new role if not exists 186 | we.acl.roles[results[i].name] = { 187 | name: results[i].name, 188 | permissions: [] 189 | }; 190 | } 191 | // export old permissions to new permission structure 192 | let permissions = results[i].permissions.split(';'); 193 | we.acl.roles[results[i].name].permissions = permissions; 194 | } 195 | } 196 | 197 | we.acl.writeRolesToConfigFile(done); 198 | }).catch( (err)=> { 199 | if (err) { 200 | we.log.error(err); 201 | } 202 | done(); 203 | }); 204 | } else { 205 | we.log.error('we-core:update:1.1.3: unknow error on find roles.js file'); 206 | done(err); 207 | } 208 | } else { 209 | we.log.info('we-core:update:1.1.3: found foles.js file in your project, skiping exportRoles'); 210 | done(); 211 | } 212 | }); 213 | 214 | } 215 | ], done); 216 | } 217 | }, 218 | { 219 | version: '1.2.6', 220 | update(we, done) { 221 | we.log.info( 222 | 'instaling we-plugin-editor-summernote in this project, editor feature now are in diferent npm modules' 223 | ); 224 | 225 | let exec = require('child_process').exec, child; 226 | child = exec('npm install --save we-plugin-editor-summernote', 227 | function afterInstall(error) { 228 | we.log.info('DONE we-plugin-editor-summernote install'); 229 | done(error); 230 | }); 231 | } 232 | }, 233 | { 234 | version: '1.8.0', 235 | update(we, done) { 236 | we.log.info( 237 | 'registering all installed plugins to work with new we.js plugin manager' 238 | ); 239 | 240 | const pkg = require( path.resolve(we.projectPath, 'package.json') ), 241 | nodeModulesPath = path.resolve(we.projectPath, 'node_modules'); 242 | 243 | if (!pkg.wejs) pkg.wejs = {}; 244 | if (!pkg.wejs.plugins) pkg.wejs.plugins = {}; 245 | 246 | for (let n in pkg.dependencies) { 247 | if (n != 'we-core' && 248 | oldIsPlugin( path.resolve(nodeModulesPath, n)) 249 | ) { 250 | pkg.wejs.plugins[n] = true; 251 | } 252 | } 253 | 254 | fs.writeFile( 255 | path.resolve(we.projectPath, 'package.json'), 256 | JSON.stringify(pkg, null, 2), 257 | { flags: 'w' }, 258 | function (err) { 259 | if (err) return done(err); 260 | 261 | let We = require('./src/index.js'), 262 | weUp = new We({ 263 | bootstrapMode: 'install' 264 | }); 265 | 266 | weUp.bootstrap( (err)=> { 267 | if (err) return done(err); 268 | done(); 269 | return null; 270 | }); 271 | } 272 | ); 273 | 274 | // old we.js is plugin function to check is one npm module is plugin 275 | function oldIsPlugin (nodeModulePath) { 276 | // then check if the npm module is one plugin 277 | try { 278 | if (fs.statSync( path.resolve( nodeModulePath, 'plugin.js' ) )) { 279 | return true; 280 | } else { 281 | return false; 282 | } 283 | } catch (e) { 284 | return false; 285 | } 286 | } 287 | } 288 | } 289 | ]; 290 | } 291 | }; -------------------------------------------------------------------------------- /test/tests/requests/resource.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'), 2 | request = require('supertest'), 3 | helpers = require('we-test-tools').helpers, 4 | Chance = require('chance'), 5 | chance = new Chance(); 6 | 7 | let _, http, we; 8 | 9 | function postStub(creatorId) { 10 | return { 11 | title: chance.sentence({words: 5}), 12 | text: chance.paragraph(), 13 | creatorId: creatorId 14 | }; 15 | } 16 | 17 | describe('resourceRequests', function() { 18 | before(function (done) { 19 | http = helpers.getHttp(); 20 | we = helpers.getWe(); 21 | _ = we.utils._; 22 | return done(); 23 | }); 24 | 25 | afterEach(function(done){ 26 | const models = we.db.models; 27 | 28 | Promise.all([ 29 | models.tag.destroy({truncate: true}), 30 | models.post.destroy({truncate: true}) 31 | ]) 32 | .then(function() { 33 | done(); 34 | }) 35 | .catch(done); 36 | }); 37 | describe('json', function() { 38 | describe('GET /post', function(){ 39 | it ('should get posts list', function (done) { 40 | var posts = [ 41 | postStub(), 42 | postStub(), 43 | postStub() 44 | ]; 45 | 46 | we.db.models.post.bulkCreate(posts) 47 | .spread(function(){ 48 | request(http) 49 | .get('/post') 50 | .set('Accept', 'application/json') 51 | .expect(200) 52 | .end(function (err, res) { 53 | if (err) { 54 | console.error('res.text>',res.text); 55 | throw err; 56 | } 57 | 58 | for (var i = 0; i < posts.length; i++) { 59 | assert(res.body.post[i].id); 60 | } 61 | 62 | assert.equal(res.body.meta.count, 3); 63 | 64 | done(); 65 | }); 66 | }) 67 | .catch(done); 68 | }); 69 | 70 | it ('/post/count should get posts count', function (done) { 71 | var posts = [ 72 | postStub(), 73 | postStub(), 74 | postStub() 75 | ]; 76 | 77 | we.db.models.post.bulkCreate(posts) 78 | .spread(function() { 79 | request(http) 80 | .get('/post/count') 81 | .set('Accept', 'application/json') 82 | .expect(200) 83 | .end(function (err, res) { 84 | if (err) { 85 | console.error('res.text>',res.text); 86 | throw err; 87 | } 88 | 89 | assert(res.body.count, `Count should be present in response`); 90 | assert.equal(res.body.count, 3, `Count should be 3`); 91 | 92 | done(); 93 | }); 94 | }) 95 | .catch(done); 96 | }); 97 | it ('should search for posts by title', function (done) { 98 | var posts = [ 99 | postStub(), 100 | postStub(), 101 | postStub() 102 | ]; 103 | 104 | we.db.models.post.bulkCreate(posts) 105 | .spread(function(){ 106 | request(http) 107 | .get('/post?title='+posts[1].title) 108 | .set('Accept', 'application/json') 109 | .expect(200) 110 | .end(function (err, res) { 111 | if (err) throw err; 112 | 113 | assert.equal(res.body.post.length, 1); 114 | assert(res.body.post[0].id, posts[1].title); 115 | assert.equal(res.body.post[0].title, posts[1].title); 116 | assert.equal(res.body.post[0].text, posts[1].text); 117 | 118 | assert.equal(res.body.meta.count, 1); 119 | 120 | done(); 121 | }); 122 | }).catch(done); 123 | }); 124 | 125 | 126 | it ('should search for posts by text', function (done) { 127 | var posts = [ 128 | postStub(), 129 | postStub(), 130 | postStub(), 131 | postStub() 132 | ]; 133 | 134 | var searchText = ' mussum ipsum'; 135 | 136 | posts[1].text += searchText; 137 | posts[2].text += searchText; 138 | 139 | we.db.models.post.bulkCreate(posts) 140 | .spread(function(){ 141 | request(http) 142 | .get('/post?text='+searchText) 143 | .set('Accept', 'application/json') 144 | .expect(200) 145 | .end(function (err, res) { 146 | if (err) throw err; 147 | 148 | assert.equal(res.body.post.length, 2); 149 | 150 | res.body.post.forEach(function(p){ 151 | assert(p.text.indexOf(searchText) >-1); 152 | }); 153 | 154 | assert.equal(res.body.meta.count, 2); 155 | 156 | done(); 157 | }); 158 | }).catch(done); 159 | }); 160 | 161 | it ('should search for posts by text with and and inTitleAndText, orWithComaParser search in q param', function (done) { 162 | var posts = [ 163 | postStub(), 164 | postStub(), 165 | postStub(), 166 | postStub() 167 | ]; 168 | 169 | var searchText = ' mussum ipsum'; 170 | var searchText2 = '2222m ipsum'; 171 | 172 | posts[1].title = searchText; 173 | posts[1].text = searchText; 174 | 175 | posts[2].title = searchText2; 176 | posts[2].text = searchText2; 177 | 178 | we.db.models.post.bulkCreate(posts) 179 | .spread(function(){ 180 | request(http) 181 | .get('/post?q='+searchText+','+searchText2) 182 | .set('Accept', 'application/json') 183 | .expect(200) 184 | .end(function (err, res) { 185 | if (err) throw err; 186 | assert.equal(res.body.post.length, 2); 187 | assert.equal(res.body.meta.count, 2); 188 | 189 | done(); 190 | }); 191 | }).catch(done); 192 | }); 193 | }); 194 | 195 | describe('GET /post/:id', function(){ 196 | it ('should get one post', function (done) { 197 | we.db.models.post.create(postStub()) 198 | .then(function (p) { 199 | request(http) 200 | .get('/post/'+p.id) 201 | .set('Accept', 'application/json') 202 | .expect(200) 203 | .end(function (err, res) { 204 | if (err) throw err; 205 | 206 | assert(res.body.post.id); 207 | assert.equal(res.body.post.title, p.title); 208 | assert.equal(res.body.post.text, p.text); 209 | 210 | done(); 211 | }); 212 | }).catch(done); 213 | }); 214 | 215 | it ('should return 404 to not found', function (done) { 216 | var info = we.log.info; 217 | we.log.info = function() {}; 218 | request(http) 219 | .get('/post/123213131') 220 | .expect(404) 221 | .set('Accept', 'application/json') 222 | .end(function (err, res) { 223 | if (err) throw err; 224 | 225 | assert(!res.body.post); 226 | we.log.info = info; 227 | done(); 228 | }); 229 | }); 230 | }); 231 | 232 | describe('PUT /post/:id', function(){ 233 | it ('should update one post attr', function (done) { 234 | we.db.models.post.create(postStub()) 235 | .then(function (p) { 236 | 237 | var updateData = { 238 | title: 'iIIeeei' 239 | }; 240 | request(http) 241 | .put('/post/'+p.id) 242 | .set('Accept', 'application/json') 243 | .send(updateData) 244 | .expect(200) 245 | .end(function (err, res) { 246 | if (err) throw err; 247 | 248 | assert(res.body.post.id); 249 | assert.equal(res.body.post.title, updateData.title); 250 | assert.equal(res.body.post.text, p.text); 251 | 252 | done(); 253 | }); 254 | }).catch(done); 255 | }); 256 | }); 257 | 258 | describe('DELETE /post/:id', function(){ 259 | it ('should delete one post', function (done) { 260 | we.db.models.post.create(postStub()) 261 | .then(function (p) { 262 | request(http) 263 | .delete('/post/'+p.id) 264 | .set('Accept', 'application/json') 265 | .expect(204) 266 | .end(function (err, res) { 267 | if (err) throw err; 268 | 269 | assert(!res.text); 270 | 271 | we.db.models.post.findById(p.id) 272 | .then(function(ps){ 273 | assert(!ps); 274 | done(); 275 | }).catch(done); 276 | }); 277 | }).catch(done); 278 | }); 279 | }); 280 | 281 | describe('POST /post', function(){ 282 | it ('should create one resource with valid data', function (done) { 283 | var p = postStub(); 284 | request(http) 285 | .post('/post') 286 | .send(p) 287 | .set('Accept', 'application/json') 288 | .expect(201) 289 | .end(function (err, res) { 290 | if (err) throw err; 291 | 292 | assert(res.body.post.id); 293 | assert.equal(res.body.post.title, p.title); 294 | assert.equal(res.body.post.text, p.text); 295 | 296 | done(); 297 | }); 298 | }); 299 | 300 | it ('should return error if not set an not null attr', function (done) { 301 | var p = postStub(); 302 | p.title = null; 303 | 304 | request(http) 305 | .post('/post') 306 | .send(p) 307 | .set('Accept', 'application/json') 308 | .expect(400) 309 | .end(function (err, res) { 310 | if (err) throw err; 311 | 312 | // assert(!res.body.post.id); 313 | 314 | assert.equal(res.body.messages[0].status, 'danger'); 315 | assert.equal(res.body.messages[0].message, 'post.title cannot be null'); 316 | 317 | // assert.equal(res.body.post.title, p.title); 318 | // assert.equal(res.body.post.text, p.text); 319 | 320 | done(); 321 | }); 322 | }); 323 | 324 | it ('should not redirect if html plugin not is installed and Accept have the html format', function (done) { 325 | var p = postStub(); 326 | request(http) 327 | .post('/post') 328 | .send(p) 329 | .set('Accept', 'application/json, text/plain, */*') 330 | .expect(201) 331 | .end(function (err, res) { 332 | if (err) { 333 | throw err; 334 | } 335 | 336 | assert(res.body.post.id); 337 | assert.equal(res.body.post.title, p.title); 338 | assert.equal(res.body.post.text, p.text); 339 | 340 | done(); 341 | }); 342 | }); 343 | 344 | }); 345 | }); 346 | }); 347 | -------------------------------------------------------------------------------- /test/tests/requests/pluralized-resource.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'), 2 | request = require('supertest'), 3 | path = require('path'), 4 | Chance = require('chance'), 5 | We = require('../../../src'), 6 | chance = new Chance(); 7 | 8 | let we, _, http; 9 | 10 | describe('resource.pluralized', function(){ 11 | 12 | // prepare we.js core and load app features with pluralization: 13 | before(function (callback) { 14 | this.slow(100); 15 | 16 | we = new We({ 17 | bootstrapMode: 'test' 18 | }); 19 | 20 | we.bootstrap({ 21 | // disable access log 22 | enableRequestLog: false, 23 | router: { 24 | pluralize: true 25 | }, 26 | port: 9988, 27 | i18n: { 28 | directory: path.resolve(process.cwd(), 'config/locales'), 29 | updateFiles: true, 30 | locales: ['en-us'] 31 | }, 32 | themes: {} 33 | }, callback); 34 | }); 35 | 36 | // start the server: 37 | before(function (callback) { 38 | we.startServer(callback); 39 | }); 40 | 41 | before(function (done) { 42 | http = we.http; 43 | _ = we.utils._; 44 | return done(); 45 | }); 46 | 47 | function postStub(creatorId) { 48 | return { 49 | title: chance.sentence({words: 5}), 50 | text: chance.paragraph(), 51 | creatorId: creatorId 52 | }; 53 | } 54 | 55 | describe('resourceRequests', function() { 56 | afterEach(function(done){ 57 | const models = we.db.models; 58 | 59 | Promise.all([ 60 | models.tag.destroy({truncate: true}), 61 | models.post.destroy({truncate: true}) 62 | ]) 63 | .then(function() { 64 | done(); 65 | }) 66 | .catch(done); 67 | }); 68 | describe('json', function() { 69 | describe('GET /posts', function(){ 70 | it ('should get posts list', function (done) { 71 | 72 | var posts = [ 73 | postStub(), 74 | postStub(), 75 | postStub() 76 | ]; 77 | 78 | we.db.models.post.bulkCreate(posts) 79 | .spread(function() { 80 | request(http) 81 | .get('/posts') 82 | .set('Accept', 'application/json') 83 | .expect(200) 84 | .end(function (err, res) { 85 | if (err) { 86 | console.error('res.text>',res.text); 87 | throw err; 88 | } 89 | 90 | for (var i = 0; i < posts.length; i++) { 91 | assert(res.body.post[i].id); 92 | } 93 | 94 | assert.equal(res.body.meta.count, 3); 95 | 96 | done(); 97 | }); 98 | }) 99 | .catch(done); 100 | }); 101 | 102 | it ('/posts/count should get posts count', function (done) { 103 | var posts = [ 104 | postStub(), 105 | postStub(), 106 | postStub() 107 | ]; 108 | 109 | we.db.models.post.bulkCreate(posts) 110 | .spread(function() { 111 | request(http) 112 | .get('/posts/count') 113 | .set('Accept', 'application/json') 114 | .expect(200) 115 | .end(function (err, res) { 116 | if (err) { 117 | console.error('res.text>',res.text); 118 | throw err; 119 | } 120 | 121 | assert(res.body.count, `Count should be present in response`); 122 | assert.equal(res.body.count, 3, `Count should be 3`); 123 | 124 | done(); 125 | }); 126 | }) 127 | .catch(done); 128 | }); 129 | it ('should search for posts by title', function (done) { 130 | var posts = [ 131 | postStub(), 132 | postStub(), 133 | postStub() 134 | ]; 135 | 136 | we.db.models.post.bulkCreate(posts) 137 | .spread(function(){ 138 | request(http) 139 | .get('/posts?title='+posts[1].title) 140 | .set('Accept', 'application/json') 141 | .expect(200) 142 | .end(function (err, res) { 143 | if (err) throw err; 144 | 145 | assert.equal(res.body.post.length, 1); 146 | assert(res.body.post[0].id, posts[1].title); 147 | assert.equal(res.body.post[0].title, posts[1].title); 148 | assert.equal(res.body.post[0].text, posts[1].text); 149 | 150 | assert.equal(res.body.meta.count, 1); 151 | 152 | done(); 153 | }); 154 | }).catch(done); 155 | }); 156 | 157 | 158 | it ('should search for posts by text', function (done) { 159 | var posts = [ 160 | postStub(), 161 | postStub(), 162 | postStub(), 163 | postStub() 164 | ]; 165 | 166 | var searchText = ' mussum ipsum'; 167 | 168 | posts[1].text += searchText; 169 | posts[2].text += searchText; 170 | 171 | we.db.models.post.bulkCreate(posts) 172 | .spread(function(){ 173 | request(http) 174 | .get('/posts?text='+searchText) 175 | .set('Accept', 'application/json') 176 | .expect(200) 177 | .end(function (err, res) { 178 | if (err) throw err; 179 | 180 | assert.equal(res.body.post.length, 2); 181 | 182 | res.body.post.forEach(function(p){ 183 | assert(p.text.indexOf(searchText) >-1); 184 | }) 185 | 186 | assert.equal(res.body.meta.count, 2); 187 | 188 | done(); 189 | }); 190 | }).catch(done); 191 | }); 192 | 193 | it ('should search for posts by text with and and inTitleAndText, orWithComaParser search in q param', function (done) { 194 | var posts = [ 195 | postStub(), 196 | postStub(), 197 | postStub(), 198 | postStub() 199 | ]; 200 | 201 | var searchText = ' mussum ipsum'; 202 | var searchText2 = '2222m ipsum'; 203 | 204 | posts[1].title = searchText; 205 | posts[1].text = searchText; 206 | 207 | posts[2].title = searchText2; 208 | posts[2].text = searchText2; 209 | 210 | we.db.models.post.bulkCreate(posts) 211 | .spread(function(){ 212 | request(http) 213 | .get('/posts?q='+searchText+','+searchText2) 214 | .set('Accept', 'application/json') 215 | .expect(200) 216 | .end(function (err, res) { 217 | if (err) throw err; 218 | assert.equal(res.body.post.length, 2); 219 | assert.equal(res.body.meta.count, 2); 220 | 221 | done(); 222 | }); 223 | }).catch(done); 224 | }); 225 | }); 226 | 227 | describe('GET /posts/:id', function(){ 228 | it ('should get one post', function (done) { 229 | we.db.models.post 230 | .create(postStub()) 231 | .then(function (p) { 232 | request(http) 233 | .get('/posts/'+p.id) 234 | .set('Accept', 'application/json') 235 | .expect(200) 236 | .end(function (err, res) { 237 | if (err) throw err; 238 | 239 | assert(res.body.post.id); 240 | assert.equal(res.body.post.title, p.title); 241 | assert.equal(res.body.post.text, p.text); 242 | 243 | done(); 244 | }); 245 | }).catch(done); 246 | }); 247 | 248 | it ('should return 404 to not found', function (done) { 249 | var info = we.log.info; 250 | we.log.info = function() {}; 251 | request(http) 252 | .get('/posts/123213131') 253 | .expect(404) 254 | .set('Accept', 'application/json') 255 | .end(function (err, res) { 256 | if (err) throw err; 257 | 258 | assert(!res.body.post); 259 | we.log.info = info; 260 | done(); 261 | }); 262 | }); 263 | }); 264 | 265 | describe('PUT /posts/:id', function(){ 266 | it ('should update one post attr', function (done) { 267 | we.db.models.post.create(postStub()) 268 | .then(function (p) { 269 | 270 | var updateData = { 271 | title: 'iIIeeei' 272 | }; 273 | request(http) 274 | .put('/posts/'+p.id) 275 | .set('Accept', 'application/json') 276 | .send(updateData) 277 | .expect(200) 278 | .end(function (err, res) { 279 | if (err) throw err; 280 | 281 | assert(res.body.post.id); 282 | assert.equal(res.body.post.title, updateData.title); 283 | assert.equal(res.body.post.text, p.text); 284 | 285 | done(); 286 | }); 287 | }).catch(done); 288 | }); 289 | }); 290 | 291 | describe('DELETE /posts/:id', function(){ 292 | it ('should delete one post', function (done) { 293 | we.db.models.post.create(postStub()) 294 | .then(function (p) { 295 | request(http) 296 | .delete('/posts/'+p.id) 297 | .set('Accept', 'application/json') 298 | .expect(204) 299 | .end(function (err, res) { 300 | if (err) throw err; 301 | 302 | assert(!res.text); 303 | 304 | we.db.models.post.findById(p.id) 305 | .then(function(ps){ 306 | assert(!ps); 307 | done(); 308 | }).catch(done); 309 | }); 310 | }).catch(done); 311 | }); 312 | }); 313 | 314 | describe('POST /posts', function(){ 315 | it ('should create one resource with valid data', function (done) { 316 | var p = postStub(); 317 | request(http) 318 | .post('/posts') 319 | .send(p) 320 | .set('Accept', 'application/json') 321 | .expect(201) 322 | .end(function (err, res) { 323 | if (err) throw err; 324 | 325 | assert(res.body.post.id); 326 | assert.equal(res.body.post.title, p.title); 327 | assert.equal(res.body.post.text, p.text); 328 | 329 | done(); 330 | }); 331 | }); 332 | 333 | it ('should return error if not set an not null attr', function (done) { 334 | var p = postStub(); 335 | p.title = null; 336 | 337 | request(http) 338 | .post('/posts') 339 | .send(p) 340 | .set('Accept', 'application/json') 341 | .expect(400) 342 | .end(function (err, res) { 343 | if (err) throw err; 344 | 345 | // assert(!res.body.post.id); 346 | 347 | assert.equal(res.body.messages[0].status, 'danger'); 348 | assert.equal(res.body.messages[0].message, 'post.title cannot be null'); 349 | 350 | done(); 351 | }); 352 | }); 353 | }); 354 | }); 355 | }); 356 | 357 | }); // end resource.pluralized -------------------------------------------------------------------------------- /src/PluginManager/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'), 2 | fs = require('fs'); 3 | let projectPath = process.cwd(); 4 | 5 | /** 6 | * Plugin manager, load, valid and store avaible plugins 7 | * 8 | * @type {Object} 9 | */ 10 | function PluginManager (we) { 11 | this.we = we; 12 | 13 | projectPath = we.projectPath; 14 | // npm module folder from node_modules 15 | this.nodeModulesPath = path.resolve(projectPath, 'node_modules'); 16 | 17 | this.configs = we.configs; 18 | this.plugins = {}; 19 | this.pluginNames = this.getPluginsList(); 20 | // a list of plugin.js files get from npm module folder 21 | this.pluginFiles = {}; 22 | // array with all plugin paths 23 | this.pluginPaths = []; 24 | // plugin records from db 25 | this.records = []; 26 | // a list of plugins to install 27 | this.pluginsToInstall = {}; 28 | } 29 | // flag to check if plugins already is loaded 30 | PluginManager.prototype.pluginsLoaded = false; 31 | // function to check if npm module is plugin 32 | PluginManager.prototype.isPlugin = require('./isPlugin.js'); 33 | // return the name of all enabled plugins 34 | PluginManager.prototype.getPluginNames = function () { 35 | return this.pluginNames; 36 | }; 37 | // load one plugin running related plugin.js file 38 | PluginManager.prototype.loadPlugin = function (pluginFile, npmModuleName, projectPath) { 39 | this.plugins[npmModuleName] = require(pluginFile)( projectPath , this.we.class.Plugin); 40 | }; 41 | /** 42 | * Get plugin names list from project package.json 43 | * 44 | * @return {Array} Plugin names list 45 | */ 46 | PluginManager.prototype.getPluginsList = function () { 47 | let names = []; 48 | 49 | // dev or test modules 50 | if (this.we.env != 'prod') { 51 | if (this.we.projectPackageJSON.wejs && this.we.projectPackageJSON.wejs.devPlugins) { 52 | for (let name in this.we.projectPackageJSON.wejs.devPlugins) { 53 | if (this.we.projectPackageJSON.wejs.devPlugins[name]) { 54 | names.push(name); 55 | } 56 | } 57 | } 58 | } 59 | 60 | // modules avaible in all envs 61 | if (this.we.projectPackageJSON.wejs && this.we.projectPackageJSON.wejs.plugins) { 62 | for (let name in this.we.projectPackageJSON.wejs.plugins) { 63 | if (this.we.projectPackageJSON.wejs.plugins[name]) { 64 | names.push(name); 65 | } 66 | } 67 | } 68 | 69 | // 70 | if (this.we.projectPackageJSON.name != 'we-core') { 71 | // move we-core to plugin list start 72 | let weCoreIndex = names.indexOf('we-core'); 73 | if (weCoreIndex == -1) { 74 | // not is in plugins list then add before all 75 | names.unshift('we-core'); 76 | } else if (weCoreIndex !== -1 && weCoreIndex > 0) { 77 | // move we-core to load after others plugins 78 | names.unshift(names.splice(weCoreIndex, 1)); 79 | } 80 | } 81 | 82 | return names; 83 | }; 84 | 85 | /** 86 | * Load and register all avaible plugins 87 | * 88 | * @param {object} we we.js object 89 | * @param {Function} cb callback 90 | */ 91 | PluginManager.prototype.loadPlugins = function (we, done) { 92 | // only load one time 93 | if (this.pluginsLoaded) return this.plugins; 94 | 95 | let newPluginNames = we.utils._.cloneDeep(this.pluginNames); 96 | 97 | this.pluginNames.forEach(name => { 98 | // load project bellow 99 | if (name == 'project') return; 100 | 101 | // get full path 102 | let pluginPath = path.resolve(this.nodeModulesPath, name); 103 | 104 | // check if is plugin 105 | if ( !this.isPlugin(pluginPath) ) { 106 | // if not is plugin, remove from array and show log 107 | let index = newPluginNames.indexOf(name); 108 | newPluginNames.splice(index, 1); 109 | 110 | we.log.warn('pluginManager:'+name+' not is plugin'); 111 | return; 112 | } 113 | 114 | // save plugin path 115 | this.pluginPaths.push(pluginPath); 116 | // resolve full plugin file path 117 | let pluginFile = path.resolve( pluginPath, 'plugin.js' ); 118 | // save this plugin file 119 | this.pluginFiles[name] = pluginFile; 120 | // load plugin resources 121 | this.loadPlugin(pluginFile, name, projectPath); 122 | // check if needs to install this plugin 123 | if (!this.isInstalled(name)) { 124 | this.pluginsToInstall[name] = pluginFile; 125 | } 126 | }); 127 | 128 | this.pluginNames = newPluginNames; 129 | 130 | this.pluginNames.push('project'); 131 | // - then load project as plugin if it have the plugin.js file 132 | let projectFile = path.resolve(projectPath, 'plugin.js'); 133 | this.pluginFiles.project = projectFile; 134 | // after all plugins load the project as plugin 135 | this.loadProjectAsPlugin(); 136 | // check if needs to install the project 137 | if (!this.isInstalled('project')) { 138 | this.pluginsToInstall.project = projectFile; 139 | } 140 | 141 | // load done 142 | this.pluginsLoaded = true; 143 | 144 | done(null, this.plugins); 145 | }; 146 | /** 147 | * Check if one plugin is installed 148 | * 149 | * @param {String} name plugin Name 150 | * @return {Boolean} 151 | */ 152 | PluginManager.prototype.isInstalled = function (name) { 153 | for (let i = 0; i < this.records.length; i++) { 154 | if (this.records[i].name == name) { 155 | return true; 156 | } 157 | } 158 | return false; 159 | }; 160 | 161 | /** 162 | * Get the plugin install.js script if is avaible 163 | * @param {String} name plugin name 164 | */ 165 | PluginManager.prototype.getPluginInstallScript = function (name) { 166 | let pluginFolder; 167 | // get folder, for suport with project plugin 168 | if (name == 'project') { 169 | pluginFolder = projectPath; 170 | } else { 171 | pluginFolder = path.resolve(this.nodeModulesPath, name); 172 | } 173 | // get the install file 174 | let installFile = path.resolve(pluginFolder, 'install.js'); 175 | let install; 176 | try { 177 | return install = require(installFile); 178 | } catch (e) { 179 | if (e.code == 'MODULE_NOT_FOUND') { 180 | // this plugin dont have the install file 181 | return null; 182 | } 183 | // unknow error 184 | throw(e); 185 | } 186 | }; 187 | 188 | PluginManager.prototype.installPlugin = function (name, done) { 189 | let install = this.getPluginInstallScript(name); 190 | let we = this.we; 191 | 192 | // dont have the install script 193 | if (!install || !install.install) { 194 | we.log.info('Plugin '+ name + ' dont have install method'); 195 | return done(); 196 | } 197 | // run the install script if avaible 198 | install.install(we, function afterRunPluginInstallMethod (err) { 199 | if (err) return done(err); 200 | 201 | we.log.info('Plugin '+ name + ' installed'); 202 | return done(); 203 | }); 204 | }; 205 | 206 | PluginManager.prototype.registerPlugin = function (name, done) { 207 | let we =this.we; 208 | let pluginManager = this; 209 | 210 | let filename; 211 | if (name == 'project') { 212 | filename = 'plugin.js'; 213 | } else { 214 | filename = 'node_modules/'+ name + '/plugin.js'; 215 | } 216 | 217 | let install = this.getPluginInstallScript(name); 218 | 219 | let version = '0.0.0'; 220 | 221 | // get version of last update 222 | if (install && install.updates) { 223 | let updates = install.updates(we); 224 | if (updates && updates.length) { 225 | version = updates[updates.length - 1].version; 226 | } 227 | } 228 | 229 | we.db.models.plugin.create({ 230 | filename: filename, 231 | name: name, 232 | version: version, 233 | status: 1 234 | }) 235 | .then(function (r) { 236 | we.log.info('Plugin '+name+' registered with id: '+ r.id); 237 | // push to plugin record array 238 | pluginManager.records.push(r); 239 | done(); 240 | }) 241 | .catch(done); 242 | }; 243 | 244 | /** 245 | * Load all plugin settings from DB 246 | * 247 | * @param {Object} we we.js object 248 | * @param {Function} cb callback 249 | */ 250 | PluginManager.prototype.loadPluginsSettingsFromDB = function (we, cb) { 251 | let pluginManager = this; 252 | 253 | return we.db.models.plugin 254 | .findAll({ 255 | order: [ 256 | ['weight', 'ASC'], 257 | ['id', 'ASC'] 258 | ] 259 | }) 260 | .then(function (plugins) { 261 | // move we-core to start of array if exists 262 | for (let i = 0; i < plugins.length; i++) { 263 | if (plugins[i].name == 'we-core') { 264 | plugins.unshift(plugins.splice(i, 1)[0]); 265 | break; 266 | } 267 | } 268 | 269 | pluginManager.records = plugins; 270 | 271 | cb(null, plugins); 272 | }) 273 | .catch(cb); 274 | }; 275 | 276 | /** 277 | * Get one plugin record from pluginManager.records array 278 | * 279 | * @param {String} name pluginName 280 | * @return {Object} sequelize plugin record 281 | */ 282 | PluginManager.prototype.getPluginRecord = function (name) { 283 | for (let l = 0; l < this.records.length; l++) { 284 | if (this.records[l].name == name) 285 | return this.records[l]; 286 | } 287 | return null; 288 | }; 289 | 290 | PluginManager.prototype.getPluginsToUpdate = function (done) { 291 | const we = this.we; 292 | let pluginsToUpdate = []; 293 | let name, installFile, updates, pluginRecord; 294 | 295 | for (let i = 0; i < this.pluginNames.length; i++) { 296 | name = this.pluginNames[i]; 297 | installFile = this.getPluginInstallScript(name); 298 | 299 | // skip if dont have updates 300 | if (!installFile || !installFile.updates) continue; 301 | 302 | updates = installFile.updates(we); 303 | 304 | if (!updates.length) continue; 305 | 306 | pluginRecord = this.getPluginRecord(name); 307 | 308 | if (!pluginRecord) continue; 309 | 310 | if (pluginRecord.version == '0.0.0') { 311 | pluginsToUpdate.push({ 312 | name: name, 313 | installFile: installFile, 314 | record: pluginRecord 315 | }); 316 | continue; 317 | } 318 | 319 | let firstUpdateFound = false; 320 | for (let j = 0; j < updates.length; j++) { 321 | if (firstUpdateFound) { 322 | pluginsToUpdate.push({ 323 | name: name, 324 | installFile: installFile, 325 | record: pluginRecord 326 | }); 327 | break; 328 | } 329 | 330 | if (!firstUpdateFound && updates[j].version == pluginRecord.version) { 331 | firstUpdateFound = true; 332 | } 333 | } 334 | } 335 | 336 | done(null, pluginsToUpdate); 337 | }; 338 | 339 | PluginManager.prototype.runPluginUpdates = function (name, done) { 340 | let we = this.we; 341 | let installFile = this.getPluginInstallScript(name); 342 | let updates = installFile.updates(we); 343 | let pluginRecord = this.getPluginRecord(name); 344 | 345 | let updatesToRun = []; 346 | 347 | // get all updates to run for this plugin 348 | if (pluginRecord.version == '0.0.0') { 349 | updatesToRun = updates; 350 | } else { 351 | let firstUpdateFound = false; 352 | for (let i = 0; i < updates.length; i++) { 353 | if (firstUpdateFound) { 354 | updatesToRun.push(updates[i]); 355 | } 356 | if (!firstUpdateFound && updates[i].version == pluginRecord.version) { 357 | firstUpdateFound = true; 358 | } 359 | } 360 | } 361 | 362 | we.utils.async.eachSeries(updatesToRun, function (up, next) { 363 | // run the update fn 364 | up.update(we, function (err) { 365 | if (err) return next (err); 366 | // update the plugin version in db 367 | pluginRecord.version = up.version; 368 | 369 | return pluginRecord.save() 370 | .then(function () { 371 | we.log.info('Plugin '+name+ ' updated to: ' + up.version); 372 | next(); 373 | }) 374 | .catch(next); 375 | }); 376 | }, done); 377 | }; 378 | 379 | /** 380 | * Check is project have a plugin.js file and if yes load it as plugin 381 | */ 382 | PluginManager.prototype.loadProjectAsPlugin = function () { 383 | let file = path.join( projectPath, 'plugin.js' ); 384 | 385 | if (!fs.existsSync(file)) { 386 | // current project dont have plugin features if dont have the plugin.js file 387 | this.plugins.project = new this.we.class.Plugin(projectPath); 388 | return null; 389 | } 390 | 391 | // load project plugin.js file if exists 392 | this.loadPlugin(file, 'project', projectPath); 393 | }; 394 | 395 | // exports pluginManager 396 | module.exports = PluginManager; 397 | -------------------------------------------------------------------------------- /src/responses/methods/index.js: -------------------------------------------------------------------------------- 1 | const haveAndAcceptsHtmlResponse = require('../../Router/haveAndAcceptsHtmlResponse.js'); 2 | 3 | module.exports = { 4 | /** 5 | * res.ok default success response 6 | * 7 | * By defalt they will get the record from res.locals.data 8 | * 9 | * @param {Object} data opctional data 10 | */ 11 | ok(data) { 12 | const res = this.res, 13 | req = this.req, 14 | we = req.we; 15 | 16 | res.status(200); 17 | 18 | if (!data) { 19 | data = res.locals.data || {}; 20 | } else { 21 | if (!res.locals.data) res.locals.data = data; 22 | } 23 | 24 | // use this hook in one we.js plugin to change a res.ok response 25 | we.hooks.trigger('we:before:send:okResponse', { 26 | req: req, 27 | res: res, 28 | data: data 29 | }, (err)=> { 30 | if (err) we.log.error(err); 31 | 32 | // if accepts html and have the html response format: 33 | if (haveAndAcceptsHtmlResponse(req, res)) { 34 | if (req.method == 'POST' && res.locals.action == 'edit' && res.locals.redirectTo) { 35 | return res.redirect(res.locals.redirectTo); 36 | } 37 | } 38 | 39 | res.format(we.responses.formaters); 40 | }); 41 | }, 42 | /** 43 | * Record created response 44 | * 45 | * By defalt they will get the record from res.locals.data 46 | * 47 | * @param {Object} data record data 48 | */ 49 | created(data) { 50 | const res = this.res, 51 | req = this.req, 52 | we = req.we; 53 | 54 | res.status(201); 55 | 56 | if (!data) { 57 | data = res.locals.data || {}; 58 | } else { 59 | if (!res.locals.data) res.locals.data = data; 60 | } 61 | 62 | // use this hook in one we.js plugin to change a res.ok response 63 | we.hooks.trigger('we:before:send:createdResponse', { 64 | req: req, 65 | res: res, 66 | data: data 67 | }, (err)=> { 68 | if (err) we.log.error(err); 69 | 70 | if (haveAndAcceptsHtmlResponse(req, res)) { 71 | // redirect if are one html response 72 | if (res.locals.skipRedirect) { 73 | return res.view(data); 74 | } else if (res.locals.redirectTo) { 75 | return res.redirect(res.locals.redirectTo); 76 | } else { 77 | // push id to paramsArray for use in urlTo 78 | req.paramsArray.push(res.locals.data.id); 79 | // redirect to content after create 80 | return res.redirect(we.router.urlTo(res.locals.model + '.findOne', req.paramsArray)); 81 | } 82 | } 83 | 84 | res.format(we.responses.formaters); 85 | }); 86 | }, 87 | /** 88 | * Record updated response 89 | * 90 | * By defalt they will get the record from res.locals.data 91 | * 92 | * @param {Object} data optional data 93 | */ 94 | updated(data) { 95 | const res = this.res, 96 | req = this.req, 97 | we = req.we; 98 | 99 | res.status(200); 100 | 101 | if (!data) { 102 | data = res.locals.data || {}; 103 | } else { 104 | if (!res.locals.data) res.locals.data = data; 105 | } 106 | 107 | // use this hook in one we.js plugin to change a res.ok response 108 | we.hooks.trigger('we:before:send:updatedResponse', { 109 | req: req, 110 | res: res, 111 | data: data 112 | }, (err)=> { 113 | if (err) we.log.error(err); 114 | 115 | if (haveAndAcceptsHtmlResponse(req, res)) { 116 | // if is edit record use the redirectTo feature 117 | if (res.locals.redirectTo) { 118 | return res.redirect(res.locals.redirectTo); 119 | } else { 120 | // push id to paramsArray for use in urlTo 121 | req.paramsArray.push(res.locals.data.id); 122 | // redirect to content after create 123 | return res.redirect(we.router.urlTo(res.locals.model + '.findOne', req.paramsArray)); 124 | } 125 | } 126 | 127 | res.format(we.responses.formaters); 128 | }); 129 | }, 130 | /** 131 | * Deleted response 132 | * 133 | * redirect for html responses 134 | */ 135 | deleted() { 136 | const res = this.res, 137 | req = this.req, 138 | we = req.we; 139 | 140 | res.status(204); 141 | 142 | // use this hook in one we.js plugin to change a res.ok response 143 | we.hooks.trigger('we:before:send:deletedResponse', { 144 | req: req, 145 | res: res 146 | }, ()=> { 147 | 148 | if (haveAndAcceptsHtmlResponse(req, res)) { 149 | if ( 150 | res.locals.redirectTo && 151 | (we.router.urlTo(res.locals.model + '.findOne', req.paramsArray) != res.locals.redirectTo) 152 | ) { 153 | return res.redirect(res.locals.redirectTo); 154 | } else { 155 | res.locals.deleteRedirectUrl = we.router.urlTo(res.locals.model + '.find', req.paramsArray); 156 | return res.redirect((res.locals.deleteRedirectUrl || '/')); 157 | } 158 | } 159 | 160 | res.format(req.we.responses.formaters); 161 | }); 162 | }, 163 | 164 | /** 165 | * View response usefull if we have the we-plugin-view installed to send html pages in response 166 | * 167 | * @param {Object} data Data to send that overrides data from res.locals.data 168 | */ 169 | view(data) { 170 | const req = this.req, 171 | res = this.res; 172 | 173 | if (!data) { 174 | data = res.locals.data || {}; 175 | } else { 176 | if (!res.locals.data) res.locals.data = data; 177 | } 178 | 179 | if (req.haveAlias) { 180 | // is a target how have alias then redirect to it 181 | res.writeHead(307, { 182 | 'Location': req.haveAlias.alias, 183 | 'Content-Type': 'text/plain', 184 | 'Cache-Control':'public, max-age=345600', 185 | 'Expires': new Date(Date.now() + 345600000).toUTCString() 186 | }); 187 | 188 | return res.send(); 189 | } 190 | 191 | if (req.method && req.method == 'HEAD') { 192 | // HEAD requests dont have body data then ignore format. 193 | return res.send(); 194 | } 195 | 196 | res.format(req.we.responses.formaters); 197 | }, 198 | 199 | /** 200 | * Forbidden response 201 | * 202 | * @param {String} data Optional extra message 203 | */ 204 | forbidden(data) { 205 | const res = this.res, 206 | req = this.req; 207 | 208 | let __ = ( res.locals.__ || req.we.i18n.__ ); 209 | 210 | if (typeof data == 'string') { 211 | res.addMessage('error', { 212 | text: data 213 | }); 214 | 215 | data = null; 216 | } 217 | 218 | res.status(403); 219 | 220 | res.locals.title = __('response.forbidden.title'); 221 | 222 | if (haveAndAcceptsHtmlResponse(req, res)) { 223 | res.locals.layoutName = 'fullwidth'; 224 | res.locals.template = '403'; 225 | } 226 | // delete the data that user dont have access: 227 | delete res.locals.data; 228 | // add one message with forbidden to send in response: 229 | res.addMessage('warn', { text: 'forbidden' }); 230 | 231 | res.format(req.we.responses.formaters); 232 | }, 233 | 234 | /** 235 | * Not found response 236 | * 237 | * @param {String} data optional 404 error message 238 | */ 239 | notFound(data) { 240 | const res = this.res, 241 | req = this.req, 242 | we = req.we; 243 | 244 | if (typeof data == 'string') { 245 | res.addMessage('error', { 246 | text: data 247 | }); 248 | 249 | data = null; 250 | } 251 | 252 | res.locals.data = null; 253 | 254 | if (we.config.enable404Log) { 255 | if (we.env == 'dev') { 256 | console.trace('404', { 257 | method: req.method, 258 | path: req.path 259 | }); 260 | } else { 261 | we.log.info('Not found 404 ', { 262 | url: req.url, 263 | method: req.method, 264 | query: req.query, 265 | controller: res.locals.controller, 266 | action: res.locals.action 267 | }); 268 | } 269 | } 270 | 271 | res.locals.title = req.__('response.notFound.title'); 272 | 273 | res.status(404); 274 | 275 | if (haveAndAcceptsHtmlResponse(req, res)) { 276 | res.locals.layoutName = 'fullwidth'; 277 | res.locals.template = '404'; 278 | } 279 | 280 | delete res.locals.data; 281 | 282 | res.format(we.responses.formaters); 283 | }, 284 | /** 285 | * Server error response 286 | * 287 | * @param {Object} data the error 288 | */ 289 | serverError(data) { 290 | const res = this.res, 291 | req = this.req; 292 | 293 | let __ = ( req.__ || res.locals.__ || req.we.i18n.__ ); 294 | 295 | res.status(500); 296 | 297 | req.we.log.error('ServerError:', data); 298 | 299 | res.locals.title = __('response.serveError.title'); 300 | 301 | if (data && typeof data == 'string') { 302 | res.addMessage('error', String(data)); 303 | } 304 | 305 | if (haveAndAcceptsHtmlResponse(req, res)) { 306 | res.locals.template = '500'; 307 | res.locals.layoutName = 'fullwidth'; 308 | } 309 | 310 | delete res.locals.data; 311 | 312 | // send the response 313 | res.format(req.we.responses.formaters); 314 | }, 315 | 316 | /** 317 | * bad request response 318 | * 319 | * @param {Obejct|String} data message 320 | */ 321 | badRequest(data) { 322 | const res = this.res, 323 | req = this.req; 324 | 325 | res.status(400); 326 | 327 | if (req.we.env == 'dev') { 328 | console.trace('400', req.path); 329 | } 330 | 331 | if (data && typeof data == 'string') { 332 | res.addMessage('warning', String(data)); 333 | } 334 | 335 | if (haveAndAcceptsHtmlResponse(req, res)) { 336 | // if is html 337 | if (!res.locals.template) res.locals.template = '400'; 338 | } 339 | 340 | delete res.locals.data; 341 | 342 | // send the response 343 | res.format(req.we.responses.formaters); 344 | }, 345 | 346 | /** 347 | * Sequelize query error parser 348 | * 349 | * @param {Object} err The database error 350 | */ 351 | queryError(err) { 352 | const res = this.res, 353 | req = this.req, 354 | log = req.we.log; 355 | 356 | let __ = ( req.__ || res.locals.__ || req.we.i18n.__ ); 357 | 358 | if (err) { 359 | // parse all sequelize validation erros for html (we-plugin-view) 360 | if ( 361 | haveAndAcceptsHtmlResponse(req, res) && 362 | err.name === 'SequelizeValidationError' 363 | ) { 364 | // query validation error ... 365 | res.locals.validationError = {}; 366 | 367 | err.errors.forEach( (err)=> { 368 | if (!res.locals.validationError[err.path]) 369 | res.locals.validationError[err.path] = []; 370 | 371 | res.locals.validationError[err.path].push({ 372 | field: err.path, 373 | rule: err.type, 374 | message: __(err.message) 375 | }); 376 | }); 377 | 378 | } else if (err.name === 'SequelizeDatabaseError') { 379 | // parse sequelize database errors 380 | if (err.message) { 381 | res.addMessage('error', err.message); 382 | } 383 | } else if (typeof err == 'string') { 384 | res.addMessage('error', err); 385 | } else if (err.name != 'SequelizeValidationError') { 386 | log.error('responses.queryError:unknowError ', { 387 | path: req.path, 388 | error: { 389 | message: err.message, 390 | name: err.name, 391 | stack: err.stack 392 | } 393 | }); 394 | } 395 | 396 | // default error handler, push erros to messages and let response formaters resolve how to format this messages 397 | 398 | if (err.errors) { 399 | err.errors.forEach( (e)=> { 400 | res.addMessage('error', e.message, { 401 | field: e.path, 402 | rule: e.type, 403 | errorName: err.name, 404 | value: e.value, 405 | level: 'error', 406 | code: ( e.code || err.code ) // code if avaible 407 | }); 408 | }); 409 | } 410 | } 411 | 412 | if (err && err.name == 'SequelizeValidationError') { 413 | res.status(400); 414 | } else { 415 | res.status(500); 416 | delete res.locals.data; 417 | } 418 | 419 | res.format(req.we.responses.formaters); 420 | }, 421 | 422 | /** 423 | * We.js core redirect 424 | * 425 | * @param {String} s response status 426 | * @param {String} p path 427 | */ 428 | goTo(s ,p) { 429 | // save locals messages to flash 430 | this.res.moveLocalsMessagesToFlash(); 431 | // use default redirect 432 | if (p) { 433 | this.res.redirect(s, p); 434 | } else { 435 | this.res.redirect(s); 436 | } 437 | } 438 | }; -------------------------------------------------------------------------------- /src/class/Plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We.js Plugin prototype file 3 | */ 4 | 5 | const _ = require('lodash'), 6 | path = require('path'), 7 | fs = require('fs'), 8 | async = require('async'); 9 | 10 | module.exports = function getPluginPrototype (we) { 11 | /** 12 | * We.js plugin Class constructor 13 | * 14 | * @param {string} name plugin npm pakage name 15 | * @param {string} projectPath project path where the plugin is instaled 16 | */ 17 | function Plugin (pluginPath) { 18 | this.pluginPath = pluginPath; 19 | this.we = we; 20 | 21 | this.events = this.we.events; 22 | this.hooks = this.we.hooks; 23 | this.router = this.we.router; 24 | /** 25 | * Plugin Assets 26 | * @type {Object} 27 | */ 28 | this.assets = { 29 | js: {}, 30 | css: {} 31 | }; 32 | 33 | this['package.json'] = require( path.join( pluginPath , 'package.json') ); 34 | 35 | this.controllersPath = path.join( this.pluginPath, this.controllerFolder ); 36 | this.modelsPath = path.join( this.pluginPath, this.modelFolder ); 37 | this.modelHooksPath = path.join( this.pluginPath, this.modelHookFolder ); 38 | this.modelInstanceMethodsPath = path.join( this.pluginPath, this.modelInstanceMethodFolder ); 39 | this.modelClassMethodsPath = path.join( this.pluginPath, this.modelClassMethodFolder ); 40 | 41 | this.searchParsersPath = path.join( this.pluginPath, this.searchParsersFolder ); 42 | this.searchTargetsPath = path.join( this.pluginPath, this.searchTargetsFolder ); 43 | 44 | this.templatesPath = this.pluginPath + '/server/templates'; 45 | this.helpersPath = this.pluginPath + '/server/helpers'; 46 | this.resourcesPath = this.pluginPath + '/server/resources'; 47 | this.routesPath = this.pluginPath + '/server/routes'; 48 | 49 | this.helpers = {}; 50 | this.layouts = {}; 51 | this.templates = {}; 52 | 53 | /** 54 | * Default plugin config object 55 | * 56 | * @type {Object} 57 | */ 58 | this.configs = {}; 59 | 60 | 61 | /** 62 | * Default plugin resources 63 | * 64 | * @type {Object} 65 | */ 66 | this.controllers = {}; 67 | this.models = {}; 68 | this.routes = {}; 69 | 70 | this.appFiles = []; 71 | this.appAdminFiles = []; 72 | } 73 | 74 | /** 75 | * Default initializer function, override in plugin.js file if need 76 | * 77 | * @param {Object} we we.js object 78 | * @param {Function} cb callback 79 | */ 80 | Plugin.prototype.init = function initPlugin(we, cb) { return cb(); }; 81 | 82 | /** 83 | * Set plugin config 84 | * @param {Object} config 85 | */ 86 | Plugin.prototype.setConfigs = function setConfigs (configs) { 87 | this.configs = configs; 88 | }; 89 | 90 | // default plugin paths 91 | Plugin.prototype.controllerFolder = 'server/controllers'; 92 | Plugin.prototype.modelFolder = 'server/models'; 93 | Plugin.prototype.modelHookFolder = 'server/models/hooks'; 94 | Plugin.prototype.modelInstanceMethodFolder = 'server/models/instanceMethods'; 95 | Plugin.prototype.modelClassMethodFolder = 'server/models/classMethods'; 96 | Plugin.prototype.searchParsersFolder = 'server/search/parsers'; 97 | Plugin.prototype.searchTargetsFolder = 'server/search/targets'; 98 | 99 | /** 100 | * Set plugin routes 101 | * 102 | * @param {object} routes 103 | */ 104 | Plugin.prototype.setRoutes = function setRoutes (routes) { 105 | const routePaths = Object.keys(routes); 106 | let routePath; 107 | for (let i = routePaths.length - 1; i >= 0; i--) { 108 | routePath = routePaths[i]; 109 | this.setRoute(routePath, routes[routePath]); 110 | } 111 | }; 112 | 113 | /** 114 | * Set one route in plugin routes 115 | * @param {string} path route path 116 | * @param {object} configs route configs how will be avaible as res.locals 117 | */ 118 | Plugin.prototype.setRoute = function setRoute (routePath, configs) { 119 | this.routes[routePath] = configs; 120 | this.routes[routePath].path = routePath; 121 | this.routes[routePath].pluginPath = this.pluginPath; 122 | }; 123 | 124 | Plugin.prototype.setResource = function setResource (opts) { 125 | let router = this.we.router, 126 | fullName = (opts.namePrefix || '') + opts.name; 127 | 128 | // first save resource in name or merge route options if exists 129 | if (!router.resourcesByName[fullName]) { 130 | router.resourcesByName[fullName] = opts; 131 | } else { 132 | _.merge(router.resourcesByName[fullName], opts); 133 | } 134 | 135 | if (opts.parent) { 136 | // is subroute 137 | if (!router.resourcesByName[opts.parent]) { 138 | // parent dont are set 139 | // temporary create parent resource wit subroutes attr 140 | router.resourcesByName[opts.parent] = { subRoutes: {} }; 141 | // add reference to route in parent subroutes 142 | router.resourcesByName[opts.parent].subRoutes[fullName] = router.resourcesByName[fullName]; 143 | } else { 144 | // parent resource is set 145 | if (!router.resourcesByName[opts.parent].subRoutes) { 146 | // add subRoutes object if dont are set 147 | router.resourcesByName[opts.parent].subRoutes = {}; 148 | } 149 | if (!router.resourcesByName[opts.parent].subRoutes[fullName]) { 150 | router.resourcesByName[opts.parent].subRoutes[fullName] = {}; 151 | } 152 | // add reference to route in parent resource subroutes 153 | router.resourcesByName[opts.parent].subRoutes[fullName] = router.resourcesByName[fullName]; 154 | } 155 | } else { 156 | // is route route 157 | router.resources[fullName] = router.resourcesByName[fullName]; 158 | } 159 | }; 160 | 161 | /** 162 | * Set plugin layouts 163 | * @param {object} layouts 164 | */ 165 | Plugin.prototype.setLayouts = function setLayouts (layouts) { 166 | this.layouts = layouts; 167 | }; 168 | 169 | /** 170 | * Load all we.js plugin features 171 | * 172 | * Auto load avaible for plugins, helpers ... 173 | * 174 | */ 175 | Plugin.prototype.loadFeatures = function loadFeatures (we, cb) { 176 | const self = this; 177 | 178 | if (self.fastLoader) { 179 | // fast loader option for load faster plugins: 180 | self.fastLoader(we, (err)=> { 181 | if (err) return cb(err); 182 | // plugin loader hook, for allow hook extensions: 183 | we.hooks.trigger('plugin:load:features', { 184 | plugin: self, we: we 185 | }, cb); 186 | }); 187 | } else { 188 | // find and load all plugin features ... 189 | async.parallel([ 190 | next => self.loadSearchParsers(next), 191 | next => self.loadSearchTargets(next), 192 | next => self.loadControllers(next), 193 | next => self.loadModelHooks(next), 194 | next => self.loadInstanceMethods(next), 195 | next => self.loadClassMethods(next), 196 | next => self.loadModels(next), 197 | next => self.loadResources(next), 198 | next => self.loadRoutes(next), 199 | function loaderHookExtensor (done) { 200 | we.hooks.trigger('plugin:load:features', { 201 | plugin: self, we: we 202 | }, done); 203 | }, 204 | ], cb); 205 | } 206 | }; 207 | 208 | /** 209 | * Get generic featureFiles 210 | * Read feature dir, check if are an .js file and return full path and name of each file 211 | * 212 | * @param {String} fgPath feature path 213 | * @param {Function} done callback 214 | */ 215 | 216 | Plugin.prototype.getGenericFeatureFiles = function getGenericFeatureFiles (fgPath, done) { 217 | fs.readdir(fgPath, (e, fileNames) => { 218 | if (e) { 219 | if (e.code !== 'ENOENT') return done(e); 220 | return done(null, []); 221 | } 222 | 223 | done(null, fileNames 224 | .filter(fileName => { return fileName.endsWith('.js'); }) 225 | .map(fileName => { 226 | return { 227 | name: fileName.slice(0, -3), 228 | path: path.join(fgPath, fileName) 229 | }; 230 | })); 231 | }); 232 | }; 233 | 234 | /** 235 | * load plugin controllers 236 | */ 237 | Plugin.prototype.loadControllers = function loadControllers (done) { 238 | this.getGenericFeatureFiles(this.controllersPath, (e, modules) => { 239 | if (e) return done(e); 240 | 241 | modules.forEach(m => { 242 | let attrs = require(m.path); 243 | attrs._controllersPath = this.controllersPath; 244 | we.controllers[m.name] = new we.class.Controller(attrs); 245 | }); 246 | 247 | done(); 248 | }); 249 | }; 250 | 251 | /** 252 | * load plugin model hooks 253 | */ 254 | Plugin.prototype.loadModelHooks = function loadModelHooks (done) { 255 | this.getGenericFeatureFiles(this.modelHooksPath, (e, modules) => { 256 | if (e) return done(e); 257 | 258 | modules.forEach(m => { 259 | we.db.modelHooks[m.name] = require(m.path).bind({ we: we }); 260 | }); 261 | 262 | done(); 263 | }); 264 | }; 265 | 266 | /** 267 | * load plugin model instance methods 268 | */ 269 | Plugin.prototype.loadInstanceMethods = function loadInstanceMethods (done) { 270 | this.getGenericFeatureFiles(this.modelInstanceMethodsPath, (e, modules) => { 271 | if (e) return done(e); 272 | 273 | modules.forEach(m => { 274 | we.db.modelInstanceMethods[m.name] = require(m.path); 275 | }); 276 | 277 | done(); 278 | }); 279 | }; 280 | 281 | /** 282 | * load plugin model class methods 283 | */ 284 | Plugin.prototype.loadClassMethods = function loadClassMethods (done) { 285 | this.getGenericFeatureFiles(this.modelClassMethodsPath, (e, modules) => { 286 | if (e) return done(e); 287 | 288 | modules.forEach(m => { 289 | we.db.modelClassMethods[m.name] = require(m.path); 290 | }); 291 | 292 | done(); 293 | }); 294 | }; 295 | 296 | /** 297 | * load plugin search parsers 298 | */ 299 | Plugin.prototype.loadSearchParsers = function loadSearchParsers (done) { 300 | this.getGenericFeatureFiles(this.searchParsersPath, (e, modules) => { 301 | if (e) return done(e); 302 | 303 | modules.forEach(m => { 304 | we.router.search.parsers[m.name] = require(m.path).bind({ we: we }); 305 | }); 306 | 307 | done(); 308 | }); 309 | }; 310 | 311 | /** 312 | * load plugin search targets 313 | */ 314 | Plugin.prototype.loadSearchTargets = function loadSearchTargets (done) { 315 | this.getGenericFeatureFiles(this.searchTargetsPath, (e, modules) => { 316 | if (e) return done(e); 317 | 318 | modules.forEach(m => { 319 | we.router.search.targets[m.name] = require(m.path).bind({ we: we }); 320 | }); 321 | 322 | done(); 323 | }); 324 | }; 325 | 326 | /** 327 | * load plugin models with suport to JSON and .js file formats 328 | */ 329 | Plugin.prototype.loadModels = function loadModels (done) { 330 | fs.readdir(this.modelsPath, (e, fileNames) => { 331 | if (e) { 332 | if (e.code !== 'ENOENT') return done(e); 333 | return done(); 334 | } 335 | 336 | let name; 337 | 338 | fileNames.forEach(fileName => { 339 | if (fileName.endsWith('.js')) { 340 | // js model 341 | name = fileName.slice(0, -3); 342 | we.db.modelsConfigs[name] = require(path.join( this.modelsPath, fileName) )(we); 343 | } else if (fileName.endsWith('.json')) { 344 | // json model 345 | name = fileName.slice(0, -5); 346 | we.db.modelsConfigs[name] = 347 | we.db.defineModelFromJson( require(path.join(this.modelsPath, fileName)), we); 348 | } 349 | }); 350 | 351 | done(); 352 | }); 353 | }; 354 | 355 | /** 356 | * Load route resources from folder server/resources 357 | */ 358 | Plugin.prototype.loadResources = function loadResources (cb) { 359 | fs.readdir(this.resourcesPath, (err, list) => { 360 | if (err) { 361 | if (err.code === 'ENOENT') return cb(); 362 | return cb(err); 363 | } 364 | 365 | list 366 | .map(item => { return path.join(this.resourcesPath, item); }) 367 | .forEach(p => { this.setResource( require(p) ); }); 368 | 369 | cb(); 370 | }); 371 | }; 372 | 373 | /** 374 | * Load routes from folder server/routes 375 | */ 376 | Plugin.prototype.loadRoutes = function loadRoutes (cb) { 377 | fs.readdir(this.routesPath, (err, list) => { 378 | if (err) { 379 | if (err.code === 'ENOENT') return cb(); 380 | return cb(err); 381 | } 382 | 383 | list 384 | .map(item => { return path.join(this.routesPath, item); }) 385 | .forEach(p => { this.setRoutes( require(p) ); }); 386 | 387 | cb(); 388 | }); 389 | }; 390 | 391 | // -- Add css and js to we-plugin-view assets feature 392 | 393 | Plugin.prototype.addJs = function addJs (fileName, cfg) { 394 | this.assets.js[fileName] = cfg; 395 | }; 396 | 397 | Plugin.prototype.addCss = function addCss (fileName, cfg) { 398 | this.assets.css[fileName] = cfg; 399 | }; 400 | 401 | return Plugin; 402 | }; -------------------------------------------------------------------------------- /test/tests/requests/resource_jsonAPI.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'), 2 | request = require('supertest'), 3 | helpers = require('we-test-tools').helpers, 4 | Chance = require('chance'), 5 | chance = new Chance(); 6 | 7 | let _, http, we; 8 | 9 | function postStub (creatorId, jsonAPI) { 10 | if (jsonAPI) { 11 | return { 12 | data: { 13 | attributes: { 14 | title: chance.sentence({words: 5}), 15 | text: chance.paragraph(), 16 | creatorId: creatorId 17 | }, 18 | relationships: { 19 | creator: [ { id: creatorId, type: 'user '}] 20 | } 21 | } 22 | }; 23 | } else { 24 | return { 25 | title: chance.sentence({words: 5}), 26 | text: chance.paragraph(), 27 | creatorId: creatorId, 28 | tags: [{ 29 | text: chance.word(), 30 | }, { 31 | text: chance.word() 32 | }] 33 | }; 34 | } 35 | } 36 | 37 | describe('resourceRequests_jsonAPI', function() { 38 | let su; 39 | 40 | before(function (done) { 41 | http = helpers.getHttp(); 42 | we = helpers.getWe(); 43 | _ = we.utils._; 44 | return done(); 45 | }); 46 | 47 | before(function(done) { 48 | we.db.models.user.create({ 49 | displayName: 'Testonildo' 50 | }) 51 | .then(function(u) { 52 | su = u; 53 | return u; 54 | }) 55 | .nodeify(done); 56 | }); 57 | 58 | afterEach(function(done) { 59 | const models = we.db.models; 60 | 61 | Promise.all([ 62 | models.tag.destroy({truncate: true}), 63 | models.post.destroy({truncate: true}) 64 | ]) 65 | .then(function() { 66 | done(); 67 | }) 68 | .catch(done); 69 | }); 70 | 71 | describe('json', function() { 72 | describe('GET /post', function() { 73 | it ('should get posts list formated with jsonAPI', function (done) { 74 | let posts = [ 75 | postStub(su.id), 76 | postStub(su.id), 77 | postStub(su.id) 78 | ]; 79 | 80 | let postsByTitle = {}; 81 | posts.forEach(function(p){ 82 | postsByTitle[p.title] = p; 83 | }); 84 | 85 | we.utils.async.eachSeries(posts, function(p, next) { 86 | we.db.models.post 87 | .create(p, { 88 | include: [{ 89 | model: we.db.models.tag, 90 | as: 'tags' 91 | }] 92 | }) 93 | .then(function() { 94 | next(); 95 | return null; 96 | }) 97 | .catch(next); 98 | }, function(err) { 99 | if (err) return done(err); 100 | 101 | request(http) 102 | .get('/post') 103 | .set('Content-Type', 'application/vnd.api+json') 104 | .set('Accept', 'application/vnd.api+json') 105 | .expect(200) 106 | .end(function (err, res) { 107 | if (err) { 108 | console.log('res.body>', res.body); 109 | throw err; 110 | } 111 | 112 | assert(res.body.data); 113 | assert(_.isArray(res.body.data) ); 114 | 115 | res.body.data.forEach(function (p) { 116 | 117 | assert(p.relationships); 118 | assert(p.attributes); 119 | assert(p.id); 120 | 121 | let pn = postsByTitle[p.attributes.title]; 122 | assert(pn); 123 | 124 | assert(p.id, pn.id); 125 | 126 | assert.equal(p.attributes.title, pn.title); 127 | assert.equal(p.attributes.text, pn.text); 128 | 129 | assert(p.relationships); 130 | assert(p.relationships.tags); 131 | assert.equal(p.relationships.tags.data.length, 2); 132 | }); 133 | 134 | assert.equal(res.body.meta.count, 3); 135 | 136 | done(); 137 | }); 138 | }); 139 | 140 | }); 141 | 142 | it ('should search for posts by title', function (done) { 143 | let posts = [ 144 | postStub(), 145 | postStub(), 146 | postStub() 147 | ]; 148 | 149 | we.db.models.post.bulkCreate(posts) 150 | .spread(function(){ 151 | request(http) 152 | .get('/post?title='+posts[1].title) 153 | .set('Content-Type', 'application/vnd.api+json') 154 | .set('Accept', 'application/vnd.api+json') 155 | .expect(200) 156 | .end(function (err, res) { 157 | if (err) throw err; 158 | 159 | assert.equal(res.body.data.length, 1); 160 | assert(res.body.data[0].id, posts[1].title); 161 | assert(res.body.data[0].type, 'post'); 162 | assert.equal(res.body.data[0].attributes.title, posts[1].title); 163 | assert.equal(res.body.data[0].attributes.text, posts[1].text); 164 | 165 | assert.equal(res.body.meta.count, 1); 166 | 167 | done(); 168 | }); 169 | }).catch(done); 170 | }); 171 | 172 | 173 | it ('should search for posts by text', function (done) { 174 | let posts = [ 175 | postStub(), 176 | postStub(), 177 | postStub(), 178 | postStub() 179 | ]; 180 | 181 | let searchText = ' mussum ipsum'; 182 | 183 | posts[1].text += searchText; 184 | posts[2].text += searchText; 185 | 186 | we.db.models.post.bulkCreate(posts) 187 | .spread(function(){ 188 | request(http) 189 | .get('/post?text='+searchText) 190 | .set('Content-Type', 'application/vnd.api+json') 191 | .set('Accept', 'application/vnd.api+json') 192 | .expect(200) 193 | .end(function (err, res) { 194 | if (err) throw err; 195 | 196 | assert.equal(res.body.data.length, 2); 197 | 198 | res.body.data.forEach(function(p){ 199 | assert.equal(p.type, 'post') 200 | assert(p.attributes.text.indexOf(searchText) >-1) 201 | }) 202 | 203 | assert.equal(res.body.meta.count, 2) 204 | 205 | done() 206 | }); 207 | }) 208 | .catch(done) 209 | }); 210 | 211 | it ('should search for posts by text with and and inTitleAndText, orWithComaParser search in q param', function (done) { 212 | let posts = [ 213 | postStub(), 214 | postStub(), 215 | postStub(), 216 | postStub() 217 | ]; 218 | 219 | let searchText = ' mussum ipsum'; 220 | let searchText2 = '2222m ipsum'; 221 | 222 | posts[1].title = searchText; 223 | posts[1].text = searchText; 224 | 225 | posts[2].title = searchText2; 226 | posts[2].text = searchText2; 227 | 228 | we.db.models.post.bulkCreate(posts) 229 | .spread(function(){ 230 | request(http) 231 | .get('/post?q='+searchText+','+searchText2) 232 | .set('Content-Type', 'application/vnd.api+json') 233 | .set('Accept', 'application/vnd.api+json') 234 | .expect(200) 235 | .end(function (err, res) { 236 | if (err) { 237 | console.log('res.text>', res.text) 238 | throw err 239 | } 240 | 241 | assert.equal(res.body.data.length, 2); 242 | assert.equal(res.body.meta.count, 2); 243 | 244 | done(); 245 | }); 246 | }).catch(done); 247 | }); 248 | }); 249 | 250 | describe('GET /post/:id', function(){ 251 | it ('should get one post', function (done) { 252 | we.db.models.post.create(postStub(su.id)) 253 | .then(function (p) { 254 | request(http) 255 | .get('/post/'+p.id) 256 | .set('Content-Type', 'application/vnd.api+json') 257 | .set('Accept', 'application/vnd.api+json') 258 | .expect(200) 259 | .end(function (err, res) { 260 | if (err) throw err; 261 | 262 | assert(res.body.data.id); 263 | assert.equal(res.body.data.id, p.id); 264 | assert.equal(res.body.data.type, 'post'); 265 | 266 | assert.equal(res.body.data.attributes.title, p.title); 267 | assert.equal(res.body.data.attributes.text, p.text); 268 | 269 | done(); 270 | }); 271 | return null; 272 | }) 273 | .catch(done); 274 | }); 275 | 276 | it ('should return 404 to not found', function (done) { 277 | let info = we.log.info; 278 | we.log.info = function() {}; 279 | request(http) 280 | .get('/post/1232131') 281 | .expect(404) 282 | .end(function (err, res) { 283 | if (err) throw err; 284 | 285 | assert(!res.body.post); 286 | we.log.info = info; 287 | done(); 288 | }); 289 | }); 290 | }); 291 | 292 | describe('PUT /post/:id', function(){ 293 | it ('should update one post attr', function (done) { 294 | we.db.models.post.create(postStub()) 295 | .then(function (p) { 296 | 297 | let updateData = { 298 | data: { 299 | attributes: { 300 | title: 'iIIeeei' 301 | } 302 | } 303 | }; 304 | request(http) 305 | .put('/post/'+p.id) 306 | .send(updateData) 307 | .set('Content-Type', 'application/vnd.api+json') 308 | .set('Accept', 'application/vnd.api+json') 309 | .expect(200) 310 | .end(function (err, res) { 311 | if (err) throw err; 312 | 313 | assert(res.body.data.id); 314 | assert.equal(res.body.data.attributes.title, updateData.data.attributes.title); 315 | assert.equal(res.body.data.attributes.text, p.text); 316 | 317 | done(); 318 | }); 319 | return null; 320 | }) 321 | .catch(done); 322 | }); 323 | }); 324 | 325 | describe('DELETE /post/:id', function(){ 326 | it ('should delete one post', function (done) { 327 | we.db.models.post.create(postStub()) 328 | .then(function (p) { 329 | request(http) 330 | .delete('/post/'+p.id) 331 | .set('Content-Type', 'application/vnd.api+json') 332 | .set('Accept', 'application/vnd.api+json') 333 | .expect(204) 334 | .end(function (err, res) { 335 | if (err) throw err; 336 | 337 | assert(!res.text); 338 | 339 | we.db.models.post.findById(p.id) 340 | .then(function(ps){ 341 | assert(!ps); 342 | done(); 343 | return null; 344 | }) 345 | .catch(done); 346 | }); 347 | return null; 348 | }) 349 | .catch(done); 350 | }); 351 | }); 352 | 353 | describe('POST /post', function(){ 354 | it ('should create one resource with valid data', function (done) { 355 | let p = postStub(null, true); 356 | request(http) 357 | .post('/post') 358 | .send(p) 359 | .set('Content-Type', 'application/vnd.api+json') 360 | .set('Accept', 'application/vnd.api+json') 361 | .expect(201) 362 | .end(function (err, res) { 363 | if (err) throw err; 364 | 365 | assert(res.body.data.id); 366 | assert.equal(res.body.data.type, 'post'); 367 | assert.equal(res.body.data.attributes.title, p.data.attributes.title); 368 | assert.equal(res.body.data.attributes.text, p.data.attributes.text); 369 | 370 | done(); 371 | }); 372 | }); 373 | 374 | it ('should create one resource with valid data and JSONApi post data', function (done) { 375 | let p = postStub(null, true); 376 | request(http) 377 | .post('/post') 378 | .send(p) 379 | .set('Content-Type', 'application/vnd.api+json') 380 | .set('Accept', 'application/vnd.api+json') 381 | .expect(201) 382 | .end(function (err, res) { 383 | if (err) { 384 | console.log('res.text>', res.text) 385 | throw err 386 | } 387 | 388 | assert(res.body.data.id); 389 | assert.equal(res.body.data.type, 'post'); 390 | assert.equal(res.body.data.attributes.title, p.data.attributes.title); 391 | assert.equal(res.body.data.attributes.text, p.data.attributes.text); 392 | 393 | done(); 394 | }); 395 | }); 396 | 397 | it ('should return error if not set an not null attr', function (done) { 398 | let p = postStub(null, true); 399 | p.data.attributes.title = null; 400 | 401 | request(http) 402 | .post('/post') 403 | .send(p) 404 | .set('Content-Type', 'application/vnd.api+json') 405 | .set('Accept', 'application/vnd.api+json') 406 | .expect(400) 407 | .end(function (err, res) { 408 | if (err) throw err; 409 | 410 | assert(!res.body.data.id); 411 | assert.equal(res.body.errors[0].status, 400); 412 | assert.equal(res.body.errors[0].title, 'post.title cannot be null'); 413 | 414 | // assert.equal(res.body.data.attributes.title, p.data.attributes.title); 415 | // assert.equal(res.body.data.attributes.text, p.data.attributes.text); 416 | 417 | done(); 418 | }); 419 | }); 420 | }); 421 | }); 422 | }); 423 | --------------------------------------------------------------------------------