├── .husky ├── .gitignore └── pre-commit ├── .eslintignore ├── .prettierignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── codeql-analysis.yml ├── src ├── types │ ├── Noop.js │ ├── StringType.js │ └── NumericType.js ├── constants │ ├── stringTypes.js │ ├── staticMethods.js │ ├── deferrables.js │ ├── numericTypes.js │ ├── basicTypes.js │ ├── staticModelMethods.js │ └── hooks.js ├── utils │ ├── index.js │ ├── fileFilter.js │ └── serialCommaList.js ├── checks │ ├── checkUniqueIndex.js │ ├── checkNonUniqueIndex.js │ ├── checkModelName.js │ ├── checkPropertyExists.js │ ├── checkHookDefined.js │ ├── checkUniqueCompoundIndex.js │ └── utils.js ├── mockSequelize.js ├── index.js ├── dataTypes.js ├── mockModels.js └── sequelize.js ├── test ├── .eslintrc.js ├── unitTestHelper.js ├── models │ ├── Simple.js │ ├── HasHooks.js │ └── Indexed.js └── unit │ ├── types │ ├── Noop.test.js │ ├── StringType.test.js │ └── NumericType.test.js │ ├── utils │ ├── fileFilter.test.js │ └── serialCommaList.test.js │ ├── sequelize.test.js │ ├── checks │ ├── checkModelName.test.js │ ├── checkPropertyExists.test.js │ ├── checkUniqueIndex.test.js │ ├── checkNonUniqueIndex.test.js │ ├── checkUniqueCompoundIndex.test.js │ └── checkHookDefined.test.js │ ├── listModels.test.js │ ├── stringTypes.test.js │ ├── mockSequelize.test.js │ ├── makeMockModels.test.js │ ├── numberTypes.test.js │ ├── dataTypes.test.js │ └── constants │ └── hooks.test.js ├── .snyk ├── .eslintrc.js ├── SECURITY.md ├── .circleci └── config.yml ├── LICENSE ├── .gitignore ├── package.json ├── CONTRIBUTING.md ├── README.md └── logo └── horizontal.svg /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no-install lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | .* 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [davesag] 4 | -------------------------------------------------------------------------------- /src/types/Noop.js: -------------------------------------------------------------------------------- 1 | function Noop() { 2 | return Noop 3 | } 4 | 5 | module.exports = Noop 6 | -------------------------------------------------------------------------------- /src/constants/stringTypes.js: -------------------------------------------------------------------------------- 1 | const stringTypes = ['CHAR', 'STRING'] 2 | 3 | module.exports = stringTypes 4 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'padded-blocks': 0, 4 | 'no-unused-vars': 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/unitTestHelper.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const sinonChai = require('sinon-chai') 3 | 4 | chai.use(sinonChai) 5 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | patch: {} 5 | -------------------------------------------------------------------------------- /src/types/StringType.js: -------------------------------------------------------------------------------- 1 | function StringType() { 2 | return StringType 3 | } 4 | 5 | StringType.BINARY = StringType 6 | 7 | module.exports = StringType 8 | -------------------------------------------------------------------------------- /src/constants/staticMethods.js: -------------------------------------------------------------------------------- 1 | const staticMethods = ['and', 'cast', 'col', 'fn', 'json', 'literal', 'or', 'useCLS', 'where'] 2 | 3 | module.exports = staticMethods 4 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | const fileFilter = require('./fileFilter') 2 | const serialCommaList = require('./serialCommaList') 3 | 4 | module.exports = { fileFilter, serialCommaList } 5 | -------------------------------------------------------------------------------- /src/types/NumericType.js: -------------------------------------------------------------------------------- 1 | function NumericType() { 2 | return NumericType 3 | } 4 | 5 | NumericType.UNSIGNED = NumericType 6 | NumericType.ZEROFILL = NumericType 7 | 8 | module.exports = NumericType 9 | -------------------------------------------------------------------------------- /src/checks/checkUniqueIndex.js: -------------------------------------------------------------------------------- 1 | const { checkIndex } = require('./utils') 2 | 3 | const checkUniqueIndex = instance => indexName => checkIndex(instance, indexName, true) 4 | 5 | module.exports = checkUniqueIndex 6 | -------------------------------------------------------------------------------- /src/checks/checkNonUniqueIndex.js: -------------------------------------------------------------------------------- 1 | const { checkIndex } = require('./utils') 2 | 3 | const checkNonUniqueIndex = instance => indexName => checkIndex(instance, indexName) 4 | 5 | module.exports = checkNonUniqueIndex 6 | -------------------------------------------------------------------------------- /src/constants/deferrables.js: -------------------------------------------------------------------------------- 1 | const deferrables = [ 2 | 'INITIALLY_IMMEDIATE', 3 | 'INITIALLY_DEFERRED', 4 | 'NOT', 5 | 'SET_DEFERRED', 6 | 'SET_IMMEDIATE' 7 | ] 8 | 9 | module.exports = deferrables 10 | -------------------------------------------------------------------------------- /src/constants/numericTypes.js: -------------------------------------------------------------------------------- 1 | const numericTypes = [ 2 | 'BIGINT', 3 | 'DECIMAL', 4 | 'DOUBLE', 5 | 'DOUBLE PRECISION', 6 | 'FLOAT', 7 | 'INTEGER', 8 | 'REAL', 9 | 'SMALLINT', 10 | 'TINYINT' 11 | ] 12 | 13 | module.exports = numericTypes 14 | -------------------------------------------------------------------------------- /src/checks/checkModelName.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const checkModelName = model => modelName => { 4 | it(`is named '${modelName}'`, () => { 5 | expect(model.modelName).to.equal(modelName) 6 | }) 7 | } 8 | 9 | module.exports = checkModelName 10 | -------------------------------------------------------------------------------- /src/checks/checkPropertyExists.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const checkPropertyExists = instance => propName => { 4 | it(`has property ${propName}`, () => { 5 | expect(instance).to.have.property(propName) 6 | }) 7 | } 8 | 9 | module.exports = checkPropertyExists 10 | -------------------------------------------------------------------------------- /src/checks/checkHookDefined.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const checkHookDefined = instance => hookName => { 4 | it(`defined the ${hookName} hook`, () => { 5 | expect(instance.hooks[hookName]).to.be.a('function') 6 | }) 7 | } 8 | 9 | module.exports = checkHookDefined 10 | -------------------------------------------------------------------------------- /src/mockSequelize.js: -------------------------------------------------------------------------------- 1 | const { spy } = require('sinon') 2 | 3 | const DataTypes = require('./dataTypes') 4 | 5 | class Model {} 6 | Model.init = spy() 7 | Model.belongsToMany = spy() 8 | Model.belongsTo = spy() 9 | Model.hasMany = spy() 10 | Model.hasOne = spy() 11 | 12 | module.exports = { Model, DataTypes } 13 | -------------------------------------------------------------------------------- /test/models/Simple.js: -------------------------------------------------------------------------------- 1 | const model = (sequelize, DataTypes) => { 2 | const Simple = sequelize.define('Simple', { 3 | name: { 4 | type: DataTypes.STRING, 5 | allowNull: false, 6 | validate: { 7 | notEmpty: true 8 | } 9 | } 10 | }) 11 | 12 | return Simple 13 | } 14 | 15 | module.exports = model 16 | -------------------------------------------------------------------------------- /test/unit/types/Noop.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const Noop = require('../../../src/types/Noop') 3 | 4 | describe('src/types/Noop', () => { 5 | it('is a function', () => { 6 | expect(Noop).to.be.a('function') 7 | }) 8 | 9 | it('returns itself', () => { 10 | expect(Noop()).to.equal(Noop) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['standard', 'plugin:prettier/recommended'], 3 | plugins: ['mocha'], 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | es6: true, 9 | node: true, 10 | mocha: true 11 | }, 12 | rules: { 13 | 'prettier/prettier': ['error', { singleQuote: true, semi: false }], 14 | 'no-unused-expressions': 0 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/unit/utils/fileFilter.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { fileFilter } = require('../../../src/utils') 3 | 4 | describe('src/utils/fileFilter', () => { 5 | const input = ['test.js', 'skip-this', 'index.js', '.skip.this.js'] 6 | const expected = ['test.js'] 7 | 8 | it('filters correctly', () => { 9 | expect(input.filter(fileFilter('.js'))).to.deep.equal(expected) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/utils/fileFilter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a file filter function that removes the given suffix before comparing file names 3 | * 4 | * @param {*} suffix '.js' or '.jsx' etc. 5 | * @returns {Function} a function that can be supplied to `array.filter` to filter an array of file names 6 | */ 7 | const fileFilter = suffix => file => 8 | file.indexOf('.') !== 0 && file !== `index${suffix}` && file.slice(-suffix.length) === suffix 9 | 10 | module.exports = fileFilter 11 | -------------------------------------------------------------------------------- /test/unit/types/StringType.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const StringType = require('../../../src/types/StringType') 3 | 4 | describe('src/types/StringType', () => { 5 | it('is a function', () => { 6 | expect(StringType).to.be.a('function') 7 | }) 8 | 9 | it('returns itself', () => { 10 | expect(StringType()).to.equal(StringType) 11 | }) 12 | 13 | it('has property BINARY', () => { 14 | expect(StringType).to.have.property('BINARY', StringType) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/unit/sequelize.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const sequelize = require('../../src/sequelize') 4 | const staticMethods = require('../../src/constants/staticMethods') 5 | 6 | describe('src/sequelize', () => { 7 | it('has define', () => { 8 | expect(sequelize).to.have.property('define') 9 | expect(sequelize.define).to.be.a('function') 10 | }) 11 | 12 | staticMethods.forEach(method => { 13 | it(`has static method ${method}`, () => { 14 | expect(sequelize[method]).to.be.a('function') 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/unit/checks/checkModelName.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { sequelize, dataTypes, checkModelName } = require('../../../src') 3 | const SimpleModel = require('../../models/Simple') 4 | 5 | describe('src/checkModelName', () => { 6 | const Model = SimpleModel(sequelize, dataTypes) 7 | 8 | context('happy path', () => { 9 | checkModelName(Model)('Simple') 10 | }) 11 | 12 | context('unhappy path', () => { 13 | it('fails the test', () => 14 | expect(() => { 15 | checkModelName(Model)('Not So Simple') 16 | }).to.throw) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.x | :white_check_mark: | 8 | | < 1.x | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Report a vulnerability to [the author](https://about.me/davesag) directly. 13 | 14 | You can expect to get an update on a reported vulnerability within 2 working days. 15 | 16 | ## Vulnerabilities in development dependencies 17 | 18 | I'm using `dependabot` and `snyk` to scan for security issues and I update dependencies in the `develop` branch regularly. 19 | -------------------------------------------------------------------------------- /src/constants/basicTypes.js: -------------------------------------------------------------------------------- 1 | // see http://docs.sequelizejs.com/variable/index.html#static-variable-DataTypes 2 | const basicTypes = [ 3 | 'ABSTRACT', 4 | 'ARRAY', 5 | 'BLOB', 6 | 'BOOLEAN', 7 | 'CIDR', 8 | 'DATE', 9 | 'DATEONLY', 10 | 'ENUM', 11 | 'GEOGRAPHY', 12 | 'GEOMETRY', 13 | 'HSTORE', 14 | 'INET', 15 | 'JSON', 16 | 'JSONB', 17 | 'JSONTYPE', 18 | 'MACADDR', 19 | 'MEDIUMINT', 20 | 'NOW', 21 | 'NUMBER', 22 | 'NUMERIC', 23 | 'RANGE', 24 | 'TEXT', 25 | 'TIME', 26 | 'UUID', 27 | 'UUIDV1', 28 | 'UUIDV4', 29 | 'VIRTUAL' 30 | ] 31 | 32 | module.exports = basicTypes 33 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@4.7.0 5 | codecov: codecov/codecov@3.2.0 6 | 7 | jobs: 8 | test: 9 | executor: 10 | name: node/default 11 | tag: 'current' 12 | steps: 13 | - checkout 14 | - node/install-packages 15 | - run: 16 | name: Javascript Linter 17 | command: npm run lint 18 | - run: 19 | name: Unit tests with code coverage 20 | command: npm run test:unit:cov 21 | 22 | workflows: 23 | node-tests: 24 | jobs: 25 | - test: 26 | post-steps: 27 | - codecov/upload 28 | -------------------------------------------------------------------------------- /test/unit/utils/serialCommaList.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { serialCommaList } = require('../../../src/utils') 3 | 4 | describe('src/utils/serialCommaList', () => { 5 | const doTest = ([words, expected]) => { 6 | it(`displays '${words}' as '${expected}'`, () => { 7 | expect(serialCommaList(words)).to.equal(expected) 8 | }) 9 | } 10 | 11 | ;[ 12 | [undefined, undefined], 13 | ['not an array', 'not an array'], 14 | [['alice'], 'alice'], 15 | [['alice', 'bob'], 'alice and bob'], 16 | [['alice', 'bob', 'charlie'], 'alice, bob, and charlie'] 17 | ].forEach(doTest) 18 | }) 19 | -------------------------------------------------------------------------------- /test/unit/types/NumericType.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const NumericType = require('../../../src/types/NumericType') 3 | 4 | describe('src/types/NumericType', () => { 5 | it('is a function', () => { 6 | expect(NumericType).to.be.a('function') 7 | }) 8 | 9 | it('returns itself', () => { 10 | expect(NumericType()).to.equal(NumericType) 11 | }) 12 | 13 | it('has property UNSIGNED', () => { 14 | expect(NumericType).to.have.property('UNSIGNED', NumericType) 15 | }) 16 | 17 | it('has property ZEROFILL', () => { 18 | expect(NumericType).to.have.property('ZEROFILL', NumericType) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/unit/listModels.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const { listModels } = require('../../src') 4 | 5 | describe('src/listModels', () => { 6 | const expected = ['HasHooks', 'Indexed', 'Simple'] 7 | context('default suffix', () => { 8 | const models = listModels('test/models') 9 | 10 | it('lists the models', () => { 11 | expect(models).to.deep.equal(expected) 12 | }) 13 | }) 14 | 15 | context('custom suffix', () => { 16 | const models = listModels('test/models', '.js') 17 | 18 | it('lists the models', () => { 19 | expect(models).to.deep.equal(expected) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/checks/checkUniqueCompoundIndex.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { serialCommaList } = require('../utils') 3 | 4 | /** 5 | * @deprecated both `checkUniqueIndex` and `checkNonUniqueIndex` will now check for either simple or composite indexes. 6 | */ 7 | const checkUniqueCompoundIndex = instance => indexes => { 8 | it(`indexed an unique index of ${serialCommaList(indexes)}`, () => { 9 | expect( 10 | instance.indexes.find( 11 | index => index.unique === true && index.fields.join('') === indexes.join('') 12 | ) 13 | ).not.to.be.undefined 14 | }) 15 | } 16 | 17 | module.exports = checkUniqueCompoundIndex 18 | -------------------------------------------------------------------------------- /test/unit/checks/checkPropertyExists.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const { sequelize, dataTypes, checkPropertyExists } = require('../../../src') 4 | const SimpleModel = require('../../models/Simple') 5 | 6 | describe('src/checkPropertyExists', () => { 7 | const Model = SimpleModel(sequelize, dataTypes) 8 | const instance = new Model() 9 | 10 | context('happy path', () => { 11 | ;['name'].forEach(checkPropertyExists(instance)) 12 | }) 13 | 14 | context('unhappy path', () => { 15 | it('fails the test', () => 16 | expect(() => { 17 | checkPropertyExists(instance)('no name') 18 | }).to.throw) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/unit/checks/checkUniqueIndex.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const { sequelize, dataTypes, checkUniqueIndex } = require('../../../src') 4 | const IndexedModel = require('../../models/Indexed') 5 | 6 | describe('src/checkUniqueIndex', () => { 7 | const Model = IndexedModel(sequelize, dataTypes) 8 | const instance = new Model() 9 | 10 | context('happy path', () => { 11 | ;['uuid', ['name', 'lunch']].forEach(checkUniqueIndex(instance)) 12 | }) 13 | 14 | context('unhappy path', () => { 15 | it('fails the test', () => 16 | expect(() => { 17 | checkUniqueIndex(instance)('no such index') 18 | }).to.throw) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic dependabot.yml file with 2 | # minimum configuration for two package managers 3 | 4 | version: 2 5 | updates: 6 | # Enable version updates for npm 7 | - package-ecosystem: "npm" 8 | # Look for `package.json` and `lock` files in the `root` directory 9 | directory: "/" 10 | # Check the npm registry for updates every day (weekdays) 11 | schedule: 12 | interval: "daily" 13 | 14 | # Enable version updates for Docker 15 | # - package-ecosystem: "docker" 16 | # # Look for a `Dockerfile` in the `root` directory 17 | # directory: "/" 18 | # # Check for updates once a week 19 | # schedule: 20 | # interval: "weekly" 21 | -------------------------------------------------------------------------------- /test/unit/checks/checkNonUniqueIndex.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const { sequelize, dataTypes, checkNonUniqueIndex } = require('../../../src') 4 | const IndexedModel = require('../../models/Indexed') 5 | 6 | describe('src/checkNonUniqueIndex', () => { 7 | const Model = IndexedModel(sequelize, dataTypes) 8 | const instance = new Model() 9 | 10 | context('happy path', () => { 11 | ;['name', ['coffee', 'lunch']].forEach(checkNonUniqueIndex(instance)) 12 | }) 13 | 14 | context('unhappy path', () => { 15 | it('fails the test', () => 16 | expect(() => { 17 | checkNonUniqueIndex(instance)('no name') 18 | }).to.throw) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/unit/stringTypes.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const { dataTypes } = require('../../src') 4 | 5 | describe('src/dataTypes#string', () => { 6 | const doTest = dataType => { 7 | describe(`Testing ${dataType}`, () => { 8 | context('non function', () => { 9 | it(`supports ${dataType} with BINARY`, () => { 10 | expect(dataTypes[dataType].BINARY).to.exist 11 | }) 12 | }) 13 | 14 | context('function', () => { 15 | it(`supports ${dataType}() with BINARY`, () => { 16 | expect(dataTypes[dataType](10).BINARY).to.exist 17 | }) 18 | }) 19 | }) 20 | } 21 | ;['CHAR', 'STRING'].forEach(doTest) 22 | }) 23 | -------------------------------------------------------------------------------- /test/unit/checks/checkUniqueCompoundIndex.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const { sequelize, dataTypes, checkUniqueCompoundIndex } = require('../../../src') 4 | const IndexedModel = require('../../models/Indexed') 5 | 6 | describe('src/checkUniqueCompoundIndex', () => { 7 | const Model = IndexedModel(sequelize, dataTypes) 8 | const instance = new Model() 9 | 10 | context('happy path', () => { 11 | ;[['name', 'lunch']].forEach(checkUniqueCompoundIndex(instance)) 12 | }) 13 | 14 | context('unhappy path', () => { 15 | it('fails the test', () => 16 | expect(() => { 17 | checkUniqueCompoundIndex(instance)('no such index') 18 | }).to.throw) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/utils/serialCommaList.js: -------------------------------------------------------------------------------- 1 | const oxford = words => `${words.slice(0, -1).join(', ')}, and ${words[words.length - 1]}` 2 | 3 | /** 4 | * Returns a formatted list of words, separated by commas 5 | * as per the Oxford (aka serial) Comma rules. 6 | * 7 | * https://en.wikipedia.org/wiki/Serial_comma 8 | * 9 | * @param {array} words The array of words 10 | * @returns {string} A list of words with an Oxford comma as appropriate 11 | */ 12 | const serialCommaList = words => 13 | Array.isArray(words) && words.length 14 | ? words.length === 1 15 | ? words[0] 16 | : words.length === 2 17 | ? `${words[0]} and ${words[1]}` 18 | : oxford(words) 19 | : words 20 | 21 | module.exports = serialCommaList 22 | -------------------------------------------------------------------------------- /test/models/HasHooks.js: -------------------------------------------------------------------------------- 1 | const model = (sequelize, DataTypes) => { 2 | const HasHooks = sequelize.define( 3 | 'HasHooks', 4 | { 5 | name: { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | validate: { 9 | notEmpty: true 10 | } 11 | } 12 | }, 13 | { 14 | hooks: { 15 | beforeValidate: hooker => { 16 | hooker.name = 'Alice' 17 | } 18 | } 19 | } 20 | ) 21 | 22 | HasHooks.hook('afterValidate', hooker => { 23 | hooker.name = 'Bob' 24 | }) 25 | 26 | HasHooks.addHook('afterCreate', 'removeMe', hooker => { 27 | hooker.name = 'Carla' 28 | }) 29 | 30 | return HasHooks 31 | } 32 | 33 | module.exports = model 34 | -------------------------------------------------------------------------------- /test/unit/mockSequelize.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const Sequelize = require('../../src/mockSequelize') 4 | const DataTypes = require('../../src/dataTypes') 5 | 6 | describe('src/mockSequelize', () => { 7 | it('has Model', () => { 8 | expect(Sequelize).to.have.property('Model') 9 | }) 10 | 11 | it('Model is a class', () => { 12 | expect(Sequelize.Model).to.be.a('function') 13 | expect(Sequelize.Model.constructor).to.be.a('function') 14 | }) 15 | 16 | it('Model has a static init function', () => { 17 | expect(Sequelize.Model.init).to.be.a('function') 18 | }) 19 | 20 | it('has DataTypes', () => { 21 | expect(Sequelize).to.have.property('DataTypes', DataTypes) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/models/Indexed.js: -------------------------------------------------------------------------------- 1 | const model = (sequelize, DataTypes) => { 2 | const Indexed = sequelize.define( 3 | 'Indexed', 4 | { 5 | name: { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | validate: { 9 | notEmpty: true 10 | } 11 | }, 12 | lunch: DataTypes.STRING, 13 | coffee: DataTypes.STRING, 14 | uuid: DataTypes.UUID 15 | }, 16 | { 17 | indexes: [ 18 | { unique: true, fields: ['uuid'] }, 19 | { unique: false, fields: ['name'] }, 20 | { unique: true, fields: ['name', 'lunch'] }, 21 | { fields: ['coffee', 'lunch'] } // leave out index: false here to test falsiness 22 | ] 23 | } 24 | ) 25 | 26 | return Indexed 27 | } 28 | 29 | module.exports = model 30 | -------------------------------------------------------------------------------- /src/constants/staticModelMethods.js: -------------------------------------------------------------------------------- 1 | const syncMethods = [ 2 | 'addScope', 3 | 'belongsTo', 4 | 'belongsToMany', 5 | 'build', 6 | 'getTableName', 7 | 'hasMany', 8 | 'hasOne', 9 | 'init', 10 | 'removeAttribute', 11 | 'schema', 12 | 'scope', 13 | 'unscoped' 14 | ] 15 | 16 | const asyncMethods = [ 17 | 'aggregate', 18 | 'bulkCreate', 19 | 'count', 20 | 'create', 21 | 'decrement', 22 | 'describe', 23 | 'destroy', 24 | 'drop', 25 | 'findAll', 26 | 'findAndCountAll', 27 | 'findByPk', 28 | 'findCreateFind', 29 | 'findOne', 30 | 'findOrBuild', 31 | 'findOrCreate', 32 | 'increment', 33 | 'max', 34 | 'min', 35 | 'restore', 36 | 'sum', 37 | 'sync', 38 | 'truncate', 39 | 'update', 40 | 'upsert' 41 | ] 42 | 43 | module.exports = { syncMethods, asyncMethods } 44 | -------------------------------------------------------------------------------- /test/unit/checks/checkHookDefined.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { sequelize, dataTypes, checkHookDefined } = require('../../../src') 3 | const HasHooksModel = require('../../models/HasHooks') 4 | 5 | describe('src/checkHookDefined', () => { 6 | context('when hooks are defined', () => { 7 | const Model = HasHooksModel(sequelize, dataTypes) 8 | const instance = new Model() 9 | 10 | ;['beforeValidate', 'afterValidate', 'afterCreate'].forEach(checkHookDefined(instance)) 11 | }) 12 | 13 | context('when hooks not defined', () => { 14 | const Model = HasHooksModel(sequelize, dataTypes) 15 | const instance = new Model() 16 | 17 | it('fails the test', () => 18 | expect(() => { 19 | checkHookDefined(instance)('not a hook') 20 | }).to.throw) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/unit/makeMockModels.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const { makeMockModels, listModels } = require('../../src') 4 | 5 | describe('src/makeMockModels', () => { 6 | const doTests = ([label, suffix]) => { 7 | const mockModels = makeMockModels({ Fake: 'a fake' }, 'test/models', suffix) 8 | const models = listModels('test/models', suffix) 9 | 10 | context(label, () => { 11 | const doTest = model => { 12 | it(`has the model ${model}`, () => { 13 | expect(mockModels).to.have.property(model) 14 | }) 15 | } 16 | ;[...models, 'Fake'].forEach(doTest) 17 | 18 | it("adds '@noCallThru: true'", () => { 19 | expect(mockModels).to.have.property('@noCallThru', true) 20 | }) 21 | }) 22 | } 23 | 24 | ;[['default suffix'], ['custom suffix', '.js']].forEach(doTests) 25 | }) 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const checkHookDefined = require('./checks/checkHookDefined') 2 | const checkModelName = require('./checks/checkModelName') 3 | const checkNonUniqueIndex = require('./checks/checkNonUniqueIndex') 4 | const checkPropertyExists = require('./checks/checkPropertyExists') 5 | const checkUniqueCompoundIndex = require('./checks/checkUniqueCompoundIndex') 6 | const checkUniqueIndex = require('./checks/checkUniqueIndex') 7 | const dataTypes = require('./dataTypes') 8 | const sequelize = require('./sequelize') 9 | const { makeMockModels, listModels } = require('./mockModels') 10 | const Sequelize = require('./mockSequelize') 11 | 12 | module.exports = { 13 | checkHookDefined, 14 | checkModelName, 15 | checkNonUniqueIndex, 16 | checkPropertyExists, 17 | checkUniqueCompoundIndex, 18 | checkUniqueIndex, 19 | dataTypes, 20 | listModels, 21 | makeMockModels, 22 | sequelize, 23 | Sequelize 24 | } 25 | -------------------------------------------------------------------------------- /src/dataTypes.js: -------------------------------------------------------------------------------- 1 | const Noop = require('./types/Noop') 2 | const NumericType = require('./types/NumericType') 3 | const StringType = require('./types/StringType') 4 | const basicTypes = require('./constants/basicTypes') 5 | const numericTypes = require('./constants/numericTypes') 6 | const stringTypes = require('./constants/stringTypes') 7 | const deferrables = require('./constants/deferrables') 8 | 9 | const basicDataTypes = basicTypes.reduce((acc, elem) => { 10 | acc[elem] = Noop 11 | return acc 12 | }, {}) 13 | 14 | const numericDataTypes = numericTypes.reduce((acc, elem) => { 15 | acc[elem] = NumericType 16 | return acc 17 | }, basicDataTypes) 18 | 19 | const dataTypes = stringTypes.reduce((acc, elem) => { 20 | acc[elem] = StringType 21 | return acc 22 | }, numericDataTypes) 23 | 24 | const Deferrable = deferrables.reduce((acc, elem) => { 25 | acc[elem] = elem 26 | return acc 27 | }, {}) 28 | 29 | module.exports = { 30 | ...dataTypes, 31 | Deferrable 32 | } 33 | -------------------------------------------------------------------------------- /src/constants/hooks.js: -------------------------------------------------------------------------------- 1 | // ref https://github.com/sequelize/sequelize/blob/master/lib/hooks.js#L7 2 | const hooks = [ 3 | 'beforeValidate', 4 | 'afterValidate', 5 | 'validationFailed', 6 | 'beforeCreate', 7 | 'afterCreate', 8 | 'beforeDestroy', 9 | 'afterDestroy', 10 | 'beforeRestore', 11 | 'afterRestore', 12 | 'beforeUpdate', 13 | 'afterUpdate', 14 | 'beforeSave', 15 | 'afterSave', 16 | 'beforeUpsert', 17 | 'afterUpsert', 18 | 'beforeBulkCreate', 19 | 'afterBulkCreate', 20 | 'beforeBulkDestroy', 21 | 'afterBulkDestroy', 22 | 'beforeBulkRestore', 23 | 'afterBulkRestore', 24 | 'beforeBulkUpdate', 25 | 'afterBulkUpdate', 26 | 'beforeFind', 27 | 'beforeFindAfterExpandIncludeAll', 28 | 'beforeFindAfterOptions', 29 | 'afterFind', 30 | 'beforeCount', 31 | 'beforeDefine', 32 | 'afterDefine', 33 | 'beforeInit', 34 | 'afterInit', 35 | 'beforeAssociate', 36 | 'afterAssociate', 37 | 'beforeConnect', 38 | 'afterConnect', 39 | 'beforeSync', 40 | 'afterSync', 41 | 'beforeBulkSync', 42 | 'afterBulkSync' 43 | ] 44 | 45 | module.exports = hooks 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dave Sag 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # macOS finder metadata 64 | .DS_Store 65 | 66 | # VS Code config 67 | .vscode/ 68 | -------------------------------------------------------------------------------- /src/mockModels.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { fileFilter } = require('./utils') 4 | 5 | const PROJECT_ROOT = process.cwd() 6 | 7 | let sequelizerc 8 | const sequelizercPath = path.join(PROJECT_ROOT, '.sequelizerc') 9 | 10 | try { 11 | sequelizerc = require(sequelizercPath) 12 | } catch (err) { 13 | /* istanbul ignore next */ 14 | if (!err.code === 'MODULE_NOT_FOUND') throw err 15 | } 16 | 17 | const DEFAULT_SUFFIX = '.js' 18 | const DEFAULT_MODELS_FOLDER = sequelizerc 19 | ? /* istanbul ignore next */ sequelizerc['models-path'] 20 | : path.join(PROJECT_ROOT, 'src', 'models') 21 | 22 | const makeName = suffix => file => file.slice(0, -suffix.length) 23 | 24 | const listToObject = (acc, elem) => { 25 | acc[elem] = elem 26 | return acc 27 | } 28 | 29 | // 30 | const listModels = ( 31 | /* istanbul ignore next */ folder = DEFAULT_MODELS_FOLDER, 32 | suffix = DEFAULT_SUFFIX 33 | ) => fs.readdirSync(folder).filter(fileFilter(suffix)).map(makeName(suffix)) 34 | 35 | const finder = (folder, suffix) => listModels(folder, suffix).reduce(listToObject, {}) 36 | 37 | const makeMockModels = (models, folder, suffix) => ({ 38 | ...finder(folder, suffix), 39 | ...models, 40 | '@noCallThru': true 41 | }) 42 | 43 | module.exports = { 44 | makeMockModels, 45 | listModels 46 | } 47 | -------------------------------------------------------------------------------- /src/checks/utils.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { serialCommaList } = require('../utils') 3 | 4 | // if unique is true then expect index.unique to be true too, but 5 | // if unique is false then index.unique can simply be be falsy 6 | const matchUniqueness = (index, unique) => (unique ? index.unique === unique : !index.unique) 7 | const prefix = unique => (unique ? 'n ' : ' non-') 8 | 9 | const checkSingleIndex = (instance, unique) => indexName => { 10 | it(`indexed a${prefix(unique)}unique ${indexName}`, () => { 11 | expect( 12 | instance.indexes.find( 13 | index => matchUniqueness(index, unique) && index.fields[0] === indexName 14 | ) 15 | ).not.to.be.undefined 16 | }) 17 | } 18 | 19 | const checkAllIndexes = (instance, unique) => indexNames => { 20 | context(`indexed a${prefix(unique)}unique composite of [${serialCommaList(indexNames)}]`, () => { 21 | indexNames.forEach((indexName, i) => { 22 | it(`includes ${indexName} at ${i}`, () => { 23 | expect( 24 | instance.indexes.find( 25 | index => matchUniqueness(index, unique) && index.fields[i] === indexName 26 | ) 27 | ).not.to.be.undefined 28 | }) 29 | }) 30 | }) 31 | } 32 | 33 | const checkIndex = (instance, indexNameOrNames, unique = false) => 34 | (Array.isArray(indexNameOrNames) ? checkAllIndexes : checkSingleIndex)( 35 | instance, 36 | unique 37 | )(indexNameOrNames) 38 | 39 | module.exports = { checkIndex } 40 | -------------------------------------------------------------------------------- /src/sequelize.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const hooks = require('./constants/hooks') 3 | const staticMethods = require('./constants/staticMethods') 4 | const { syncMethods, asyncMethods } = require('./constants/staticModelMethods') 5 | 6 | const sequelize = { 7 | define: (modelName, modelDefn, metaData = {}) => { 8 | const model = function () {} 9 | model.modelName = modelName 10 | 11 | const attachHook = name => hook => { 12 | if (!model.prototype.hooks) 13 | model.prototype.hooks = metaData.hooks || /* istanbul ignore next */ {} 14 | model.prototype.hooks[name] = hook 15 | } 16 | 17 | const attachProp = key => { 18 | model.prototype[key] = modelDefn[key] 19 | } 20 | 21 | const addStatic = key => { 22 | model[key] = sinon.stub() 23 | } 24 | 25 | hooks.forEach(hook => { 26 | model[hook] = attachHook(hook) 27 | }) 28 | 29 | model.addHook = (hookType, name, hook) => 30 | typeof name === 'function' ? attachHook(hookType)(name) : attachHook(hookType)(hook) 31 | 32 | model.hook = model.addHook 33 | 34 | syncMethods.forEach(addStatic) 35 | asyncMethods.forEach(addStatic) 36 | 37 | model.isHierarchy = sinon.spy() 38 | 39 | model.prototype.update = sinon.stub() 40 | model.prototype.reload = sinon.stub() 41 | model.prototype.set = sinon.spy() 42 | Object.keys(modelDefn).forEach(attachProp) 43 | 44 | model.prototype.indexes = metaData.indexes 45 | model.prototype.scopes = metaData.scopes 46 | model.prototype.validate = metaData.validate 47 | return model 48 | } 49 | } 50 | 51 | staticMethods.forEach(method => { 52 | sequelize[method] = sinon.stub() 53 | }) 54 | 55 | module.exports = sequelize 56 | -------------------------------------------------------------------------------- /test/unit/numberTypes.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const { dataTypes } = require('../../src') 4 | 5 | describe('src/dataTypes#numeric', () => { 6 | const doTest = dataType => { 7 | describe(`Testing ${dataType}`, () => { 8 | context('non function', () => { 9 | it(`supports ${dataType} with ZEROFILL`, () => { 10 | expect(dataTypes[dataType].ZEROFILL).to.exist 11 | }) 12 | 13 | it(`supports ${dataType} with UNSIGNED`, () => { 14 | expect(dataTypes[dataType](10).UNSIGNED).to.exist 15 | }) 16 | 17 | it(`supports ${dataType} with UNSIGNED.ZEROFILL`, () => { 18 | expect(dataTypes[dataType].UNSIGNED.ZEROFILL).to.exist 19 | }) 20 | 21 | it(`supports ${dataType} with ZEROFILL.UNSIGNED`, () => { 22 | expect(dataTypes[dataType].ZEROFILL.UNSIGNED).to.exist 23 | }) 24 | }) 25 | 26 | context('function', () => { 27 | it(`supports ${dataType}() with ZEROFILL`, () => { 28 | expect(dataTypes[dataType](10).ZEROFILL).to.exist 29 | }) 30 | 31 | it(`supports ${dataType}() with UNSIGNED`, () => { 32 | expect(dataTypes[dataType](10).UNSIGNED).to.exist 33 | }) 34 | 35 | it(`supports ${dataType}() with UNSIGNED.ZEROFILL`, () => { 36 | expect(dataTypes[dataType](10).UNSIGNED.ZEROFILL).to.exist 37 | }) 38 | 39 | it(`supports ${dataType}() with ZEROFILL.UNSIGNED`, () => { 40 | expect(dataTypes[dataType](10).ZEROFILL.UNSIGNED).to.exist 41 | }) 42 | }) 43 | }) 44 | } 45 | ;[ 46 | 'BIGINT', 47 | 'DECIMAL', 48 | 'DOUBLE', 49 | 'DOUBLE PRECISION', 50 | 'FLOAT', 51 | 'INTEGER', 52 | 'REAL', 53 | 'SMALLINT', 54 | 'TINYINT' 55 | ].forEach(doTest) 56 | }) 57 | -------------------------------------------------------------------------------- /test/unit/dataTypes.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | const dataTypes = require('../../src/dataTypes') 4 | const Noop = require('../../src/types/Noop') 5 | const NumericType = require('../../src/types/NumericType') 6 | const StringType = require('../../src/types/StringType') 7 | 8 | const basicTypes = [ 9 | 'ABSTRACT', 10 | 'ARRAY', 11 | 'BLOB', 12 | 'BOOLEAN', 13 | 'CIDR', 14 | 'DATE', 15 | 'DATEONLY', 16 | 'ENUM', 17 | 'GEOGRAPHY', 18 | 'GEOMETRY', 19 | 'HSTORE', 20 | 'INET', 21 | 'JSON', 22 | 'JSONB', 23 | 'JSONTYPE', 24 | 'MACADDR', 25 | 'MEDIUMINT', 26 | 'NOW', 27 | 'NUMBER', 28 | 'NUMERIC', 29 | 'RANGE', 30 | 'TEXT', 31 | 'TIME', 32 | 'UUID', 33 | 'UUIDV1', 34 | 'UUIDV4', 35 | 'VIRTUAL' 36 | ] 37 | 38 | const numericTypes = [ 39 | 'BIGINT', 40 | 'DECIMAL', 41 | 'DOUBLE', 42 | 'DOUBLE PRECISION', 43 | 'FLOAT', 44 | 'INTEGER', 45 | 'REAL', 46 | 'SMALLINT', 47 | 'TINYINT' 48 | ] 49 | 50 | const stringTypes = ['CHAR', 'STRING'] 51 | 52 | const deferrables = [ 53 | 'INITIALLY_IMMEDIATE', 54 | 'INITIALLY_DEFERRED', 55 | 'NOT', 56 | 'SET_DEFERRED', 57 | 'SET_IMMEDIATE' 58 | ] 59 | 60 | describe('dataTypes', () => { 61 | it('is an object', () => { 62 | expect(dataTypes).to.be.an('object') 63 | }) 64 | 65 | it('has Deferrable', () => { 66 | expect(dataTypes).to.have.property('Deferrable') 67 | expect(dataTypes.Deferrable).to.have.keys(deferrables) 68 | }) 69 | 70 | it('has basicTypes', () => { 71 | basicTypes.forEach(type => { 72 | expect(dataTypes).to.have.property(type, Noop) 73 | }) 74 | }) 75 | 76 | it('has numericTypes', () => { 77 | numericTypes.forEach(type => { 78 | expect(dataTypes).to.have.property(type, NumericType) 79 | }) 80 | }) 81 | 82 | it('has stringTypes', () => { 83 | stringTypes.forEach(type => { 84 | expect(dataTypes).to.have.property(type, StringType) 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [develop, master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [develop] 9 | schedule: 10 | - cron: '0 4 * * 0' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['javascript'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /test/unit/constants/hooks.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const hooks = require('../../../src/constants/hooks') 3 | 4 | // pulled from https://github.com/sequelize/sequelize/blob/master/lib/hooks.js#L8 5 | // whenever sequelize gets updated check that this is up to date 6 | const hookTypes = { 7 | beforeValidate: { params: 2 }, 8 | afterValidate: { params: 2 }, 9 | validationFailed: { params: 3 }, 10 | beforeCreate: { params: 2 }, 11 | afterCreate: { params: 2 }, 12 | beforeDestroy: { params: 2 }, 13 | afterDestroy: { params: 2 }, 14 | beforeRestore: { params: 2 }, 15 | afterRestore: { params: 2 }, 16 | beforeUpdate: { params: 2 }, 17 | afterUpdate: { params: 2 }, 18 | beforeSave: { params: 2, proxies: ['beforeUpdate', 'beforeCreate'] }, 19 | afterSave: { params: 2, proxies: ['afterUpdate', 'afterCreate'] }, 20 | beforeUpsert: { params: 2 }, 21 | afterUpsert: { params: 2 }, 22 | beforeBulkCreate: { params: 2 }, 23 | afterBulkCreate: { params: 2 }, 24 | beforeBulkDestroy: { params: 1 }, 25 | afterBulkDestroy: { params: 1 }, 26 | beforeBulkRestore: { params: 1 }, 27 | afterBulkRestore: { params: 1 }, 28 | beforeBulkUpdate: { params: 1 }, 29 | afterBulkUpdate: { params: 1 }, 30 | beforeFind: { params: 1 }, 31 | beforeFindAfterExpandIncludeAll: { params: 1 }, 32 | beforeFindAfterOptions: { params: 1 }, 33 | afterFind: { params: 2 }, 34 | beforeCount: { params: 1 }, 35 | beforeDefine: { params: 2, sync: true, noModel: true }, 36 | afterDefine: { params: 1, sync: true, noModel: true }, 37 | beforeInit: { params: 2, sync: true, noModel: true }, 38 | afterInit: { params: 1, sync: true, noModel: true }, 39 | beforeAssociate: { params: 2, sync: true }, 40 | afterAssociate: { params: 2, sync: true }, 41 | beforeConnect: { params: 1, noModel: true }, 42 | afterConnect: { params: 2, noModel: true }, 43 | beforeSync: { params: 1 }, 44 | afterSync: { params: 1 }, 45 | beforeBulkSync: { params: 1 }, 46 | afterBulkSync: { params: 1 } 47 | } 48 | 49 | const allHooks = Object.keys(hookTypes).reduce((acc, elem) => { 50 | acc.push(elem) 51 | if (hookTypes[elem].proxies) acc.concat(hookTypes[elem].proxies) 52 | return acc 53 | }, []) 54 | 55 | describe('src/hooks', () => { 56 | it('lists all the hooks', () => { 57 | expect(hooks).to.include.members(allHooks) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequelize-test-helpers", 3 | "version": "1.4.3", 4 | "description": "A collection of utilities to help with unit-testing sequelize models", 5 | "author": "Dave Sag (https://github.com/davesag)", 6 | "contributors": [ 7 | "Gerhard Kubion (https://github.com/gerhardkubion)", 8 | "Ben Winchester (https://github.com/bmw2621)", 9 | "Jasper Stam (https://github.com/stam)", 10 | "Oscar Dub (https://github.com/odub)" 11 | ], 12 | "type": "commonjs", 13 | "license": "MIT", 14 | "funding": { 15 | "type": "github", 16 | "url": "https://github.com/sponsors/davesag" 17 | }, 18 | "main": "src/index.js", 19 | "engines": { 20 | "node": ">= 8.10.0" 21 | }, 22 | "files": [ 23 | "CONTRIBUTING.md", 24 | "src", 25 | "logo/horizontal.svg" 26 | ], 27 | "directories": { 28 | "lib": "src", 29 | "test": "test" 30 | }, 31 | "scripts": { 32 | "eslint-check": "eslint --print-config src/index.js | eslint-config-prettier-check", 33 | "lint": "eslint .", 34 | "prettier": "prettier --write '**/*.{js,json,md}'", 35 | "test": "npm run test:unit", 36 | "test:unit": "NODE_ENV=test mocha ./test/unit/ --require ./test/unitTestHelper.js --recursive", 37 | "test:unit:cov": "NODE_ENV=test nyc mocha ./test/unit/ --require ./test/unitTestHelper.js --recursive", 38 | "snyk-protect": "snyk-protect" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/davesag/sequelize-test-helpers.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/davesag/sequelize-test-helpers/issues" 46 | }, 47 | "homepage": "https://github.com/davesag/sequelize-test-helpers#readme", 48 | "keywords": [ 49 | "sequelize", 50 | "mock", 51 | "mocking", 52 | "unit-testing", 53 | "mocha", 54 | "sinon", 55 | "chai" 56 | ], 57 | "devDependencies": { 58 | "ajv": "^8.12.0", 59 | "chai": "^4.3.10", 60 | "eslint": "^8.57.0", 61 | "eslint-config-prettier": "^9.1.0", 62 | "eslint-config-standard": "^17.1.0", 63 | "eslint-plugin-import": "^2.29.1", 64 | "eslint-plugin-mocha": "^10.4.3", 65 | "eslint-plugin-n": "^16.6.2", 66 | "eslint-plugin-node": "^11.1.0", 67 | "eslint-plugin-prettier": "^5.1.3", 68 | "eslint-plugin-promise": "^6.0.0", 69 | "husky": "^9.0.11", 70 | "lint-staged": "^15.2.2", 71 | "mocha": "^10.4.0", 72 | "nyc": "^15.1.0", 73 | "prettier": "^3.2.5", 74 | "proxyquire": "^2.1.3", 75 | "sinon": "^17.0.1", 76 | "sinon-chai": "^3.5.0" 77 | }, 78 | "peerDependencies": { 79 | "chai": ">= 4", 80 | "sinon": ">= 10.0.0" 81 | }, 82 | "prettier": { 83 | "semi": false, 84 | "singleQuote": true, 85 | "proseWrap": "never", 86 | "arrowParens": "avoid", 87 | "trailingComma": "none", 88 | "printWidth": 100 89 | }, 90 | "lint-staged": { 91 | "**/*.{js,json,md}": [ 92 | "prettier --write" 93 | ] 94 | }, 95 | "nyc": { 96 | "check-coverage": true, 97 | "per-file": true, 98 | "lines": 100, 99 | "statements": 100, 100 | "functions": 100, 101 | "branches": 100, 102 | "include": [ 103 | "src/**/*.js" 104 | ], 105 | "exclude": [ 106 | "index.js" 107 | ], 108 | "reporter": [ 109 | "lcov", 110 | "text" 111 | ], 112 | "all": true, 113 | "cache": true 114 | }, 115 | "snyk": true 116 | } 117 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to this project 2 | 3 | ## Development Environment 4 | 5 | All development is assumed to be done on a Mac running a modern version of OS X but ought to be pretty much the same no matter what unixy environment you use. 6 | 7 | ## Development Process 8 | 9 | All development is to follow the [standard git-flow](http://nvie.com/posts/a-successful-git-branching-model/) process, modified to allow for code-reviews. 10 | 11 | See this handy, if ugly, [cheat sheet](http://danielkummer.github.io/git-flow-cheatsheet/). 12 | 13 | ### Setup 14 | 15 | 1. Fork this repo into your personal GitHub account 16 | 2. clone your fork to your local development machine 17 | 3. Set this repo as the `upstream` repo `git remote add upstream ` 18 | 4. Disallow direct pushing to upstream `git remote set-url --push upstream no_push` 19 | 5. create a local `main` branch `git checkout -b main` and test it via `git pull upstream main` 20 | 6. ensure you have installed the [`git-flow` command line helpers](https://github.com/nvie/gitflow) and [`git-flow-completion` utils](https://github.com/bobthecow/git-flow-completion) then run `git flow init -d`. 21 | 22 | #### Optional Git Setup 23 | 24 | Set up `git` to always `rebase` rather than merge. 25 | 26 | ```sh 27 | git config --global branch.autosetuprebase always 28 | ``` 29 | 30 | Make sure `git` knows you are using your correct email. 31 | 32 | ```sh 33 | git config user.email "username@domain.suffix" 34 | ``` 35 | 36 | ### Working on new features 37 | 38 | 1. Create a "feature branch" for the change you wish to make via `git flow feature start {feature_name}`. See below for how to name features. 39 | 2. Now work on your changes locally until you are happy the issue is resolved. See below for how to name commit messages. 40 | 3. `git flow feature publish {feature_name}` will push it back up to your fork on GitHub. 41 | 4. Use `git flow feature pull {remote_name} {feature_name}` to bring in any other changes, If other people have also merged changes in, and you can't merge your PR automatically you'll need to `rebase` their changes into your changes and then `--force` push the resulting changes using standard `git` commands. 42 | 5. Use GitHub to raise a Pull Request. Add labels as appropriate, and set one or more reviewers. Then paste the url of the PR into the `#development` Slack channel with a request for someone to please review the changes. See below for how to name pull requests. 43 | 6. Respond to any comments as appropriate, making changes and `git push` ing further changes as appropriate. 44 | 7. When all comments are dealt and the PR finally gets a :+1: from someone else then merge the PR. _Note we will not be using the `git flow feature finish`_ option as that merges into develop automatically without the option for review. [see this stackexchange for more on that](http://programmers.stackexchange.com/questions/187723/code-review-with-git-flow-and-github). 45 | 8. In your command-line `git checkout develop` then `git pull upstream develop` to get the latest code and `git branch -D feature/{branchname}` to delete the old feature branch. 46 | 47 | #### Hotfixes and Support branches 48 | 49 | It's basically the same process but use the word `hotfix` or `support` instead of `feature`. `git flow` knows what to do. Just keep in mind that any changes are going to happen to your fork, and not the upstream repo. If you need to merge a `hotfix` into upstream main you may only do it va a reviewed pull request. 50 | 51 | ### Releasing to production 52 | 53 | 1. `git flow release start {tag.number}` (using semantic versioning) 54 | 2. commit any changes to version info in `package.json` then `git flow release publish {tag.number}` 55 | 3. `git flow release finish {tag.number}` merges the release into `main` of your fork, tags it, merges that back into `develop` on your fork and removes the release branch. 56 | 4. Now go back to GitHub and raise a Pull Request to merge the upstream main from your fork's `main` branch. When that goes through you are done. 57 | 5. In your command-line go back and clean up any outstanding branches and `git pull upstream` your local `main` and `develop` branches to ensure everything on your local machine is up to date with everyone's changes. 58 | 59 | Note you will **never** push changes directly to the upstream project, _only to your own fork_. 60 | 61 | **Changes may only be introduced into the upstream project via a properly reviewed pull request.** 62 | 63 | ## Naming things 64 | 65 | There are various systems, including GitHub itself, which will pick up the issue numbers from commit messages and pull requests and automatically associate them with the issues. It is therefore desirable to use a formal naming scheme for features, commit messages and pull requests. 66 | 67 | ### Features 68 | 69 | Features must be named per the following pattern `#{issue number}/{some_descriptive-text}` — so for example, if you are working on issue `ABC-1` with the title "do the thing", call your feature `ABC-1/do_the-thing`. Obviously use your common sense to avoid making the feature names too long. 70 | 71 | Note this will creating a feature via `git flow` will create a branch called `feature/{issue number}/{some_descriptive-text}`. 72 | 73 | ### Commit Messages 74 | 75 | When commiting something use the `-m` flag to add a short commit message of the format `{issue number} summary of what you changed`. So for example if you are working on issue `ABC-1` and you added a method to the `aardvark_controller` you might use the following commit message `"ABC-1 added anteater method to aardvark controller"` 76 | 77 | Commit messages ought to be in the past tense. 78 | 79 | In general try to group file changes wherever appropriate, so if your controller change also involved updating something in a helper file, the one commit message can happily encompas the changes to both files. The message ought to reflect the main aim of the change. 80 | 81 | ### Pull Requests 82 | 83 | Pull requests must be named as follows `[issue type, issue number] high level description of change`. The following Issue Types are recognised 84 | 85 | - `Bug Fix` - the change fixes a bug 86 | - `Feature` - the change adds a new feature (the usual issue type) 87 | - `Documentation` — The change is a documentation only change 88 | - `Optimisation` - The change is an optimisation of the code base without any functional changes 89 | 90 | If your change does not fit any of these categories, use `Feature`. Likewise if your change is not tied to an issue number you may use `n/a` instead. 91 | 92 | So to use the above example your Pull Request would be named `[Feature, ABC-1] added anteater to aardvark` 93 | 94 | ### You built it, you merge it 95 | 96 | A developer must be responsible for their own work, from accepting a task through to merging to production. With that in mind if you review another developer's PR, please don't then merge it yourself. As a general rule you must let the developer merge her own PRs. 97 | 98 | Likewise, don't expect someone else to merge your PR. Unless you do not have write permission on a project, you will always aim to take personal responsibility for the quality of the code that gets merged in. 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Horizontal Logo](logo/horizontal.svg) 2 | 3 | A collection of utilities to help with unit-testing [Sequelize](http://docs.sequelizejs.com) models and code that needs those models. 4 | 5 | [![NPM](https://nodei.co/npm/sequelize-test-helpers.png)](https://nodei.co/npm/sequelize-test-helpers/) 6 | 7 | ## Related Projects 8 | 9 | - [`sequelize-pg-utilities`](https://github.com/davesag/sequelize-pg-utilities) — Simple utilities that help you manage your Sequelize configuration. 10 | 11 | ## How to use 12 | 13 | ### Prerequisites 14 | 15 | This library assumes: 16 | 17 | 1. You are using [`chai`](http://www.chaijs.com) — Version 4 or better. 18 | 2. You are using [`sinon`](http://sinonjs.org) — Version 5 or better. 19 | 3. Using [`mocha`](https://mochajs.org) is also recommended, but as long as you are using `chai` and `sinon` this should work with any test runner. 20 | 21 | **Note** Jest is not supported unless you are also using `sinon` and `chai`, which is unlikely. 22 | 23 | ### Installation 24 | 25 | Add `sequelize-test-helpers` as a `devDependency`: 26 | 27 | ```sh 28 | npm i -D sequelize-test-helpers 29 | ``` 30 | 31 | ## Examples 32 | 33 | ### Unit testing models created with `sequelize.define` 34 | 35 | **Note**: See below for how to test models created using `Model.init` 36 | 37 | Let's say you have a Sequelize model `User` as follows: 38 | 39 | #### `src/models/User.js` 40 | 41 | ```js 42 | const model = (sequelize, DataTypes) => { 43 | const User = sequelize.define( 44 | 'User', 45 | { 46 | age: { 47 | type: DataTypes.INTEGER.UNSIGNED 48 | }, 49 | firstName: { 50 | type: DataTypes.STRING, 51 | allowNull: false, 52 | validate: { 53 | notEmpty: true 54 | } 55 | }, 56 | lastName: { 57 | type: DataTypes.STRING, 58 | allowNull: false, 59 | validate: { 60 | notEmpty: true 61 | } 62 | }, 63 | email: { 64 | type: DataTypes.STRING, 65 | allowNull: false, 66 | unique: true, 67 | lowercase: true, 68 | validate: { 69 | isEmail: true, 70 | notEmpty: true 71 | } 72 | }, 73 | token: { 74 | type: DataTypes.STRING, 75 | validate: { 76 | notEmpty: true 77 | } 78 | } 79 | }, 80 | { 81 | indexes: [ 82 | { unique: true, fields: ['email'] }, 83 | { unique: true, fields: ['token'] }, 84 | { unique: false, fields: ['firstName', 'lastName'] } 85 | ] 86 | } 87 | ) 88 | 89 | User.associate = ({ Company }) => { 90 | User.belongsTo(Company) 91 | } 92 | 93 | return User 94 | } 95 | 96 | module.exports = model 97 | ``` 98 | 99 | You can use `sequelize-test-helpers` to unit-test this with `mocha` as follows: 100 | 101 | #### `test/unit/models/User.spec.js` 102 | 103 | ```js 104 | const { expect } = require('chai') 105 | 106 | const { 107 | sequelize, 108 | dataTypes, 109 | checkModelName, 110 | checkUniqueIndex, 111 | checkPropertyExists 112 | } = require('sequelize-test-helpers') 113 | 114 | const UserModel = require('../../src/models/User') 115 | 116 | describe('src/models/User', () => { 117 | const User = UserModel(sequelize, dataTypes) 118 | const user = new User() 119 | 120 | checkModelName(User)('User') 121 | 122 | context('properties', () => { 123 | ;['age', 'firstName', 'lastName', 'email', 'token'].forEach(checkPropertyExists(user)) 124 | }) 125 | 126 | context('associations', () => { 127 | const Company = 'some dummy company' 128 | 129 | before(() => { 130 | User.associate({ Company }) 131 | }) 132 | 133 | it('defined a belongsTo association with Company', () => { 134 | expect(User.belongsTo).to.have.been.calledWith(Company) 135 | }) 136 | }) 137 | 138 | context('indexes', () => { 139 | context('unique', () => { 140 | ;['email', 'token'].forEach(checkUniqueIndex(user)) 141 | }) 142 | 143 | context('non unique (and also composite in this example)', () => { 144 | ;[['firstName', 'lastName']].forEach(checkNonUniqueIndex(user)) 145 | }) 146 | }) 147 | }) 148 | ``` 149 | 150 | ### Built-in checks 151 | 152 | | Check | What it does | 153 | | --------------------- | ----------------------------------------------------- | 154 | | `checkHookDefined` | Checks that a particular hook is defined. | 155 | | `checkModelName` | Checks that the model is named correctly. | 156 | | `checkNonUniqueIndex` | Checks that a specific non-unique index is defined. | 157 | | `checkPropertyExists` | Checks that the model has defined the given property. | 158 | | `checkUniqueIndex` | Checks that a specific unique index is defined. | 159 | 160 | #### Deprecation notice 161 | 162 | | Check | Note | 163 | | -------------------------- | ------------------------------------------------------ | 164 | | `checkUniqueCompoundIndex` | Use either `checkUniqueIndex` or `checkNonUniqueIndex` | 165 | 166 | ### Checking associations 167 | 168 | The various association functions are stubbed so you can simply invoke the the model's `associate` function in a `before` block then use `sinon`'s standard expectation syntax to check they were called with the correct values. 169 | 170 | #### `hasOne` 171 | 172 | ```js 173 | it("defined a hasOne association with Image as 'profilePic'", () => { 174 | expect(User.hasOne).to.have.been.calledWith(Image, { 175 | as: 'profilePic' 176 | }) 177 | }) 178 | ``` 179 | 180 | #### `belongsTo` 181 | 182 | ```js 183 | it('defined a belongsTo association with Company', () => { 184 | expect(User.belongsTo).to.have.been.calledWith(Company) 185 | }) 186 | ``` 187 | 188 | #### `hasMany` 189 | 190 | ```js 191 | it("defined a hasMany association with User as 'employees'", () => { 192 | expect(Company.hasMany).to.have.been.calledWith(User, { 193 | as: 'employees' 194 | }) 195 | }) 196 | ``` 197 | 198 | #### `belongsToMany` 199 | 200 | ```js 201 | it("defined a belongsToMany association with Category through CategoriesCompanies as 'categories'", () => { 202 | expect(Company.belongsToMany).to.have.been.calledWith(Category, { 203 | through: CategoriesCompanies, 204 | as: 'categories' 205 | }) 206 | }) 207 | ``` 208 | 209 | ### Unit testing code that requires `models` 210 | 211 | Let's say you have a utility function that takes some data and uses it to update a user record. If the user does not exist it returns `null`. (Yes I know this is a contrived example) 212 | 213 | #### `src/utils/save.js` 214 | 215 | ```js 216 | const { User } = require('../models') 217 | 218 | const save = async ({ id, ...data }) => { 219 | const user = await User.findOne({ where: { id } }) 220 | if (user) return await user.update(data) 221 | return null 222 | } 223 | 224 | module.exports = save 225 | ``` 226 | 227 | You want to unit-test this without invoking a database connection (so you can't `require('src/models')` in your test). 228 | 229 | This is where `makeMockModels`, `sinon`, and [`proxyquire`](https://github.com/thlorenz/proxyquire) come in handy. 230 | 231 | #### `test/unit/utils/save.test.js` 232 | 233 | ```js 234 | const { expect } = require('chai') 235 | const { match, stub, resetHistory } = require('sinon') 236 | const proxyquire = require('proxyquire') 237 | 238 | const { makeMockModels } = require('sequelize-test-helpers') 239 | 240 | describe('src/utils/save', () => { 241 | const User = { findOne: stub() } 242 | const mockModels = makeMockModels({ User }) 243 | 244 | const save = proxyquire('../../../src/utils/save', { 245 | '../models': mockModels 246 | }) 247 | 248 | const id = 1 249 | const data = { 250 | firstName: 'Testy', 251 | lastName: 'McTestFace', 252 | email: 'testy.mctestface.test.tes', 253 | token: 'some-token' 254 | } 255 | const fakeUser = { id, ...data, update: stub() } 256 | 257 | let result 258 | 259 | context('user does not exist', () => { 260 | before(async () => { 261 | User.findOne.resolves(undefined) 262 | result = await save({ id, ...data }) 263 | }) 264 | 265 | after(resetHistory) 266 | 267 | it('called User.findOne', () => { 268 | expect(User.findOne).to.have.been.calledWith(match({ where: { id } })) 269 | }) 270 | 271 | it("didn't call user.update", () => { 272 | expect(fakeUser.update).not.to.have.been.called 273 | }) 274 | 275 | it('returned null', () => { 276 | expect(result).to.be.null 277 | }) 278 | }) 279 | 280 | context('user exists', () => { 281 | before(async () => { 282 | fakeUser.update.resolves(fakeUser) 283 | User.findOne.resolves(fakeUser) 284 | result = await save({ id, ...data }) 285 | }) 286 | 287 | after(resetHistory) 288 | 289 | it('called User.findOne', () => { 290 | expect(User.findOne).to.have.been.calledWith(match({ where: { id } })) 291 | }) 292 | 293 | it('called user.update', () => { 294 | expect(fakeUser.update).to.have.been.calledWith(match(data)) 295 | }) 296 | 297 | it('returned the user', () => { 298 | expect(result).to.deep.equal(fakeUser) 299 | }) 300 | }) 301 | }) 302 | ``` 303 | 304 | As a convenience, `makeMockModels` will automatically populate your `mockModels` with mocks of all of the models defined in your `src/models` folder (or if you have a `.sequelizerc` file it will look for the `models-path` in that). Simply override any of the specific models you need to do stuff with. 305 | 306 | ### Testing models created with `Model.init` 307 | 308 | Sequelize also allows you to create models by extending `Sequelize.Model` and invoking its static `init` function as follows: 309 | 310 | **Note**: creating your models this way makes it harder to test their use. 311 | 312 | ```js 313 | const { Model, DataTypes } = require('sequelize') 314 | 315 | const factory = sequelize => { 316 | class User extends Model {} 317 | User.init( 318 | { 319 | firstName: DataTypes.STRING, 320 | lastName: DataTypes.STRING 321 | }, 322 | { sequelize, modelName: 'User' } 323 | ) 324 | return User 325 | } 326 | 327 | module.exports = factory 328 | ``` 329 | 330 | You can test this using `sequelize-test-helpers`, `sinon`, and `proxyquire`. 331 | 332 | ```js 333 | const { expect } = require('chai') 334 | const { spy } = require('sinon') 335 | const proxyquire = require('proxyquire') 336 | const { sequelize, Sequelize } = require('sequelize-test-helpers') 337 | 338 | describe('src/models/User', () => { 339 | const { DataTypes } = Sequelize 340 | 341 | const UserFactory = proxyquire('src/models/User', { 342 | sequelize: Sequelize 343 | }) 344 | 345 | let User 346 | 347 | before(() => { 348 | User = UserFactory(sequelize) 349 | }) 350 | 351 | // It's important you do this 352 | after(() => { 353 | User.init.resetHistory() 354 | }) 355 | 356 | it('called User.init with the correct parameters', () => { 357 | expect(User.init).to.have.been.calledWith( 358 | { 359 | firstName: DataTypes.STRING, 360 | lastName: DataTypes.STRING 361 | }, 362 | { 363 | sequelize, 364 | modelName: 'User' 365 | } 366 | ) 367 | }) 368 | }) 369 | ``` 370 | 371 | ### Listing your models 372 | 373 | Assuming your `src/models/index.js` (or your equivalent) exports all your models, it's useful to be able to generate a list of their names. 374 | 375 | ```js 376 | const { listModels } = require('sequelize-test-helpers') 377 | 378 | console.log(listModels()) // will spit out a list of your model names. 379 | ``` 380 | 381 | Similarly to `makeMockModels` above, `listModels` will find all of the models defined in your `src/models` folder (or if you have a `.sequelizerc` file it will look for the `models-path` in that). 382 | 383 | ## Custom `models` paths and custom file suffixes 384 | 385 | By default `makeMockModels` and `listModels` will both look for your models in files ending with `.js` in either the models path defined in `.sequelizerc`, or in `src/models`. If however your models are not `.js` files and the `models` folder is somewhere else you can pass in a custom models folder path and a custom suffix. 386 | 387 | - `listModels(customModelsFolder, customSuffix)` 388 | 389 | ```js 390 | const modelNames = listModels('models', '.ts') 391 | ``` 392 | 393 | - `makeMockModels(yourCustomModels, customModelsFolder, customSuffix)` 394 | 395 | ```js 396 | const models = makeMockModels({ User: { findOne: stub() } }, 'models', '.ts') 397 | ``` 398 | 399 | ## Development 400 | 401 | ### Branches 402 | 403 | 404 | | Branch | Status | Coverage | Audit | Notes | 405 | | ------ | ------ | -------- | ----- | ----- | 406 | | `develop` | [![CircleCI](https://circleci.com/gh/davesag/sequelize-test-helpers/tree/develop.svg?style=svg)](https://circleci.com/gh/davesag/sequelize-test-helpers/tree/develop) | [![codecov](https://codecov.io/gh/davesag/sequelize-test-helpers/branch/develop/graph/badge.svg)](https://codecov.io/gh/davesag/sequelize-test-helpers) | [![Vulnerabilities](https://snyk.io/test/github/davesag/sequelize-test-helpers/develop/badge.svg)](https://snyk.io/test/github/davesag/sequelize-test-helpers/develop) | Work in progress | 407 | | `main` | [![CircleCI](https://circleci.com/gh/davesag/sequelize-test-helpers/tree/main.svg?style=svg)](https://circleci.com/gh/davesag/sequelize-test-helpers/tree/main) | [![codecov](https://codecov.io/gh/davesag/sequelize-test-helpers/branch/main/graph/badge.svg)](https://codecov.io/gh/davesag/sequelize-test-helpers) | [![Vulnerabilities](https://snyk.io/test/github/davesag/sequelize-test-helpers/main/badge.svg)](https://snyk.io/test/github/davesag/sequelize-test-helpers/main) | Latest stable release | 408 | 409 | ### Development Prerequisites 410 | 411 | - [NodeJS](https://nodejs.org). I use [`nvm`](https://github.com/creationix/nvm) to manage Node versions — `brew install nvm`. 412 | 413 | ### Initialisation 414 | 415 | ```sh 416 | npm install 417 | ``` 418 | 419 | ### Test it 420 | 421 | - `npm test` — runs the unit tests 422 | - `npm run test:unit:cov` — runs the unit tests with code coverage 423 | 424 | ### Lint it 425 | 426 | ```sh 427 | npm run lint 428 | ``` 429 | 430 | ## Contributing 431 | 432 | Please see the [contributing notes](CONTRIBUTING.md). 433 | 434 | ## Thanks 435 | 436 | - Thanks to [`reallinfo`](https://github.com/reallinfo) for the logo. 437 | -------------------------------------------------------------------------------- /logo/horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1249 | --------------------------------------------------------------------------------