├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .travis.yml ├── License ├── README.md ├── changelog.md ├── index.js ├── lib ├── defineAll.js ├── defineFromFolder.js ├── errors.js ├── index.js ├── patches.js └── utils.js ├── package-lock.json ├── package.json └── test ├── .eslintrc.js ├── all.test.js ├── config ├── config.js └── options.js ├── example ├── Task2.js └── User2.js └── support.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # sequelize-definer module 2 | # Editor config 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | indent_size = 2 9 | 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.{json,yml,md}] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.* 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * ESLint config 4 | * ------------------*/ 5 | 6 | 'use strict'; 7 | 8 | // Exports 9 | 10 | module.exports = { 11 | extends: [ 12 | '@overlookmotel/eslint-config', 13 | '@overlookmotel/eslint-config-node' 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | before_script: 4 | - "mysql -e 'create database sequelize_test;'" 5 | - "psql -c 'create database sequelize_test;' -U postgres" 6 | - "export SEQ_MYSQL_USER=root" 7 | - "export SEQ_PG_USER=postgres" 8 | - "export SEQ_PG_PW=postgres" 9 | - 'if [ "$SEQ_VERSION" ]; then npm install sequelize@^$SEQ_VERSION.0.0; fi' 10 | 11 | script: 12 | - "npm run ci" 13 | 14 | node_js: 15 | - "13" 16 | - "12" 17 | - "10" 18 | 19 | branches: 20 | except: 21 | - /^v\d+\./ 22 | 23 | env: 24 | - DB=mysql DIALECT=mysql 25 | - DB=mysql DIALECT=postgres 26 | - DB=mysql DIALECT=postgres-native 27 | - DB=mysql DIALECT=sqlite 28 | - DB=mysql DIALECT=mariadb 29 | - DB=mysql DIALECT=mssql 30 | - DB=mysql DIALECT=mysql SEQ_VERSION=2 31 | - DB=mysql DIALECT=postgres SEQ_VERSION=2 32 | - DB=mysql DIALECT=postgres-native SEQ_VERSION=2 33 | - DB=mysql DIALECT=sqlite SEQ_VERSION=2 34 | - DB=mysql DIALECT=mariadb SEQ_VERSION=2 35 | - DB=mysql DIALECT=mssql SEQ_VERSION=2 36 | 37 | matrix: 38 | fast_finish: true 39 | include: 40 | - node_js: "12" 41 | env: DB=mysql DIALECT=mysql COVERAGE=true 42 | allow_failures: 43 | - env: DB=mysql DIALECT=mysql COVERAGE=true 44 | 45 | addons: 46 | postgresql: "9.3" 47 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Overlook Motel (theoverlookmotel@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sequelize-definer.js 2 | 3 | # Sequelize plugin to help easily define a set of models 4 | 5 | ## What's it for? 6 | 7 | This plugin for [Sequelize](http://sequelizejs.com/) provides two utility functions to make it easier to define a set of models at once, either from a Javascript object or a folder. 8 | 9 | ## Current status 10 | 11 | [![NPM version](https://img.shields.io/npm/v/sequelize-definer.svg)](https://www.npmjs.com/package/sequelize-definer) 12 | [![Build Status](https://img.shields.io/travis/overlookmotel/sequelize-definer/master.svg)](http://travis-ci.org/overlookmotel/sequelize-definer) 13 | [![Coverage Status](https://img.shields.io/coveralls/overlookmotel/sequelize-definer/master.svg)](https://coveralls.io/r/overlookmotel/sequelize-definer) 14 | 15 | API is stable. All features and options are fairly well tested. Works with all dialects of SQL supported by Sequelize (MySQL, Postgres, SQLite, Microsoft SQL Server). 16 | 17 | Tested with Sequelize v2.x.x and v3.x.x, but may work on Sequelize v1.7 too - please let me know if you have success with v1.7. 18 | 19 | ## Usage 20 | 21 | ### Loading module 22 | 23 | To load module: 24 | 25 | ```js 26 | var Sequelize = require('sequelize-definer')(); 27 | // NB Sequelize must also be present in `node_modules` 28 | ``` 29 | 30 | or, a more verbose form useful if chaining multiple Sequelize plugins: 31 | 32 | ```js 33 | var Sequelize = require('sequelize'); 34 | require('sequelize-definer')(Sequelize); 35 | ``` 36 | 37 | ### Defining from object 38 | #### Sequelize#defineAll( definitions [, options] ) 39 | 40 | Call `Sequelize#defineAll( definitions )`, passing an object containing the model definitions with the model names as keys.e.g.: 41 | 42 | ```js 43 | sequelize.defineAll({ 44 | User: { 45 | fields: { 46 | field1: ..., 47 | field2: ... 48 | }, 49 | options: { ... } 50 | }, 51 | Task: { 52 | fields: { 53 | field1: ..., 54 | field2: ... 55 | }, 56 | options: { ... } 57 | } 58 | }); 59 | ``` 60 | 61 | `fields` and `options` are the same as are passed to `Sequelize#define( modelName, fields, options )`. Both are optional. 62 | 63 | ### Defining from folder 64 | 65 | #### Sequelize#defineFromFolder( folderPath [, options] ) 66 | 67 | Call `Sequelize#defineFromFolder( folderPath )` passing full directory path of the folder to load model definitions from. e.g.: 68 | 69 | ```js 70 | sequelize.defineFromFolder( path.join( __dirname, 'models' ) ); 71 | ``` 72 | 73 | Example of a model file: 74 | 75 | ```js 76 | // User.js 77 | var Sequelize = require('sequelize'); 78 | 79 | module.exports = { 80 | fields: { 81 | name: Sequelize.STRING 82 | } 83 | }; 84 | ``` 85 | 86 | `defineFromFolder()` uses [require-folder-tree](https://github.com/overlookmotel/require-folder-tree/) to load the files from the folder. You can pass options to `require-folder-tree` for how the files are loaded by including an object `loadOptions` in `options` passed to `defineFromFolder()`. e.g.: 87 | 88 | ```js 89 | // Load files in sub-folders as models with name prefixed by folder name 90 | // e.g. `User/Permission.js` defines model `UserPermission` 91 | sequelize.defineFromFolder( path.join( __dirname, 'models' ), { 92 | loadOptions: { 93 | // NB flatten is always set to `true` 94 | flattenPrefix: true 95 | } 96 | } ); 97 | ``` 98 | 99 | ### Defining one-to-one or one-to-many associations 100 | 101 | You can create associations between models within the model definitions. 102 | 103 | A one-to-many association: 104 | 105 | ```js 106 | // Each Task belongs to a User and a User has many Tasks 107 | sequelize.defineAll({ 108 | User: { 109 | fields: { 110 | name: Sequelize.STRING 111 | } 112 | }, 113 | Task: { 114 | fields: { 115 | name: Sequelize.STRING, 116 | UserId: { 117 | reference: 'User' 118 | } 119 | } 120 | } 121 | }); 122 | 123 | // Equivalent to: 124 | // Task.belongsTo(User); 125 | // User.hasMany(Task); 126 | ``` 127 | 128 | For a one-to-one association (i.e. hasOne rather than hasMany), define `referenceType: 'one'`: 129 | 130 | ```js 131 | sequelize.defineAll({ 132 | User: ..., 133 | Task: { fields: { 134 | name: ..., 135 | UserId: { 136 | reference: 'User', 137 | referenceType: 'one' 138 | } 139 | } } 140 | }); 141 | 142 | // Equivalent to: 143 | // Task.belongsTo(User); 144 | // User.hasOne(Task); 145 | ``` 146 | 147 | NB The type of a field with `reference` is automatically inherited from the primary key of the referenced model, so no need to specify `type`. 148 | 149 | Other options... 150 | 151 | ```js 152 | sequelize.defineAll({ 153 | User: ..., 154 | Task: { fields: { 155 | name: ..., 156 | UserId: { 157 | reference: 'User', 158 | referenceKey: 'id', // Defaults to the primary key of the referenced model 159 | as: 'Owner', // Defaults to name of the referenced model 160 | asReverse: 'TasksToDo', // Defaults to name of this model 161 | type: Sequelize.INTEGER, // Defaults to type of the primary key field in referenced model 162 | onDelete: 'CASCADE', // Defaults to undefined (default Sequelize behaviour) 163 | onUpdate: 'CASCADE' // Defaults to undefined (default Sequelize behaviour) 164 | } 165 | } } 166 | }); 167 | 168 | // Equivalent to: 169 | // Task.belongsTo(User, { as: 'Owner', onDelete: 'CASCADE', onUpdate: 'CASCADE' }); 170 | // User.hasMany(Task, { as: 'TasksToDo', onDelete: 'CASCADE', onUpdate: 'CASCADE' }); 171 | 172 | // Then you can do: 173 | // user.getTasksToDo() 174 | // task.getOwner() 175 | ``` 176 | 177 | See `autoAssociate` option below for an even easier way to handle associations. 178 | 179 | ### Defining many-to-many associations 180 | 181 | ```js 182 | sequelize.defineAll({ 183 | User: { 184 | fields: { 185 | name: Sequelize.STRING 186 | } 187 | }, 188 | Task: { 189 | fields: { 190 | name: Sequelize.STRING 191 | }, 192 | manyToMany: { 193 | User: true 194 | } 195 | } 196 | }); 197 | 198 | // Equivalent to: 199 | // Task.belongsToMany(User); 200 | // User.belongsToMany(Task); 201 | ``` 202 | 203 | Options can also be passed: 204 | 205 | ```js 206 | sequelize.defineAll({ 207 | User: ..., 208 | Task: { 209 | fields: ..., 210 | manyToMany: { 211 | User: { 212 | onDelete: 'RESTRICT', // Defaults to undefined 213 | onUpdate: 'RESTRICT', // Defaults to undefined 214 | as: 'Worker', // Defaults to name of the referenced model 215 | asReverse: 'TasksToDo', // Defaults to name of this model 216 | through: 'UserTask', // Defaults to undefined, creating through table automatically 217 | skipFields: true // Defaults to value of options.skipFieldsOnThrough (see below) 218 | } 219 | } 220 | }, 221 | UserTask: { 222 | fields: { 223 | status: Sequelize.STRING 224 | } 225 | } 226 | }); 227 | 228 | // Equivalent to: 229 | // Task.belongsToMany(User, { through: 'UserTask', onDelete: 'RESTRICT', onUpdate: 'RESTRICT', as: 'Worker' }); 230 | // User.belongsToMany(Task, { through: 'UserTask', onDelete: 'RESTRICT', onUpdate: 'RESTRICT', as: 'TasksToDo' }); 231 | ``` 232 | 233 | ### Options 234 | 235 | All options below apply to both `defineAll()` and `defineFromFolder()`. 236 | 237 | Options can also be applied on a model-by-model basis in each model's options, except where noted below. Options set on a particular model override the global options. 238 | 239 | #### primaryKey 240 | 241 | Sets the name of the primary key attribute automatically created on all models which have no primary key defined. 242 | Defaults to `'id'` (default Sequelize behaviour). 243 | 244 | ```js 245 | sequelize.defineAll( definitions, { primaryKey: 'ID' }); 246 | ``` 247 | 248 | If a function is provided in place of a string, the function is called to get the key name. Function is called with arguments `( modelName, definition, definitions )`. 249 | 250 | ```js 251 | // Primary key for model User will be UserId 252 | sequelize.defineAll( definitions, { 253 | primaryKey: function(modelName) { 254 | return modelName + 'Id'; 255 | } 256 | }); 257 | ``` 258 | 259 | #### primaryKeyType 260 | 261 | Sets the type of the auto-created primary key attribute. 262 | Defaults to `Sequelize.INTEGER` (default Sequelize behaviour). 263 | 264 | ```js 265 | sequelize.defineAll( definitions, { primaryKeyType: Sequelize.INTEGER.UNSIGNED }); 266 | ``` 267 | 268 | #### primaryKeyAttributes 269 | 270 | Define additional attributes for primary keys. 271 | Defaults to `undefined` (default Sequelize behaviour). 272 | 273 | ```js 274 | sequelize.defineAll( definitions, { primaryKeyAttributes: { defaultValue: Sequelize.UUIDV4 } }); 275 | ``` 276 | 277 | #### primaryKeyThrough 278 | 279 | When `true`, creates an `id` column as primary key in through tables. 280 | When `false`, there is no `id` column and the primary key consists of the columns referring to the models being associated by the through table (usual Sequelize behaviour). 281 | Defaults to `false`. 282 | 283 | #### primaryKeyFirst 284 | 285 | When `true`, creates the primary key as the first column in the table. 286 | Defaults to `false`. 287 | 288 | #### associateThrough 289 | 290 | When `true`, associates through tables with their joined models. 291 | Defaults to `false`. 292 | 293 | This allows you to do e.g. `TaskUser.findAll( { include: [ Task, User ] } )` 294 | 295 | `associateThrough` option can also be overridden on an individual many-to-many join with the `manyToMany` object's `associate` option. 296 | 297 | #### autoAssociate 298 | 299 | When `true`, automatically creates associations where a column name matches the model name + primary key of another model. No need to specify `reference` as in the association examples above. If you have a standardized naming convention, this makes it really easy and natural to define associations. Defaults to `false`. 300 | 301 | ```js 302 | sequelize.defineAll({ 303 | User: { 304 | fields: { 305 | name: Sequelize.STRING 306 | } 307 | }, 308 | Task: { 309 | fields: { 310 | name: Sequelize.STRING, 311 | UserId: { allowNull: false } 312 | } 313 | } 314 | }, { 315 | // Options 316 | autoAssociate: true 317 | }); 318 | 319 | // This automatically runs 320 | // Task.belongsTo(User); 321 | // User.hasMany(Task); 322 | ``` 323 | 324 | To prevent a particular field being auto-associated, set `reference` on the field to `null`. 325 | 326 | #### fields 327 | 328 | Adds the fields provided to every model defined. 329 | Defaults to `undefined`. 330 | 331 | ```js 332 | sequelize.defineAll( definitions, { fields: { 333 | createdByUserId: { 334 | type: Sequelize.INTEGER, 335 | references: 'Users' 336 | } 337 | } }); 338 | ``` 339 | 340 | If a function is provided in place of an object, the function is called to get the field definition. Function is called with arguments `( modelName, definition, definitions )`. 341 | 342 | ```js 343 | sequelize.defineAll( definitions, { 344 | autoAssociate: true, 345 | fields: { 346 | createdByUserId: function(modelName) { 347 | return { 348 | reference: 'User', 349 | asReverse: 'created' + Sequelize.Utils.pluralize(modelName) 350 | }; 351 | } 352 | } 353 | }); 354 | 355 | // Equivalent to e.g. 356 | // Task.belongsTo(User, { as: 'createdByUser' }); 357 | // User.hasMany(Task, { as: 'createdTasks' }); 358 | ``` 359 | 360 | To skip adding all extra fields on a particular model, set `skipFields` to `true` in that model's options. 361 | To skip adding a particular extra field, include that field in the model's `fields` object as `null`. 362 | 363 | #### skipFieldsOnThrough 364 | 365 | When `true`, does not add extra fields defined with `options.fields` to through tables. 366 | Defaults to `false`. 367 | 368 | To skip adding extra fields on a particular many-to-many association's through table, set `skipFields` in the `manyToMany` object's options. 369 | 370 | #### labels 371 | 372 | When `true`, creates a `label` attribute on each field, with a human-readable version of the field name. 373 | Defaults to global define option set in `new Sequelize()` or `false`. 374 | 375 | #### freezeTableName 376 | 377 | When `true`, table names are the same as model names provided, not pluralized as per default Sequelize behaviour. 378 | Defaults to global define option set in `new Sequelize()` or `false`. 379 | 380 | #### lowercaseTableName 381 | 382 | When `true`, table names have first letter lower cased. 383 | Defaults to global define option set in `new Sequelize()` or `false`. 384 | 385 | #### camelThrough 386 | 387 | When `true`, creates through model names in camelcase (i.e. 'taskUser' rather than 'taskuser'). 388 | Defaults to global define option set in `new Sequelize()` or `false` (default Sequelize behaviour). 389 | 390 | `camelThrough` option can also be overridden on an individual many-to-many join with the `manyToMany` object's `camel` option. 391 | 392 | ### Errors 393 | 394 | Errors thrown by the plugin are of type `DefinerError`. The error class can be accessed at `Sequelize.DefinerError`. 395 | 396 | ## Versioning 397 | 398 | This module follows [semver](https://semver.org/). Breaking changes will only be made in major version updates. 399 | 400 | All active NodeJS release lines are supported (v10+ at time of writing). After a release line of NodeJS reaches end of life according to [Node's LTS schedule](https://nodejs.org/en/about/releases/), support for that version of Node may be dropped at any time, and this will not be considered a breaking change. Dropping support for a Node version will be made in a minor version update (e.g. 1.2.0 to 1.3.0). If you are using a Node version which is approaching end of life, pin your dependency of this module to patch updates only using tilde (`~`) e.g. `~1.2.3` to avoid breakages. 401 | 402 | ## Tests 403 | 404 | Use `npm test` to run the tests. Use `npm run cover` to check coverage. 405 | Requires a database called 'sequelize_test' and a db user 'sequelize_test' with no password. 406 | 407 | ## Changelog 408 | 409 | See [changelog.md](https://github.com/overlookmotel/sequelize-definer/blob/master/changelog.md) 410 | 411 | ## Issues 412 | 413 | If you discover a bug, please raise an issue on Github. https://github.com/overlookmotel/sequelize-definer/issues 414 | 415 | ### Known issues 416 | 417 | * Does not create foreign key constraint on a 'hasOne' relation defined by setting 'reference' in field definition 418 | 419 | ## Contribution 420 | 421 | Pull requests are very welcome. Please: 422 | 423 | * ensure all tests pass before submitting PR 424 | * add an entry to changelog 425 | * add tests for new features 426 | * document new functionality/API additions in README 427 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.6.2 4 | 5 | Bug fixes: 6 | 7 | * Fix missing `index.js` file in NPM 8 | 9 | ## 0.6.1 10 | 11 | Docs: 12 | 13 | * Reformat README 14 | 15 | ## 0.6.0 16 | 17 | Features: 18 | 19 | * Support Sequelize v5 20 | * Support multiple many-to-many relationships between same models 21 | 22 | Minor: 23 | 24 | * Drop support for Node versions < 10 25 | 26 | Bug fixes: 27 | 28 | * Fix pluralization of through table names 29 | * Fix missing `var` 30 | * Replace `attributes` with `rawAttributes` 31 | 32 | Refactor: 33 | 34 | * Refactor utils 35 | * Replace use of Lodash with ES6 JS 36 | * Break up long lines 37 | * Fix ESLint errors 38 | * `index.js` entry point 39 | 40 | Dependencies: 41 | 42 | * Update dependencies 43 | 44 | No code: 45 | 46 | * Header comments 47 | * Reformat code comments 48 | * Fix spacing in `package.json` 49 | 50 | Docs: 51 | 52 | * Versioning policy 53 | * Reverse changelog order 54 | * Update license year 55 | * Remove license indentation 56 | 57 | Dev: 58 | 59 | * Use NPM v9 for development 60 | * Replace make with npm scripts 61 | * Clean up after `cover` NPM script even if fails 62 | * Replace JSHint with ESLint 63 | * ESLint process all dot files 64 | * Simplify lint scripts 65 | * `package-lock.json` 66 | * Update `package-lock.json` format 67 | * Update NPM ignore 68 | * Replace `.npmignore` with `files` list in `package.json` 69 | * `package.json` re-order dev dependencies 70 | * `.gitattributes` file 71 | * Fix `.gitignore` 72 | * Remove `npm-debug.log` from `.gitignore` 73 | * Update `.editorconfig` 74 | * Remove `sudo` from Travis CI config 75 | * Reformat Travis CI config 76 | * Update dev dependencies 77 | 78 | ## 0.5.0 79 | 80 | * `lowercaseTableName` option [feat] 81 | * `autoAssociate` option match upper case model names 82 | * Avoid errors on Sequelize versions without `Utils._` [fix] 83 | * Avoid errors on Sequelize versions without `Utils.uppercaseFirst` [fix] 84 | 85 | ## 0.4.6 86 | 87 | * Bug fix: Field names on many-to-many self-associations 88 | * Throw error if non-existent models referenced 89 | * Update `semver-select` dependency 90 | * Update `mysql` dev dependency in line with Sequelize v3.12.2 91 | * Update dev dependencies 92 | * License update 93 | * README 94 | 95 | ## 0.4.5 96 | 97 | * MSSQL config for tests 98 | 99 | ## 0.4.4 100 | 101 | * Rename `SequelizeHierarchyError` to `HierarchyError` 102 | * Documentation for errors 103 | 104 | ## 0.4.3 105 | 106 | * Patches use `Sequelize.version` for version number where available 107 | 108 | ## 0.4.2 109 | 110 | * Update dependency mysql in line with Sequelize v3.7.1 111 | * Update dependency lodash 112 | * Update dev dependencies 113 | 114 | ## 0.4.1 115 | 116 | * Patch for `Utils.underscoredIf()` to support Sequelize > 3.5.0 117 | 118 | ## 0.4.0 119 | 120 | * Support for Sequelize v3.x.x 121 | * Bug fix: Apply auto-increment by default on INTEGER/BIGINT primary key fields 122 | * Bug fix: Use `referenceKey` rather than `referencesKey` 123 | * Better support for underscored field names as references 124 | * Update dependencies 125 | * Update dev dependencies in line with Sequelize v3.2.0 126 | * Travis runs tests with Sequelize v3 and v2 127 | * Test code coverage & Travis sends to coveralls 128 | * Run jshint on tests 129 | * Disable Travis dependency cache 130 | * Update README badges to use shields.io 131 | 132 | Breaking changes: 133 | 134 | * Bug fix: Apply auto-increment by default on INTEGER/BIGINT primary key fields 135 | * Bug fix: Use `referenceKey` rather than `referencesKey` 136 | 137 | ## 0.3.4 138 | 139 | * Move testExample to test/example 140 | 141 | ## 0.3.3 142 | 143 | * Remove relative path to sequelize in tests 144 | 145 | ## 0.3.2 146 | 147 | * Replace `utils.endsWith` with `_.endsWith` 148 | 149 | ## 0.3.1 150 | 151 | * Loosen sequelize dependency version to v2.x.x 152 | * Update mysql module dependency in line with sequelize v2.1.0 153 | * Update lodash dependency 154 | * Update dev dependencies 155 | * Fix bug processing `options.fields` 156 | * README contribution section 157 | 158 | ## 0.3.0 159 | 160 | * `primaryKeyAttributes` option 161 | * Update require-folder-tree dependency to v1.1.0 (allows custom transformations of file names) 162 | * Code tidy 163 | 164 | ## 0.2.47 165 | 166 | * Fix bug with labels on timestamp fields 167 | 168 | ## 0.2.46 169 | 170 | * Update sequelize dependency to v2.0.0+ 171 | * Update dev dependencies in line with sequelize v2.0.5 172 | * Update test support files in line with sequelize v2.0.5 173 | * Support for Microsoft SQL Server 174 | * Remove use of deprecated sequelize API 175 | * Code tidy in test/support.js 176 | * Fix tests for Sequelize v2 177 | * Travis runs tests against node 0.10 and 0.12 178 | * Travis uses correct database users 179 | * README update 180 | * README code examples tagged as Javascript 181 | 182 | ## 0.2.45 183 | 184 | * Set sequelize dependency to ~2.0.0-rc3 (tilde) 185 | * README known issue hasOne foreign keys 186 | * README notes on contribution 187 | * Remove all trailing whitespace 188 | * Travis runs on new container infrastructure 189 | * Update db dev dependencies in line with Sequelize 2.0.0-rc8 190 | * Update dev dependencies 191 | 192 | ## 0.2.44 193 | 194 | * Lock sequelize dev dependency to 2.0.0-rc3 195 | 196 | ## 0.2.8 197 | 198 | * Lock sequelize dependency to 2.0.0-rc3 (errors with rc4) 199 | 200 | ## 0.2.7 201 | 202 | * Updated sequelize dependency to v2.0.0-rc3 203 | * Only auto-increment on primary key if INTEGER type 204 | * Default value for UUID primary key fields 205 | * JSHint ignores redefinition of `Promise` 206 | 207 | ## 0.2.6 208 | 209 | * Remove temporary patch for inflection's incorrect pluralization of "permission" 210 | 211 | ## 0.2.5 212 | 213 | * Temporary patch for inflection's incorrect pluralization of "permission" pending PR to fix it permanently 214 | * Specify to use latest Sequelize version from Github in package.json rather than .travis.yml 215 | 216 | ## 0.2.4 217 | 218 | * `primaryKeyThrough` option 219 | 220 | ## 0.2.3 221 | 222 | * Code tidy 223 | * Save `reference` to fields in through models if `associateThrough` option set 224 | 225 | ## 0.2.2 226 | 227 | * Bug fix: utility function `functionValue` was not passing arguments 228 | * Update db library dependencies in line with Sequelize 229 | * Remove definer-related options from options passed to `Sequelize#define()` 230 | * Where a model field is a reference to another model (an ID for association), `reference` attribute of the field set to model name of referenced model 231 | * `humanize()` utility function handles empty string/null/undefined 232 | * Amend travis config file to use `npm install` to install Sequelize's dependencies after getting latest master from git 233 | * Added `editorconfig` file 234 | 235 | ## 0.2.1 236 | 237 | * `labels` option inherits from global options defined with `new Sequelize()` 238 | * `humanize` utility function handles underscore style 239 | 240 | ## 0.2.0 241 | 242 | Now works with all dialects of SQL supported by Sequelize (MySQL, Postgres, SQLite) 243 | 244 | * Updated README 245 | 246 | ## 0.1.4 247 | 248 | * Travis loads sequelize dependency from Github repo master branch not npm 249 | * Tests db user sequelize_test 250 | * Travis uses db user travis 251 | * Updated README 252 | 253 | ## 0.1.3 254 | 255 | * JSHint included in tests 256 | * Set versions for mocha & chai dependencies 257 | * Travis integration 258 | * Updated README 259 | 260 | ## 0.1.2 261 | 262 | * Added own lodash dependency, rather than using `Sequelize.Utils._` 263 | * Moved define functions into own files 264 | 265 | ## 0.1.1 266 | 267 | * Minor code tidy 268 | * Added licenses to package.json 269 | * Sequelize peer dependency 270 | 271 | ## 0.1.0 272 | 273 | * All options definable in individual model options 274 | * All input will not be touched (cloned before internal modification) 275 | * Labels for createdAt, updatedAt, deletedAt fields 276 | * Support for `underscored` and `underscoredAll` options 277 | * Bug fixes 278 | * Changed README for flatten options passed to require-folder-tree 279 | * Tests for all options 280 | 281 | ## 0.0.2 282 | 283 | * Extra fields can be overridden by null/false field 284 | * `skipFields` option 285 | * Bug fixes 286 | * Tests for basic functionality 287 | 288 | ## 0.0.1 289 | 290 | * Initial release 291 | 292 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Entry point 4 | * ------------------*/ 5 | 6 | 'use strict'; 7 | 8 | // Exports 9 | 10 | module.exports = require('./lib/index.js'); 11 | -------------------------------------------------------------------------------- /lib/defineAll.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Sequelize#defineAll() method 4 | * ------------------*/ 5 | 6 | 'use strict'; 7 | 8 | // Modules 9 | const _ = require('lodash'); 10 | 11 | // Imports 12 | const utils = require('./utils.js'), 13 | patchesFn = require('./patches.js'); 14 | 15 | // Exports 16 | 17 | /** 18 | * Define a set of models at once. 19 | * Called as `sequelize.defineAll()`. 20 | * @param {Array} definitions - Object with names of the models as keys 21 | * @param {Object} [defineOptions] - Options object 22 | * @returns {Object} - Sequelize instance 23 | */ 24 | module.exports = function(definitions, defineOptions) { 25 | const sequelize = this, 26 | {Sequelize} = sequelize, 27 | {Utils} = Sequelize, 28 | patches = patchesFn(Sequelize); 29 | 30 | const globalOptions = sequelize.options.define || {}; 31 | 32 | defineOptions = Object.assign({ // eslint-disable-line prefer-object-spread 33 | primaryKey: 'id', 34 | primaryKeyAttributes: {}, 35 | primaryKeyFirst: false, 36 | primaryKeyThrough: false, 37 | autoAssociate: false, 38 | associateThrough: false, 39 | // onDelete: undefined, 40 | // onUpdate: undefined, 41 | // fields: undefined, 42 | skipFieldsOnThrough: false, 43 | labels: globalOptions.labels || false, 44 | freezeTableName: globalOptions.freezeTableName || false, 45 | lowercaseTableName: globalOptions.lowercaseTableName || false, 46 | underscored: globalOptions.underscored || false, 47 | underscoredAll: globalOptions.underscoredAll || false, 48 | camelThrough: globalOptions.camelThrough || false 49 | }, defineOptions); 50 | 51 | if (defineOptions.primaryKeyType) { 52 | defineOptions.primaryKeyAttributes.type = defineOptions.primaryKeyType; 53 | delete defineOptions.primaryKeyType; 54 | } else { 55 | defineOptions.primaryKeyAttributes.type = Sequelize.INTEGER; 56 | } 57 | 58 | // Prep definitions 59 | const primaryKeys = {}; 60 | 61 | definitions = _.mapValues(definitions, (definition, modelName) => prepOne(modelName, definition)); 62 | 63 | // Process definitions 64 | const relationshipsOne = []; 65 | const relationshipsMany = []; 66 | 67 | _.forIn(definitions, (definition, modelName) => { 68 | defineOne(modelName, definition); 69 | }); 70 | 71 | // Create relationships 72 | defineRelationships(); 73 | 74 | // Return sequelize instance (for chaining) 75 | return sequelize; 76 | 77 | /* 78 | * Functions 79 | */ 80 | 81 | function prepOne(modelName, definition) { 82 | definition = _.clone(definition); 83 | 84 | // Inherit options from defineOptions (except fields and through-related options) 85 | const fields = _.clone(definition.fields || {}), 86 | options = _.clone(definition.options || {}); 87 | definition.fields = fields; 88 | definition.options = options; 89 | 90 | utils.defaultsNoUndef(options, defineOptions); 91 | ['fields', 'associateThrough', 'skipFieldsOnThrough', 'camelThrough'].forEach((fieldName) => { 92 | delete options[fieldName]; 93 | }); 94 | 95 | if (options.primaryKeyType) { 96 | options.primaryKeyAttributes.type = options.primaryKeyType; 97 | delete options.primaryKeyType; 98 | } 99 | 100 | options.primaryKeyAttributes = _.defaults({ 101 | primaryKey: true, 102 | _autoGenerated: true 103 | }, options.primaryKeyAttributes, defineOptions.primaryKeyAttributes, { 104 | allowNull: false 105 | }); 106 | 107 | if ( 108 | [Sequelize.INTEGER.key, Sequelize.BIGINT.key] 109 | .includes(options.primaryKeyAttributes.type.key) 110 | ) { 111 | options.primaryKeyAttributes.autoIncrement = true; 112 | } 113 | if ( 114 | options.primaryKeyAttributes.type.key === Sequelize.UUID.key 115 | && !options.primaryKeyAttributes.defaultValue 116 | ) { 117 | options.primaryKeyAttributes.defaultValue = Sequelize.UUIDV4; 118 | } 119 | 120 | // Add additional fields 121 | if (defineOptions.fields && !options.skipFields) { 122 | _.forIn(defineOptions.fields, (field, fieldName) => { 123 | if (fields[fieldName] !== undefined) return; 124 | field = utils.functionValue(field, modelName, definition, definitions); 125 | if (field instanceof Sequelize.ABSTRACT) field = {type: field}; 126 | fields[fieldName] = _.clone(field); 127 | }); 128 | } 129 | 130 | // Delete empty fields and set field type where shorthand definition used 131 | _.forIn(fields, (field, fieldName) => { 132 | if (!field) { 133 | delete fields[fieldName]; 134 | } else if (!_.isPlainObject(field)) { 135 | fields[fieldName] = {type: field}; 136 | } 137 | }); 138 | 139 | // Find primary key 140 | let key; 141 | _.forIn(fields, (field, fieldName) => { // eslint-disable-line consistent-return 142 | if (field.primaryKey) { 143 | key = fieldName; 144 | return false; 145 | } 146 | }); 147 | 148 | // If no primary key, create primary key column 149 | if (!key) { 150 | if (!options.primaryKey) { 151 | throw new Sequelize.DefinerError(`Model '${modelName}' has no primary key defined`); 152 | } 153 | 154 | key = utils.functionValue(options.primaryKey, modelName, definition, definitions); 155 | 156 | if (options.primaryKeyFirst) { 157 | utils.unshift(fields, key, options.primaryKeyAttributes); 158 | } else { 159 | fields[key] = options.primaryKeyAttributes; 160 | } 161 | } 162 | 163 | definition.primaryKey = key; 164 | const primaryKeyDef = {model: modelName, field: key}; 165 | primaryKeys[modelName + utils.uppercaseFirst(key)] = primaryKeyDef; 166 | primaryKeys[utils.lowercaseFirst(modelName) + utils.uppercaseFirst(key)] = primaryKeyDef; 167 | primaryKeys[`${modelName}_${key}`] = primaryKeyDef; 168 | primaryKeys[`${utils.lowercaseFirst(modelName)}_${key}`] = primaryKeyDef; 169 | 170 | // Get table name 171 | if (!options.tableName) { 172 | options.tableName = options.freezeTableName ? modelName : makeTableName(modelName, options); 173 | } 174 | 175 | // Return definition 176 | return definition; 177 | } 178 | 179 | function defineOne(modelName, definition) { 180 | const {fields, options} = definition; 181 | 182 | // Populate details of fields 183 | _.forIn(fields, (params, fieldName) => { 184 | params = _.clone(params); 185 | fields[fieldName] = params; 186 | 187 | let label = fieldName; 188 | 189 | // Process one-to-one/one-to-many associations 190 | let {reference} = params; 191 | if (reference !== undefined && !reference) { 192 | delete params.reference; 193 | delete params.referenceKey; 194 | } else if (reference || (options.autoAssociate && primaryKeys[fieldName])) { 195 | // Field references another model 196 | if (reference) { 197 | if (!definitions[reference]) { 198 | throw new Sequelize.DefinerError( 199 | `Referenced model '${reference}' in field '${fieldName}' does not exist` 200 | ); 201 | } 202 | 203 | if (!params.referenceKey) params.referenceKey = definitions[reference].primaryKey; 204 | 205 | let as; 206 | if (fieldName.endsWith(utils.uppercaseFirst(params.referenceKey))) { 207 | as = fieldName.slice(0, -params.referenceKey.length); 208 | if (!params.as && as !== reference) params.as = as; 209 | label = params.as || as; 210 | } else if (fieldName.endsWith(`_${params.referenceKey}`)) { 211 | as = fieldName.slice(0, -params.referenceKey.length - 1); 212 | if (!params.as && as !== reference) params.as = as; 213 | label = params.as || as; 214 | } else { 215 | label = reference; 216 | } 217 | } else { 218 | reference = primaryKeys[fieldName].model; 219 | params.reference = reference; 220 | if (!params.referenceKey) params.referenceKey = primaryKeys[fieldName].field; 221 | label = reference; 222 | } 223 | 224 | patches.setReferences(params, { 225 | model: definitions[reference].options.tableName, 226 | key: params.referenceKey 227 | }); 228 | 229 | const referenceType = utils.pop(params, 'referenceType') || 'many'; 230 | if (!['one', 'many'].includes(referenceType)) { 231 | throw new Sequelize.DefinerError( 232 | `referenceType must be either 'one' or 'many' in '${modelName}.${fieldName}'` 233 | ); 234 | } 235 | 236 | utils.defaultsNoUndef(params, { 237 | onDelete: options.onDelete, 238 | onUpdate: options.onUpdate 239 | }); 240 | 241 | const relationshipOptions = { 242 | modelName1: modelName, 243 | modelName2: reference, 244 | foreignKey: fieldName, 245 | referenceType: `has${utils.uppercaseFirst(referenceType)}` 246 | }; 247 | 248 | utils.extendNoUndef(relationshipOptions, { 249 | as: params.as, 250 | asReverse: params.asReverse, 251 | onDelete: params.onDelete, 252 | onUpdate: params.onUpdate 253 | }); 254 | 255 | // Set field type according to referred to field 256 | const referenceField = definitions[reference].fields[params.referenceKey]; 257 | if (!referenceField) { 258 | throw new Sequelize.DefinerError( 259 | `referenceKey defined in '${modelName}.${fieldName}' refers to ` 260 | + `nonexistent field '${reference}.${params.referenceKey}'` 261 | ); 262 | } 263 | 264 | params.type = referenceField.type; 265 | 266 | // Store details in relationships array 267 | relationshipsOne.push(relationshipOptions); 268 | } 269 | 270 | // Create label 271 | if (options.labels && !params.label) { 272 | params.label = (label === 'id' ? 'ID' : utils.humanize(label)); 273 | } 274 | }); 275 | 276 | // Process many-to-many relationships 277 | if (definition.manyToMany) { 278 | _.forIn(definition.manyToMany, (optsArr, modelName2) => { 279 | if (!Array.isArray(optsArr)) optsArr = [optsArr]; 280 | 281 | for (let opts of optsArr) { 282 | if (opts === true) opts = {}; 283 | relationshipsMany.push(_.defaults({modelName1: modelName, modelName2}, opts)); 284 | } 285 | }); 286 | } 287 | 288 | // Define model in sequelize 289 | const defineOptions = _.clone(options); // eslint-disable-line no-shadow 290 | [ 291 | 'primaryKey', 'primaryKeyAttributes', 'primaryKeyFirst', 292 | 'autoAssociate', 'labels', 'onDelete', 'onUpdate' 293 | ].forEach((optionName) => { 294 | delete defineOptions[optionName]; 295 | }); 296 | 297 | const model = sequelize.define(modelName, fields, defineOptions); 298 | 299 | // Label createdAt, updatedAt, deletedAt fields 300 | if (options.labels) { 301 | ['createdAt', 'updatedAt', 'deletedAt'].forEach((fieldName) => { 302 | fieldName = model._timestampAttributes[fieldName]; 303 | if (fieldName && model.rawAttributes[fieldName]) { 304 | model.rawAttributes[fieldName].label = utils.humanize(fieldName); 305 | } 306 | }); 307 | } 308 | 309 | // Done 310 | return model; 311 | } 312 | 313 | function defineRelationships() { 314 | // Define relationships 315 | const {models} = sequelize; 316 | 317 | // Define one-to-one/one-to-many relationships 318 | for (const options of relationshipsOne) { 319 | const model1 = models[utils.pop(options, 'modelName1')], 320 | model2 = models[utils.pop(options, 'modelName2')]; 321 | 322 | const asReverse = utils.pop(options, 'asReverse'); 323 | 324 | let joinOptions = _.clone(options); 325 | utils.setNoUndef(joinOptions, 'as', asReverse); 326 | // NB referenceType = 'hasOne' or 'hasMany' 327 | model2[options.referenceType](model1, joinOptions); 328 | 329 | joinOptions = _.clone(options); 330 | model1.belongsTo(model2, options); 331 | } 332 | 333 | // Define many-to-many relationships 334 | for (const options of relationshipsMany) { 335 | utils.defaultsNoUndef(options, { 336 | primaryKeyFirst: defineOptions.primaryKeyFirst, 337 | primaryKeyThrough: defineOptions.primaryKeyThrough, 338 | associate: defineOptions.associateThrough, 339 | skipFields: defineOptions.skipFieldsOnThrough, 340 | labels: defineOptions.labels, 341 | freezeTableName: defineOptions.freezeTableName, 342 | lowercaseTableName: defineOptions.lowercaseTableName, 343 | underscored: defineOptions.underscored, 344 | underscoredAll: defineOptions.underscoredAll, 345 | camel: defineOptions.camelThrough, 346 | onDelete: defineOptions.onDelete, 347 | onUpdate: defineOptions.onUpdate 348 | }); 349 | 350 | const modelName1 = utils.pop(options, 'modelName1'), 351 | modelName2 = utils.pop(options, 'modelName2'), 352 | model1 = models[modelName1], 353 | model2 = models[modelName2]; 354 | 355 | if (!model2) { 356 | throw new Sequelize.DefinerError( 357 | `Model '${modelName2}' referenced in many-to-many association ` 358 | + `in model '${modelName1}' does not exist` 359 | ); 360 | } 361 | if (modelName1 === modelName2 && (!options.as || !options.asReverse)) { 362 | throw new Sequelize.DefinerError( 363 | `'as' and 'asReverse' must be provided for many-to-many self-joins in model '${modelName1}'` 364 | ); 365 | } 366 | 367 | const key1 = definitions[modelName1].primaryKey, 368 | key2 = definitions[modelName2].primaryKey, 369 | foreignKey1 = patches.underscoredIf( 370 | `${modelName1}${utils.uppercaseFirst(key1)}`, 371 | options.underscored 372 | ), 373 | foreignKey2 = patches.underscoredIf( 374 | (modelName1 === modelName2 ? Utils.singularize(options.as) : modelName2) 375 | + utils.uppercaseFirst(key2), 376 | options.underscored 377 | ); 378 | 379 | let through, 380 | throughExisting = false; 381 | if (options.through) { 382 | // Use defined through table 383 | through = models[options.through]; 384 | if (!through) { 385 | throw new Sequelize.DefinerError( 386 | `Model '${options.through}' referenced as through model to '${modelName2}' ` 387 | + `in many-to-many association in model '${modelName1}' does not exist` 388 | ); 389 | } 390 | 391 | throughExisting = true; 392 | } else { 393 | // Make through table 394 | const fields = {}; 395 | fields[foreignKey1] = { 396 | type: definitions[modelName1].fields[key1].type, 397 | allowNull: false, 398 | reference: null, 399 | _autoGenerated: true 400 | }; 401 | fields[foreignKey2] = { 402 | type: definitions[modelName2].fields[key2].type, 403 | allowNull: false, 404 | reference: null, 405 | _autoGenerated: true 406 | }; 407 | 408 | let modelNameAs = options.as ? Utils.singularize(options.as) : modelName2; 409 | if (options.camel) modelNameAs = utils.uppercaseFirst(modelNameAs); 410 | const modelName = patches.underscoredIf( 411 | `${modelName1}${modelNameAs}`, 412 | options.underscoredAll 413 | ); 414 | const tableName = options.tableName || ( 415 | options.freezeTableName 416 | ? modelName 417 | : makeTableName(Utils.pluralize(modelName1) + modelNameAs, options) 418 | ); 419 | 420 | if (models[modelName]) { 421 | throw new Sequelize.DefinerError( 422 | `Through model '${modelName}' between '${modelName1}' and ` 423 | + `'${modelName2}${options.as ? ` (${options.as})` : ''}' already exists` 424 | ); 425 | } 426 | 427 | const throughOptions = { 428 | tableName, 429 | labels: false, 430 | skipFields: options.skipFields 431 | }; 432 | 433 | const definition = prepOne(modelName, {fields, options: throughOptions}); 434 | through = defineOne(modelName, definition); 435 | } 436 | options.through = through; 437 | 438 | if (options.primaryKeyThrough) { 439 | delete through.rawAttributes[options.through.primaryKeyAttribute]._autoGenerated; 440 | } 441 | 442 | const asReverse = utils.pop(options, 'asReverse'); 443 | 444 | let joinOptions = _.clone(options); 445 | joinOptions.foreignKey = foreignKey1; 446 | model1.belongsToMany(model2, joinOptions); 447 | 448 | joinOptions = _.clone(options); 449 | utils.setNoUndef(joinOptions, 'as', asReverse); 450 | joinOptions.foreignKey = foreignKey2; 451 | model2.belongsToMany(model1, joinOptions); 452 | 453 | // Move keys to first fields 454 | if (options.primaryKeyFirst && throughExisting) { 455 | const pos = options.primaryKeyThrough ? 1 : 0; 456 | utils.objectSplice( 457 | through.attributes, foreignKey2, utils.pop(through.attributes, foreignKey2), pos 458 | ); 459 | utils.objectSplice( 460 | through.attributes, foreignKey1, utils.pop(through.attributes, foreignKey1), pos 461 | ); 462 | utils.objectSplice( 463 | through.tableAttributes, 464 | foreignKey2, 465 | utils.pop(through.tableAttributes, foreignKey2), 466 | pos 467 | ); 468 | utils.objectSplice( 469 | through.tableAttributes, 470 | foreignKey1, 471 | utils.pop(through.tableAttributes, foreignKey1), 472 | pos 473 | ); 474 | } 475 | 476 | // Create labels 477 | if (options.labels) { 478 | through.attributes[foreignKey1].label = utils.humanize(modelName1); 479 | through.attributes[foreignKey2].label = utils.humanize(modelName2); 480 | } 481 | 482 | // Associate through model to dependents 483 | if (options.associate) { 484 | through.belongsTo(model1, {foreignKey: foreignKey1}); 485 | through.belongsTo(model2, {foreignKey: foreignKey2}); 486 | through.rawAttributes[foreignKey1].reference = modelName1; 487 | through.rawAttributes[foreignKey2].reference = modelName2; 488 | } 489 | } 490 | } 491 | 492 | function makeTableName(modelName, options) { 493 | let tableName = modelName; 494 | if (options.lowercaseTableName) tableName = utils.lowercaseFirst(tableName); 495 | return patches.underscoredIf(Utils.pluralize(tableName), options.underscoredAll); 496 | } 497 | }; 498 | -------------------------------------------------------------------------------- /lib/defineFromFolder.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Sequelize#defineFromFolder() method 4 | * ------------------*/ 5 | 6 | 'use strict'; 7 | 8 | // Modules 9 | const _ = require('lodash'), 10 | requireFolderTree = require('require-folder-tree'); 11 | 12 | // Imports 13 | const utils = require('./utils.js'); 14 | 15 | // Exports 16 | 17 | /** 18 | * Define a set of models at once from a folder. 19 | * Called as `sequelize.defineFromFolder()`. 20 | * @param {string} path - Path to folder containing model definitions 21 | * @param {Object} [options] - Options object 22 | * @returns {Object} - Sequelize instance 23 | */ 24 | module.exports = function(path, options) { 25 | options = _.clone(options || {}); 26 | 27 | const loadOptions = _.clone(utils.pop(options, 'loadOptions') || {}); 28 | loadOptions.flatten = true; 29 | 30 | const definitions = requireFolderTree(path, loadOptions); 31 | 32 | return this.defineAll(definitions, options); 33 | }; 34 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Errors 4 | * ------------------*/ 5 | 6 | 'use strict'; 7 | 8 | // Modules 9 | const util = require('util'); 10 | 11 | // Exports 12 | module.exports = { 13 | init(Sequelize) { 14 | // Define errors 15 | const errors = {}; 16 | 17 | // General error for all definer errors 18 | errors.DefinerError = function(message) { 19 | Sequelize.Error.call(this, message); 20 | this.name = 'SequelizeDefinerError'; 21 | }; 22 | util.inherits(errors.DefinerError, Sequelize.Error); 23 | 24 | // Alias for error for backward-compatibility 25 | errors.SequelizeDefinerError = errors.DefinerError; 26 | 27 | // Add errors to Sequelize and sequelize 28 | Object.assign(Sequelize, errors); 29 | Object.assign(Sequelize.prototype, errors); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Entry point 4 | * ------------------*/ 5 | 6 | 'use strict'; 7 | 8 | // Imports 9 | const errors = require('./errors.js'), 10 | defineAll = require('./defineAll.js'), 11 | defineFromFolder = require('./defineFromFolder.js'); 12 | 13 | // Exports 14 | module.exports = function(Sequelize) { 15 | if (!Sequelize) Sequelize = require('sequelize'); // eslint-disable-line global-require 16 | 17 | // Add custom errors to Sequelize 18 | errors.init(Sequelize); 19 | 20 | // Add methods to sequelize 21 | Object.assign(Sequelize.prototype, { 22 | defineAll, 23 | defineFromFolder 24 | }); 25 | 26 | // Return Sequelize 27 | return Sequelize; 28 | }; 29 | -------------------------------------------------------------------------------- /lib/patches.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Patched versions of sequelize methods 4 | * which unify interface across Sequlize versions 5 | * ------------------*/ 6 | 7 | 'use strict'; 8 | 9 | // Modules 10 | const semverSelect = require('semver-select'); 11 | 12 | const sequelizeVersionImported = require('sequelize/package.json').version; 13 | 14 | // Exports 15 | 16 | // function to define patches 17 | module.exports = function(Sequelize) { 18 | // Get Sequelize version 19 | const sequelizeVersion = Sequelize.version || sequelizeVersionImported; 20 | 21 | // Define patches 22 | return semverSelect.object(sequelizeVersion, { 23 | setReferences: { 24 | '<3.0.1': function(params, references) { 25 | params.references = references.model; 26 | params.referencesKey = references.key; 27 | }, 28 | '*': function(params, references) { 29 | params.references = references; 30 | } 31 | }, 32 | 33 | underscoredIf: { 34 | '<3.5.0': Sequelize.Utils._ && Sequelize.Utils._.underscoredIf, 35 | '*': Sequelize.Utils.underscoredIf 36 | } 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Utility functions 4 | * ------------------*/ 5 | 6 | 'use strict'; 7 | 8 | // Modules 9 | const _ = require('lodash'); 10 | 11 | // Exports 12 | 13 | module.exports = { 14 | objectSplice, 15 | unshift, 16 | pop, 17 | setNoUndef, 18 | defaultNoUndef, 19 | extendNoUndef, 20 | defaultsNoUndef, 21 | humanize, 22 | uppercaseFirst, 23 | lowercaseFirst, 24 | functionValue 25 | }; 26 | 27 | // Acts on object, inserts key/value pair at position specified by index (integer) 28 | function objectSplice(obj, key, value, index) { 29 | // Extract values after index position 30 | const temp = {}; 31 | if (index === undefined) index = 0; 32 | 33 | let i = 0; 34 | for (const thisKey in obj) { 35 | if (i >= index) { 36 | temp[thisKey] = obj[thisKey]; 37 | delete obj[thisKey]; 38 | } 39 | i++; 40 | } 41 | 42 | // Insert new key/value 43 | obj[key] = value; 44 | 45 | // Return values back to obj 46 | for (const thisKey in temp) { 47 | obj[thisKey] = temp[thisKey]; 48 | } 49 | 50 | // Done 51 | return obj; 52 | } 53 | 54 | // Acts on object, inserts key/value pair at 1st position 55 | function unshift(obj, key, value) { 56 | return objectSplice(obj, key, value, 0); 57 | } 58 | 59 | // Removes obj[key] and returns it 60 | function pop(obj, key) { 61 | const value = obj[key]; 62 | delete obj[key]; 63 | return value; 64 | } 65 | 66 | // setNoUndef(obj, key [,value] [,value] [,value]) 67 | // Sets obj[key] = value, but deleting undefined values 68 | function setNoUndef(obj, key, ...values) { 69 | for (const value of values) { 70 | if (value !== undefined) { 71 | obj[key] = value; 72 | return obj; 73 | } 74 | } 75 | 76 | delete obj[key]; 77 | 78 | return obj; 79 | } 80 | 81 | // defaultNoUndef(obj, key [,value] [,value] [,value]) 82 | // Sets obj[key] = value if obj[key] is undefined 83 | function defaultNoUndef(obj, key, ...values) { 84 | if (obj[key] === undefined) { 85 | for (const value of values) { 86 | if (value !== undefined) { 87 | obj[key] = value; 88 | break; 89 | } 90 | } 91 | } 92 | 93 | return obj; 94 | } 95 | 96 | // extendNoUndef: function(obj [,fromObj] [,fromObj] [,fromObj]) 97 | // Sets all keys from fromObj onto obj, deleting undefined values 98 | function extendNoUndef(obj, ...fromObjs) { 99 | for (const fromObj of fromObjs) { 100 | _.forIn(fromObj, (value, key) => { 101 | if (value === undefined) { 102 | delete obj[key]; 103 | } else { 104 | obj[key] = value; 105 | } 106 | }); 107 | } 108 | 109 | return obj; 110 | } 111 | 112 | // defaultsNoUndef: function(obj [,fromObj] [,fromObj] [,fromObj]) 113 | // Sets all keys from fromObj onto obj where obj[key] is undefined, ignoring undefined values 114 | function defaultsNoUndef(obj, ...fromObjs) { 115 | for (const fromObj of fromObjs) { 116 | _.forIn(fromObj, (value, key) => { 117 | if (obj[key] === undefined && value !== undefined) obj[key] = value; 118 | }); 119 | } 120 | 121 | return obj; 122 | } 123 | 124 | // String format conversion from camelCase or underscored format to human-readable format 125 | // e.g. 'fooBar' -> 'Foo Bar', 'foo_bar' -> 'Foo Bar' 126 | function humanize(str) { 127 | if (str == null || str === '') return ''; 128 | str = (`${str}`).replace(/[-_\s]+(.)?/g, (match, c) => (c ? c.toUpperCase() : '')); 129 | return str.slice(0, 1).toUpperCase() + str.slice(1).replace(/([A-Z])/g, ' $1'); 130 | } 131 | 132 | function uppercaseFirst(str) { 133 | return str.slice(0, 1).toUpperCase() + str.slice(1); 134 | } 135 | 136 | function lowercaseFirst(str) { 137 | return str.slice(0, 1).toLowerCase() + str.slice(1); 138 | } 139 | 140 | // If value is function, run it with arguments and return result, otherwise return input 141 | function functionValue(val, ...args) { 142 | if (!_.isFunction(val)) return val; 143 | return val(...args); 144 | } 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequelize-definer", 3 | "version": "0.6.2", 4 | "description": "Sequelize plugin to help easily define a set of models", 5 | "main": "./index.js", 6 | "files": [ 7 | "index.js", 8 | "lib/**/*.js" 9 | ], 10 | "author": { 11 | "name": "Overlook Motel" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/overlookmotel/sequelize-definer.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/overlookmotel/sequelize-definer/issues" 19 | }, 20 | "dependencies": { 21 | "lodash": "^4.17.21", 22 | "require-folder-tree": "^1.4.7", 23 | "semver-select": "^1.1.0" 24 | }, 25 | "peerDependencies": { 26 | "sequelize": "2.0.0 - 5.x.x" 27 | }, 28 | "devDependencies": { 29 | "@overlookmotel/eslint-config": "^10.1.0", 30 | "@overlookmotel/eslint-config-node": "^4.1.0", 31 | "@overlookmotel/eslint-config-tests": "^6.0.0", 32 | "chai": "^4.3.7", 33 | "chai-as-promised": "^7.1.1", 34 | "coveralls": "^3.1.1", 35 | "cross-env": "^7.0.3", 36 | "eslint": "^8.38.0", 37 | "istanbul": "^0.4.5", 38 | "mocha": "^10.2.0", 39 | "mysql2": "^2.1.0", 40 | "npm-run-all": "^4.1.5", 41 | "pg": "^8.0.0", 42 | "pg-hstore": "^2.3.3", 43 | "sequelize": "2.0.0 - 5.x.x", 44 | "sqlite3": "^4.1.1", 45 | "tedious": "8.2.0" 46 | }, 47 | "keywords": [ 48 | "sequelize", 49 | "sequelize-plugin", 50 | "define", 51 | "model" 52 | ], 53 | "scripts": { 54 | "test": "npm run lint && npm run test-main", 55 | "lint": "eslint .", 56 | "lint-fix": "eslint . --fix", 57 | "test-mysql": "cross-env DIALECT=mysql npm run test-main", 58 | "test-postgres": "cross-env DIALECT=postgres npm run test-main", 59 | "test-postgres-native": "cross-env DIALECT=postgres-native npm run test-main", 60 | "test-sqlite": "cross-env DIALECT=sqlite npm run test-main", 61 | "test-mssql": "cross-env DIALECT=mssql npm run test-main", 62 | "test-main": "mocha --check-leaks --colors -t 10000 -R spec 'test/**/*.test.js'", 63 | "cover": "npm-run-all -c cover-main cover-cleanup", 64 | "coveralls": "npm run cover-main && cat ./coverage/lcov.info | coveralls; npm run cover-cleanup", 65 | "cover-main": "cross-env COVERAGE=true istanbul cover _mocha --report lcovonly -- -t 30000 -R spec \"test/**/*.test.js\"", 66 | "cover-cleanup": "rm -rf coverage", 67 | "ci": "if [ $COVERAGE ]; then npm run coveralls; else npm test; fi" 68 | }, 69 | "engines": { 70 | "node": ">=10" 71 | }, 72 | "readmeFilename": "README.md", 73 | "license": "MIT" 74 | } 75 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * Sequelize definer 3 | * ESLint tests config 4 | * ------------------*/ 5 | 6 | 'use strict'; 7 | 8 | // Exports 9 | 10 | module.exports = { 11 | extends: [ 12 | '@overlookmotel/eslint-config-tests' 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /test/all.test.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * Sequelize definer 3 | * Tests 4 | * ------------------*/ 5 | 6 | /* eslint-disable no-invalid-this */ 7 | 8 | 'use strict'; 9 | 10 | // Modules 11 | const chai = require('chai'), 12 | {expect} = chai, 13 | promised = require('chai-as-promised'), 14 | _ = require('lodash'), 15 | pathModule = require('path'), 16 | semverSelect = require('semver-select'); 17 | 18 | // Imports 19 | const Support = require('./support.js'), 20 | {Sequelize} = Support; 21 | 22 | // Init 23 | chai.use(promised); 24 | chai.config.includeStack = true; 25 | 26 | // Tests 27 | 28 | // eslint-disable-next-line global-require 29 | const sequelizeVersion = Sequelize.version || require('sequelize/package.json').version; 30 | 31 | console.log('Sequelize version:', sequelizeVersion); // eslint-disable-line no-console 32 | 33 | describe(Support.getTestDialectTeaser('Tests'), () => { 34 | beforeEach(function() { 35 | this.models = this.sequelize.models; 36 | 37 | const removeModel = semverSelect(sequelizeVersion, { 38 | '<3.0.0': this.sequelize.modelManager.removeDAO, 39 | '*': this.sequelize.modelManager.removeModel 40 | }).bind(this.sequelize.modelManager); 41 | 42 | _.forIn(this.models, (model) => { 43 | removeModel(model); 44 | }); 45 | }); 46 | 47 | describe('defineAll', () => { 48 | describe('defines', () => { 49 | beforeEach(function() { 50 | this.definitions = { 51 | User: { 52 | fields: { 53 | name: Sequelize.STRING(50) 54 | } 55 | }, 56 | Task: { 57 | fields: { 58 | name: Sequelize.STRING(50) 59 | } 60 | } 61 | }; 62 | 63 | this.sequelize.defineAll(this.definitions); 64 | }); 65 | 66 | it('defines all models', function() { 67 | _.forIn(this.definitions, (definition, modelName) => { 68 | expect(this.models[modelName]).to.be.ok; 69 | expect(this.models[modelName].tableName).to.equal(`${modelName}s`); 70 | }); 71 | }); 72 | 73 | it('creates primary keys', function() { 74 | _.forIn(this.definitions, (definition, modelName) => { 75 | expect(this.models[modelName].rawAttributes.id).to.be.ok; 76 | expect(this.models[modelName].rawAttributes.id.primaryKey).to.be.true; 77 | expect(this.models[modelName].rawAttributes.id.autoIncrement).to.be.true; 78 | }); 79 | }); 80 | }); 81 | 82 | describe('associations', () => { 83 | describe('one-to-one', () => { 84 | it('creates association', function() { 85 | const definitions = { 86 | User: { 87 | fields: { 88 | name: Sequelize.STRING(50) 89 | } 90 | }, 91 | Task: { 92 | fields: { 93 | name: Sequelize.STRING(50), 94 | UserId: { 95 | reference: 'User', 96 | referenceType: 'one' 97 | } 98 | } 99 | } 100 | }; 101 | 102 | this.sequelize.defineAll(definitions); 103 | 104 | const {models} = this; 105 | expect(models.Task.associations.User).to.be.ok; 106 | expect(models.Task.associations.User.target).to.equal(models.User); 107 | expect(models.Task.associations.User.isSingleAssociation).to.be.true; 108 | 109 | expect(models.User.associations.Task).to.be.ok; 110 | expect(models.User.associations.Task.target).to.equal(models.Task); 111 | expect(models.User.associations.Task.isSingleAssociation).to.be.true; 112 | }); 113 | 114 | it('uses as and asReverse', function() { 115 | const definitions = { 116 | User: { 117 | fields: { 118 | name: Sequelize.STRING(50) 119 | } 120 | }, 121 | Task: { 122 | fields: { 123 | name: Sequelize.STRING(50), 124 | UserId: { 125 | reference: 'User', 126 | referenceType: 'one', 127 | as: 'Owner', 128 | asReverse: 'OwnedTask' 129 | } 130 | } 131 | } 132 | }; 133 | 134 | this.sequelize.defineAll(definitions); 135 | 136 | const {models} = this; 137 | expect(models.Task.associations.Owner).to.be.ok; 138 | expect(models.Task.associations.Owner.target).to.equal(models.User); 139 | expect(models.Task.associations.Owner.as).to.equal('Owner'); 140 | 141 | expect(models.User.associations.OwnedTask).to.be.ok; 142 | expect(models.User.associations.OwnedTask.target).to.equal(models.Task); 143 | expect(models.User.associations.OwnedTask.as).to.equal('OwnedTask'); 144 | }); 145 | }); 146 | 147 | describe('one-to-many', () => { 148 | it('creates association', function() { 149 | const definitions = { 150 | User: { 151 | fields: { 152 | name: Sequelize.STRING(50) 153 | } 154 | }, 155 | Task: { 156 | fields: { 157 | name: Sequelize.STRING(50), 158 | UserId: { 159 | reference: 'User' 160 | } 161 | } 162 | } 163 | }; 164 | 165 | this.sequelize.defineAll(definitions); 166 | 167 | const {models} = this; 168 | let associations = _.values(models.Task.associations), 169 | association = associations[0]; 170 | expect(associations).to.have.length(1); 171 | expect(association.target).to.equal(models.User); 172 | expect(association.as).to.equal('User'); 173 | expect(association.isSingleAssociation).to.be.true; 174 | 175 | associations = _.values(models.User.associations); 176 | association = associations[0]; 177 | expect(associations).to.have.length(1); 178 | expect(association.target).to.equal(models.Task); 179 | expect(association.as).to.equal('Tasks'); 180 | expect(association.isMultiAssociation).to.be.true; 181 | }); 182 | 183 | it('deduces foreign key', function() { 184 | const definitions = { 185 | User: { 186 | fields: { 187 | name: Sequelize.STRING(50) 188 | } 189 | }, 190 | Task: { 191 | fields: { 192 | name: Sequelize.STRING(50), 193 | WorkerId: { 194 | reference: 'User' 195 | } 196 | } 197 | } 198 | }; 199 | 200 | this.sequelize.defineAll(definitions); 201 | 202 | const {models} = this; 203 | let associations = _.values(models.Task.associations), 204 | association = associations[0]; 205 | expect(associations).to.have.length(1); 206 | expect(association.target).to.equal(models.User); 207 | expect(association.as).to.equal('Worker'); 208 | expect(association.isSingleAssociation).to.be.true; 209 | 210 | associations = _.values(models.User.associations); 211 | association = associations[0]; 212 | expect(associations).to.have.length(1); 213 | expect(association.target).to.equal(models.Task); 214 | expect(association.as).to.equal('Tasks'); 215 | expect(association.isMultiAssociation).to.be.true; 216 | }); 217 | 218 | it('uses as and asReverse', function() { 219 | const definitions = { 220 | User: { 221 | fields: { 222 | name: Sequelize.STRING(50) 223 | } 224 | }, 225 | Task: { 226 | fields: { 227 | name: Sequelize.STRING(50), 228 | UserId: { 229 | reference: 'User', 230 | as: 'Owner', 231 | asReverse: 'OwnedTasks' 232 | } 233 | } 234 | } 235 | }; 236 | 237 | this.sequelize.defineAll(definitions); 238 | 239 | const {models} = this; 240 | expect(models.Task.associations.Owner).to.be.ok; 241 | expect(models.Task.associations.Owner.target).to.equal(models.User); 242 | expect(models.Task.associations.Owner.as).to.equal('Owner'); 243 | 244 | expect(models.User.associations.OwnedTasks).to.be.ok; 245 | expect(models.User.associations.OwnedTasks.target).to.equal(models.Task); 246 | expect(models.User.associations.OwnedTasks.as).to.equal('OwnedTasks'); 247 | }); 248 | }); 249 | 250 | describe('many-to-many', () => { 251 | it('creates association', function() { 252 | const definitions = { 253 | User: { 254 | fields: { 255 | name: Sequelize.STRING(50) 256 | } 257 | }, 258 | Task: { 259 | fields: { 260 | name: Sequelize.STRING(50) 261 | }, 262 | manyToMany: { 263 | User: true 264 | } 265 | } 266 | }; 267 | 268 | this.sequelize.defineAll(definitions); 269 | 270 | const {models} = this; 271 | let associations = _.values(models.Task.associations), 272 | association = associations[0]; 273 | expect(associations).to.have.length(1); 274 | expect(association.target).to.equal(models.User); 275 | expect(association.as).to.equal('Users'); 276 | expect(association.isMultiAssociation).to.be.true; 277 | 278 | associations = _.values(models.User.associations); 279 | association = associations[0]; 280 | expect(associations).to.have.length(1); 281 | expect(association.target).to.equal(models.Task); 282 | expect(association.as).to.equal('Tasks'); 283 | expect(association.isMultiAssociation).to.be.true; 284 | 285 | expect(models.TaskUser).to.be.ok; 286 | expect(models.TaskUser.rawAttributes.TaskId).to.be.ok; 287 | expect(models.TaskUser.rawAttributes.UserId).to.be.ok; 288 | expect(models.TaskUser.rawAttributes.id).not.to.exist; 289 | }); 290 | 291 | it('uses as and asReverse', function() { 292 | const definitions = { 293 | User: { 294 | fields: { 295 | name: Sequelize.STRING(50) 296 | } 297 | }, 298 | Task: { 299 | fields: { 300 | name: Sequelize.STRING(50) 301 | }, 302 | manyToMany: { 303 | User: { 304 | as: 'Owners', 305 | asReverse: 'OwnedTasks' 306 | } 307 | } 308 | } 309 | }; 310 | 311 | this.sequelize.defineAll(definitions); 312 | 313 | const {models} = this; 314 | expect(models.Task.associations.Owners).to.be.ok; 315 | expect(models.Task.associations.Owners.target).to.equal(models.User); 316 | expect(models.Task.associations.Owners.as).to.equal('Owners'); 317 | 318 | expect(models.User.associations.OwnedTasks).to.be.ok; 319 | expect(models.User.associations.OwnedTasks.target).to.equal(models.Task); 320 | expect(models.User.associations.OwnedTasks.as).to.equal('OwnedTasks'); 321 | 322 | expect(models.TaskOwner).to.be.ok; 323 | expect(models.TaskOwner.rawAttributes.UserId).to.be.ok; 324 | expect(models.TaskOwner.rawAttributes.TaskId).to.be.ok; 325 | expect(models.TaskOwner.rawAttributes.id).not.to.exist; 326 | }); 327 | 328 | it('uses through', function() { 329 | const definitions = { 330 | User: { 331 | fields: { 332 | name: Sequelize.STRING(50) 333 | } 334 | }, 335 | Task: { 336 | fields: { 337 | name: Sequelize.STRING(50) 338 | }, 339 | manyToMany: { 340 | User: { 341 | through: 'Join' 342 | } 343 | } 344 | }, 345 | Join: { 346 | fields: { 347 | status: Sequelize.STRING(50) 348 | } 349 | } 350 | }; 351 | 352 | this.sequelize.defineAll(definitions); 353 | 354 | const {models} = this; 355 | let associations = _.values(models.Task.associations), 356 | association = associations[0]; 357 | expect(associations).to.have.length(1); 358 | expect(association.target).to.equal(models.User); 359 | expect(association.as).to.equal('Users'); 360 | expect(association.isMultiAssociation).to.be.true; 361 | 362 | associations = _.values(models.User.associations); 363 | association = associations[0]; 364 | expect(associations).to.have.length(1); 365 | expect(association.target).to.equal(models.Task); 366 | expect(association.as).to.equal('Tasks'); 367 | expect(association.isMultiAssociation).to.be.true; 368 | 369 | expect(models.Join.rawAttributes.UserId).to.be.ok; 370 | expect(models.Join.rawAttributes.TaskId).to.be.ok; 371 | expect(models.Join.rawAttributes.id).not.to.exist; 372 | }); 373 | 374 | it('handles self-association', function() { 375 | const definitions = { 376 | Task: { 377 | fields: { 378 | name: Sequelize.STRING(50) 379 | }, 380 | manyToMany: { 381 | Task: { 382 | through: 'Join', 383 | as: 'DoBefores', 384 | asReverse: 'DoAfters' 385 | } 386 | } 387 | }, 388 | Join: { 389 | fields: { 390 | status: Sequelize.STRING(50) 391 | } 392 | } 393 | }; 394 | 395 | this.sequelize.defineAll(definitions); 396 | 397 | const {models} = this; 398 | const associations = _.values(models.Task.associations); 399 | expect(associations).to.have.length(2); 400 | 401 | let association = associations[0]; 402 | expect(association.target).to.equal(models.Task); 403 | expect(association.as).to.equal('DoBefores'); 404 | expect(association.isMultiAssociation).to.be.true; 405 | 406 | association = associations[1]; 407 | expect(association.target).to.equal(models.Task); 408 | expect(association.as).to.equal('DoAfters'); 409 | expect(association.isMultiAssociation).to.be.true; 410 | 411 | expect(models.Join.rawAttributes.TaskId).to.be.ok; 412 | expect(models.Join.rawAttributes.DoBeforeId).to.be.ok; 413 | expect(models.Join.rawAttributes.id).not.to.exist; 414 | }); 415 | 416 | describe('options', () => { 417 | it('primaryKeyThrough', function() { 418 | const definitions = { 419 | User: { 420 | fields: { 421 | name: Sequelize.STRING(50) 422 | } 423 | }, 424 | Task: { 425 | fields: { 426 | name: Sequelize.STRING(50) 427 | }, 428 | manyToMany: { 429 | User: true 430 | } 431 | } 432 | }; 433 | 434 | this.sequelize.defineAll(definitions, {primaryKeyThrough: true}); 435 | 436 | const {models} = this; 437 | expect(models.TaskUser.rawAttributes.id).to.be.ok; 438 | }); 439 | 440 | it('associateThrough', function() { 441 | const definitions = { 442 | User: { 443 | fields: { 444 | name: Sequelize.STRING(50) 445 | } 446 | }, 447 | Task: { 448 | fields: { 449 | name: Sequelize.STRING(50) 450 | }, 451 | manyToMany: { 452 | User: true 453 | } 454 | } 455 | }; 456 | 457 | this.sequelize.defineAll(definitions, {associateThrough: true}); 458 | 459 | const {models} = this; 460 | expect(models.TaskUser.associations.User).to.be.ok; 461 | expect(models.TaskUser.associations.User.target).to.equal(models.User); 462 | expect(models.TaskUser.associations.User.as).to.equal('User'); 463 | 464 | expect(models.TaskUser.associations.Task).to.be.ok; 465 | expect(models.TaskUser.associations.Task.target).to.equal(models.Task); 466 | expect(models.TaskUser.associations.Task.as).to.equal('Task'); 467 | }); 468 | 469 | describe('skipFieldsOnThrough', () => { 470 | it('fields added if false', function() { 471 | const definitions = { 472 | User: { 473 | fields: { 474 | name: Sequelize.STRING(50) 475 | } 476 | }, 477 | Task: { 478 | fields: { 479 | name: Sequelize.STRING(50) 480 | }, 481 | manyToMany: { 482 | User: true 483 | } 484 | } 485 | }; 486 | 487 | this.sequelize.defineAll(definitions, {fields: {moon: Sequelize.STRING}}); 488 | 489 | expect(this.models.TaskUser.rawAttributes.moon).to.be.ok; 490 | }); 491 | 492 | it('fields not added if true', function() { 493 | const definitions = { 494 | User: { 495 | fields: { 496 | name: Sequelize.STRING(50) 497 | } 498 | }, 499 | Task: { 500 | fields: { 501 | name: Sequelize.STRING(50) 502 | }, 503 | manyToMany: { 504 | User: true 505 | } 506 | } 507 | }; 508 | 509 | this.sequelize.defineAll(definitions, { 510 | fields: {moon: Sequelize.STRING}, 511 | skipFieldsOnThrough: true 512 | }); 513 | 514 | expect(this.models.TaskUser.rawAttributes.moon).not.to.exist; 515 | }); 516 | }); 517 | 518 | it('camelThrough', function() { 519 | const definitions = { 520 | user: { 521 | fields: { 522 | name: Sequelize.STRING(50) 523 | } 524 | }, 525 | task: { 526 | fields: { 527 | name: Sequelize.STRING(50) 528 | }, 529 | manyToMany: { 530 | user: true 531 | } 532 | } 533 | }; 534 | 535 | this.sequelize.defineAll(definitions, {camelThrough: true}); 536 | 537 | expect(this.models.taskUser).to.be.ok; 538 | }); 539 | }); 540 | }); 541 | }); 542 | 543 | describe('options', () => { 544 | it('primaryKey', function() { 545 | const definitions = { 546 | User: { 547 | fields: { 548 | name: Sequelize.STRING(50) 549 | } 550 | } 551 | }; 552 | 553 | this.sequelize.defineAll(definitions, {primaryKey: 'foo'}); 554 | 555 | expect(this.models.User.rawAttributes.foo).to.be.ok; 556 | expect(this.models.User.rawAttributes.foo.primaryKey).to.be.true; 557 | }); 558 | 559 | it('primaryKeyType', function() { 560 | const definitions = { 561 | User: { 562 | fields: { 563 | name: Sequelize.STRING(50) 564 | } 565 | } 566 | }; 567 | 568 | this.sequelize.defineAll(definitions, {primaryKeyType: Sequelize.STRING(10)}); 569 | 570 | expect(this.models.User.rawAttributes.id).to.be.ok; 571 | expect(this.models.User.rawAttributes.id.primaryKey).to.be.true; 572 | expect(this.models.User.rawAttributes.id.type).to.be.instanceof(Sequelize.STRING); 573 | }); 574 | 575 | it('primaryKeyAttributes', function() { 576 | const definitions = { 577 | User: { 578 | fields: { 579 | name: Sequelize.STRING(50) 580 | } 581 | } 582 | }; 583 | 584 | this.sequelize.defineAll(definitions, {primaryKeyAttributes: {foo: 'bar'}}); 585 | 586 | expect(this.models.User.rawAttributes.id).to.be.ok; 587 | expect(this.models.User.rawAttributes.id.primaryKey).to.be.true; 588 | expect(this.models.User.rawAttributes.id.foo).to.equal('bar'); 589 | }); 590 | 591 | it('primaryKeyFirst', function() { 592 | const definitions = { 593 | User: { 594 | fields: { 595 | name: Sequelize.STRING(50) 596 | } 597 | } 598 | }; 599 | 600 | this.sequelize.defineAll(definitions, {primaryKeyFirst: true}); 601 | 602 | expect(this.models.User.rawAttributes.id).to.be.ok; 603 | expect(this.models.User.rawAttributes.id.primaryKey).to.be.true; 604 | expect(Object.keys(this.models.User.rawAttributes)[0]).to.equal('id'); 605 | }); 606 | 607 | it('autoAssociate', function() { 608 | const definitions = { 609 | User: { 610 | fields: { 611 | name: Sequelize.STRING(50) 612 | } 613 | }, 614 | Task: { 615 | fields: { 616 | name: Sequelize.STRING(50), 617 | UserId: {} 618 | } 619 | } 620 | }; 621 | 622 | this.sequelize.defineAll(definitions, {autoAssociate: true}); 623 | 624 | const {models} = this; 625 | let associations = _.values(models.Task.associations), 626 | association = associations[0]; 627 | expect(associations).to.have.length(1); 628 | expect(association.target).to.equal(models.User); 629 | expect(association.as).to.equal('User'); 630 | expect(association.isSingleAssociation).to.be.true; 631 | 632 | associations = _.values(models.User.associations); 633 | association = associations[0]; 634 | expect(associations).to.have.length(1); 635 | expect(association.target).to.equal(models.Task); 636 | expect(association.as).to.equal('Tasks'); 637 | expect(association.isMultiAssociation).to.be.true; 638 | }); 639 | 640 | it('fields', function() { 641 | const definitions = { 642 | User: { 643 | fields: { 644 | name: Sequelize.STRING(50) 645 | } 646 | } 647 | }; 648 | 649 | this.sequelize.defineAll(definitions, {fields: {moon: Sequelize.STRING}}); 650 | 651 | expect(this.models.User.rawAttributes.moon).to.be.ok; 652 | }); 653 | 654 | it('labels', function() { 655 | const definitions = { 656 | User: { 657 | fields: { 658 | name: Sequelize.STRING(50), 659 | numberOfUnits: Sequelize.INTEGER 660 | } 661 | } 662 | }; 663 | 664 | this.sequelize.defineAll(definitions, {labels: true}); 665 | 666 | const attributes = this.models.User.rawAttributes; 667 | expect(attributes.id.label).to.equal('ID'); 668 | expect(attributes.name.label).to.equal('Name'); 669 | expect(attributes.numberOfUnits.label).to.equal('Number Of Units'); 670 | expect(attributes.createdAt.label).to.equal('Created At'); 671 | expect(attributes.updatedAt.label).to.equal('Updated At'); 672 | }); 673 | 674 | it('freezeTableName', function() { 675 | const definitions = { 676 | User: { 677 | fields: { 678 | name: Sequelize.STRING(50) 679 | } 680 | } 681 | }; 682 | 683 | this.sequelize.defineAll(definitions, {freezeTableName: true}); 684 | 685 | expect(this.models.User.tableName).to.equal('User'); 686 | }); 687 | }); 688 | }); 689 | 690 | describe('defineFromFolder', () => { 691 | it('defines all models', function() { 692 | this.sequelize.defineFromFolder(pathModule.join(__dirname, './example')); 693 | 694 | expect(this.models.Task2).to.be.ok; 695 | expect(this.models.User2).to.be.ok; 696 | }); 697 | }); 698 | }); 699 | -------------------------------------------------------------------------------- /test/config/config.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Tests config 4 | * ------------------*/ 5 | 6 | /* eslint-disable indent, key-spacing, no-multi-spaces, object-shorthand, prefer-template, no-bitwise, max-len */ 7 | 8 | 'use strict'; 9 | 10 | module.exports = { 11 | username: process.env.SEQ_USER || 'sequelize_test', 12 | password: process.env.SEQ_PW || null, 13 | database: process.env.SEQ_DB || 'sequelize_test', 14 | host: process.env.SEQ_HOST || '127.0.0.1', 15 | pool: { 16 | maxConnections: process.env.SEQ_POOL_MAX || 5, 17 | maxIdleTime: process.env.SEQ_POOL_IDLE || 30000 18 | }, 19 | 20 | rand: function() { 21 | return parseInt(Math.random() * 999, 10); 22 | }, 23 | 24 | mssql: { 25 | database: process.env.SEQ_MSSQL_DB || process.env.SEQ_DB || ('sequelize-test-' + ~~(Math.random() * 100)), 26 | username: process.env.SEQ_MSSQL_USER || process.env.SEQ_USER || 'sequelize', 27 | password: process.env.SEQ_MSSQL_PW || process.env.SEQ_PW || 'nEGkLma26gXVHFUAHJxcmsrK', 28 | host: process.env.SEQ_MSSQL_HOST || process.env.SEQ_HOST || 'mssql.sequelizejs.com', 29 | port: process.env.SEQ_MSSQL_PORT || process.env.SEQ_PORT || 11433, 30 | pool: { 31 | maxConnections: process.env.SEQ_MSSQL_POOL_MAX || process.env.SEQ_POOL_MAX || 5, 32 | maxIdleTime: process.env.SEQ_MSSQL_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000 33 | } 34 | }, 35 | 36 | // Make maxIdleTime small so that tests exit promptly 37 | mysql: { 38 | database: process.env.SEQ_MYSQL_DB || process.env.SEQ_DB || 'sequelize_test', 39 | username: process.env.SEQ_MYSQL_USER || process.env.SEQ_USER || 'sequelize_test', 40 | password: process.env.SEQ_MYSQL_PW || process.env.SEQ_PW || null, 41 | host: process.env.SEQ_MYSQL_HOST || process.env.SEQ_HOST || '127.0.0.1', 42 | port: process.env.SEQ_MYSQL_PORT || process.env.SEQ_PORT || 3306, 43 | pool: { 44 | maxConnections: process.env.SEQ_MYSQL_POOL_MAX || process.env.SEQ_POOL_MAX || 5, 45 | maxIdleTime: process.env.SEQ_MYSQL_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000 46 | } 47 | }, 48 | 49 | sqlite: { 50 | }, 51 | 52 | postgres: { 53 | database: process.env.SEQ_PG_DB || process.env.SEQ_DB || 'sequelize_test', 54 | username: process.env.SEQ_PG_USER || process.env.SEQ_USER || 'sequelize_test', 55 | password: process.env.SEQ_PG_PW || process.env.SEQ_PW || null, 56 | host: process.env.SEQ_PG_HOST || process.env.SEQ_HOST || '127.0.0.1', 57 | port: process.env.SEQ_PG_PORT || process.env.SEQ_PORT || 5432, 58 | pool: { 59 | maxConnections: process.env.SEQ_PG_POOL_MAX || process.env.SEQ_POOL_MAX || 5, 60 | maxIdleTime: process.env.SEQ_PG_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000 61 | } 62 | }, 63 | 64 | mariadb: { 65 | database: process.env.SEQ_MYSQL_DB || process.env.SEQ_DB || 'sequelize_test', 66 | username: process.env.SEQ_MYSQL_USER || process.env.SEQ_USER || 'sequelize_test', 67 | password: process.env.SEQ_MYSQL_PW || process.env.SEQ_PW || null, 68 | host: process.env.SEQ_MYSQL_HOST || process.env.SEQ_HOST || '127.0.0.1', 69 | port: process.env.SEQ_MYSQL_PORT || process.env.SEQ_PORT || 3306, 70 | pool: { 71 | maxConnections: process.env.SEQ_MYSQL_POOL_MAX || process.env.SEQ_POOL_MAX || 5, 72 | maxIdleTime: process.env.SEQ_MYSQL_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000 73 | } 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /test/config/options.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Tests options 4 | * ------------------*/ 5 | 6 | 'use strict'; 7 | 8 | // Modules 9 | const path = require('path'); 10 | 11 | // Exports 12 | 13 | module.exports = { 14 | configFile: path.resolve('config', 'database.json'), 15 | migrationsPath: path.resolve('db', 'migrate') 16 | }; 17 | -------------------------------------------------------------------------------- /test/example/Task2.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Tests fixtures 4 | * Task2 model definition 5 | * ------------------*/ 6 | 7 | 'use strict'; 8 | 9 | // Modules 10 | const Sequelize = require('sequelize'); 11 | 12 | // Exports 13 | 14 | module.exports = { 15 | fields: { 16 | name: Sequelize.STRING 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /test/example/User2.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * sequelize-definer module 3 | * Tests fixtures 4 | * User2 model definition 5 | * ------------------*/ 6 | 7 | 'use strict'; 8 | 9 | // Modules 10 | const Sequelize = require('sequelize'); 11 | 12 | // Exports 13 | 14 | module.exports = { 15 | fields: { 16 | name: Sequelize.STRING 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /test/support.js: -------------------------------------------------------------------------------- 1 | /* -------------------- 2 | * Sequelize definer 3 | * Tests support functions 4 | * ------------------*/ 5 | 6 | 'use strict'; 7 | 8 | // Modules 9 | const fs = require('fs'), 10 | path = require('path'), 11 | _ = require('lodash'), 12 | Sequelize = require('sequelize'), 13 | DataTypes = require('sequelize/lib/data-types'), 14 | chai = require('chai'), 15 | {expect} = chai, 16 | chaiAsPromised = require('chai-as-promised'); 17 | 18 | // Imports 19 | const Config = require('./config/config.js'); 20 | require('../lib/index.js')(Sequelize); 21 | 22 | // Init 23 | chai.use(chaiAsPromised); 24 | 25 | // Make sure errors get thrown when testing 26 | Sequelize.Promise.onPossiblyUnhandledRejection((e) => { throw e; }); 27 | Sequelize.Promise.longStackTraces(); 28 | 29 | // Exports 30 | 31 | const Support = { 32 | Sequelize, 33 | 34 | initTests(options) { 35 | const sequelize = this.createSequelizeInstance(options); 36 | 37 | this.clearDatabase(sequelize, () => { 38 | if (options.context) { 39 | options.context.sequelize = sequelize; 40 | } 41 | 42 | if (options.beforeComplete) { 43 | options.beforeComplete(sequelize, DataTypes); 44 | } 45 | 46 | if (options.onComplete) { 47 | options.onComplete(sequelize, DataTypes); 48 | } 49 | }); 50 | }, 51 | 52 | prepareTransactionTest(sequelize, callback) { // eslint-disable-line consistent-return 53 | const dialect = Support.getTestDialect(); 54 | 55 | if (dialect === 'sqlite') { 56 | const p = path.join(__dirname, 'tmp', 'db.sqlite'); 57 | 58 | return new Sequelize.Promise(((resolve) => { 59 | // We cannot promisify exists, since exists does not follow node callback convention - 60 | // first argument is a boolean, not an error / null 61 | if (fs.existsSync(p)) { 62 | resolve(Sequelize.Promise.promisify(fs.unlink)(p)); 63 | } else { 64 | resolve(); 65 | } 66 | })).then(() => { // eslint-disable-line consistent-return 67 | const options = Sequelize.Utils._.extend({}, sequelize.options, {storage: p}), 68 | _sequelize = new Sequelize(sequelize.config.database, null, null, options); 69 | 70 | if (callback) { 71 | _sequelize.sync({force: true}).success(() => { callback(_sequelize); }); 72 | } else { 73 | return _sequelize.sync({force: true}).return(_sequelize); 74 | } 75 | }); 76 | } 77 | if (callback) { 78 | callback(sequelize); 79 | } else { 80 | return Sequelize.Promise.resolve(sequelize); 81 | } 82 | }, 83 | 84 | createSequelizeInstance(options) { 85 | options = options || {}; 86 | options.dialect = this.getTestDialect(); 87 | 88 | const config = Config[options.dialect]; 89 | 90 | const sequelizeOptions = _.defaults(options, { 91 | host: options.host || config.host, 92 | logging: (process.env.SEQ_LOG ? console.log : false), // eslint-disable-line no-console 93 | dialect: options.dialect, 94 | port: options.port || process.env.SEQ_PORT || config.port, 95 | pool: config.pool, 96 | dialectOptions: options.dialectOptions || {} 97 | }); 98 | 99 | if (process.env.DIALECT === 'postgres-native') { 100 | sequelizeOptions.native = true; 101 | } 102 | 103 | if (config.storage) { 104 | sequelizeOptions.storage = config.storage; 105 | } 106 | 107 | return this.getSequelizeInstance( 108 | config.database, 109 | config.username, 110 | config.password, 111 | sequelizeOptions 112 | ); 113 | }, 114 | 115 | getSequelizeInstance(db, user, pass, options) { 116 | options = options || {}; 117 | options.dialect = options.dialect || this.getTestDialect(); 118 | return new Sequelize(db, user, pass, options); 119 | }, 120 | 121 | clearDatabase(sequelize) { 122 | return sequelize 123 | .getQueryInterface() 124 | .dropAllTables() 125 | .then(() => { 126 | sequelize.modelManager.daos = []; 127 | sequelize.models = {}; 128 | 129 | return sequelize 130 | .getQueryInterface() 131 | .dropAllEnums(); 132 | }); 133 | }, 134 | 135 | getSupportedDialects() { 136 | return fs.readdirSync(`${__dirname}/../node_modules/sequelize/lib/dialects`).filter(file => ((file.indexOf('.js') === -1) && (file.indexOf('abstract') === -1))); 137 | }, 138 | 139 | checkMatchForDialects(dialect, value, expectations) { 140 | if (expectations[dialect]) { 141 | expect(value).to.match(expectations[dialect]); 142 | } else { 143 | throw new Error(`Undefined expectation for "${dialect}"!`); 144 | } 145 | }, 146 | 147 | getTestDialect() { 148 | let envDialect = process.env.DIALECT || 'mysql'; 149 | 150 | if (envDialect === 'postgres-native') { 151 | envDialect = 'postgres'; 152 | } 153 | 154 | if (this.getSupportedDialects().indexOf(envDialect) === -1) { 155 | throw new Error(`The dialect you have passed is unknown. Did you really mean: ${envDialect}`); 156 | } 157 | 158 | return envDialect; 159 | }, 160 | 161 | dialectIsMySQL(strict) { 162 | const envDialect = process.env.DIALECT || 'mysql'; 163 | if (strict === undefined) { 164 | strict = false; 165 | } 166 | 167 | if (strict) { 168 | return envDialect === 'mysql'; 169 | } 170 | return ['mysql', 'mariadb'].indexOf(envDialect) !== -1; 171 | }, 172 | 173 | getTestDialectTeaser(moduleName) { 174 | let dialect = this.getTestDialect(); 175 | 176 | if (process.env.DIALECT === 'postgres-native') { 177 | dialect = 'postgres-native'; 178 | } 179 | 180 | return `[${dialect.toUpperCase()}] ${moduleName}`; 181 | }, 182 | 183 | getTestUrl(config) { 184 | let url; 185 | const dbConfig = config[config.dialect]; 186 | 187 | if (config.dialect === 'sqlite') { 188 | url = `sqlite://${dbConfig.storage}`; 189 | } else { 190 | let credentials = dbConfig.username; 191 | if (dbConfig.password) { 192 | credentials += `:${dbConfig.password}`; 193 | } 194 | 195 | url = `${config.dialect}://${credentials}@${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`; 196 | } 197 | return url; 198 | }, 199 | 200 | expectsql(query, expectations) { 201 | let expectation = expectations[Support.sequelize.dialect.name]; 202 | 203 | if (!expectation && Support.sequelize.dialect.name === 'mariadb') { 204 | expectation = expectations.mysql; 205 | } 206 | 207 | if (!expectation) { 208 | expectation = expectations.default 209 | .replace(/\[/g, Support.sequelize.dialect.TICK_CHAR_LEFT) 210 | .replace(/\]/g, Support.sequelize.dialect.TICK_CHAR_RIGHT); 211 | } 212 | 213 | expect(query).to.equal(expectation); 214 | } 215 | }; 216 | 217 | beforeEach(function() { 218 | this.sequelize = Support.sequelize; // eslint-disable-line no-invalid-this 219 | }); 220 | 221 | Support.sequelize = Support.createSequelizeInstance(); 222 | module.exports = Support; 223 | --------------------------------------------------------------------------------