├── .travis.yml ├── api └── models │ ├── Stage.js │ ├── Group.js │ └── User.js ├── config ├── seed.js └── models.js ├── seeds ├── production │ ├── StageSeed.js │ └── UserSeed.js ├── development │ ├── StageSeed.js │ └── UserSeed.js └── test │ ├── StageSeed.js │ └── UserSeed.js ├── fixtures ├── development │ ├── StageSeed.js │ └── UserSeed.js ├── production │ ├── StageSeed.js │ └── UserSeed.js └── test │ ├── StageSeed.js │ └── UserSeed.js ├── .npmignore ├── .jshintrc ├── .gitignore ├── Gruntfile.js ├── test ├── bootstrap.spec.js ├── associations.spec.js └── seed.spec.js ├── package.json ├── index.js ├── doc.html ├── ASSOCIATION.md ├── lib ├── load.js └── work.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /api/models/Stage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sample model 3 | * @type {Object} 4 | */ 5 | module.exports = { 6 | attributes: { 7 | name: { 8 | type: 'string' 9 | } 10 | } 11 | }; -------------------------------------------------------------------------------- /config/seed.js: -------------------------------------------------------------------------------- 1 | module.exports.seed = { 2 | //directory where migration resides 3 | //relative to `sails.appPath` 4 | path: 'seeds', 5 | 6 | //enable or disable seeding 7 | active: true 8 | } -------------------------------------------------------------------------------- /seeds/production/StageSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //array of data to seed 6 | //it may also be an object 7 | module.exports = [{ 8 | name: faker.internet.userName(), 9 | }]; -------------------------------------------------------------------------------- /fixtures/development/StageSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //array of data to seed 6 | //it may also be an object 7 | module.exports = [{ 8 | name: faker.internet.userName(), 9 | }]; -------------------------------------------------------------------------------- /fixtures/production/StageSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //array of data to seed 6 | //it may also be an object 7 | module.exports = [{ 8 | name: faker.internet.userName(), 9 | }]; -------------------------------------------------------------------------------- /seeds/development/StageSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //array of data to seed 6 | //it may also be an object 7 | module.exports = [{ 8 | name: faker.internet.userName(), 9 | }]; -------------------------------------------------------------------------------- /fixtures/development/UserSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //array of data to seed 6 | //it may also be an object 7 | module.exports = [{ 8 | username: faker.internet.userName(), 9 | email: faker.internet.email() 10 | }]; -------------------------------------------------------------------------------- /fixtures/production/UserSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //array of data to seed 6 | //it may also be an object 7 | module.exports = [{ 8 | username: faker.internet.userName(), 9 | email: faker.internet.email() 10 | }]; -------------------------------------------------------------------------------- /seeds/development/UserSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //array of data to seed 6 | //it may also be an object 7 | module.exports = [{ 8 | username: faker.internet.userName(), 9 | email: faker.internet.email() 10 | }]; -------------------------------------------------------------------------------- /seeds/production/UserSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //array of data to seed 6 | //it may also be an object 7 | module.exports = [{ 8 | username: faker.internet.userName(), 9 | email: faker.internet.email() 10 | }]; 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | api 2 | config 3 | node_modules 4 | ssl 5 | .DS_STORE 6 | *~ 7 | .idea 8 | nbproject 9 | test 10 | .git 11 | .gitignore 12 | .tmp 13 | *.swo 14 | *.swp 15 | *.swn 16 | *.swm 17 | .jshintrc 18 | .editorconfig 19 | doc.html 20 | seeds 21 | fixtures 22 | .travis.yml 23 | Grintfile -------------------------------------------------------------------------------- /fixtures/test/StageSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //function to be evaluated to obtain data 6 | //it may also be an object or array 7 | module.exports = function(done) { 8 | 9 | var data = [{ 10 | name: faker.internet.userName(), 11 | }, { 12 | name: faker.internet.userName(), 13 | }]; 14 | 15 | done(null, data); 16 | }; -------------------------------------------------------------------------------- /seeds/test/StageSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //function to be evaluated to obtain data 6 | //it may also be an object or array 7 | module.exports = function(done) { 8 | 9 | var data = [{ 10 | name: faker.internet.userName(), 11 | }, { 12 | name: faker.internet.userName(), 13 | }]; 14 | 15 | done(null, data); 16 | }; -------------------------------------------------------------------------------- /fixtures/test/UserSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //function to be evaluated to obtain data 6 | //it may also be an object or array 7 | module.exports = function(done) { 8 | 9 | var data = [{ 10 | username: faker.internet.userName(), 11 | email: faker.internet.email() 12 | }, { 13 | username: faker.internet.userName(), 14 | email: faker.internet.email() 15 | }]; 16 | 17 | done(null, data); 18 | }; -------------------------------------------------------------------------------- /seeds/test/UserSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var faker = require('faker'); 4 | 5 | //function to be evaluated to obtain data 6 | //it may also be an object or array 7 | module.exports = function(done) { 8 | 9 | var data = [{ 10 | username: faker.internet.userName(), 11 | email: faker.internet.email() 12 | }, { 13 | username: faker.internet.userName(), 14 | email: faker.internet.email() 15 | }]; 16 | 17 | done(null, data); 18 | }; 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "esnext": true, 8 | "immed": true, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "node": true, 13 | "mocha": true, 14 | "quotmark": "single", 15 | "strict": true, 16 | "undef": true, 17 | "unused": true, 18 | "expr": true, 19 | "ignore": true, 20 | "globals": { 21 | "User": true, 22 | "sails": true, 23 | "_": true, 24 | "async": true 25 | } 26 | } -------------------------------------------------------------------------------- /api/models/Group.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sample model 3 | * @type {Object} 4 | */ 5 | module.exports = { 6 | attributes: { 7 | name: { 8 | type: 'string' 9 | }, 10 | // associations 11 | hasOneUser: { 12 | model: 'user' 13 | }, 14 | // many-to-one 15 | hasManyUsers: { 16 | collection: 'user', 17 | via: 'hasOneGroup', 18 | }, 19 | // many-to-many 20 | manyManyUsers: { 21 | collection: 'user', 22 | via: 'manyManyGroups', 23 | dominant: true 24 | } 25 | } 26 | }; -------------------------------------------------------------------------------- /api/models/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sample model 3 | * @type {Object} 4 | */ 5 | module.exports = { 6 | attributes: { 7 | username: { 8 | type: 'string' 9 | }, 10 | email: { 11 | type: 'email' 12 | }, 13 | // associations 14 | hasOneGroup: { 15 | model: 'group' 16 | }, 17 | // many-to-one 18 | hasManyGroups: { 19 | collection: 'group', 20 | via: 'hasOneUser' 21 | }, 22 | // many-to-many 23 | manyManyGroups: { 24 | collection: 'group', 25 | via: 'manyManyUsers' 26 | } 27 | } 28 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | #Temporary data 6 | .tmp 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # Deployed apps should consider commenting this line out: 27 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 28 | node_modules 29 | doc 30 | startup.sh -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Add the grunt-mocha-test and jshint tasks. 6 | grunt.loadNpmTasks('grunt-mocha-test'); 7 | grunt.loadNpmTasks('grunt-contrib-jshint'); 8 | 9 | grunt.initConfig({ 10 | // Configure a mochaTest task 11 | mochaTest: { 12 | test: { 13 | options: { 14 | reporter: 'spec', 15 | timeout: 20000 16 | }, 17 | src: ['test/**/*.js'] 18 | } 19 | }, 20 | jshint: { 21 | options: { 22 | reporter: require('jshint-stylish'), 23 | jshintrc: '.jshintrc' 24 | }, 25 | all: [ 26 | 'Gruntfile.js', 27 | 'index.js', 28 | 'lib/**/*.js', 29 | 'test/**/*.js' 30 | ] 31 | } 32 | }); 33 | 34 | //custom tasks 35 | grunt.registerTask('default', ['jshint', 'mochaTest']); 36 | grunt.registerTask('test', ['jshint', 'mochaTest']); 37 | 38 | }; -------------------------------------------------------------------------------- /test/bootstrap.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This file is useful when you want to execute some 5 | * code before and after running your tests 6 | * (e.g. lifting and lowering your sails application): 7 | */ 8 | var sails = require('sails'); 9 | 10 | /** 11 | * Lifting sails before all tests 12 | */ 13 | before(function(done) { 14 | sails 15 | .lift({ // configuration for testing purposes 16 | port: 7070, 17 | environment: 'test', 18 | log: { 19 | noShip: true, 20 | level: 'info' //lets see debug 21 | }, 22 | models: { 23 | migrate: 'drop' 24 | }, 25 | hooks: { 26 | sockets: false, 27 | views: false, 28 | http: false, 29 | pubsub: false, 30 | grunt: false //we dont need grunt in test 31 | } 32 | }, function(error, sails) { 33 | if (error) { 34 | return done(error); 35 | } 36 | done(null, sails); 37 | }); 38 | }); 39 | 40 | 41 | /** 42 | * Lowering sails after done testing 43 | */ 44 | after(function(done) { 45 | User 46 | .destroy() 47 | .then(function( /*result*/ ) { 48 | sails.lower(done); 49 | }) 50 | .catch(function(error) { 51 | done(error); 52 | }); 53 | }); -------------------------------------------------------------------------------- /config/models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default model configuration 3 | * (sails.config.models) 4 | * 5 | * Unless you override them, the following properties will be included 6 | * in each of your models. 7 | * 8 | * For more info on Sails models, see: 9 | * http://sailsjs.org/#/documentation/concepts/ORM 10 | */ 11 | module.exports.models = { 12 | 13 | /*************************************************************************** 14 | * * 15 | * Your app's default connection. i.e. the name of one of your app's * 16 | * connections (see `config/connections.js`) * 17 | * * 18 | ***************************************************************************/ 19 | // connection: 'localDiskDb', 20 | 21 | /*************************************************************************** 22 | * * 23 | * How and whether Sails will attempt to automatically rebuild the * 24 | * tables/collections/etc. in your schema. * 25 | * * 26 | * See http://sailsjs.org/#/documentation/concepts/ORM/model-settings.html * 27 | * * 28 | ***************************************************************************/ 29 | // migrate: 'alter' 30 | 31 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-hook-seed", 3 | "version": "0.2.6", 4 | "description": "DRY data seeding for sails", 5 | "main": "index.js", 6 | "scripts": { 7 | "pretest": "npm link && npm link sails-hook-seed", 8 | "test": "grunt test", 9 | "posttest": "rm -rf .tmp" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/lykmapipo/sails-hook-seed.git" 14 | }, 15 | "author": "Lally Elias", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/lykmapipo/sails-hook-seed/issues" 19 | }, 20 | "homepage": "https://github.com/lykmapipo/sails-hook-seed", 21 | "contributors": [{ 22 | "name": "lykmapipo", 23 | "github": "https://github.com/lykmapipo" 24 | }, { 25 | "name": "pschuegr", 26 | "github": "https://github.com/pschuegr" 27 | }, { 28 | "name": "givanse", 29 | "github": "https://github.com/givanse" 30 | }], 31 | "keywords": [ 32 | "sails", 33 | "sail", 34 | "hook", 35 | "seed", 36 | "fixtures", 37 | "dump", 38 | "database", 39 | "load", 40 | "populate", 41 | "initialize", 42 | "test", 43 | "testing", 44 | "develop", 45 | "development", 46 | "production", 47 | "factory" 48 | ], 49 | "sails": { 50 | "isHook": true 51 | }, 52 | "dependencies": { 53 | "include-all": "^0.1.6" 54 | }, 55 | "devDependencies": { 56 | "chai": "^1.10.0", 57 | "faker": "^2.1.2", 58 | "grunt": "^0.4.5", 59 | "grunt-contrib-jshint": "^0.11.2", 60 | "grunt-mocha-test": "^0.12.7", 61 | "jshint-stylish": "^1.0.1", 62 | "mocha": "^2.1.0", 63 | "sails": "^0.11.0", 64 | "sails-disk": "^0.10.7" 65 | } 66 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var path = require('path'); 5 | var loadSeeds = require(path.join(__dirname, 'lib', 'load')); 6 | /** 7 | * @description DRY data seeding for sails. 8 | 9 | * @param {Object} sails a sails application 10 | * @return {Object} sails-hook-seed which follow installable sails-hook spec 11 | */ 12 | module.exports = function(sails) { 13 | //return hook 14 | return { 15 | 16 | //Defaults configurations 17 | defaults: { 18 | //set seeding to be active by default 19 | active: true, 20 | 21 | //directory where migration resides 22 | //relative to `sails.appPath` 23 | path: 'seeds' 24 | }, 25 | 26 | //Runs automatically when the hook initializes 27 | initialize: function(done) { 28 | //reference this hook 29 | var hook = this; 30 | 31 | //extend defaults configuration 32 | //with provided configuration from sails 33 | //config 34 | var config = 35 | _.extend(hook.defaults, sails.config.seed); 36 | 37 | //if seeding is disabled back-off 38 | if (!config.active) { 39 | done(); 40 | } 41 | 42 | //continue with seeding 43 | else { 44 | // Lets wait on some of the sails core hooks to 45 | // finish loading before 46 | // load `sails-hook-seed` 47 | var eventsToWaitFor = []; 48 | 49 | if (sails.hooks.orm) { 50 | eventsToWaitFor.push('hook:orm:loaded'); 51 | } 52 | 53 | if (sails.hooks.pubsub) { 54 | eventsToWaitFor.push('hook:pubsub:loaded'); 55 | } 56 | 57 | sails 58 | .after(eventsToWaitFor, function() { 59 | //load seeds 60 | loadSeeds(config, done); 61 | }); 62 | } 63 | 64 | } 65 | }; 66 | 67 | }; -------------------------------------------------------------------------------- /doc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | sails-hook-seed | DRY data seeding for sails. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 |
36 |
37 |

sails-hook-seed

38 | 44 |
45 |
46 | 47 | 48 |
49 |
50 | 51 |
52 | 55 |
56 |
57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /ASSOCIATION.md: -------------------------------------------------------------------------------- 1 | # Association seeding 2 | 3 | ## Givance merge 4 | ```sh 5 | Fast-forward 6 | api/models/Group.js | 24 +++++ 7 | api/models/User.js | 16 +++- 8 | lib/load.js | 19 ++-- 9 | lib/work.js | 69 +++++++++++++-- 10 | seeds/development/UserSeed.js | 2 +- 11 | seeds/production/UserSeed.js | 2 +- 12 | seeds/test/UserSeed.js | 2 +- 13 | test/associations.spec.js | 198 ++++++++++++++++++++++++++++++++++++++++++ 14 | test/seed.spec.js | 10 +-- 15 | 9 files changed, 319 insertions(+), 23 deletions(-) 16 | create mode 100644 api/models/Group.js 17 | create mode 100644 test/associations.spec.js 18 | ``` 19 | 20 | ## Associations Dump 21 | ```javascript 22 | associations: { 23 | [ 24 | { alias: 'hasOneGroup', 25 | type: 'model', 26 | model: 'group' }, 27 | { alias: 'hasManyGroups', 28 | type: 'collection', 29 | collection: 'group', 30 | via: 'hasOneUser' }, 31 | { alias: 'manyManyGroups', 32 | type: 'collection', 33 | collection: 'group', 34 | via: 'manyManyUsers' } 35 | ] 36 | }; 37 | ``` 38 | 39 | ## Note 40 | - Seeds are applied in parallel with no order 41 | - Undestand `sails` association implementation 42 | - Understand sails `via`, `dominant` and other associations terminology 43 | - Make use of `seeds` to drive seeding and not `models` to drive seeding when implementing seeding logics 44 | 45 | ## Reference Models 46 | We will be using [sailsjs association models](http://sailsjs.org/#!/documentation/concepts/ORM) as reference models to be used in implementing `association seeding` in `sails-hook-seed`. 47 | 48 | ## Object Based Implementation 49 | 50 | ### One Way Associations 51 | 52 | 53 | ### Two Way Associations 54 | 55 | ### One-to-One 56 | 57 | 58 | 59 | # References 60 | - [Rails FixtureSet](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html) 61 | - [ActiveRecord Fixtures](http://api.rubyonrails.org/v3.2.8/classes/ActiveRecord/Fixtures.html) 62 | - [TestFixtures API](http://api.rubyonrails.org/classes/ActiveRecord/TestFixtures.html) 63 | - [Test Fixture Class Methods](http://api.rubyonrails.org/classes/ActiveRecord/TestFixtures/ClassMethods.html) 64 | - [FixtureSet ClassCache](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet/ClassCache.html) -------------------------------------------------------------------------------- /lib/load.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var path = require('path'); 5 | var prepareWork = require(path.join(__dirname, 'work')); 6 | 7 | 8 | /** 9 | * @function 10 | * @description loading seed's data into configured model persistent storage 11 | * @param {Object} config seed hook configurations 12 | * @param {Function} done a callback to invoke on after seeding 13 | */ 14 | module.exports = function(config, done) { 15 | //guess current sails environment 16 | var environment = sails.config.environment || 'test'; 17 | 18 | //deduce seeds path to use 19 | //based on current environment 20 | var seedsPath = 21 | path.join(sails.config.appPath, config.path, environment); 22 | 23 | //log seed environment 24 | sails.log.debug('start seeding %s data', environment); 25 | 26 | //log seed location 27 | sails.log.debug('seeding from %s', seedsPath); 28 | 29 | //load all seeds available 30 | //in `seedsPath` 31 | var seeds = require('include-all')({ 32 | dirname: seedsPath, 33 | filter: /(.+Seed)\.js$/, 34 | excludeDirs: /^\.(git|svn)$/, 35 | optional: true 36 | }); 37 | 38 | //prepare seeding work to perfom 39 | var work = prepareWork(seeds); 40 | 41 | //if there is a work to perform 42 | if (_.size(work) > 0) { 43 | 44 | async 45 | .waterfall([ 46 | function seedModels(next) { 47 | //now lets do the work 48 | //in parallel fashion 49 | async.parallel(work, next); 50 | }, 51 | function seedAssociations(associationsWork, next) { 52 | // flatten lists 53 | associationsWork = [].concat.apply([], associationsWork); 54 | 55 | if (_.size(associationsWork) > 0) { 56 | 57 | //seed associations if available 58 | sails.log.debug('load associations'); 59 | 60 | //TODO what results to log? 61 | async.parallel(associationsWork, next); 62 | } else { 63 | next(); 64 | } 65 | } 66 | ], 67 | function(error, results) { 68 | //signal seeding complete 69 | sails.log.debug('complete seeding %s data', environment); 70 | 71 | done(error, { 72 | environment: environment, 73 | data: results 74 | }); 75 | }); 76 | } 77 | //nothing to perform back-off 78 | else { 79 | done(); 80 | } 81 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sails-hook-seed 2 | ==================== 3 | 4 | [![Build Status](https://travis-ci.org/lykmapipo/sails-hook-seed.svg?branch=master)](https://travis-ci.org/lykmapipo/sails-hook-seed) 5 | 6 | 7 | DRY data seeding for sails. 8 | 9 | Simplify seeding data to your persistent storage of your choice based on the current running environment obtained from `sails.config.environment` of your application. You may use `sails-hook-seed` during `test`, `development` and even seed your application with default data during deployment in `production` environment. 10 | 11 | *Note: This requires Sails v0.11.0+. If v0.11.0+ isn't published to NPM yet, you'll need to install it via Github.* 12 | 13 | ## Installation 14 | ```js 15 | $ npm install --save sails-hook-seed 16 | ``` 17 | 18 | *You may opt to install [Faker](https://github.com/marak/Faker.js/) as your test and development seed generator* 19 | ```js 20 | $ npm install --save-dev faker 21 | ``` 22 | 23 | ## Usage 24 | By default `sails-hook-seed` look for environment specific seeds in the `seeds` directory inside `sails.appPath` of your application. Example, if you need to seed your application during `test` you will have to create `seeds/test` and add `model seed files` inside it. 25 | 26 | `sails-hook-seed` will load any file suffix-ed with `Seed` as a seed. Example, if you want to seed your `User` model during `test` your need to write your seed as folow: 27 | 28 | ```js 29 | //in seed/test/UserSeed.js 30 | var faker = require('faker'); 31 | 32 | //array of plain object 33 | //to seed in User model 34 | module.exports = [{ 35 | username: faker.internet.userName(), 36 | email: faker.internet.email() 37 | }]; 38 | ``` 39 | 40 | ## Seed Types 41 | `sails-hook-seed` accept `array type`, `plain object` and `functional` type seeds. 42 | 43 | #### Object Seed Type 44 | ```js 45 | //in seed/test/UserSeed.js 46 | var faker = require('faker'); 47 | 48 | //object to seed 49 | //in User model 50 | module.exports = { 51 | username: faker.internet.userName(), 52 | email: faker.internet.email() 53 | }; 54 | ``` 55 | 56 | #### Array Seed Type 57 | ```js 58 | //in seed/test/UserSeed.js 59 | var faker = require('faker'); 60 | 61 | //array of data to seed 62 | module.exports = [{ 63 | username: faker.internet.userName(), 64 | email: faker.internet.email() 65 | }]; 66 | ``` 67 | 68 | #### Functional Seed Type 69 | ```js 70 | //in seed/test/UserSeed.js 71 | var faker = require('faker'); 72 | 73 | //function to be evaluated to obtain data 74 | module.exports = function(done) { 75 | 76 | var data = [{ 77 | username: faker.internet.userName(), 78 | email: faker.internet.email() 79 | }, { 80 | username: faker.internet.userName(), 81 | email: faker.internet.email() 82 | }]; 83 | 84 | //remember to tell when your are done 85 | done(null, data); 86 | }; 87 | ``` 88 | 89 | 90 | The same convection must be followed for `development` and `production` environment. 91 | 92 | *Note: Environement specific folder are named after their environment name, e.g if `sails.config.environment` is `test`, then to make sure your test seeds are loaded they must be placed under `seed/test` folder for `sails-hook-seed` to pick and apply your seeds. Your may look this repo `seeds folder` to see example* 93 | 94 | ## Configuration 95 | `sails-hook-seed` accept application defined configuration by utilizing sails configuration api. In sails `config` directory add `config/seed.js` and you will be able to override all the defauts configurations. 96 | 97 | Simply, copy the below and add it to your `config/seed.js` 98 | ```js 99 | module.exports.seed = { 100 | //directory where migration resides 101 | //relative to `sails.appPath` 102 | path: 'seeds', 103 | 104 | //enable or disable seeding 105 | active: true 106 | } 107 | ``` 108 | 109 | ## Testing 110 | 111 | * Clone this repository 112 | 113 | * Install `grunt-cli` global 114 | 115 | ```sh 116 | $ npm install -g grunt-cli 117 | ``` 118 | 119 | * Install all development dependencies 120 | 121 | ```sh 122 | $ npm install 123 | ``` 124 | 125 | * Then run test 126 | 127 | ```sh 128 | $ npm test 129 | ``` 130 | 131 | ## Contribute 132 | 133 | Fork this repo and push in your ideas. 134 | Do not forget to add a bit of test(s) of what value you adding. 135 | 136 | # TODO 137 | - [ ] Allow specifying custom find attributes(criteria) in seeds 138 | - [ ] Remove associations and collection data type from seed finder 139 | - [ ] Adding seed dependencies management to allow one seed to depend on other seed(s) 140 | - [ ] Use rails migrations dependencies style to allow seeding for associations(inspiration) 141 | - [ ] Adding beforeSeed hook(s) 142 | - [ ] Adding afterSeed hook(s) 143 | - [ ] Add ability to specify seed run type(EVERY,ONCE) 144 | - [ ] Add seed phases (cleanup, prepare, seed, done) 145 | - [ ] Make seed event emmitter to allow them to interact and trigger other seeds 146 | - [ ] Add ability to clean up all seeded data 147 | - [ ] Controll depth of loading associations 148 | - [ ] Add ability to disable individual seed 149 | - [ ] Try to use delete/create in test and development to see if timeout issue will be solved 150 | - [ ] Make sure in production we use findOrCreate 151 | - [ ] Fix timeout issues 152 | 153 | ## Licence 154 | 155 | Copyright (c) 2015 lykmapipo & Contributors 156 | 157 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 158 | 159 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 160 | 161 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 162 | -------------------------------------------------------------------------------- /lib/work.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * @function 6 | * @description log seed activities 7 | * @param {Object} model valid sails model 8 | * @param {Object} data data to seed into model 9 | */ 10 | function log(model, data) { 11 | //TODO handle log level configurations 12 | //TODO check if logging is allowed in current environment 13 | 14 | //convert seed data into string 15 | var seedAsString = JSON.stringify(data); 16 | 17 | //if convertion is success 18 | if (seedAsString) { 19 | var ellipsis = '...'; 20 | 21 | //deduce maximum logging message length 22 | var debugLength = 50 - ellipsis.length; 23 | 24 | //if seed string length is greater than 25 | //maximum allowed seed log message 26 | //reduce seed string to the maximum allowed 27 | //log message length 28 | if (seedAsString.length > debugLength) { 29 | seedAsString = seedAsString.substring(0, debugLength) + ellipsis; 30 | } 31 | } 32 | 33 | //TODO use provided log level from configuration 34 | sails.log.debug('%s %s', model.adapter.identity, seedAsString); 35 | } 36 | 37 | 38 | /** 39 | * @function 40 | * @description apply model associations 41 | * @param {String} modelIdentity model name 42 | * @param {Object} record stored record 43 | * @param {Object} association association meta data and data 44 | * @param {Function} next callback 45 | */ 46 | function applyAssociation(modelIdentity, record, association, next) { 47 | var message = modelIdentity + ' { id: ' + record.id + ', ' + 48 | association.alias + ': ' + 49 | association.idsList + ' }'; 50 | 51 | sails.log.debug(message); 52 | 53 | record[association.alias].add(association.idsList); 54 | 55 | record.save(next); 56 | } 57 | 58 | 59 | /** 60 | * @function 61 | * @description find or create model 62 | * @param {Object} model valid sails model 63 | * @param {Object} data data seed 64 | * @param {Function} next callback to be invoked on success or error 65 | */ 66 | function findOrCreate(model, dataObject, next) { 67 | // prepare pendingAssociations list 68 | var pendingAssociations = []; 69 | 70 | //visit all model association and prepare 71 | //migrations work 72 | for (var i = 0; i < model.associations.length; i++) { 73 | var association = model.associations[i]; 74 | 75 | //TODO what about `model` associations 76 | if (association.type !== 'collection') { 77 | continue; 78 | } 79 | 80 | if (!dataObject[association.alias]) { 81 | continue; 82 | } 83 | 84 | association = { 85 | alias: model.associations[i].alias, 86 | idsList: dataObject[association.alias] 87 | }; 88 | 89 | // remove association ids from the seed object 90 | delete dataObject[association.alias]; 91 | 92 | pendingAssociations.push(association); 93 | } 94 | 95 | model.findOrCreate(dataObject, dataObject, function(error, record) { 96 | //TODO do we log before performing an action or after performing it 97 | //TODO what this log inform? 98 | log(model, dataObject); 99 | 100 | if (error) { 101 | sails.log.error(error.message); 102 | return next(error); 103 | } 104 | 105 | var modelIdentity = model.adapter.identity; 106 | var associationsWork = []; 107 | pendingAssociations.forEach(function(association) { 108 | var work = function(next) { 109 | applyAssociation(modelIdentity, record, association, next); 110 | }; 111 | associationsWork.push(work); 112 | }); 113 | 114 | //TODO what about created record? 115 | //is there no need to return it 116 | next(null, associationsWork); 117 | }); 118 | } 119 | 120 | 121 | /** 122 | * @function 123 | * @description prepare work to be performed during seeding the data 124 | * @param {Object} seeds environment specific loaded seeds from the seeds directory 125 | * @return {Array} a collection of works to be performed during data loading 126 | */ 127 | exports = module.exports = function(seeds) { 128 | //work to be done 129 | //in parallel during 130 | //data seeding 131 | var work = []; 132 | 133 | //prepare all seeds 134 | //data for parallel execution 135 | _.keys(seeds) 136 | .forEach(function(seed) { 137 | // deduce model globalId 138 | var modelGlobalId = seed.replace(/Seed$/, '').toLowerCase(); 139 | 140 | //grab sails model from its globalId 141 | //NOTE!: this is safe cause other may 142 | //enable model to be global but others 143 | //may not 144 | var Model = sails.models[modelGlobalId]; 145 | 146 | //grab data to load 147 | //from the seed data attribute 148 | var seedData = seeds[seed]; 149 | 150 | //prepare work from seed data 151 | exports.prepare(work, Model, seedData); 152 | 153 | }); 154 | 155 | return work; 156 | }; 157 | 158 | 159 | /** 160 | * @description Take seed data and check if it is of array or object type 161 | * and prepare work to be performed from it 162 | * @param {Array} work A collection of database queries to be 163 | * performed to seed data into database 164 | * @param {Object} model A valid sails model 165 | * @param {Object|Array|Function} seedData An array or object contains 166 | * data or a function to be evaluated 167 | * to obtain data to seeded into database 168 | */ 169 | exports.prepare = function(work, model, seedData) { 170 | //is data just a plain object 171 | if (_.isPlainObject(seedData)) { 172 | //push work to be done 173 | work.push(function(next) { 174 | //create seed function 175 | findOrCreate(model, seedData, next); 176 | }); 177 | } 178 | 179 | //is array data 180 | if (_.isArray(seedData)) { 181 | _.forEach(seedData, function(data) { 182 | //push work to be done 183 | work.push(function(next) { 184 | //create seed function 185 | findOrCreate(model, data, next); 186 | }); 187 | }); 188 | } 189 | 190 | //is functional data 191 | if (_.isFunction(seedData)) { 192 | //evaluate function to obtain data 193 | seedData(function(error, data) { 194 | //current log error and continue 195 | // 196 | //TODO should we throw? 197 | if (error) { 198 | sails.log.error(error); 199 | } 200 | 201 | //invoke prepare with data 202 | else { 203 | exports.prepare(work, model, data); 204 | } 205 | }); 206 | } 207 | }; -------------------------------------------------------------------------------- /test/associations.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var expect = require('chai').expect; 5 | var path = require('path'); 6 | 7 | var prepareWork = require(path.join(__dirname, '..', 'lib', 'work')); 8 | 9 | describe('Hook#seed associations', function() { 10 | 11 | beforeEach('clean up seeds', function(done) { 12 | sails.models.user.destroy(1, function() { 13 | sails.models.group.destroy(1, done); 14 | }); 15 | }); 16 | 17 | 18 | it('should be able to associate one-to-one', function(done) { 19 | var seeds = { 20 | UserSeed: { 21 | id: 1, 22 | username: 'user one-to-one', 23 | hasOneGroup: 1 24 | }, 25 | GroupSeed: { 26 | id: 1, 27 | name: 'group one-to-one', 28 | hasOneUser: 1 29 | } 30 | }; 31 | 32 | var work = prepareWork(seeds); 33 | 34 | async.parallel(work, function(error, associationsWork) { 35 | if (error) { 36 | done(error); 37 | } else { 38 | associationsWork = [].concat.apply([], associationsWork); 39 | // verify associations work list 40 | expect(associationsWork).to.be.a('array'); 41 | expect(associationsWork.length).to.be.equal(0); 42 | 43 | // verify the association 44 | sails.models.group 45 | .findOne(1) 46 | .populate('hasOneUser') 47 | .exec(function(err, group) { 48 | if (err) { 49 | return done(err); 50 | } 51 | 52 | expect(group.hasOneUser.username) 53 | .to.be.equal('user one-to-one'); 54 | 55 | sails.models.user 56 | .findOne(1) 57 | .populate('hasOneGroup') 58 | .exec(function(err, user) { 59 | if (err) { 60 | return done(err); 61 | } 62 | 63 | expect(user.hasOneGroup.name) 64 | .to.be.equal('group one-to-one'); 65 | 66 | sails.models.group.destroy(1); 67 | sails.models.user.destroy(1, done); 68 | }); 69 | }); 70 | } 71 | }); 72 | }); 73 | 74 | 75 | it('should be able to associate many-to-one', function(done) { 76 | var seeds = { 77 | UserSeed: { 78 | id: 1 79 | }, 80 | GroupSeed: { 81 | name: 'group many-to-one', 82 | hasManyUsers: [1] 83 | } 84 | }; 85 | 86 | var work = prepareWork(seeds); 87 | 88 | async.parallel(work, function(error, associationsWork) { 89 | if (error) { 90 | done(error); 91 | } else { 92 | associationsWork = [].concat.apply([], associationsWork); 93 | // verify associations work list 94 | expect(associationsWork).to.be.a('array'); 95 | expect(associationsWork.length).to.be.equal(1); 96 | 97 | var associationWork = associationsWork[0]; 98 | expect(associationWork).to.be.a('function'); 99 | 100 | // execute work and apply the Group association 101 | associationWork(function(err, record) { 102 | if (err) { 103 | return done(err); 104 | } 105 | var expectedGroupName = 'group many-to-one'; 106 | expect(record.name).to.be.equal(expectedGroupName); 107 | expect(record.hasManyUsers.length).to.be.equal(1); 108 | 109 | // verify the association from the User 110 | sails.models.user 111 | .findOne(1) 112 | .populate('hasOneGroup') 113 | .exec(function(err, user) { 114 | if (err) { 115 | return done(err); 116 | } 117 | 118 | expect(user.hasOneGroup.name) 119 | .to.be.equal(expectedGroupName); 120 | 121 | done(); 122 | }); 123 | }); 124 | } 125 | }); 126 | }); 127 | 128 | 129 | it('should be able to associate many-to-many', function(done) { 130 | var seeds = { 131 | UserSeed: { 132 | id: 1 133 | }, 134 | GroupSeed: { 135 | name: 'group many-to-many', 136 | manyManyUsers: [1] 137 | } 138 | }; 139 | 140 | var work = prepareWork(seeds); 141 | 142 | async.parallel(work, function(error, associationsWork) { 143 | if (error) { 144 | done(error); 145 | } else { 146 | associationsWork = [].concat.apply([], associationsWork); 147 | // verify associations work list 148 | expect(associationsWork).to.be.a('array'); 149 | expect(associationsWork.length).to.be.equal(1); 150 | 151 | var associationWork = associationsWork[0]; 152 | expect(associationWork).to.be.a('function'); 153 | 154 | // execute work and apply the Group association 155 | associationWork(function(err, record) { 156 | if (err) { 157 | return done(err); 158 | } 159 | var expectedGroupName = 'group many-to-many'; 160 | expect(record.name).to.be.equal(expectedGroupName); 161 | expect(record.manyManyUsers.length).to.be.equal(1); 162 | 163 | // verify the association from the User 164 | sails.models.user 165 | .findOne(1) 166 | .populate('manyManyGroups') 167 | .exec(function(err, user) { 168 | if (err) { 169 | return done(err); 170 | } 171 | 172 | var groups = user.manyManyGroups; 173 | expect(groups.length).to.be.equal(1); 174 | var group = groups[0]; 175 | expect(group.name).to.be.equal(expectedGroupName); 176 | 177 | done(); 178 | }); 179 | }); 180 | } 181 | }); 182 | }); 183 | 184 | 185 | it('should throw an error of type insert', function(done) { 186 | var seeds = { 187 | GroupSeed: { 188 | name: 'group one', 189 | manyManyUsers: [999] 190 | } 191 | }; 192 | 193 | var work = prepareWork(seeds)[0]; 194 | 195 | work(function(error, associationsWork) { 196 | if (error) { 197 | done(error); 198 | } else { 199 | var associationWork = associationsWork[0]; 200 | expect(associationWork).to.be.a('function'); 201 | 202 | // execute work and apply the Group association 203 | associationWork(function(err, record) { 204 | err = err[0]; 205 | expect(err.type).to.be.equal('insert'); 206 | var expected = 'group_manymanyusers__user_manymanygroups'; 207 | expect(err.collection).to.be.equal(expected); 208 | done(null, record); 209 | }); 210 | } 211 | }); 212 | }); 213 | 214 | }); -------------------------------------------------------------------------------- /test/seed.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var expect = require('chai').expect; 5 | var faker = require('faker'); 6 | var path = require('path'); 7 | 8 | var prepareWork = require(path.join(__dirname, '..', 'lib', 'work')); 9 | var loadSeeds = require(path.join(__dirname, '..', 'lib', 'load')); 10 | 11 | describe('Hook#seed', function() { 12 | 13 | it('should be loaded as installable hook', function(done) { 14 | expect(sails.hooks.seed).to.not.be.null; 15 | done(); 16 | }); 17 | 18 | it('should load persistent storage with the provided seeds', function(done) { 19 | User 20 | .count(function(error, count) { 21 | if (error) { 22 | done(error); 23 | } else { 24 | expect(count).to.be.above(0); 25 | done(); 26 | } 27 | }); 28 | }); 29 | 30 | 31 | it('should be able to prepare work(s) to be performed from `array` seeds type', function(done) { 32 | var seeds = { 33 | UserSeed: [{ 34 | username: faker.internet.userName(), 35 | email: faker.internet.email() 36 | }, { 37 | username: faker.internet.userName(), 38 | email: faker.internet.email() 39 | }] 40 | }; 41 | 42 | var works = prepareWork(seeds); 43 | 44 | expect(works).to.be.a('array'); 45 | expect(works.length).to.be.equal(2); 46 | 47 | var work = works[0]; 48 | 49 | expect(work).to.be.a('function'); 50 | 51 | //note! 52 | //since a work its just a wrapper for 53 | //Model.findOrCreate 54 | //lets be sure its doing 55 | //what it supposed to do 56 | work(function(error, user) { 57 | if (error) { 58 | done(error); 59 | } else { 60 | expect(user.id).to.not.be.null; 61 | expect(user.username).to.not.be.null; 62 | expect(user.email).to.not.be.null; 63 | done(); 64 | } 65 | }); 66 | }); 67 | 68 | 69 | it('should be able to prepare work to be performed from `object` seed type', function(done) { 70 | var seeds = { 71 | UserSeed: { 72 | username: faker.internet.userName(), 73 | email: faker.internet.email() 74 | } 75 | }; 76 | 77 | var works = prepareWork(seeds); 78 | 79 | expect(works).to.be.a('array'); 80 | expect(works.length).to.be.equal(1); 81 | 82 | var work = works[0]; 83 | 84 | expect(work).to.be.a('function'); 85 | 86 | //note! 87 | //since a work its just a wrapper for 88 | //Model.findOrCreate 89 | //lets be sure its doing 90 | //what it supposed to do 91 | work(function(error, user) { 92 | if (error) { 93 | done(error); 94 | } else { 95 | expect(user.id).to.not.be.null; 96 | expect(user.username).to.not.be.null; 97 | expect(user.email).to.not.be.null; 98 | done(); 99 | } 100 | }); 101 | }); 102 | 103 | it('should be able to prepare work(s) to be performed from `function` seeds type', function(done) { 104 | var seeds = { 105 | UserSeed: function(done) { 106 | 107 | var data = [{ 108 | username: faker.internet.userName(), 109 | email: faker.internet.email() 110 | }, { 111 | username: faker.internet.userName(), 112 | email: faker.internet.email() 113 | }]; 114 | 115 | done(null, data); 116 | } 117 | }; 118 | 119 | var works = prepareWork(seeds); 120 | 121 | expect(works).to.be.a('array'); 122 | expect(works.length).to.be.equal(2); 123 | 124 | var work = works[0]; 125 | 126 | expect(work).to.be.a('function'); 127 | 128 | //note! 129 | //since a work its just a wrapper for 130 | //Model.findOrCreate 131 | //lets be sure its doing 132 | //what it supposed to do 133 | work(function(error, user) { 134 | if (error) { 135 | done(error); 136 | } else { 137 | expect(user.id).to.not.be.null; 138 | expect(user.username).to.not.be.null; 139 | expect(user.email).to.not.be.null; 140 | done(); 141 | } 142 | }); 143 | }); 144 | 145 | it('should be able to prepare work(s) to be performed from seeds start with `S` letter', function(done) { 146 | var seeds = { 147 | StageSeed: function(done) { 148 | 149 | var data = [{ 150 | name: faker.internet.userName(), 151 | }, { 152 | name: faker.internet.userName(), 153 | }]; 154 | 155 | done(null, data); 156 | } 157 | }; 158 | 159 | var works = prepareWork(seeds); 160 | 161 | expect(works).to.be.a('array'); 162 | expect(works.length).to.be.equal(2); 163 | 164 | var work = works[0]; 165 | 166 | expect(work).to.be.a('function'); 167 | 168 | //note! 169 | //since a work its just a wrapper for 170 | //Model.findOrCreate 171 | //lets be sure its doing 172 | //what it supposed to do 173 | work(function(error, stage) { 174 | if (error) { 175 | done(error); 176 | } else { 177 | expect(stage.id).to.not.be.null; 178 | expect(stage.name).to.not.be.null; 179 | done(); 180 | } 181 | }); 182 | }); 183 | 184 | 185 | it('should be able to load test environment specific seeds', function(done) { 186 | sails.config.environment = 'test'; 187 | var config = { 188 | path: 'seeds' 189 | }; 190 | 191 | loadSeeds(config, function(error, result) { 192 | if (error) { 193 | done(error); 194 | } else { 195 | expect(result.environment).to.equal('test'); 196 | expect(result.data).to.not.be.null; 197 | done(); 198 | } 199 | }); 200 | 201 | }); 202 | 203 | 204 | it('should be able to load development environment specific seeds', function(done) { 205 | sails.config.environment = 'development'; 206 | var config = { 207 | path: 'seeds' 208 | }; 209 | 210 | loadSeeds(config, function(error, result) { 211 | if (error) { 212 | done(error); 213 | } else { 214 | expect(result.environment).to.equal('development'); 215 | expect(result.data).to.not.be.null; 216 | done(); 217 | } 218 | }); 219 | }); 220 | 221 | 222 | it('should be able to load production environment specific seeds', function(done) { 223 | sails.config.environment = 'production'; 224 | var config = { 225 | path: 'seeds' 226 | }; 227 | 228 | loadSeeds(config, function(error, result) { 229 | if (error) { 230 | done(error); 231 | } else { 232 | expect(result.environment).to.equal('production'); 233 | expect(result.data).to.not.be.null; 234 | done(); 235 | } 236 | }); 237 | }); 238 | 239 | 240 | it('should be able to load seeds from custom path', function(done) { 241 | sails.config.environment = 'test'; 242 | var config = { 243 | path: 'fixtures' 244 | }; 245 | 246 | loadSeeds(config, function(error, result) { 247 | if (error) { 248 | done(error); 249 | } else { 250 | expect(result.environment).to.equal('test'); 251 | expect(result.data).to.not.be.null; 252 | done(); 253 | } 254 | }); 255 | }); 256 | 257 | }); --------------------------------------------------------------------------------