├── test ├── helpers │ ├── sampleApp │ │ ├── config │ │ │ ├── session.js │ │ │ ├── globals.js │ │ │ ├── datastores.js │ │ │ └── models.js │ │ ├── api │ │ │ └── models │ │ │ │ ├── Hometown.js │ │ │ │ ├── Pet.js │ │ │ │ ├── Company.js │ │ │ │ ├── User.js │ │ │ │ ├── Group.js │ │ │ │ └── Role.js │ │ └── package.json │ ├── fixtures.js │ └── sailsHelper.js ├── overwriting.spec.js ├── emptying.spec.js ├── basic.spec.js └── sampleApp.spec.js ├── .travis.yml ├── .mocharc.js ├── .gitignore ├── LICENSE ├── package.json ├── .github └── workflows │ └── codeql-analysis.yml ├── lib ├── util.js └── index.js └── README.md /test/helpers/sampleApp/config/session.js: -------------------------------------------------------------------------------- 1 | module.exports.session = { secret: 'silly-secret' } 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "12" 5 | - "14" 6 | services: 7 | - mongodb 8 | 9 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extension: ['js'], 5 | reporter: 'spec', 6 | timeout: 10000 7 | }; 8 | -------------------------------------------------------------------------------- /test/helpers/sampleApp/config/globals.js: -------------------------------------------------------------------------------- 1 | module.exports.globals = { 2 | _: false, 3 | async: false, 4 | sails: true, 5 | models: true 6 | } 7 | -------------------------------------------------------------------------------- /test/helpers/sampleApp/api/models/Hometown.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | attributes: { 4 | name: { type: 'string', required: true } 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | nbproject 3 | .DS_Store 4 | npm-debug.log 5 | .tmp 6 | package-lock.json 7 | test/helpers/sampleApp/.tmp 8 | test/helpers/sampleApp/package-lock.json 9 | -------------------------------------------------------------------------------- /test/helpers/sampleApp/config/datastores.js: -------------------------------------------------------------------------------- 1 | module.exports.datastores = { 2 | default: { 3 | adapter: 'sails-mongo', 4 | host: 'localhost', 5 | port: 27017, 6 | database: 'sails-hook-fixtures-testdb' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/helpers/sampleApp/api/models/Pet.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | attributes: { 4 | name: { type: 'string', required: true }, 5 | 6 | /* A Pet is owned by one owner */ 7 | owner: { 8 | model: 'user' 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/helpers/sampleApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-sample-app", 3 | "version": "1.0.0", 4 | "description": "Sample App", 5 | "dependencies": { 6 | "sails": "~1.2.4", 7 | "sails-hook-orm": "^3.0.1", 8 | "sails-mongo": "Josebaseba/sails-mongo#master" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/helpers/sampleApp/api/models/Company.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | attributes: { 3 | name: { 4 | type: 'string', 5 | required: true 6 | }, 7 | companyGroups: { 8 | collection: 'group', 9 | via: 'company', 10 | dominant: true 11 | }, 12 | hometown: { 13 | model: 'hometown', 14 | required: true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/helpers/sampleApp/config/models.js: -------------------------------------------------------------------------------- 1 | module.exports.models = { 2 | datastore: 'default', 3 | migrate: 'drop', 4 | attributes: { 5 | createdAt: { 6 | type: 'ref', 7 | autoCreatedAt: true 8 | }, 9 | updatedAt: { 10 | type: 'ref', 11 | autoUpdatedAt: true 12 | }, 13 | id: { 14 | type: 'string', 15 | columnName: '_id' 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/helpers/sampleApp/api/models/User.js: -------------------------------------------------------------------------------- 1 | const User = { 2 | // Enforce model schema in the case of schemaless databases 3 | schema: true, 4 | 5 | attributes: { 6 | username: { type: 'string', unique: true, required: true }, 7 | /* A User can belong to many Groups */ 8 | groups: { 9 | collection: 'group', 10 | via: 'users' 11 | }, 12 | 13 | /* A User can have many Roles */ 14 | roles: { 15 | collection: 'role', 16 | via: 'users' 17 | }, 18 | 19 | /* A User can have many pets */ 20 | pets: { 21 | collection: 'pet', 22 | via: 'owner' 23 | }, 24 | 25 | /* A user has one Hometown */ 26 | home: { 27 | model: 'hometown' 28 | } 29 | 30 | } 31 | 32 | } 33 | 34 | module.exports = User 35 | -------------------------------------------------------------------------------- /test/helpers/sampleApp/api/models/Group.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Group.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | attributes: { 10 | name: { 11 | type: 'string', 12 | required: true 13 | }, 14 | /* A Group can contain many Users */ 15 | users: { 16 | collection: 'user', 17 | via: 'groups', 18 | dominant: true 19 | }, 20 | 21 | /* A group can have many Roles */ 22 | roles: { 23 | collection: 'role', 24 | via: 'groups' 25 | }, 26 | 27 | /* A company */ 28 | company: { 29 | collection: 'company', 30 | via: 'companyGroups' 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/helpers/sampleApp/api/models/Role.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Role.js 3 | * 4 | * @description :: A Role represents something a User or Group can be, like an 5 | administrator, manager or a reader. Naming of roles is left to 6 | to user of the application. Roles can be granted by an admin. 7 | * @docs :: http://sailsjs.org/#!documentation/models 8 | */ 9 | 10 | module.exports = { 11 | 12 | attributes: { 13 | name: { type: 'string', unique: true }, 14 | 15 | /* Users that have this Role */ 16 | users: { 17 | collection: 'user', 18 | via: 'roles', 19 | dominant: true 20 | }, 21 | /* Groups that have this Role */ 22 | groups: { 23 | collection: 'group', 24 | via: 'roles', 25 | dominant: true 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/overwriting.spec.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const fixtures = require('./helpers/fixtures') 3 | const chai = require('chai') 4 | chai 5 | .use(require('chai-as-promised')) 6 | 7 | const { reloadSails, lowerSails } = require('./helpers/sailsHelper') 8 | 9 | describe('Test overwriting ::', function () { 10 | let sails 11 | 12 | before(async () => { 13 | sails = await reloadSails(sails, fixtures) 14 | }) 15 | after(async () => { 16 | return lowerSails(sails) 17 | }) 18 | 19 | it('Should create new User models when reloading', async function () { 20 | 21 | const results = await sails.models.user.find() 22 | const idsBefore = _.map(results, 'id') 23 | sails = await reloadSails(sails) 24 | const results2 = await sails.models.user.find() 25 | const idsAfter = _.map(results2, 'id') 26 | idsAfter.should.not.equal(idsBefore) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/emptying.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const _ = require('lodash') 3 | const chai = require('chai') 4 | chai 5 | .use(require('chai-as-promised')) 6 | 7 | const { reloadSails, lowerSails } = require('./helpers/sailsHelper') 8 | 9 | describe('Test with weird fixtures ::', function () { 10 | let sails 11 | 12 | after(async () => { 13 | return lowerSails(sails) 14 | }) 15 | 16 | it('Should handle a missing "order" attribute', async () => { 17 | try { 18 | sails = await reloadSails(sails, {}) 19 | } catch (err) { 20 | assert(false, 'Should not throw') 21 | } 22 | }) 23 | }) 24 | 25 | describe('Test emptying ::', function () { 26 | let sails 27 | 28 | after(async () => { 29 | return lowerSails(sails) 30 | }) 31 | 32 | it('Should empty the Pet collection', async () => { 33 | sails = await reloadSails(sails, { empty: ['Pet'] }) 34 | const pets = await sails.models.pet.find() 35 | pets.should.have.length(0) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Luis Lobo Borobia 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 | -------------------------------------------------------------------------------- /test/helpers/fixtures.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | order: ['Hometown', 'User', 'Pet', 'Group', 'Role', 'Company'], 3 | overwrite: ['User'], 4 | User: [ 5 | { 6 | username: 'test' 7 | }, 8 | { 9 | username: 'admin' 10 | }, 11 | { 12 | username: 'Ash Ketchum', 13 | models: { 14 | home: ['Pallet Town'] 15 | } 16 | } 17 | ], 18 | Hometown: [ 19 | { 20 | name: 'Pallet Town' 21 | } 22 | ], 23 | Role: [ 24 | { 25 | name: 'admin', 26 | collections: { 27 | user: { username: 'admin' } 28 | } 29 | }, 30 | { 31 | name: 'user', 32 | collections: { 33 | user: { username: 'test' } 34 | } 35 | }, 36 | { 37 | name: 'reader', 38 | collections: { 39 | group: ['Ordinary group'] 40 | } 41 | } 42 | ], 43 | Group: [ 44 | { 45 | name: 'Admin group', 46 | collections: { 47 | user: { username: ['admin'] } 48 | } 49 | }, 50 | { 51 | name: 'Ordinary group', 52 | collections: { 53 | user: { username: ['test'] } 54 | } 55 | } 56 | ], 57 | Pet: [ 58 | { 59 | name: 'Pikachu', 60 | models: { 61 | owner: { username: 'Ash Ketchum' } 62 | } 63 | } 64 | ], 65 | Company: [ 66 | { 67 | name: 'Sweet & Co', 68 | models: { 69 | hometown: ['Pallet Town'] 70 | }, 71 | collections: { 72 | group: ['Admin group', 'Ordinary group'] 73 | } 74 | }, 75 | { 76 | id: '56ff2dcde7db1b663d0ae57e', 77 | name: 'Nice & Co', 78 | models: { 79 | hometown: ['Pallet Town'] 80 | }, 81 | collections: { 82 | group: ['Admin group', 'Ordinary group'] 83 | } 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /test/basic.spec.js: -------------------------------------------------------------------------------- 1 | /* global before, after, describe, it */ 2 | const Sails = require('sails').Sails 3 | const fixtures = require('./helpers/fixtures') 4 | 5 | const { lowerSails } = require('./helpers/sailsHelper') 6 | 7 | describe('Test without models ::', async () => { 8 | // app wrapper 9 | let sails 10 | 11 | // before, lift sails 12 | before(async () => { 13 | return new Promise((resolve, reject) => { 14 | // Try to lift 15 | new Sails().load({ 16 | hooks: { 17 | fixtures: require('../lib'), 18 | grunt: false 19 | }, 20 | log: { 21 | level: 'error' 22 | }, 23 | fixtures: fixtures 24 | }, function (err, _sails) { 25 | if (err) { 26 | reject(err) 27 | } 28 | sails = _sails 29 | resolve() 30 | }) 31 | }) 32 | }) 33 | 34 | after(async () => { 35 | return lowerSails(sails) 36 | }) 37 | 38 | it('should not crash when there are no models', async () => { 39 | return true 40 | }) 41 | }).timeout(10000) 42 | 43 | describe('Test without fixtures ::', async () => { 44 | // app wrapper 45 | let sails 46 | 47 | // before, lift sails 48 | before(async () => { 49 | return new Promise((resolve, reject) => { 50 | // Try to lift 51 | new Sails().load({ 52 | hooks: { 53 | fixtures: require('../lib'), 54 | grunt: false 55 | }, 56 | log: { 57 | level: 'error' 58 | } 59 | }, function (err, _sails) { 60 | if (err) { return reject(err) } 61 | sails = _sails 62 | return resolve() 63 | }) 64 | }) 65 | }) 66 | 67 | after(async () => { 68 | return lowerSails(sails) 69 | }) 70 | 71 | it('should not crash when there are no fixtures', function (done) { 72 | return done() 73 | }) 74 | }).timeout(10000) 75 | -------------------------------------------------------------------------------- /test/helpers/sailsHelper.js: -------------------------------------------------------------------------------- 1 | const Sails = require('sails').Sails 2 | const path = require('path') 3 | const debug = require('debug')('sails-hook-fixtures:sailsHelper') 4 | const resetCache = require('resnap')() 5 | const _ = require('lodash') 6 | 7 | async function loadSails (fixtures) { 8 | debug('loadSails', fixtures) 9 | if (!fixtures) { 10 | resetCache(); 11 | fixtures = require('./fixtures') 12 | } 13 | 14 | return new Promise((resolve, reject) => { 15 | // Try to lift 16 | new Sails().load({ 17 | appPath: path.join(__dirname, 'sampleApp'), 18 | hooks: { 19 | fixtures: require('../../lib'), 20 | grunt: false, 21 | views: false, 22 | blueprints: false 23 | }, 24 | log: { 25 | level: 'silent' 26 | }, 27 | datastores: { 28 | default: { 29 | adapter: 'sails-mongo', 30 | host: 'localhost', 31 | port: 27017, 32 | database: 'sails-hook-fixtures-testdb' 33 | } 34 | }, 35 | models: { 36 | datastore: 'default', 37 | migrate: 'drop' 38 | }, 39 | fixtures: fixtures 40 | }, function (err, sails) { 41 | if (err) { 42 | reject(err) 43 | } 44 | resolve(sails) 45 | }) 46 | }) 47 | } 48 | 49 | async function reloadSails (sails, fixtures) { 50 | await lowerSails(sails) 51 | resetCache() 52 | 53 | sails = await loadSails(fixtures) 54 | return sails 55 | } 56 | 57 | async function lowerSails (sails) { 58 | debug('lowerSails') 59 | return new Promise((resolve, reject) => { 60 | if (sails && _.isFunction(sails.lower)) { 61 | sails.lower({}, (err) => { 62 | if (err) { 63 | reject(err) 64 | } 65 | resolve() 66 | }) 67 | } 68 | resolve() 69 | }) 70 | } 71 | 72 | module.exports = { 73 | reloadSails, 74 | loadSails, 75 | lowerSails 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-hook-fixtures", 3 | "version": "2.0.0", 4 | "description": "Automatically install database fixtures with relations to your database when you lift Sails", 5 | "main": "lib/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha test/*.spec.js", 11 | "pretest": "cd test/helpers/sampleApp; npm i --silent --no-audit --no-fund ;cd ../../.." 12 | }, 13 | "sails": { 14 | "isHook": true 15 | }, 16 | "engines": { 17 | "npm": "^6.0.0", 18 | "node": ">=10" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/luislobo/sails-hook-fixtures" 23 | }, 24 | "keywords": [ 25 | "Sails.JS", 26 | "fixtures", 27 | "hook" 28 | ], 29 | "author": { 30 | "name": "Luis Lobo Borobia", 31 | "email": "luislobo@gmail.com", 32 | "url": "https://github.com/luislobo" 33 | }, 34 | "contributors": [ 35 | { 36 | "name": "Arryon Tijsma", 37 | "email": "arryon@gmail.com", 38 | "url": "https://github.com/luislobo", 39 | "contributions": 24, 40 | "additions": 1587, 41 | "deletions": 347, 42 | "hireable": null 43 | }, 44 | { 45 | "name": "Wesley Overdijk", 46 | "email": "r.w.overdijk@gmail.com", 47 | "url": "https://github.com/RWOverdijk", 48 | "contributions": 1, 49 | "additions": 1, 50 | "deletions": 3, 51 | "hireable": true 52 | }, 53 | { 54 | "name": "Luis Lobo Borobia", 55 | "email": "luislobo@gmail.com", 56 | "url": "https://github.com/luislobo", 57 | "contributions": 11, 58 | "additions": 239, 59 | "deletions": 176, 60 | "hireable": null 61 | } 62 | ], 63 | "license": "MIT", 64 | "bugs": { 65 | "url": "https://github.com/luislobo/sails-hook-fixtures/issues" 66 | }, 67 | "homepage": "https://github.com/luislobo/sails-hook-fixtures", 68 | "dependencies": { 69 | "bluebird": "3.7.2", 70 | "debug": "^4.1.1", 71 | "lodash": "4.17.19", 72 | "supports-color": "^7.1.0" 73 | }, 74 | "devDependencies": { 75 | "bcryptjs": "~2.4.3", 76 | "chai": "4.2.0", 77 | "chai-as-promised": "7.1.1", 78 | "mocha": "~8.0.1", 79 | "mongodb": "^3.5.9", 80 | "resnap": "^1.0.1", 81 | "sails": "~1.2.4", 82 | "standard": "~14.3.4" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 20 * * 1' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /test/sampleApp.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const fixtures = require('./helpers/fixtures') 3 | const _ = require('lodash') 4 | const chai = require('chai') 5 | chai.should() 6 | chai 7 | .use(require('chai-as-promised')) 8 | 9 | const { reloadSails, lowerSails } = require('./helpers/sailsHelper') 10 | 11 | describe('Test with one empty fixture :: ', () => { 12 | let sails 13 | 14 | before(async () => { 15 | delete require.cache[require.resolve('./helpers/fixtures')] 16 | const fixtures = _.cloneDeep(require('./helpers/fixtures')) 17 | // empty roles to test empty list 18 | fixtures.Role = [] 19 | 20 | sails = await reloadSails(sails, fixtures) 21 | }) 22 | after(async () => { 23 | return lowerSails(sails) 24 | }) 25 | 26 | it('Should disregard an empty fixture array', function () { 27 | }) 28 | }) 29 | 30 | describe('Test with models ::', () => { 31 | 32 | let sails 33 | before(async () => { 34 | delete require.cache[require.resolve('./helpers/fixtures')] 35 | const fixtures = require('./helpers/fixtures') 36 | sails = await reloadSails(sails, fixtures) 37 | }) 38 | after(async () => { 39 | return lowerSails(sails) 40 | }) 41 | 42 | it('Should have made three user documents', async function () { 43 | const results = await sails.models.user.find() 44 | results.should.be.an('array').to.have.length(3) 45 | 46 | }) 47 | 48 | it('Should have made two group documents', async function () { 49 | const results = await sails.models.group.find() 50 | results.should.have.length(2) 51 | }) 52 | 53 | it('Should have made three role documents', async function () { 54 | const results = await sails.models.role.find() 55 | results.should.have.length(3) 56 | }) 57 | 58 | it('Should have made two company documents', async function () { 59 | const results = await sails.models.company.find() 60 | results.should.have.length(2) 61 | }) 62 | 63 | it('Should have added an association to group "admin" to user "admin"', async function () { 64 | const admin = await sails.models.user.findOne({ username: 'admin' }).populate('groups') 65 | assert(_.some(admin.groups, { name: 'Admin group' })) 66 | }) 67 | 68 | it('Should have added roles to all users', async function () { 69 | const users = await sails.models.user.find().populate('roles') 70 | const roles = _.map(users, function (user) { 71 | return user.roles 72 | }) 73 | assert(_.some(roles, function (role) { 74 | return !_.isEmpty(role) 75 | })) 76 | }) 77 | 78 | it('Should Pikachu is owned by Ash Ketchum', async function () { 79 | const Pet = sails.models.pet 80 | const pikachu = await Pet.findOne({ name: 'Pikachu' }).populate('owner') 81 | pikachu.owner.username.should.equal('Ash Ketchum') 82 | }) 83 | 84 | it('Should say "Pallet Town" is Ash Ketchum\'s hometown', async function () { 85 | const ash = await sails.models.user.findOne({ username: 'Ash Ketchum' }).populate('home') 86 | ash.home.name.should.equal('Pallet Town') 87 | }) 88 | }).timeout(10000) 89 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird') 2 | const _ = require('lodash') 3 | const debug = require('debug')('sails-hook-fixtures:util') 4 | 5 | async function findAssociationIds (seed, key, collection, sails, singular) { 6 | debug('findAssociationIds', seed, key, collection, singular) 7 | var Model = sails.models[key] 8 | if (Model === undefined) { 9 | throw new Error('Undefined model ' + key) 10 | } 11 | // if the collection is of the form of an Array, assume we have to look for attribute 'name' 12 | // else, it will just be regular query 13 | const query = (collection instanceof Array ? { name: collection } : collection) 14 | const results = await Model.find(query) 15 | if (results.length === 0) { 16 | throw new Error('No models existed matching query: ' + JSON.stringify(query)) 17 | } else { 18 | // pluck the ids, append them to the correct attribute and return the 19 | // object with the correct ids for further promise map iteration 20 | const ids = _.map(results, 'id') 21 | if (singular === true) { 22 | return ids[0] 23 | } else { 24 | return ids 25 | } 26 | } 27 | 28 | } 29 | 30 | /** 31 | * Search the alias for a given collection 32 | */ 33 | function getAlias (Model, associationName) { 34 | debug('getAlias', Model.associations, associationName) 35 | const collection = _.find(Model.associations, function (assoc) { 36 | return assoc.collection === associationName 37 | }) 38 | const model = _.find(Model.associations, function (assoc) { 39 | return assoc.alias === associationName 40 | }) 41 | if (collection === undefined && model === undefined) { 42 | throw new Error('non-existing association: ' + associationName) 43 | } 44 | return collection ? collection.alias : model.model 45 | } 46 | 47 | async function addCollections (Model, seed, sails) { 48 | debug('addCollections', seed) 49 | // the collection keys resemble the models to which the many side of the association belong 50 | var collectionkeys = Object.keys(seed.collections) 51 | // sails.log.debug(collectionkeys); 52 | // construct object of promises to put in props 53 | var props = {} 54 | _.each(collectionkeys, function (key) { 55 | props[getAlias(Model, key)] = findAssociationIds(seed, key, seed.collections[key], sails) 56 | }) 57 | const collections = await Promise.props(props) 58 | 59 | // got a composite collections object, merge it with seed and return 60 | // sails.log.debug("Got collections: "); 61 | // sails.log.debug(collections); 62 | delete seed.collections 63 | return _.merge(seed, collections) 64 | 65 | } 66 | 67 | async function addModels (Model, seed, sails) { 68 | debug('addModels', seed) 69 | const modelkeys = Object.keys(seed.models) 70 | // sails.log.debug(modelkeys); 71 | const props = {} 72 | _.each(modelkeys, function (key) { 73 | props[key] = findAssociationIds(seed, getAlias(Model, key), seed.models[key], sails, true) 74 | }) 75 | const modelsobj = await Promise.props(props) 76 | 77 | // sails.log.debug("Got models: "); 78 | // sails.log.debug(modelsobj); 79 | delete seed.models 80 | return _.merge(seed, modelsobj) 81 | } 82 | 83 | function addAssociation (Model, seeds, sails) { 84 | debug('addAssociation', seeds) 85 | // sails.log.debug("Associate the following seeds: "); 86 | // sails.log.debug(seeds); 87 | return Promise.map(seeds, async function (seed) { 88 | // no associations are present, return as is 89 | if (seed.collections === undefined && seed.models === undefined) { 90 | return seed 91 | } 92 | if (seed.collections && seed.models) { 93 | // both are present 94 | const seedWithCollections = await addCollections(Model, seed, sails) 95 | return addModels(Model, seedWithCollections, sails) 96 | } else if (!seed.collections && seed.models) { 97 | // only model 98 | return addModels(Model, seed, sails) 99 | } else if (seed.collections && !seed.models) { 100 | // only collections 101 | return addCollections(Model, seed, sails) 102 | } 103 | }) 104 | } 105 | 106 | module.exports.create = async function (Model, fixtures, opts, sails) { 107 | debug('create', fixtures, opts) 108 | const result = await Model.count() 109 | 110 | if (result > 0) { 111 | // only throw here if we don't want to overwrite the model 112 | if (!_.includes(opts.overwrite, opts.modelName)) { 113 | throw new Error('Model not empty, skipping fixture') 114 | } else { 115 | // we want to delete all documents 116 | await Model.destroy({}) 117 | } 118 | } 119 | // actual creation in case of empty collection 120 | const populatedFixtures = await addAssociation(Model, fixtures, sails) 121 | debug('result of population', populatedFixtures) 122 | return Model.createEach(populatedFixtures).fetch() 123 | } 124 | 125 | module.exports.empty = async function (Model, opts, sails) { 126 | debug('empty', opts) 127 | const modelName = opts.modelName 128 | if (_.includes(opts.empty, opts.modelName)) { 129 | const results = await Model.find() 130 | if (results.length > 0) { 131 | sails.log.info('Removing ' + results.length + ' existing documents from ' + modelName) 132 | return Model.destroy({ id: _.map(results, 'id') }).fetch() 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Some code adapted from a permissions-hook in the making: 3 | * https://github.com/tjwebb/sails-permissions/blob/master/api/hooks/permissions-api/index.js#L26 4 | */ 5 | const Promise = require('bluebird') 6 | const util = require('./util') 7 | const _ = require('lodash') 8 | const debug = require('debug')('sails-hook-fixtures:index') 9 | 10 | function NoFixturesError () {} 11 | 12 | NoFixturesError.prototype = Object.create(Error.prototype) 13 | 14 | async function singleFixture (name, obj, hook, opts, sails) { 15 | debug('singleFixture', name, obj, hook, opts) 16 | if (obj.length === 0) { 17 | sails.log.verbose('Skipping empty fixtures list for ' + name) 18 | sails.emit(hook) 19 | return 20 | } 21 | sails.log.verbose('Installing ' + obj.length + ' fixtures for ' + name) 22 | const Model = sails.models[name.toLowerCase()] // can be undefined 23 | if (Model === undefined) { 24 | // fail silently 25 | sails.log.info('Undefined model `' + name + '`') 26 | sails.emit(hook) 27 | } else { 28 | // add the fixture name to options for easy access 29 | // add the fixture name to options for easy access 30 | opts.modelName = name 31 | 32 | // try to create the fixtures 33 | try { 34 | const created = await util.create(Model, obj, opts, sails) 35 | debug('created', created) 36 | } catch (err) { 37 | sails.log.error('Got error with ' + name + ' fixture: ' + err) 38 | } finally { 39 | debug({ hook }) 40 | sails.emit(hook) 41 | } 42 | } 43 | } 44 | 45 | function emptyExistingCollections (opts, sails) { 46 | debug('emptyExistingCollections', opts) 47 | return Promise.map(opts.empty, function (name) { 48 | const Model = sails.models[name.toLowerCase()] // can be undefined 49 | if (Model === undefined) { 50 | // fail silently 51 | sails.log.info('Undefined model for emptying: `' + name + '`') 52 | return 53 | } 54 | // add the fixture name to options for easy access 55 | opts.modelName = name 56 | return util.empty(Model, opts, sails) 57 | }) 58 | } 59 | 60 | /* 61 | * The real workhorse of installing fixtures starts here. Re-made with promises, 62 | * Should be a whole lot easier to interpret now 63 | */ 64 | 65 | async function installFixtures (sails, cb) { 66 | debug('installFixtures') 67 | // when the last fixture has been injected, the `loaded` hook will be called, 68 | // to which we subscribe the callback function of the hook 69 | sails.after('hook:fixture:loaded', function () { 70 | cb() 71 | }) 72 | 73 | const fixtures = sails.config.fixtures 74 | if (fixtures === undefined) { 75 | // go silently instead of throwing error 76 | sails.log.info('Unable to install fixtures: no config.fixtures is defined') 77 | sails.emit('hook:fixture:loaded') 78 | return 79 | } 80 | 81 | // initialize options 82 | const opts = { 83 | overwrite: fixtures.overwrite || [], 84 | empty: fixtures.empty || [] 85 | } 86 | 87 | /** 88 | * empty existing collections first 89 | */ 90 | try { 91 | await emptyExistingCollections(opts, sails) 92 | 93 | /** 94 | * If we have no 'order' attribute, no need for installing fixtures 95 | */ 96 | if (!fixtures.order) { 97 | // continue silently 98 | sails.log.info('No order specified, skipping fixtures') 99 | debug('No order specified, skipping fixtures') 100 | sails.emit('hook:fixture:loaded') 101 | return 102 | } 103 | /** 104 | * Each of the fixtures defined in the order has to be injected. This is done using 105 | * the sails.emit(...) function, caught by sails.after(...). 106 | * The function singleFixture installs the fixtures of one model, then emits that it's done 107 | * using the provided hook, after which the next in line fixture will be installed 108 | */ 109 | const hooks = _.map(_.tail(fixtures.order), function (name) { return name }) 110 | debug('hooks', hooks) 111 | _.each(hooks, function (name, idx) { 112 | // if this is the last value, set the hook to loaded 113 | const next = (hooks.length === idx + 1 ? 'loaded' : hooks[idx + 1]) 114 | sails.after('hook:fixture:' + name, async function () { 115 | await singleFixture(name, fixtures[name], 'hook:fixture:' + next, opts, sails) 116 | }) 117 | }) 118 | 119 | const first = fixtures.order[0] 120 | if (fixtures.order.length > 1) { 121 | const next = fixtures.order[1] 122 | await singleFixture(first, fixtures[first], 'hook:fixture:' + next, opts, sails) 123 | } else { 124 | await singleFixture(first, fixtures[first], 'hook:fixture:loaded', opts, sails) 125 | } 126 | 127 | } catch (err) { 128 | 129 | sails.log.error(err) 130 | sails.emit('hook:fixture:loaded') 131 | } 132 | } 133 | 134 | module.exports = function (sails) { 135 | return { 136 | configure: function () { 137 | debug('configure', 'Configuring fixtures') 138 | sails.log.silly('Configuring fixtures') 139 | }, 140 | 141 | initialize: async function (cb) { 142 | debug('initialize', 'Initializing fixtures') 143 | // If no ORM, do nothing 144 | if (!sails.hooks.orm) { 145 | return cb() 146 | } 147 | sails.after('hook:orm:loaded', function () { 148 | debug('hook:orm:loaded', 'Initializing fixtures') 149 | sails.log.verbose('initializing fixtures') 150 | installFixtures(sails, cb) 151 | }) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sails-hook-fixtures 2 | [![Build status][travis-image]][travis-url] 3 | [![Dependency Status][daviddm-image]][daviddm-url] 4 | 5 | An installable hook that injects fixtures into your Sails ORM at runtime with associations! 6 | 7 | ## Installation 8 | `npm i sails-hook-fixtures` 9 | 10 | ## Usage 11 | When lifting your Sails app, any fixtures that are loaded are checked against the model 12 | tables/collections in the database. If the database table/collection is empty, the 13 | fixtures for that table/collection are automatically installed. If you want to overwrite 14 | an existing table/collection each time you lift, use the `overwrite` option. 15 | 16 | Currently, only tested against Waterline + MongoDB. 17 | 18 | ## Configuration 19 | Define your `fixtures` object so that it is loaded as `sails.config.fixtures`. 20 | For instance, export it in [sails/config/local.js](http://sailsjs.org/#!/documentation/anatomy/myApp/config/local.js.html): 21 | 22 | ```javascript 23 | // sails/config/local.js 24 | module.exports = { 25 | fixtures: { ... } 26 | } 27 | ``` 28 | 29 | The fixtures object should have an attribute `order` specifying the order in which to populate your models. 30 | This is important if you want to associate models. 31 | 32 | The fixtures for a model are configured as an array of objects. Each object will be made into a document in the database. 33 | 34 | ```javascript 35 | // sails/config/local.js 36 | fixtures: { 37 | order: ['User', 'Pet'], 38 | User: [ 39 | { 40 | username: 'Walter', 41 | state: 'New Mexico' 42 | }, 43 | { 44 | username: 'Skyler', 45 | state: 'New Mexico' 46 | } 47 | ], 48 | Pet: [ 49 | { 50 | name: 'Pikachu', 51 | type: 'Electric' 52 | }, 53 | { 54 | name: 'Charizard', 55 | type: 'Fire' 56 | } 57 | ] 58 | } 59 | ``` 60 | 61 | For an example, take a look at [the sample fixtures used in testing](https://github.com/arryon/sails-hook-fixtures/blob/master/test/helpers/fixtures.js) 62 | 63 | Each attribute other than those of the options below is assumed to be the capitalized name of your model. 64 | Inability to find the model will silently fail and continue. 65 | 66 | ## Options 67 | | attribute name | usage | example | 68 | |-----------|----------------------------------------------------------------|----------------------------| 69 | | order | Specify the order in which to inject fixtures into models | `order: ['User', 'Group']` | 70 | | overwrite | Specify which model documents to always overwrite when lifting | `overwrite: ['User']` | 71 | | empty | Specify which model documents to empty before installing any fixtures | `empty: ['Pet']` | 72 | 73 | ## Associations 74 | The hook can automatically add associations between your models if you specify them in the right way. 75 | This only works if you use Waterline! 76 | 77 | ### Terminology 78 | * **Association/relation**: relation between two models 79 | * **Fixture**: object that is injected as a document into the database 80 | * **Document**: an entry in the database 81 | 82 | ### General Usage 83 | Define associations as you would look up documents in an existing sails application. 84 | Want to add user `jason` to group `testusers`? Define a relation in the fixture object that says `user: {username: 'jason'}`. You can add every [*where* query from sails](http://sailsjs.org/#!/documentation/concepts/ORM/Querylanguage.html) to the relation definition. 85 | 86 | Does the model you want to add have an attribute `name`? Then as added bonus you can directly input an array into the relation definition. If you want to associate `jason` with groups `user` and `editor`, and your `Group` model has an attribute `name`, you can just say `group: ['user', 'editor']`. 87 | 88 | Be sure when using relations to specify the right order of injecting the fixtures! When the *User* model hasn't been populated yet, a relation to *Group* can't be added. 89 | 90 | Different relations in Sails have different ways of adding them. For an extensive overview, see the [waterline associations documentation](https://github.com/balderdashy/waterline-docs/blob/master/associations.md) 91 | 92 | ### One-to-one relations 93 | Define a `models` attribute at the owning side of the relation. If a `User` owns one `Pet`, define the fixture as: 94 | 95 | ```javascript 96 | // sails/config/local.js 97 | fixtures: { 98 | // note that the order is reversed compared 99 | // to the One-to-many relation 100 | order: ['Pet', 'User'], 101 | User: [ 102 | { 103 | username: 'Robinson Crusoe', 104 | models: { 105 | // note we can pass an array directly here 106 | // instead of a 'where' query object, because 107 | // the underlying code will assume we want 108 | // to look up the 'name' attribute. 109 | // Is equivalent to {name: 'Wilson'}. 110 | pet: ['Wilson'] 111 | } 112 | } 113 | ], 114 | Pet: [ 115 | { 116 | name: 'Wilson' 117 | } 118 | ] 119 | } 120 | ``` 121 | 122 | ### One-to-many relations 123 | work with a `models` object inside the fixture. The relation will (and must) be added at the owned side of the relation. For instance, a `User` can have mulitiple pets, but a `Pet` can only belong to one user. Insert the relation at the `Pet` fixture like this: 124 | 125 | ```javascript 126 | // sails/config/local.js 127 | fixtures: { 128 | order: ['User', 'Pet'], 129 | User: [ 130 | { 131 | username: 'Schrodinger' 132 | } 133 | ], 134 | Pet: [ 135 | { 136 | name: 'cat', 137 | models: { 138 | owner: {username: 'Schrodinger'} 139 | } 140 | } 141 | ] 142 | } 143 | ``` 144 | 145 | ### Many-to-many relations 146 | Work with a `collections` object inside the fixture. The relation will be added at the model where you insert the `collections` definition. Make sure this is the same side that has `dominant: true` inside the model definition 147 | 148 | Example: 149 | ```javascript 150 | // sails/config/local.js 151 | fixtures: { 152 | // First, inject users. Then, group can have users 153 | // added to its model attribute 154 | order: ['User', 'Group'], 155 | User: [ 156 | { 157 | username: 'Turing', 158 | }, 159 | { 160 | username: 'Asimov' 161 | } 162 | ], 163 | Group: [ 164 | { 165 | name: 'Users', 166 | collections: { 167 | // Queries a 'where' query internally 168 | // with {username: user} as object. 169 | // resulting documents are added to the 170 | // alias of the 'user' association 171 | user: {username: ['Turing', 'Asimov']} 172 | } 173 | } 174 | ] 175 | } 176 | ``` 177 | 178 | ## Changelog 179 | 180 | ### Version 2.0.0 181 | - Compatible with Sails 1.x/Waterline 0.13 182 | 183 | ### Version 1.0.2 184 | - Compatible with Sails 0.12.x 185 | 186 | ## Development 187 | Want to contribute? Clone the repository, install the dependencies with `npm install`, and get going. You can test using the command `grunt`, which will also run JSHint against your changes to see if they are syntactically correct. 188 | 189 | Note that you need a working adaptor for the database. In the test files I describe a `test` connection that uses a local mongoDB instance. Change according to your database configuration. 190 | 191 | [travis-image]: https://travis-ci.org/luislobo/sails-hook-fixtures.svg?branch=master 192 | [travis-url]: https://travis-ci.org/luislobo/sails-hook-fixtures 193 | [daviddm-image]: https://david-dm.org/luislobo/sails-hook-fixtures.svg 194 | [daviddm-url]: https://david-dm.org/luislobo/sails-hook-fixtures 195 | --------------------------------------------------------------------------------