├── .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 |
50 |
51 |
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 | [](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 | });
--------------------------------------------------------------------------------