├── .env ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docker-compose.dev.yml ├── gulpfile.js ├── lib ├── dialects │ ├── custom-datatypes.js │ ├── mysql.js │ └── postgres.js ├── sequelize-models.js └── utils.js ├── migrations_data ├── mysql │ ├── 20160222014049-init-migration.js │ └── 20160222015937-populate-migration.js └── postgres │ ├── 20160222014049-init-migration.js │ └── 20160222015937-populate-migration.js ├── mysql.conf ├── package-lock.json ├── package.json └── test ├── mysql ├── config.js ├── models │ └── User.js └── mysql.js ├── psql ├── config.js ├── models │ └── User.js └── psql.js └── utils.js /.env: -------------------------------------------------------------------------------- 1 | POSTGRES_DB=sequelize_models_db 2 | POSTGRES_USER=sequelize_models_db_user 3 | 4 | MYSQL_DATABASE=sequelize_models_db 5 | MYSQL_ALLOW_EMPTY_PASSWORD='yes' 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .npmignore 4 | .DS_Store 5 | coverage 6 | .vscode 7 | config 8 | .idea 9 | docs 10 | migrations 11 | yarn-error.log 12 | yarn.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v8 4 | - v6 5 | - v5 6 | - v4 7 | 8 | sudo: false 9 | 10 | services: 11 | - docker 12 | 13 | before_install: 14 | - docker-compose -f ./docker-compose.dev.yml up -d 15 | 16 | cache: 17 | directories: 18 | - node_modules 19 | 20 | before_script: 21 | - "gulp config-mysql" 22 | - "./node_modules/sequelize-cli/bin/sequelize db:migrate" 23 | - "gulp config-psql" 24 | - "./node_modules/sequelize-cli/bin/sequelize db:migrate" 25 | 26 | script: "gulp" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Gonzalo Bahamondez (https://github.com/gbahamondezc) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sequelize-models 2 | [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage Status](https://coveralls.io/repos/github/gbahamondezc/sequelize-models/badge.svg?branch=master)](https://coveralls.io/github/gbahamondezc/sequelize-models?branch=master) [![bitHound Code](https://www.bithound.io/github/gbahamondezc/sequelize-models/badges/code.svg)](https://www.bithound.io/github/gbahamondezc/sequelize-models) 3 | 4 | Node.js SequelizeJS ORM model utilities. 5 | 6 | sequelize-models will try to load all your database tables and associations as Sequelize JS models automatically, but if you want define your models explicitly, just need to create a model file and sequelize-models will skip the models automatic definition for that table and will use your model file, use models.path to specify the models directory to read. 7 | 8 | 9 | ### Warnings 10 | **Node.js 4.0** or latest is required to use this module. 11 | 12 | **Sequelize Models Version 1.4.0** uses SequelizeJS **4.37.x which is the current stable version** 13 | 14 | sequelize-models is a bit old project, and i'm planning rewrite from scratch to use the latest language features, feel free to suggest features creating a issue with the respective description. 15 | 16 | 17 | 18 | ### Installation 19 | 20 | ```sh 21 | $ npm install --save sequelize-models 22 | 23 | # MySQL 24 | $ npm install --save mysql2 25 | 26 | # PostgreSQL 27 | $ npm install --save pg 28 | $ npm install --save pg-hstore 29 | ``` 30 | 31 | ### Features 32 | 33 | * Auto load of Sequelize models from database schema. 34 | 35 | * Auto load models associations from database schema. 36 | 37 | * Simplified and meaningful model files syntax. 38 | 39 | * One place models and associations definitions. 40 | 41 | * MySQL and PSQL support for now (support for MSSQL as soon as possible). 42 | 43 | 44 | ### Usage 45 | 46 | Config and get schema 47 | 48 | ```js 49 | 50 | const SequelizeModels = require("sequelize-models"); 51 | 52 | var seqModels = new SequelizeModels({ 53 | // Database connection options 54 | connection : { 55 | host : "127.0.0.1", 56 | dialect : "mysql", 57 | username : "root", 58 | schema : "sequelize_test", 59 | password : "" 60 | }, 61 | 62 | // Models loading options 63 | models : { 64 | autoLoad : true, 65 | path : "/models" 66 | }, 67 | 68 | // Sequelize options passed directly to Sequelize constructor 69 | sequelizeOptions : { 70 | define : { 71 | freezeTableName : true, 72 | underscored : true 73 | } 74 | } 75 | }); 76 | 77 | 78 | seqModels.getSchema().then( schema => { 79 | // schema.models and schema.db available here 80 | }) 81 | .catch( err => { 82 | // throwing error out of the promise 83 | setTimeout( () => { throw err }); 84 | }); 85 | ``` 86 | 87 | Model Definition , file **models/User.js** 88 | 89 | ```js 90 | module.exports = { 91 | 92 | // Following http://docs.sequelizejs.com/en/latest/docs/models-definition/ 93 | tableName : "user", 94 | 95 | attributes : { 96 | name : { 97 | type : "string" 98 | }, 99 | last_name : { 100 | type : "string" 101 | }, 102 | born_date : { 103 | type : "date" 104 | } 105 | }, 106 | 107 | 108 | // Associations -> http://docs.sequelizejs.com/en/latest/docs/scopes/#associations 109 | associations : [{ 110 | type : "belongsTo", 111 | target : "Profile", 112 | options : { 113 | foreignKey : "profile_id" 114 | } 115 | }], 116 | 117 | validate : {}, 118 | indexes : [] 119 | }; 120 | ``` 121 | 122 | 123 | ### Contributing 124 | Feel free to submit a PR or create an issue for any bug fixes or feature requests, just remember if you add new features or fix a bug, **please provide the respective tests for the case**. 125 | 126 | 127 | ### Build and open code documentation 128 | ```bash 129 | $ npm install -g gulp && gulp docs 130 | ``` 131 | 132 | 133 | ### Run Tests 134 | You need edit **test/mysql/config.js** and **test/psql/config.js** with your own databases connection params, before run the steps below which are assuming that you will create a database with the name sequelize_test on each database. 135 | 136 | ```bash 137 | $ npm install gulp -g && npm install 138 | 139 | # Create and start development docker databases (needs docker & docker-compose installed) 140 | $ npm run db:up 141 | 142 | # create mysql config files 143 | $ gulp config-mysql 144 | 145 | # test data for mysql 146 | $ ./node_modules/sequelize-cli/bin/sequelize db:migrate 147 | 148 | # create psql config files 149 | $ gulp config-psql 150 | 151 | # test data for postgres 152 | $ ./node_modules/sequelize-cli/bin/sequelize db:migrate 153 | 154 | # run test 155 | $ gulp test 156 | ``` 157 | 158 | 159 | ## License 160 | MIT 161 | 162 | [npm-image]: https://badge.fury.io/js/sequelize-models.svg 163 | [npm-url]: https://npmjs.org/package/sequelize-models 164 | [travis-image]: https://travis-ci.org/gbahamondezc/sequelize-models.svg?branch=master 165 | [travis-url]: https://travis-ci.org/gbahamondezc/sequelize-models 166 | [daviddm-image]: https://david-dm.org/gbahamondezc/sequelize-models.svg?theme=shields.io 167 | [daviddm-url]: https://david-dm.org/gbahamondezc/sequelize-models 168 | [coveralls-image]: https://coveralls.io/repos/gbahamondezc/sequelize-models/badge.svg 169 | [coveralls-url]: https://coveralls.io/r/gbahamondezc/sequelize-models 170 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL test database 2 | postgres: 3 | container_name: sequelize_models_pg 4 | image: postgres 5 | env_file: 6 | - ./.env 7 | ports: 8 | - "5438:5432" 9 | 10 | 11 | # MySQL test database 12 | mysql: 13 | container_name: sequelize_models_mysql 14 | image: mysql:5.6 15 | volumes: 16 | - ./mysql.conf:/etc/my.cnf 17 | env_file: 18 | - ./.env 19 | ports: 20 | - "3308:3306" -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var path = require("path"); 4 | var gulp = require("gulp"); 5 | var eslint = require("gulp-eslint"); 6 | var excludeGitignore = require("gulp-exclude-gitignore"); 7 | var mocha = require("gulp-mocha"); 8 | var istanbul = require("gulp-istanbul"); 9 | var nsp = require("gulp-nsp"); 10 | var plumber = require("gulp-plumber"); 11 | var coveralls = require("gulp-coveralls"); 12 | var open = require("gulp-open"); 13 | var shell = require("gulp-shell"); 14 | var sequence = require("run-sequence"); 15 | var configMySQL = require("./test/mysql/config.js"); 16 | var configPSQL = require("./test/psql/config.js"); 17 | var Gutil = require("gulp-util"); 18 | var del = require("del"); 19 | 20 | 21 | function stringSrc (filename, string) { 22 | var src = require("stream").Readable({ objectMode : true }); 23 | src._read = function () { 24 | this.push( new Gutil.File({ 25 | cwd : "", 26 | base : "", 27 | path : filename, 28 | contents : new Buffer(string) 29 | })); 30 | this.push(null); 31 | }; 32 | return src; 33 | } 34 | 35 | gulp.task("clean-migrations", function() { 36 | del("./migrations/*.js"); 37 | }); 38 | 39 | gulp.task("cp-config-psql", function() { 40 | gulp.src("./migrations_data/postgres/**/*.js") 41 | .pipe(gulp.dest("./migrations")); 42 | }); 43 | 44 | 45 | gulp.task("cp-config-mysql", function() { 46 | gulp.src("./migrations_data/mysql/**/*.js") 47 | .pipe(gulp.dest("./migrations")); 48 | }); 49 | 50 | 51 | gulp.task("config-mysql", ["clean-migrations", "cp-config-mysql"], function() { 52 | var jsonObject = { 53 | development : { 54 | username : configMySQL.connection.username, 55 | password : configMySQL.connection.password, 56 | database : configMySQL.connection.schema, 57 | host : configMySQL.connection.host, 58 | dialect : configMySQL.connection.dialect, 59 | port: 3308 60 | } 61 | }; 62 | var jsString = JSON.stringify(jsonObject, null, 2); 63 | return stringSrc("config.json", jsString) 64 | .pipe( gulp.dest("./config/")); 65 | }); 66 | 67 | 68 | gulp.task("config-psql", ["clean-migrations", "cp-config-psql"], function() { 69 | var jsonObject = { 70 | development : { 71 | username : configPSQL.connection.username, 72 | password : configPSQL.connection.password, 73 | database : configPSQL.connection.schema, 74 | host : configPSQL.connection.host, 75 | dialect : configPSQL.connection.dialect, 76 | port: 5438 77 | } 78 | }; 79 | var jsString = JSON.stringify(jsonObject, null, 2); 80 | return stringSrc("config.json", jsString) 81 | .pipe( gulp.dest("./config/")); 82 | }); 83 | 84 | 85 | gulp.task("lint", function () { 86 | return gulp.src("**/*.js") 87 | .pipe( excludeGitignore()) 88 | .pipe( eslint()) 89 | .pipe( eslint.format()) 90 | .pipe( eslint.failAfterError()); 91 | }); 92 | 93 | 94 | gulp.task("docs:generate", shell.task([ 95 | "./node_modules/jsdoc/jsdoc.js --recurse \ 96 | ./lib ./Readme.md -t ./node_modules/minami/ -d ./docs" 97 | ])); 98 | 99 | 100 | gulp.task("docs:open", function () { 101 | gulp.src("./docs/index.html").pipe( open() ); 102 | }); 103 | 104 | 105 | gulp.task("docs", function () { 106 | sequence("docs:generate", "docs:open"); 107 | }); 108 | 109 | 110 | gulp.task("nsp", function (cb) { 111 | nsp({package: path.resolve("package.json")}, cb); 112 | }); 113 | 114 | 115 | gulp.task("pre-test", function () { 116 | return gulp.src("lib/**/*.js") 117 | .pipe( excludeGitignore()) 118 | .pipe( istanbul({ 119 | includeUntested : true 120 | })) 121 | .pipe( istanbul.hookRequire()); 122 | }); 123 | 124 | 125 | gulp.task("test", [ "pre-test" ], function (cb) { 126 | var mochaErr; 127 | 128 | gulp.src("test/**/*.js") 129 | .pipe( plumber()) 130 | .pipe( mocha( { reporter: "spec" } )) 131 | .on("error", function (err) { 132 | mochaErr = err; 133 | }) 134 | .pipe( istanbul.writeReports()) 135 | .on( "end", function () { 136 | cb( mochaErr ); 137 | }); 138 | }); 139 | 140 | 141 | gulp.task("watch", function () { 142 | gulp.watch([ "lib/**/*.js", "test/**" ], [ "test" ]); 143 | }); 144 | 145 | 146 | gulp.task("coveralls", [ "test" ], function () { 147 | if (!process.env.CI) { 148 | return; 149 | } 150 | return gulp.src( path.join( __dirname, "coverage/lcov.info")) 151 | .pipe( coveralls()); 152 | }); 153 | 154 | gulp.task("prepublish", [ "nsp" ]); 155 | gulp.task("default", [ "lint", "test", "coveralls" ]); 156 | -------------------------------------------------------------------------------- /lib/dialects/custom-datatypes.js: -------------------------------------------------------------------------------- 1 | // MySQL TimeStamp DataType 2 | var BaseTypes = require("sequelize/lib/data-types"); 3 | var util = require("util"); 4 | var TIMESTAMP = function () { 5 | if (!(this instanceof TIMESTAMP)) { 6 | return new TIMESTAMP(); 7 | } 8 | BaseTypes.ABSTRACT.apply(this, arguments); 9 | }; 10 | util.inherits(TIMESTAMP, BaseTypes.ABSTRACT); 11 | TIMESTAMP.prototype.key = TIMESTAMP.key = "TIMESTAMP"; 12 | exports.TIMESTAMP = TIMESTAMP; 13 | -------------------------------------------------------------------------------- /lib/dialects/mysql.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const debug = require("debug")("sequelize-models:mysql"); 4 | const _ = require("lodash"); 5 | const co = require("co"); 6 | const utils = require("../utils.js"); 7 | const CustomTypes = require("./custom-datatypes.js"); 8 | 9 | /** 10 | * MySQL adapter 11 | */ 12 | 13 | class MySQLDialect { 14 | 15 | 16 | /** 17 | * MySQL Adapter constructor 18 | * @param {Object} Sequelize a sequelize js instance 19 | * @param {Object} connection a db connections created with sequelize 20 | * @param {Object} options a set of configuration options 21 | * @constructor 22 | */ 23 | 24 | constructor(Sequelize, connection, opts) { 25 | 26 | debug("constructor"); 27 | 28 | this.models = {}; 29 | this.config = opts; 30 | 31 | this.typeMap = { 32 | varchar: Sequelize.STRING, 33 | text: Sequelize.TEXT, 34 | int: Sequelize.INTEGER, 35 | datetime: Sequelize.DATE, 36 | decimal: Sequelize.DECIMAL, 37 | double: Sequelize.DOUBLE, 38 | string: Sequelize.STRING, 39 | date: Sequelize.DATE, 40 | integer: Sequelize.INTEGER, 41 | tinyint: Sequelize.BOOLEAN, 42 | timestamp: CustomTypes.TIMESTAMP, 43 | char: Sequelize.CHAR, 44 | float: Sequelize.FLOAT, 45 | time: Sequelize.TIME, 46 | smallint: Sequelize.INTEGER, 47 | mediumint: Sequelize.INTEGER, 48 | bigint: Sequelize.BIGINT, 49 | year: Sequelize.INTEGER, 50 | blob: Sequelize.INTEGER, 51 | tinyblob: Sequelize.INTEGER, 52 | enum: Sequelize.ENUM 53 | }; 54 | 55 | /** Sequelize connected instance */ 56 | this.connection = connection; 57 | 58 | /** MySQL query to get all table information */ 59 | this.tablesQuery = "SELECT a.table_name as name, a.column_name, \ 60 | data_type, column_key, extra, column_comment, table_comment \ 61 | FROM INFORMATION_SCHEMA.COLUMNS a \ 62 | INNER JOIN INFORMATION_SCHEMA.TABLES b \ 63 | ON a.TABLE_CATALOG = b.TABLE_CATALOG \ 64 | AND a.TABLE_SCHEMA = b.TABLE_SCHEMA \ 65 | AND a.TABLE_NAME = b.TABLE_NAME \ 66 | WHERE a.TABLE_SCHEMA = ?"; 67 | 68 | /** MySql query to get all relation information */ 69 | this.relationsQuery = "SELECT * FROM INFORMATION_SCHEMA.key_column_usage \ 70 | WHERE referenced_table_schema = ?"; 71 | 72 | this.indexQuery = "SELECT column_name, index_comment FROM INFORMATION_SCHEMA.STATISTICS \ 73 | WHERE seq_in_index = 1 AND TABLE_NAME = ?" 74 | } 75 | 76 | 77 | 78 | /** 79 | * Fetch data of tables, relations, and attributes from MySQL Schema. 80 | * @param {Array} modelDefs - model definitions obtained from the defined models path 81 | * @param {Boolean} autoLoad - flag which indicates if the schema should be read automatically or not 82 | * @return {Promise} a promise that will be resolved when all data is fetched. 83 | */ 84 | 85 | loadModels(modelDefs, autoload) { 86 | 87 | debug("loadModels %o", autoload); 88 | 89 | var _this = this; 90 | 91 | return co(function* () { 92 | 93 | var yieldable = { 94 | tables: [], 95 | associations: [] 96 | }; 97 | 98 | if (autoload) { 99 | yieldable.tables = _this.connection.query(_this.tablesQuery, { 100 | replacements: [_this.config.schema], 101 | type: _this.connection.QueryTypes.SELECT 102 | }); 103 | 104 | yieldable.associations = _this.connection.query(_this.relationsQuery, { 105 | replacements: [_this.config.schema], 106 | type: _this.connection.QueryTypes.SELECT 107 | }); 108 | } 109 | 110 | var schemaData = yield yieldable; 111 | 112 | 113 | /** Get schema tables and relations in parallel */ 114 | _this.models = _this.getModels(schemaData.tables, modelDefs); 115 | 116 | // associations from schema 117 | let belongsToMany = {}; 118 | 119 | schemaData.associations.forEach(association => { 120 | 121 | let modelName = utils.camelize(association.TABLE_NAME); 122 | let attribute = association.COLUMN_NAME; 123 | let referencedModelName = utils.camelize( 124 | association.REFERENCED_TABLE_NAME 125 | ); 126 | 127 | // let referencedAttribute = association["REFERENCED_COLUMN_NAME"]; 128 | 129 | let model = _this.models[modelName]; 130 | let refModel = _this.models[referencedModelName]; 131 | 132 | if (model.primaryKeyAttributes.length === 1) { 133 | 134 | model.belongsTo(refModel, { 135 | foreignKey: { name: attribute } 136 | }); 137 | 138 | refModel.hasMany( 139 | model 140 | ); 141 | 142 | } else { 143 | 144 | let def = { fk: attribute, pk: association.REFERENCED_COLUMN_NAME, model, refModel }; 145 | if (belongsToMany[association.TABLE_NAME]) { 146 | belongsToMany[association.TABLE_NAME].push(def); 147 | } else { 148 | belongsToMany[association.TABLE_NAME] = [def]; 149 | } 150 | 151 | } 152 | 153 | }); 154 | 155 | for (let tableName in belongsToMany) { 156 | let indexes = yield _this.connection.query(_this.indexQuery, { 157 | replacements: [tableName], 158 | type: _this.connection.QueryTypes.SELECT 159 | }); 160 | indexes = indexes.reduce((a, e) => { a[e.column_name] = e.index_comment; return a; }, {}); 161 | let lookupTable = belongsToMany[tableName]; 162 | lookupTable.forEach(foreign => { 163 | if (indexes.hasOwnProperty(foreign.fk)) { 164 | lookupTable.forEach(other => { 165 | if (foreign !== other) { 166 | 167 | let fkComment = foreign.model.attributes[other.fk].comment; 168 | try { 169 | fkComment = JSON.parse(fkComment); 170 | } catch (err) { 171 | fkComment = { as: fkComment }; 172 | } 173 | 174 | let names = [fkComment.as, other.refModel.name].filter(e => e); 175 | let name = null; 176 | let i = 1; 177 | while (true) { 178 | try { 179 | let as = names.length ? (name = names.shift()) : (name + i++); 180 | foreign.refModel.belongsToMany(other.refModel, { 181 | through: foreign.model, 182 | field: other.fk, 183 | foreignKey: { 184 | name: foreign.fk, 185 | fieldName: foreign.fk 186 | }, 187 | otherKey: { 188 | name: other.fk, 189 | fieldName: other.fk 190 | }, 191 | as 192 | }); 193 | break; 194 | } catch (err) { 195 | console.error(err); 196 | if (i > 20) { 197 | console.error("Too many errors"); 198 | break; 199 | } 200 | } 201 | } 202 | } 203 | }); 204 | } 205 | }); 206 | } 207 | 208 | // Associations from model file 209 | modelDefs.forEach(modelDef => { 210 | 211 | if (!modelDef.object.associations) { 212 | return; 213 | } 214 | 215 | modelDef.object.associations.forEach(assoc => { 216 | 217 | var mainModel = _this.models[modelDef.name]; 218 | var targetModel = _this.models[assoc.target]; 219 | 220 | if (!mainModel) { 221 | throw new Error("Model [" + modelDef.name + "] Not found."); 222 | } 223 | 224 | if (!targetModel) { 225 | throw new Error( 226 | "Target Model [" + assoc.target + "] Not found for association " + assoc.type + " with [" + 227 | modelDef.name + "] model." 228 | ); 229 | } 230 | 231 | _this.models[modelDef.name][assoc.type]( 232 | _this.models[assoc.target], 233 | assoc.options 234 | ); 235 | 236 | }); 237 | }); 238 | return _this.models; 239 | }); 240 | } 241 | 242 | 243 | /** 244 | * Build and return Sequelize Models instances from MySQL Database 245 | * @param {Array} tables - Array of tables => attributes obtained autmatically from the schema 246 | * @param {Array} modelDefs - model definitions obtained from the defined models path 247 | * @return {Object} a object with each model wich represent a database table (key name) 248 | */ 249 | 250 | getModels(tables, modelDefs) { 251 | 252 | debug("getModels"); 253 | var excludeTables = _.map(modelDefs, "object.tableName"); 254 | tables = utils.filterTables(tables, excludeTables); 255 | 256 | let tableNames = _.uniq(_.map(tables, "name")); 257 | let models = {}; 258 | 259 | // Direct from database schema 260 | tableNames.map(name => { 261 | 262 | let results = _.filter(tables, { name: name }); 263 | let attribs = {}; 264 | 265 | results.map(res => { 266 | attribs[res.column_name] = { 267 | type: this.typeMap[res.data_type], 268 | primaryKey: (res.column_key === "PRI"), 269 | autoIncrement: (res.extra === "auto_increment"), 270 | comment: res.column_comment 271 | }; 272 | }); 273 | 274 | models[utils.camelize(name)] = this.connection.define( 275 | name, attribs, { comment: results[0].table_comment } 276 | ); 277 | }); 278 | 279 | 280 | 281 | // From model files reading 282 | modelDefs.forEach(modelDef => { 283 | 284 | var attributes = Object.keys(modelDef.object.attributes); 285 | 286 | attributes.forEach(attr => { 287 | modelDef.object.attributes[attr].type = 288 | this.typeMap[modelDef.object.attributes[attr].type]; 289 | }); 290 | 291 | models[modelDef.name] = this.connection.define( 292 | modelDef.object.tableName, 293 | modelDef.object.attributes, { 294 | validate: modelDef.object.validate || {}, 295 | getterMethods: modelDef.object.getterMethods || {}, 296 | setterMethods: modelDef.object.setterMethods || {}, 297 | indexes: modelDef.object.indexes || [] 298 | } 299 | ); 300 | }); 301 | 302 | return models; 303 | } 304 | 305 | } 306 | 307 | 308 | /** Expose class */ 309 | module.exports = MySQLDialect; 310 | -------------------------------------------------------------------------------- /lib/dialects/postgres.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const debug = require("debug")("sequelize-models:psql"); 4 | const _ = require("lodash"); 5 | const co = require("co"); 6 | const utils = require("../utils.js"); 7 | 8 | /** 9 | * MySQL adapter 10 | */ 11 | 12 | class PSQLDialect { 13 | /** 14 | * MySQL Adapter constructor 15 | * @param {Object} Sequelize a sequelize js instance 16 | * @param {Object} connection a db connections created with sequelize 17 | * @param {Object} options a set of configuration options 18 | * @constructor 19 | */ 20 | 21 | constructor(Sequelize, connection, opts) { 22 | debug("constructor"); 23 | 24 | this.models = {}; 25 | this.config = opts; 26 | 27 | this.typeMap = { 28 | varchar: Sequelize.STRING, 29 | text: Sequelize.TEXT, 30 | int: Sequelize.INTEGER, 31 | datetime: Sequelize.DATE, 32 | decimal: Sequelize.DECIMAL, 33 | string: Sequelize.STRING, 34 | date: Sequelize.DATE, 35 | integer: Sequelize.INTEGER, 36 | "timestamp with time zone": Sequelize.DATE, 37 | "timestamp without time zone": Sequelize.DATE, 38 | "character varying": Sequelize.STRING, 39 | bigint: Sequelize.BIGINT, 40 | bit: Sequelize.INTEGER, 41 | boolean: Sequelize.BOOLEAN, 42 | character: Sequelize.CHAR, 43 | "double precision": Sequelize.DOUBLE, 44 | json: Sequelize.STRING, 45 | jsonb: Sequelize.JSONB, 46 | money: Sequelize.INTEGER, 47 | numeric: Sequelize.INTEGER, 48 | real: Sequelize.FLOAT, 49 | smallint: Sequelize.INTEGER, 50 | time: Sequelize.TIME, 51 | timestamp: Sequelize.DATE, 52 | uuid: Sequelize.UUID, 53 | xml: Sequelize.TEXT, 54 | array: Sequelize.ARRAY 55 | }; 56 | 57 | /** Sequelize connected instance */ 58 | this.connection = connection; 59 | 60 | /** MySQL query to get all table information */ 61 | this.tablesQuery = `SELECT table_name AS name, column_name, data_type, column_default as column_key 62 | FROM information_schema.columns WHERE table_schema = 'public'`; 63 | 64 | this.relationsQuery = `SELECT 65 | tc.constraint_name AS CONSTRAINT_NAME, 66 | tc.table_name AS TABLE_NAME, 67 | kcu.column_name AS COLUMN_NAME, 68 | ccu.table_name AS REFERENCED_TABLE_NAME, 69 | ccu.column_name AS REFERENCED_COLUMN_NAME 70 | FROM information_schema.table_constraints AS tc 71 | JOIN information_schema.key_column_usage AS kcu 72 | ON tc.constraint_name = kcu.constraint_name 73 | JOIN information_schema.constraint_column_usage AS ccu 74 | ON ccu.constraint_name = tc.constraint_name 75 | WHERE constraint_type = 'FOREIGN KEY'`; 76 | } 77 | 78 | /** 79 | * Fetch data of tables, relations, and attributes from MySQL Schema. 80 | * @param {Array} modelDefs - model definitions obtained from the defined models path 81 | * @param {Boolean} autoLoad - flag which indicates if the schema should be read automatically or not 82 | * @return {Promise} a promise that will be resolved when all data is fetched. 83 | */ 84 | 85 | loadModels(modelDefs, autoload) { 86 | debug("loadModels %o", autoload); 87 | 88 | var _this = this; 89 | 90 | return co(function* () { 91 | var yieldable = { 92 | tables: [], 93 | associations: [] 94 | }; 95 | 96 | if (autoload) { 97 | yieldable.tables = _this.connection.query(_this.tablesQuery, { 98 | replacements: [_this.config.schema], 99 | type: _this.connection.QueryTypes.SELECT 100 | }); 101 | 102 | yieldable.associations = _this.connection.query(_this.relationsQuery, { 103 | replacements: [_this.config.schema], 104 | type: _this.connection.QueryTypes.SELECT 105 | }); 106 | } 107 | 108 | var schemaData = yield yieldable; 109 | 110 | /** Get schema tables and relations in parallel */ 111 | _this.models = _this.getModels(schemaData.tables, modelDefs); 112 | 113 | // associations from schema 114 | schemaData.associations.forEach(association => { 115 | let modelName = utils.camelize(association.table_name); 116 | let attribute = association.column_name; 117 | let referencedModelName = utils.camelize( 118 | association.referenced_table_name 119 | ); 120 | 121 | // let referencedAttribute = association["REFERENCED_COLUMN_NAME"]; 122 | 123 | _this.models[modelName].belongsTo(_this.models[referencedModelName], { 124 | foreignKey: { name: attribute } 125 | }); 126 | 127 | _this.models[referencedModelName].hasMany(_this.models[modelName]); 128 | }); 129 | 130 | // Associations from model file 131 | modelDefs.forEach(modelDef => { 132 | if (!modelDef.object.associations) { 133 | return; 134 | } 135 | 136 | modelDef.object.associations.forEach(assoc => { 137 | var mainModel = _this.models[modelDef.name]; 138 | var targetModel = _this.models[assoc.target]; 139 | 140 | if (!mainModel) { 141 | throw new Error("Model [" + modelDef.name + "] Not found."); 142 | } 143 | 144 | if (!targetModel) { 145 | throw new Error( 146 | "Target Model [" + 147 | assoc.target + 148 | "] Not found for association " + 149 | assoc.type + 150 | " with [" + 151 | modelDef.name + 152 | "] model." 153 | ); 154 | } 155 | 156 | _this.models[modelDef.name][assoc.type]( 157 | _this.models[assoc.target], 158 | assoc.options 159 | ); 160 | }); 161 | }); 162 | return _this.models; 163 | }); 164 | } 165 | 166 | /** 167 | * Build and return Sequelize Models instances from MySQL Database 168 | * @param {Array} tables - Array of tables => attributes obtained autmatically from the schema 169 | * @param {Array} modelDefs - model definitions obtained from the defined models path 170 | * @return {Object} a object with each model wich represent a database table (key name) 171 | */ 172 | 173 | getModels(tables, modelDefs) { 174 | debug("getModels"); 175 | var excludeTables = _.map(modelDefs, "object.tableName"); 176 | tables = utils.filterTables(tables, excludeTables); 177 | 178 | let tableNames = _.uniq(_.map(tables, "name")); 179 | let models = {}; 180 | 181 | // Direct from database schema 182 | tableNames.map(name => { 183 | let results = _.filter(tables, { name: name }); 184 | let attribs = {}; 185 | if (name !== "pg_stat_statements") { 186 | results.map(res => { 187 | attribs[res.column_name] = { 188 | type: this.typeMap[res.data_type], 189 | primaryKey: res.column_key && 190 | res.column_key.indexOf("nextval") !== -1, 191 | autoIncrement: (res.column_key && res.column_key.indexOf("nextval") !== -1) 192 | }; 193 | }); 194 | } 195 | models[utils.camelize(name)] = this.connection.define(name, attribs); 196 | }); 197 | 198 | // From model files reading 199 | modelDefs.forEach(modelDef => { 200 | var attributes = Object.keys(modelDef.object.attributes); 201 | 202 | attributes.forEach(attr => { 203 | const TYPE = modelDef.object.attributes[attr].type; 204 | 205 | if (TYPE.match(/array/)) { 206 | const type = TYPE.split("|")[1]; 207 | 208 | modelDef.object.attributes[attr].type = this.typeMap["array"]( 209 | this.typeMap[type] 210 | ); 211 | } else { 212 | modelDef.object.attributes[attr].type = this.typeMap[ 213 | modelDef.object.attributes[attr].type 214 | ]; 215 | } 216 | }); 217 | 218 | models[modelDef.name] = this.connection.define( 219 | modelDef.object.tableName, 220 | modelDef.object.attributes, 221 | { 222 | validate: modelDef.object.validate || {}, 223 | getterMethods: modelDef.object.getterMethods || {}, 224 | setterMethods: modelDef.object.setterMethods || {}, 225 | indexes: modelDef.object.indexes || [] 226 | } 227 | ); 228 | }); 229 | 230 | return models; 231 | } 232 | } 233 | 234 | /** Expose class */ 235 | module.exports = PSQLDialect; 236 | -------------------------------------------------------------------------------- /lib/sequelize-models.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const debug = require("debug")("sequelize-models"); 4 | const Sequelize = require("sequelize"); 5 | const _ = require("lodash"); 6 | const path = require("path"); 7 | const co = require("co"); 8 | const Promise = require("bluebird"); 9 | 10 | const utils = require("./utils.js"); 11 | const fs = require("fs"); 12 | 13 | const readdir = Promise.promisify( 14 | require("readdir-plus") 15 | ); 16 | 17 | 18 | /** 19 | * SequelizeModels 20 | */ 21 | 22 | class SequelizeModels { 23 | 24 | /** 25 | * SequelizeModels constructor. 26 | * @constructor 27 | * @param {Object} opts - configuration options 28 | * @param {Object} opts.connection - database connections params 29 | * @param {Srtring} opts.connection.host - hostname or ipadress of the server runing database instance 30 | * @param {Srtring} opts.connection.dialect - dialect of database engine to use 31 | * @param {Srtring} opts.connection.username - username to connect with the database instance 32 | * @param {Srtring} opts.connection.password - password to connect with the database instance 33 | * @param {Srtring} opts.connection.schema - database schema name to use with the connection 34 | * @param {Srtring} opts.connection.port - database instance server running port default 3306 35 | * @param {Object} opts.models - options to models definition 36 | * @param {String} opts.models.path - path to read the model definitions 37 | * @param {Boolean} opts.models.autoLoad - enable or disable autoloading models from database 38 | * @param {Object} opts.sequelizeOptions - parameters that will be passed directly to Sequelize 39 | * constructor 40 | */ 41 | 42 | constructor(opts) { 43 | 44 | debug("constructor %o", opts); 45 | this.opts = opts || {}; 46 | 47 | if (!this.opts.connection.port) { 48 | this.opts.connection.port = 3306; 49 | } 50 | if (!this.opts.connection) { 51 | throw new Error("Connection configuration is required"); 52 | } 53 | if (!this.opts.connection.host) { 54 | throw new Error("Connection host is required"); 55 | } 56 | if (!this.opts.connection.dialect) { 57 | throw new Error("Connection dialect is required"); 58 | } 59 | if (!this.opts.connection.username) { 60 | throw new Error("Connection username is required"); 61 | } 62 | if (undefined === this.opts.connection.password) { 63 | throw new Error("Connection password is required"); 64 | } 65 | if (!this.opts.connection.schema) { 66 | throw new Error("Connection schema name is required"); 67 | } 68 | if ("mysql" !== this.opts.connection.dialect.toLowerCase() && 69 | "postgres" !== this.opts.connection.dialect.toLowerCase()) { 70 | throw new Error("Only MySQL and PSQL dialects are supported"); 71 | } 72 | } 73 | 74 | 75 | /** 76 | * Get Sequelize Database instance passing all args in this.opts.sequelizeOptions 77 | * @return {Object} a sequelize instance already connected to database. 78 | */ 79 | 80 | getSequelizeInstance() { 81 | 82 | debug("getSequelizeInstance"); 83 | 84 | var dbOptions = { 85 | host: this.opts.connection.host, 86 | dialect: this.opts.connection.dialect, 87 | port: this.opts.connection.port 88 | }; 89 | 90 | 91 | // Default pool configuration 92 | dbOptions.pool = { 93 | max: 5, 94 | min: 0, 95 | acquire: 30000, 96 | idle: 10000 97 | }; 98 | 99 | var sequelizeOptions = _.extend( 100 | this.opts.sequelizeOptions, 101 | dbOptions 102 | ); 103 | 104 | sequelizeOptions.operatorsAliases = false; 105 | 106 | return new Sequelize( 107 | this.opts.connection.schema, 108 | this.opts.connection.username, 109 | this.opts.connection.password, 110 | sequelizeOptions 111 | ); 112 | } 113 | 114 | 115 | /** 116 | * Read and load models definition from directory 117 | * @return {Promise} a priise wich will be resolved with all models defs 118 | */ 119 | 120 | getModelDefinitions() { 121 | 122 | debug("getModelDefinitions"); 123 | 124 | var modelsDefs = []; 125 | 126 | if (!this.opts.models || !this.opts.models.path) { 127 | return Promise.resolve(modelsDefs); 128 | } 129 | 130 | 131 | var modelsDirectory = path.join(path.resolve(), this.opts.models.path); 132 | 133 | try { 134 | fs.accessSync(modelsDirectory, fs.F_OK); 135 | } catch (e) { 136 | return Promise.resolve(modelsDefs); 137 | } 138 | 139 | return new Promise(resolve => { 140 | readdir(modelsDirectory, { 141 | recursive: false, 142 | return: "fullPaths", 143 | filter: { 144 | file: /\.(js)$/i 145 | } 146 | }) 147 | .then(files => { 148 | modelsDefs = files.map(file => { 149 | return { 150 | name: utils.camelize(path.basename(/^(.+?).js$/.exec(file)[1])), 151 | object: require(file) 152 | }; 153 | }); 154 | 155 | return resolve(modelsDefs); 156 | }); 157 | }); 158 | } 159 | 160 | 161 | /** 162 | * Get Sequelize model definitions ready. 163 | * @return {Promise} get all models from the database schema 164 | */ 165 | 166 | getSchema() { 167 | 168 | debug("getSchema"); 169 | 170 | var _this = this; 171 | 172 | let dialectPath = path.join( 173 | __dirname, "dialects", 174 | this.opts.connection.dialect 175 | ); 176 | 177 | let Dialect = require(dialectPath); 178 | let dbInstance = this.getSequelizeInstance(); 179 | 180 | let dialect = new Dialect( 181 | Sequelize, dbInstance, 182 | this.opts.connection 183 | ); 184 | 185 | return co(function* () { 186 | 187 | let modelDefs = yield _this.getModelDefinitions(); 188 | let autoload = true; 189 | 190 | if (false === _this.opts.models.autoLoad) { 191 | autoload = false; 192 | } 193 | 194 | debug("options passed to load models"); 195 | 196 | let models = yield dialect.loadModels(modelDefs, autoload); 197 | 198 | return { 199 | models: models, 200 | db: dbInstance 201 | }; 202 | 203 | }); 204 | } 205 | 206 | } 207 | 208 | module.exports = SequelizeModels; 209 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | 5 | /** 6 | * Filter tables by column or table name 7 | * @param {Array} - array of tables + column from database 8 | * @return {Array} - Array of strings with Table Names to be excluded. 9 | */ 10 | 11 | module.exports = { 12 | 13 | filterTables: function (tables, exclude) { 14 | let exNames = ["SequelizeMeta"].concat(exclude); 15 | 16 | let exColumns = [ 17 | "updatedAt", "updated_at", 18 | "createdAt", "created_at" 19 | ]; 20 | 21 | _.remove(tables, table => { 22 | return _.includes(exColumns, table.column_name) || 23 | _.includes(exNames, table.name); 24 | }); 25 | 26 | return tables; 27 | }, 28 | 29 | 30 | /** 31 | * Camelize given string str 32 | * @param {String} str string to be camelized 33 | * @return {String} camilized version of given string 34 | */ 35 | 36 | camelize: function (str) { 37 | // console.log("to camelize ", str); 38 | return str.replace(/(?:^|[-_])(\w)/g, (_, c) => { 39 | return c ? c.toUpperCase() : ""; 40 | }); 41 | } 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /migrations_data/mysql/20160222014049-init-migration.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Types, done) { 5 | 6 | // Create table profile 7 | queryInterface.createTable("profile", { 8 | id : { 9 | type : Types.INTEGER, 10 | primaryKey : true, 11 | autoIncrement : true 12 | }, 13 | name : { 14 | type : Types.STRING 15 | }, 16 | created_at: { 17 | type : Types.DATE 18 | }, 19 | updated_at: { 20 | type : Types.DATE 21 | } 22 | }) 23 | .then( function() { 24 | // create table department 25 | return queryInterface.createTable("department", { 26 | id: { 27 | type : Types.INTEGER, 28 | primaryKey : true, 29 | autoIncrement : true 30 | }, 31 | name: { 32 | type: Types.STRING 33 | }, 34 | created_at: { 35 | type: Types.DATE 36 | }, 37 | updated_at: { 38 | type: Types.DATE 39 | } 40 | }); 41 | 42 | }) 43 | .then( function() { 44 | // Create table user 45 | return queryInterface.createTable("user", { 46 | id: { 47 | type : Types.INTEGER, 48 | primaryKey : true, 49 | autoIncrement : true 50 | }, 51 | name: { 52 | type: Types.STRING 53 | }, 54 | last_name: { 55 | type: Types.STRING 56 | }, 57 | born_date: { 58 | type: Types.DATE 59 | }, 60 | profile_id: { 61 | type: Types.INTEGER, 62 | references: { 63 | model : "profile", 64 | key : "id" 65 | }, 66 | onUpdate : "cascade", 67 | onDelete : "restrict" 68 | }, 69 | created_at: { 70 | type: Types.DATE 71 | }, 72 | updated_at: { 73 | type: Types.DATE 74 | } 75 | }); 76 | 77 | }) 78 | .then( function() { 79 | // Create table user_department 80 | return queryInterface.createTable("user_department", { 81 | id: { 82 | type : Types.INTEGER, 83 | primaryKey : true, 84 | autoIncrement : true 85 | }, 86 | department_id: { 87 | type : Types.INTEGER, 88 | references: { 89 | model : "department", 90 | key : "id" 91 | }, 92 | onUpdate : "cascade", 93 | onDelete : "restrict" 94 | }, 95 | user_id : { 96 | type : Types.INTEGER, 97 | references : { 98 | model : "user", 99 | key : "id" 100 | }, 101 | onUpdate : "cascade", 102 | onDelete : "restrict" 103 | }, 104 | created_at : { 105 | type : Types.DATE 106 | }, 107 | updated_at: { 108 | type: Types.DATE 109 | } 110 | }); 111 | }) 112 | .then( function() { 113 | return done(); 114 | }); 115 | }, 116 | 117 | // Revert tables creation 118 | down: function(queryInterface) { 119 | return queryInterface.dropAllTables(); 120 | } 121 | 122 | }; 123 | -------------------------------------------------------------------------------- /migrations_data/mysql/20160222015937-populate-migration.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize, done) { 5 | queryInterface.sequelize.query( 6 | "INSERT INTO profile (name, created_at, updated_at) \ 7 | VALUES \ 8 | (\"Administrator\", NOW(), NOW()), \ 9 | (\"User\", NOW(), NOW()), \ 10 | (\"Guest\", NOW(), NOW()), \ 11 | (\"Technician\", NOW(), NOW());" 12 | ) 13 | 14 | .then( function() { 15 | return queryInterface.sequelize.query( 16 | "INSERT INTO department (name, created_at, updated_at) \ 17 | VALUES \ 18 | (\"Administration\", NOW(), NOW()), \ 19 | (\"Suport\", NOW(), NOW()), \ 20 | (\"RRHH\", NOW(), NOW());" 21 | ); 22 | }) 23 | 24 | .then( function() { 25 | return queryInterface.sequelize.query( 26 | "INSERT INTO user (name, last_name, born_date, profile_id, created_at, updated_at) \ 27 | VALUES \ 28 | (\"Gonzalo\", \"Bahamondez\", NOW(), 4, NOW(), NOW()), \ 29 | (\"Alexis\", \"Saez\", NOW(), 1, NOW(), NOW()), \ 30 | (\"Diego\", \"Gutierrez\", NOW(), 4, NOW(), NOW()), \ 31 | (\"Javier\", \"Huerta\", NOW(), 2, NOW(), NOW()), \ 32 | (\"Diego\", \"Paredes\", NOW(), 3, NOW(), NOW());" 33 | ); 34 | }) 35 | 36 | .then( function() { 37 | return queryInterface.sequelize.query( 38 | "INSERT INTO user_department (department_id, user_id , created_at, updated_at) \ 39 | VALUES \ 40 | (1, 2, NOW(), NOW()), \ 41 | (2, 1, NOW(), NOW()), \ 42 | (2, 3, NOW(), NOW()), \ 43 | (3, 4, NOW(), NOW());" 44 | ); 45 | }) 46 | .then( function() { 47 | done(); 48 | }); 49 | }, 50 | down: function (queryInterface, Sequelize, done) { 51 | 52 | // Truncate all tables 53 | queryInterface.sequelize.query("TRUNCATE TABLE user_department") 54 | .then( function() { 55 | return queryInterface.sequelize 56 | .query("TRUNCATE TABLE department"); 57 | }) 58 | .then( function() { 59 | return queryInterface.sequelize 60 | .query("TRUNCATE TABLE user"); 61 | }) 62 | .then( function() { 63 | return queryInterface.sequelize 64 | .query("TRUNCATE TABLE profile"); 65 | }) 66 | .then( function() { 67 | done(); 68 | }); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /migrations_data/postgres/20160222014049-init-migration.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Types, done) { 5 | 6 | // Create table profile 7 | queryInterface.createTable("profile", { 8 | id : { 9 | type : Types.INTEGER, 10 | primaryKey : true, 11 | autoIncrement : true 12 | }, 13 | name : { 14 | type : Types.STRING 15 | }, 16 | created_at: { 17 | type : Types.DATE 18 | }, 19 | updated_at: { 20 | type : Types.DATE 21 | } 22 | }) 23 | .then( function() { 24 | // create table department 25 | return queryInterface.createTable("department", { 26 | id: { 27 | type : Types.INTEGER, 28 | primaryKey : true, 29 | autoIncrement : true 30 | }, 31 | name: { 32 | type: Types.STRING 33 | }, 34 | created_at: { 35 | type: Types.DATE 36 | }, 37 | updated_at: { 38 | type: Types.DATE 39 | } 40 | }); 41 | 42 | }) 43 | .then( function() { 44 | // Create table user 45 | return queryInterface.createTable("user", { 46 | id: { 47 | type : Types.INTEGER, 48 | primaryKey : true, 49 | autoIncrement : true 50 | }, 51 | name: { 52 | type: Types.STRING 53 | }, 54 | last_name: { 55 | type: Types.STRING 56 | }, 57 | born_date: { 58 | type: Types.DATE 59 | }, 60 | profile_id: { 61 | type: Types.INTEGER, 62 | references: { 63 | model : "profile", 64 | key : "id" 65 | }, 66 | onUpdate : "cascade", 67 | onDelete : "restrict" 68 | }, 69 | created_at: { 70 | type: Types.DATE 71 | }, 72 | updated_at: { 73 | type: Types.DATE 74 | } 75 | }); 76 | 77 | }) 78 | .then( function() { 79 | // Create table user_department 80 | return queryInterface.createTable("user_department", { 81 | id: { 82 | type : Types.INTEGER, 83 | primaryKey : true, 84 | autoIncrement : true 85 | }, 86 | department_id: { 87 | type : Types.INTEGER, 88 | references: { 89 | model : "department", 90 | key : "id" 91 | }, 92 | onUpdate : "cascade", 93 | onDelete : "restrict" 94 | }, 95 | user_id : { 96 | type : Types.INTEGER, 97 | references : { 98 | model : "user", 99 | key : "id" 100 | }, 101 | onUpdate : "cascade", 102 | onDelete : "restrict" 103 | }, 104 | created_at : { 105 | type : Types.DATE 106 | }, 107 | updated_at: { 108 | type: Types.DATE 109 | } 110 | }); 111 | }) 112 | .then( function() { 113 | return done(); 114 | }); 115 | }, 116 | 117 | // Revert tables creation 118 | down: function(queryInterface) { 119 | return queryInterface.dropAllTables(); 120 | } 121 | 122 | }; 123 | -------------------------------------------------------------------------------- /migrations_data/postgres/20160222015937-populate-migration.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize, done) { 5 | queryInterface.sequelize.query( 6 | "INSERT INTO profile (\"name\", \"created_at\", \"updated_at\") \ 7 | VALUES \ 8 | ('Administrator', now(), now()), \ 9 | ('User', now(), now()), \ 10 | ('Guest', now(), now()), \ 11 | ('Technician', now(), now());" 12 | ) 13 | 14 | .then( function() { 15 | return queryInterface.sequelize.query( 16 | "INSERT INTO department (\"name\", \"created_at\", \"updated_at\") \ 17 | VALUES \ 18 | ('Administration', now(), now()), \ 19 | ('Suport', now(), now()), \ 20 | ('RRHH', now(), now());" 21 | ); 22 | }) 23 | 24 | .then( function() { 25 | return queryInterface.sequelize.query( 26 | "INSERT INTO \"user\" (\"name\", \"last_name\", \"born_date\", \"profile_id\", \"created_at\", \"updated_at\") \ 27 | VALUES \ 28 | ('Gonzalo', 'Bahamondez', now(), 4, now(), now()), \ 29 | ('Alexis', 'Saez', now(), 1, now(), now()), \ 30 | ('Diego', 'Gutierrez', now(), 4, now(), now()), \ 31 | ('Javier', 'Huerta', now(), 2, now(), now()), \ 32 | ('Diego', 'Paredes', now(), 3, now(), now());" 33 | ); 34 | }) 35 | 36 | .then( function() { 37 | return queryInterface.sequelize.query( 38 | "INSERT INTO user_department (\"department_id\", \"user_id\" , \"created_at\", \"updated_at\") \ 39 | VALUES \ 40 | (1, 2, now(), now()), \ 41 | (2, 1, now(), now()), \ 42 | (2, 3, now(), now()), \ 43 | (3, 4, now(), now());" 44 | ); 45 | }) 46 | .then( function() { 47 | done(); 48 | }); 49 | }, 50 | down: function (queryInterface, Sequelize, done) { 51 | 52 | // Truncate all tables 53 | queryInterface.sequelize.query("TRUNCATE TABLE user_department") 54 | .then( function() { 55 | return queryInterface.sequelize 56 | .query("TRUNCATE TABLE department"); 57 | }) 58 | .then( function() { 59 | return queryInterface.sequelize 60 | .query("TRUNCATE TABLE user"); 61 | }) 62 | .then( function() { 63 | return queryInterface.sequelize 64 | .query("TRUNCATE TABLE profile"); 65 | }) 66 | .then( function() { 67 | done(); 68 | }); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /mysql.conf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; version 2 of the License. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program; if not, write to the Free Software 14 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 15 | 16 | # 17 | # The MySQL Server configuration file. 18 | # 19 | # For explanations see 20 | # http://dev.mysql.com/doc/mysql/en/server-system-variables.html 21 | 22 | [mysqld] 23 | pid-file = /var/run/mysqld/mysqld.pid 24 | socket = /var/run/mysqld/mysqld.sock 25 | datadir = /var/lib/mysql 26 | secure-file-priv= NULL 27 | # Disabling symbolic-links is recommended to prevent assorted security risks 28 | symbolic-links=0 29 | 30 | bind-address=0.0.0.0 31 | # Custom config should go here 32 | !includedir /etc/mysql/conf.d/ 33 | 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequelize-models", 3 | "version": "1.4.0", 4 | "description": "Node.js SequelizeJS ORM model utilities", 5 | "homepage": "https://github.com/gbahamondezc/sequelize-models", 6 | "author": "Gonzalo Bahamondez (https://github.com/gbahamondezc)", 7 | "files": [ 8 | "lib" 9 | ], 10 | "main": "lib/sequelize-models.js", 11 | "engines": { 12 | "node": ">= 4" 13 | }, 14 | "keywords": [ 15 | "sequelize", 16 | "sequelizeJS", 17 | "models", 18 | "load", 19 | "read", 20 | "tables", 21 | "utilities" 22 | ], 23 | "devDependencies": { 24 | "chai": "^3.5.0", 25 | "del": "^2.2.0", 26 | "dotenv": "^5.0.1", 27 | "gulp": "^3.9.0", 28 | "gulp-coveralls": "^0.1.0", 29 | "gulp-eslint": "^1.0.0", 30 | "gulp-exclude-gitignore": "^1.0.0", 31 | "gulp-istanbul": "^0.10.3", 32 | "gulp-mocha": "^2.0.0", 33 | "gulp-nsp": "^2.1.0", 34 | "gulp-open": "^1.0.0", 35 | "gulp-plumber": "^1.0.0", 36 | "gulp-shell": "^0.5.2", 37 | "gulp-util": "^3.0.7", 38 | "jsdoc": "^3.4.0", 39 | "minami": "^1.1.1", 40 | "mysql2": "^1.5.3", 41 | "pg": "7.4.1", 42 | "pg-hstore": "2.3.2", 43 | "run-sequence": "^1.2.1", 44 | "sequelize-cli": "^2.3.1", 45 | "tedious": "^1.13.2" 46 | }, 47 | "eslintConfig": { 48 | "env": { 49 | "mocha": true, 50 | "node": true, 51 | "es6": true 52 | }, 53 | "rules": { 54 | "eqeqeq": [ 55 | 2, 56 | "smart" 57 | ], 58 | "indent": [ 59 | 2, 60 | 2 61 | ], 62 | "quotes": [ 63 | 2, 64 | "double" 65 | ], 66 | "no-undef": [ 67 | 2 68 | ], 69 | "no-unused-vars": [ 70 | 2 71 | ], 72 | "no-eq-null": [ 73 | 2 74 | ], 75 | "no-multi-spaces": [ 76 | 0 77 | ], 78 | "space-in-parens": [ 79 | 0 80 | ], 81 | "no-multiple-empty-lines": [ 82 | 0 83 | ], 84 | "no-multi-str": [ 85 | 0 86 | ], 87 | "key-spacing": [ 88 | 0 89 | ], 90 | "array-bracket-spacing": [ 91 | 0 92 | ], 93 | "object-curly-spacing": [ 94 | 0 95 | ], 96 | "consistent-return": [ 97 | 0 98 | ], 99 | "space-before-function-paren": [ 100 | 0 101 | ], 102 | "padded-blocks": [ 103 | 0 104 | ], 105 | "arrow-body-style": [ 106 | 0 107 | ], 108 | "camelcase": [ 109 | 0 110 | ], 111 | "computed-property-spacing": [ 112 | 0 113 | ], 114 | "new-cap": [ 115 | 0 116 | ], 117 | "yoda": [ 118 | 0 119 | ] 120 | } 121 | }, 122 | "repository": { 123 | "type": "git", 124 | "url": "git+https://github.com/gbahamondezc/sequelize-models.git" 125 | }, 126 | "scripts": { 127 | "prepublish": "gulp prepublish", 128 | "test": "gulp", 129 | "db:up": "docker-compose -f ./docker-compose.dev.yml up -d" 130 | }, 131 | "license": "MIT", 132 | "dependencies": { 133 | "bluebird": "^3.5.1", 134 | "co": "^4.6.0", 135 | "debug": "3.1.0", 136 | "lodash": "^4.13.1", 137 | "readdir-plus": "^1.1.0", 138 | "sequelize": "^4.37.6" 139 | }, 140 | "bugs": { 141 | "url": "https://github.com/gbahamondezc/sequelize-models/issues" 142 | }, 143 | "directories": { 144 | "test": "test" 145 | }, 146 | "peerDependencies": { 147 | "pg": "7.4.1", 148 | "pg-hstore": "2.3.2", 149 | "mysql2": "1.5.3" 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /test/mysql/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | connection: { 3 | host: "127.0.0.1", 4 | dialect: "mysql", 5 | username: "root", 6 | schema: "sequelize_models_db", 7 | password: "", 8 | port: 3308 9 | }, 10 | models: { 11 | autoLoad: true, 12 | path: "/test/mysql/models" 13 | }, 14 | sequelizeOptions: { 15 | define: { 16 | freezeTableName: true, 17 | underscored: true 18 | }, 19 | logging: false 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /test/mysql/models/User.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // Following http://docs.sequelizejs.com/en/latest/docs/models-definition/ 4 | tableName : "user", 5 | 6 | attributes : { 7 | name : { 8 | type : "string" 9 | }, 10 | last_name : { 11 | type : "string" 12 | }, 13 | born_date : { 14 | type : "date" 15 | } 16 | }, 17 | 18 | 19 | // Associations -> http://docs.sequelizejs.com/en/latest/docs/scopes/#associations 20 | associations : [{ 21 | type : "belongsTo", 22 | target : "Profile", 23 | options : { 24 | foreignKey : "profile_id" 25 | } 26 | }], 27 | 28 | validate : {}, 29 | indexes : [] 30 | }; 31 | -------------------------------------------------------------------------------- /test/mysql/mysql.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const SequelizeModels = require("../../"); 4 | const assert = require("assert"); 5 | const config = require("./config.js"); 6 | var seqModels = new SequelizeModels(config); 7 | 8 | describe("MySQL -> Queries tests", function () { 9 | 10 | var dbSchema; 11 | 12 | before(function (done) { 13 | seqModels.getSchema() 14 | .then(schema => { 15 | dbSchema = schema; 16 | return done(); 17 | }) 18 | .catch(err => { 19 | return done(err); 20 | }); 21 | }); 22 | 23 | 24 | it("MySQL -> Find user by id 1", function (done) { 25 | 26 | dbSchema.models.User.findById(1) 27 | .then(function (user) { 28 | assert(user.name === "Gonzalo"); 29 | done(); 30 | }) 31 | .catch(function (err) { 32 | return done(err); 33 | }); 34 | }); 35 | 36 | 37 | it("MySQL -> Find all profiles with name Technicians including his users", function (done) { 38 | dbSchema.models.Profile.findAll({ 39 | where: { 40 | name: "Technician" 41 | }, 42 | include: dbSchema.models.User 43 | }) 44 | .then(profiles => { 45 | var profile = profiles[0]; 46 | assert(profile.name === "Technician" && profile.users.length === 2); 47 | return done(); 48 | }) 49 | .catch(err => { 50 | return done(err); 51 | }); 52 | }); 53 | 54 | 55 | 56 | it("MySQL -> Find all users with name Gonzalo including his profiles", function (done) { 57 | dbSchema.models.User.findAll({ 58 | where: { name: "Gonzalo" }, 59 | include: dbSchema.models.Profile 60 | }) 61 | .then(users => { 62 | var user = users[0]; 63 | assert(user.name === "Gonzalo" && user.profile.name === "Technician"); 64 | return done(); 65 | }) 66 | .catch(err => { 67 | return done(err); 68 | }); 69 | }); 70 | 71 | after(function() { 72 | dbSchema.db.close(); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/psql/config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | connection: { 4 | host: "127.0.0.1", 5 | dialect: "postgres", 6 | username: "sequelize_models_db_user", 7 | schema: "sequelize_models_db", 8 | password: "", 9 | port: 5438 10 | }, 11 | models: { 12 | autoLoad: true, 13 | path: "/test/psql/models" 14 | }, 15 | sequelizeOptions: { 16 | define: { 17 | freezeTableName: true, 18 | underscored: true 19 | }, 20 | logging: false 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /test/psql/models/User.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // Following http://docs.sequelizejs.com/en/latest/docs/models-definition/ 4 | tableName : "user", 5 | 6 | attributes : { 7 | name : { 8 | type : "string" 9 | }, 10 | last_name : { 11 | type : "string" 12 | }, 13 | born_date : { 14 | type : "date" 15 | } 16 | }, 17 | 18 | 19 | // Associations -> http://docs.sequelizejs.com/en/latest/docs/scopes/#associations 20 | associations : [{ 21 | type : "belongsTo", 22 | target : "Profile", 23 | options : { 24 | foreignKey : "profile_id" 25 | } 26 | }], 27 | 28 | validate : {}, 29 | indexes : [] 30 | }; 31 | -------------------------------------------------------------------------------- /test/psql/psql.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const SequelizeModels = require("../../"); 4 | const assert = require("assert"); 5 | const config = require("./config.js"); 6 | var seqModels = new SequelizeModels(config); 7 | 8 | 9 | describe("PSQL -> Queries tests", function() { 10 | 11 | var dbSchema; 12 | 13 | before( function(done) { 14 | seqModels.getSchema() 15 | .then( schema => { 16 | dbSchema = schema; 17 | return done(); 18 | }) 19 | .catch( err => { 20 | return done(err); 21 | }); 22 | }); 23 | 24 | 25 | it("PSQL -> Find user by id 1", function(done) { 26 | 27 | dbSchema.models.User.findById(1) 28 | .then( function(user) { 29 | assert( user.name === "Gonzalo" ); 30 | done(); 31 | }) 32 | .catch(function(err) { 33 | return done(err); 34 | }); 35 | }); 36 | 37 | 38 | it("PSQL -> Find all profiles with name Technicians including his users", function(done) { 39 | dbSchema.models.Profile.findAll({ 40 | where : { 41 | name : "Technician" 42 | }, 43 | include : dbSchema.models.User 44 | }) 45 | .then( profiles => { 46 | var profile = profiles[0]; 47 | assert(profile.name === "Technician" && profile.users.length === 2); 48 | return done(); 49 | }) 50 | .catch( err => { 51 | return done(err); 52 | }); 53 | }); 54 | 55 | 56 | 57 | it("PSQL -> Find all users with name Gonzalo including his profiles", function(done) { 58 | dbSchema.models.User.findAll({ 59 | where : { name : "Gonzalo" }, 60 | include : dbSchema.models.Profile 61 | }) 62 | .then( users => { 63 | var user = users[0]; 64 | assert(user.name === "Gonzalo" && user.profile.name === "Technician"); 65 | return done(); 66 | }) 67 | .catch( err => { 68 | return done(err); 69 | }); 70 | }); 71 | 72 | after(function () { 73 | dbSchema.db.close(); 74 | }); 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var assert = require("assert"); 4 | var utils = require("../lib/utils.js"); 5 | 6 | describe("Camelize return string formated as camel case", function () { 7 | var expectedString; 8 | 9 | before( function() { 10 | expectedString = "TestString"; 11 | }); 12 | 13 | it("From TestString", function () { 14 | assert( utils.camelize("TestString") === expectedString); 15 | }); 16 | it("From testString", function () { 17 | assert( utils.camelize("testString") === expectedString); 18 | }); 19 | 20 | it("From test_string", function () { 21 | assert( utils.camelize("test_String") === expectedString); 22 | }); 23 | 24 | }); 25 | 26 | 27 | 28 | describe("Should filter tables passed as first argument by name or attribs", 29 | function () { 30 | 31 | it("Ignore common cases by column_name \ 32 | ['updated_at', 'updatedAt','created_at', 'createdAt']", function () { 33 | 34 | var sampleData = [{ 35 | name : "table1", 36 | column_name: "updated_at" 37 | }, { 38 | name : "table2", 39 | column_name: "updatedAt" 40 | }, { 41 | name : "table3", 42 | column_name: "created_at" 43 | }, { 44 | name : "table4", 45 | column_name: "createdAt" 46 | }]; 47 | 48 | assert( utils.filterTables(sampleData).length === 0 ); 49 | }); 50 | 51 | 52 | it("Ignore tables with table name 'SequelizeMeta'", function () { 53 | var sampleData = [{ 54 | name : "SequelizeMeta", 55 | column_name: "some_column" 56 | }]; 57 | assert( utils.filterTables(sampleData).length === 0 ); 58 | }); 59 | 60 | 61 | it("Ignore tables from given table name exclude array as second argument", function () { 62 | 63 | var sampleData = [{ 64 | name : "some_table", 65 | column_name : "some_column" 66 | }, { 67 | name : "SequelizeMeta", 68 | column_name : "some_column" 69 | }, { 70 | name : "table4", 71 | column_name : "createdAt" 72 | }]; 73 | 74 | assert( utils.filterTables( sampleData, ["some_table"]).length === 0 ); 75 | }); 76 | }); 77 | --------------------------------------------------------------------------------