From ce81185d2a3cb1f7e7e08d04b4a086631855d810 Mon Sep 17 00:00:00 2001 From: Lorefnon Date: Sat, 12 Oct 2019 18:51:22 +0530 Subject: [PATCH 1/3] Convert Seeder to a class --- lib/seed/Seeder.js | 318 ++++++++++++++++++++++----------------------- 1 file changed, 158 insertions(+), 160 deletions(-) diff --git a/lib/seed/Seeder.js b/lib/seed/Seeder.js index 8951635495..046b4377d2 100644 --- a/lib/seed/Seeder.js +++ b/lib/seed/Seeder.js @@ -18,173 +18,171 @@ const { // The new seeds we're performing, typically called from the `knex.seed` // interface on the main `knex` object. Passes the `knex` instance performing // the seeds. -function Seeder(knex) { - this.knex = knex; - this.config = this.setConfig(knex.client.config.seeds); -} +class Seeder { + constructor(knex) { + this.knex = knex; + this.config = this.setConfig(knex.client.config.seeds); + } -// Runs seed files for the given environment. -Seeder.prototype.run = async function(config) { - this.config = this.setConfig(config); - return this._seedData() - .bind(this) - .then(([all]) => { - const files = - config && config.specific - ? all.filter((file) => file === config.specific) - : all; - - return this._runSeeds(files); - }); -}; - -// Creates a new seed file, with a given name. -Seeder.prototype.make = function(name, config) { - this.config = this.setConfig(config); - if (!name) - Bluebird.rejected( - new Error('A name must be specified for the generated seed') - ); - return this._ensureFolder(config) - .bind(this) - .then(this._generateStubTemplate) - .then(this._writeNewSeed(name)); -}; - -// Lists all available seed files as a sorted array. -Seeder.prototype._listAll = async function(config) { - this.config = this.setConfig(config); - const loadExtensions = this.config.loadExtensions; - return Bluebird.promisify(fs.readdir, { context: fs })( - this._absoluteConfigDir() - ) - .bind(this) - .then((seeds) => - filter(seeds, function(value) { - const extension = path.extname(value); - return includes(loadExtensions, extension); - }).sort() + // Runs seed files for the given environment. + async run(config) { + this.config = this.setConfig(config); + const [all] = await this._seedData(); + const files = + config && config.specific + ? all.filter((file) => file === config.specific) + : all; + return this._runSeeds(files); + } + + // Creates a new seed file, with a given name. + async make(name, config) { + this.config = this.setConfig(config); + if (!name) + throw new Error('A name must be specified for the generated seed') + await this._ensureFolder(config); + const tmpl = await this._generateStubTemplate(); + await this._writeNewSeed(name)(tmpl); + } + + // Lists all available seed files as a sorted array. + async _listAll(config) { + this.config = this.setConfig(config); + const loadExtensions = this.config.loadExtensions; + return Bluebird.promisify(fs.readdir, { context: fs })( + this._absoluteConfigDir() + ) + .bind(this) + .then((seeds) => + filter(seeds, function(value) { + const extension = path.extname(value); + return includes(loadExtensions, extension); + }).sort() + ); + } + + // Gets the seed file list from the specified seed directory. + _seedData() { + return Bluebird.join(this._listAll()); + } + + // Ensures a folder for the seeds exist, dependent on the + // seed config settings. + _ensureFolder() { + const dir = this._absoluteConfigDir(); + return Bluebird.promisify(fs.stat, { context: fs })(dir).catch(() => + Bluebird.promisify(mkdirp)(dir) ); -}; - -// Gets the seed file list from the specified seed directory. -Seeder.prototype._seedData = function() { - return Bluebird.join(this._listAll()); -}; - -// Ensures a folder for the seeds exist, dependent on the -// seed config settings. -Seeder.prototype._ensureFolder = function() { - const dir = this._absoluteConfigDir(); - return Bluebird.promisify(fs.stat, { context: fs })(dir).catch(() => - Bluebird.promisify(mkdirp)(dir) - ); -}; - -// Run seed files, in sequence. -Seeder.prototype._runSeeds = function(seeds) { - return Bluebird.all(map(seeds, bind(this._validateSeedStructure, this))) - .bind(this) - .then(function(seeds) { - return Bluebird.bind(this).then(function() { - return this._waterfallBatch(seeds); + } + + // Run seed files, in sequence. + _runSeeds(seeds) { + return Bluebird.all(map(seeds, bind(this._validateSeedStructure, this))) + .bind(this) + .then(function(seeds) { + return Bluebird.bind(this).then(function() { + return this._waterfallBatch(seeds); + }); }); - }); -}; + } -// Validates seed files by requiring and checking for a `seed` function. -Seeder.prototype._validateSeedStructure = function(name) { - const seed = require(path.join(this._absoluteConfigDir(), name)); - if (typeof seed.seed !== 'function') { - throw new Error(`Invalid seed file: ${name} must have a seed function`); + // Validates seed files by requiring and checking for a `seed` function. + _validateSeedStructure(name) { + const seed = require(path.join(this._absoluteConfigDir(), name)); + if (typeof seed.seed !== 'function') { + throw new Error(`Invalid seed file: ${name} must have a seed function`); + } + return name; } - return name; -}; -// Generates the stub template for the current seed file, returning a compiled template. -Seeder.prototype._generateStubTemplate = function() { - const stubPath = - this.config.stub || + _getStubPath() { + return this.config.stub || path.join(__dirname, 'stub', this.config.extension + '.stub'); - return Bluebird.promisify(fs.readFile, { context: fs })(stubPath).then( - (stub) => template(stub.toString(), { variable: 'd' }) - ); -}; - -// Write a new seed to disk, using the config and generated filename, -// passing any `variables` given in the config to the template. -Seeder.prototype._writeNewSeed = function(name) { - const { config } = this; - const dir = this._absoluteConfigDir(); - return function(tmpl) { - if (name[0] === '-') name = name.slice(1); - const filename = name + '.' + config.extension; - return Bluebird.promisify(fs.writeFile, { context: fs })( - path.join(dir, filename), - tmpl(config.variables || {}) - ).return(path.join(dir, filename)); - }; -}; - -// Runs a batch of seed files. -Seeder.prototype._waterfallBatch = function(seeds) { - const { knex } = this; - const seedDirectory = this._absoluteConfigDir(); - let current = Bluebird.bind({ failed: false, failedOn: 0 }); - const log = []; - each(seeds, (seed) => { - const name = path.join(seedDirectory, seed); - seed = require(name); - - // Run each seed file. - current = current.then(() => - // Nesting promise to prevent bubbling up of error on catch - Promise.resolve() - .then(() => seed.seed(knex)) - .then(() => log.push(name)) - .catch((originalError) => { - const error = new Error( - `Error while executing "${name}" seed: ${originalError.message}` - ); - error.original = originalError; - error.stack = - error.stack - .split('\n') - .slice(0, 2) - .join('\n') + - '\n' + - originalError.stack; - throw error; - }) + } + + // Generates the stub template for the current seed file, returning a compiled template. + _generateStubTemplate() { + const stubPath = this._getStubPath(); + return Bluebird.promisify(fs.readFile, { context: fs })(stubPath).then( + (stub) => template(stub.toString(), { variable: 'd' }) ); - }); - - return current.then(() => [log]); -}; - -Seeder.prototype._absoluteConfigDir = function() { - return path.resolve(process.cwd(), this.config.directory); -}; - -Seeder.prototype.setConfig = function(config) { - return extend( - { - extension: 'js', - directory: './seeds', - loadExtensions: [ - '.co', - '.coffee', - '.eg', - '.iced', - '.js', - '.litcoffee', - '.ls', - '.ts', - ], - }, - this.config || {}, - config - ); -}; + } + + // Write a new seed to disk, using the config and generated filename, + // passing any `variables` given in the config to the template. + _writeNewSeed(name) { + const { config } = this; + const dir = this._absoluteConfigDir(); + return function(tmpl) { + if (name[0] === '-') name = name.slice(1); + const filename = name + '.' + config.extension; + return Bluebird.promisify(fs.writeFile, { context: fs })( + path.join(dir, filename), + tmpl(config.variables || {}) + ).return(path.join(dir, filename)); + }; + } + + // Runs a batch of seed files. + _waterfallBatch(seeds) { + const { knex } = this; + const seedDirectory = this._absoluteConfigDir(); + let current = Bluebird.bind({ failed: false, failedOn: 0 }); + const log = []; + each(seeds, (seed) => { + const name = path.join(seedDirectory, seed); + seed = require(name); + + // Run each seed file. + current = current.then(() => + // Nesting promise to prevent bubbling up of error on catch + Promise.resolve() + .then(() => seed.seed(knex)) + .then(() => log.push(name)) + .catch((originalError) => { + const error = new Error( + `Error while executing "${name}" seed: ${originalError.message}` + ); + error.original = originalError; + error.stack = + error.stack + .split('\n') + .slice(0, 2) + .join('\n') + + '\n' + + originalError.stack; + throw error; + }) + ); + }); + + return current.then(() => [log]); + } + + _absoluteConfigDir() { + return path.resolve(process.cwd(), this.config.directory); + } + + setConfig(config) { + return extend( + { + extension: 'js', + directory: './seeds', + loadExtensions: [ + '.co', + '.coffee', + '.eg', + '.iced', + '.js', + '.litcoffee', + '.ls', + '.ts', + ], + }, + this.config || {}, + config + ); + } +} module.exports = Seeder; From 181b139171e89b719e7a2c2b12eef29ac9b28c03 Mon Sep 17 00:00:00 2001 From: Lorefnon Date: Sat, 12 Oct 2019 13:15:36 +0530 Subject: [PATCH 2/3] Prevent lodash.template from interfering with javascript string interpolation Closes #3468 --- lib/migrate/MigrationGenerator.js | 50 ++++++++++++++---------------- lib/seed/Seeder.js | 37 +++++++++++++--------- lib/util/template.js | 51 +++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 42 deletions(-) create mode 100644 lib/util/template.js diff --git a/lib/migrate/MigrationGenerator.js b/lib/migrate/MigrationGenerator.js index fc9a44a798..3235c70ac8 100644 --- a/lib/migrate/MigrationGenerator.js +++ b/lib/migrate/MigrationGenerator.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const mkdirp = require('mkdirp'); const bluebird = require('bluebird'); -const { template } = require('lodash'); +const { writeJsFileUsingTemplate } = require('../util/template'); const { getMergedConfig } = require('./configuration-merger'); class MigrationGenerator { @@ -11,17 +11,15 @@ class MigrationGenerator { } // Creates a new migration, with a given name. - make(name, config) { + async make(name, config) { this.config = getMergedConfig(config, this.config); if (!name) { return Promise.reject( new Error('A name must be specified for the generated migration') ); } - - return this._ensureFolder(config) - .then((val) => this._generateStubTemplate(val)) - .then((val) => this._writeNewMigration(name, val)); + await this._ensureFolder(config); + await this._writeNewMigration(name, config); } // Ensures a folder for the migrations exist, dependent on the migration @@ -38,34 +36,32 @@ class MigrationGenerator { return Promise.all(promises); } - // Generates the stub template for the current migration, returning a compiled - // template. - _generateStubTemplate() { - const stubPath = - this.config.stub || + _getStubPath() { + return this.config.stub || path.join(__dirname, 'stub', this.config.extension + '.stub'); + } - return bluebird - .promisify(fs.readFile, { context: fs })(stubPath) - .then((stub) => template(stub.toString(), { variable: 'd' })); + _getNewMigrationName(name) { + if (name[0] === '-') name = name.slice(1); + return yyyymmddhhmmss() + '_' + name + '.' + this.config.extension; } - // Write a new migration to disk, using the config and generated filename, - // passing any `variables` given in the config to the template. - _writeNewMigration(name, tmpl) { - const { config } = this; + _getNewMigrationPath(name) { + const fileName = this._getNewMigrationName(name); const dirs = this._absoluteConfigDirs(); const dir = dirs.slice(-1)[0]; // Get last specified directory + return path.join(dir, fileName); + } - if (name[0] === '-') name = name.slice(1); - const filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension; - - return bluebird - .promisify(fs.writeFile, { context: fs })( - path.join(dir, filename), - tmpl(config.variables || {}) - ) - .return(path.join(dir, filename)); + // Write a new migration to disk, using the config and generated filename, + // passing any `variables` given in the config to the template. + _writeNewMigration(name, config) { + return writeJsFileUsingTemplate( + this._getNewMigrationPath(name), + this._getStubPath(), + { variable: 'd' }, + config.variables || {} + ); } _absoluteConfigDirs() { diff --git a/lib/seed/Seeder.js b/lib/seed/Seeder.js index 046b4377d2..285e24166d 100644 --- a/lib/seed/Seeder.js +++ b/lib/seed/Seeder.js @@ -14,6 +14,7 @@ const { each, extend, } = require('lodash'); +const { writeJsFileUsingTemplate } = require('../util/template'); // The new seeds we're performing, typically called from the `knex.seed` // interface on the main `knex` object. Passes the `knex` instance performing @@ -39,10 +40,9 @@ class Seeder { async make(name, config) { this.config = this.setConfig(config); if (!name) - throw new Error('A name must be specified for the generated seed') + throw new Error('A name must be specified for the generated seed'); await this._ensureFolder(config); - const tmpl = await this._generateStubTemplate(); - await this._writeNewSeed(name)(tmpl); + await this._writeNewSeed(name); } // Lists all available seed files as a sorted array. @@ -96,8 +96,10 @@ class Seeder { } _getStubPath() { - return this.config.stub || - path.join(__dirname, 'stub', this.config.extension + '.stub'); + return ( + this.config.stub || + path.join(__dirname, 'stub', this.config.extension + '.stub') + ); } // Generates the stub template for the current seed file, returning a compiled template. @@ -108,19 +110,24 @@ class Seeder { ); } + _getNewStubFileName(name) { + if (name[0] === '-') name = name.slice(1); + return name + '.' + this.config.extension; + } + + _getNewStubFilePath(name) { + return path.join(this._absoluteConfigDir(), this._getNewStubFileName(name)); + } + // Write a new seed to disk, using the config and generated filename, // passing any `variables` given in the config to the template. _writeNewSeed(name) { - const { config } = this; - const dir = this._absoluteConfigDir(); - return function(tmpl) { - if (name[0] === '-') name = name.slice(1); - const filename = name + '.' + config.extension; - return Bluebird.promisify(fs.writeFile, { context: fs })( - path.join(dir, filename), - tmpl(config.variables || {}) - ).return(path.join(dir, filename)); - }; + return writeJsFileUsingTemplate( + this._getNewStubFilePath(name), + this._getStubPath(), + { variable: 'd' }, + this.config.variables || {} + ); } // Runs a batch of seed files. diff --git a/lib/util/template.js b/lib/util/template.js new file mode 100644 index 0000000000..677217a049 --- /dev/null +++ b/lib/util/template.js @@ -0,0 +1,51 @@ +const { template } = require('lodash'); +const { promisify } = require('util'); +const fs = require('fs'); + +/** + * Light wrapper over lodash templates making it safer to be used with javascript source code. + * + * In particular, doesn't interfere with use of interpolated strings in javascript. + * + * @param {string} content Template source + * @param {_.TemplateOptions} options Template options + */ +const jsSourceTemplate = (content, options) => + template(content, { + interpolate: /<%=([\s\S]+?)%>/g, + ...options, + }); + +const readFile = promisify(fs.readFile, { context: fs }); +const writeFile = promisify(fs.writeFile, { context: fs }); + +/** + * Compile the contents of specified (javascript) file as a lodash template + * + * @param {string} filePath Path of file to be used as template + * @param {_.TemplateOptions} options Lodash template options + */ +const jsFileTemplate = async (filePath, options) => { + const contentBuffer = await readFile(filePath); + return jsSourceTemplate(contentBuffer.toString(), options); +}; + +/** + * Write a javascript file using another file as a (lodash) template + * + * @param {string} targetFilePath + * @param {string} sourceFilePath + * @param {_.TemplateOptions} options options passed to lodash templates + */ +const writeJsFileUsingTemplate = async ( + targetFilePath, + sourceFilePath, + options, + variables +) => writeFile(targetFilePath, (await jsFileTemplate(sourceFilePath, options))(variables)); + +module.exports = { + jsSourceTemplate, + jsFileTemplate, + writeJsFileUsingTemplate +}; From e2ec18d086bdb872499ce5d60655b834c444c668 Mon Sep 17 00:00:00 2001 From: Lorefnon Date: Sun, 13 Oct 2019 00:52:35 +0530 Subject: [PATCH 3/3] Add tests for Seeder - Support --stub option in CLI for seed:make similar to migrate:make - Fix incorrect handling of extension configuration property in knexfile - Make this handling consistent between MigrationGenerator and Seeder --- bin/cli.js | 23 +- bin/utils/cli-config-utils.js | 23 +- lib/migrate/MigrationGenerator.js | 22 +- lib/seed/Seeder.js | 11 +- test/cli/cli-test-utils.js | 36 ++- test/cli/migrate-make.spec.js | 8 +- test/cli/seed-make.spec.js | 349 ++++++++++++++++++++++ test/index.js | 1 + test/jake-util/knexfile-stubs/knexfile.js | 4 + test/jake-util/knexfile-stubs/seed.stub | 14 + test/jake-util/knexfile-ts/knexfile.ts | 3 + test/jake-util/knexfile/knexfile.js | 3 + test/jake-util/knexfile_seeds/seed2.stub | 6 + 13 files changed, 468 insertions(+), 35 deletions(-) create mode 100644 test/cli/seed-make.spec.js create mode 100644 test/jake-util/knexfile-stubs/seed.stub create mode 100644 test/jake-util/knexfile_seeds/seed2.stub diff --git a/bin/cli.js b/bin/cli.js index 08c2c178a9..5c7597dce2 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -18,9 +18,10 @@ const { success, checkLocalModule, getMigrationExtension, + getSeedExtension, getStubPath, } = require('./utils/cli-config-utils'); -const { DEFAULT_EXT } = require('./utils/constants'); + const { listMigrations } = require('./utils/migrationsLister'); function initKnex(env, opts) { @@ -157,7 +158,7 @@ function invoke(env) { const ext = getMigrationExtension(env, opts); const configOverrides = { extension: ext }; - const stub = getStubPath(env, opts); + const stub = getStubPath('migrations', env, opts); if (stub) { configOverrides.stub = stub; } @@ -292,17 +293,23 @@ function invoke(env) { `-x [${filetypes.join('|')}]`, 'Specify the stub extension (default js)' ) + .option( + `--stub [|]`, + 'Specify the seed stub to use. If using the file must be located in config.seeds.directory' + ) .action((name) => { const opts = commander.opts(); opts.client = opts.client || 'sqlite3'; // We don't really care about client when creating seeds const instance = initKnex(env, opts); - const ext = ( - argv.x || - env.configuration.ext || - DEFAULT_EXT - ).toLowerCase(); + const ext = getSeedExtension(env, opts); + const configOverrides = { extension: ext }; + const stub = getStubPath('seeds', env, opts); + if (stub) { + configOverrides.stub = stub; + } + pending = instance.seed - .make(name, { extension: ext }) + .make(name, configOverrides) .then((name) => { success(color.green(`Created seed file: ${name}`)); }) diff --git a/bin/utils/cli-config-utils.js b/bin/utils/cli-config-utils.js index 0635dcae2c..a00ef72a45 100644 --- a/bin/utils/cli-config-utils.js +++ b/bin/utils/cli-config-utils.js @@ -119,9 +119,23 @@ function getMigrationExtension(env, opts) { return ext.toLowerCase(); } -function getStubPath(env, opts) { +function getSeedExtension(env, opts) { const config = resolveEnvironmentConfig(opts, env.configuration); - const stubDirectory = config.migrations && config.migrations.directory; + + let ext = DEFAULT_EXT; + if (argv.x) { + ext = argv.x; + } else if (config.seeds && config.seeds.extension) { + ext = config.seeds.extension; + } else if (config.ext) { + ext = config.ext; + } + return ext.toLowerCase(); +} + +function getStubPath(configKey, env, opts) { + const config = resolveEnvironmentConfig(opts, env.configuration); + const stubDirectory = config[configKey] && config[configKey].directory; const { stub } = argv; if (!stub) { @@ -131,10 +145,10 @@ function getStubPath(env, opts) { return stub; } - // using stub must have config.migrations.directory defined + // using stub must have config[configKey].directory defined if (!stubDirectory) { console.log(color.red('Failed to load stub'), color.magenta(stub)); - exit('config.migrations.directory in knexfile must be defined'); + exit(`config.${configKey}.directory in knexfile must be defined`); } return path.join(stubDirectory, stub); @@ -147,6 +161,7 @@ module.exports = { exit, success, checkLocalModule, + getSeedExtension, getMigrationExtension, getStubPath, }; diff --git a/lib/migrate/MigrationGenerator.js b/lib/migrate/MigrationGenerator.js index 3235c70ac8..bd2426e27c 100644 --- a/lib/migrate/MigrationGenerator.js +++ b/lib/migrate/MigrationGenerator.js @@ -1,7 +1,7 @@ const fs = require('fs'); const path = require('path'); const mkdirp = require('mkdirp'); -const bluebird = require('bluebird'); +const {promisify} = require('util'); const { writeJsFileUsingTemplate } = require('../util/template'); const { getMergedConfig } = require('./configuration-merger'); @@ -18,8 +18,9 @@ class MigrationGenerator { new Error('A name must be specified for the generated migration') ); } - await this._ensureFolder(config); - await this._writeNewMigration(name, config); + await this._ensureFolder(); + const createdMigrationFilePath = await this._writeNewMigration(name); + return createdMigrationFilePath; } // Ensures a folder for the migrations exist, dependent on the migration @@ -28,9 +29,8 @@ class MigrationGenerator { const dirs = this._absoluteConfigDirs(); const promises = dirs.map((dir) => { - return bluebird - .promisify(fs.stat, { context: fs })(dir) - .catch(() => bluebird.promisify(mkdirp)(dir)); + return promisify(fs.stat)(dir) + .catch(() => promisify(mkdirp)(dir)); }); return Promise.all(promises); @@ -55,13 +55,15 @@ class MigrationGenerator { // Write a new migration to disk, using the config and generated filename, // passing any `variables` given in the config to the template. - _writeNewMigration(name, config) { - return writeJsFileUsingTemplate( - this._getNewMigrationPath(name), + async _writeNewMigration(name) { + const migrationPath = this._getNewMigrationPath(name); + await writeJsFileUsingTemplate( + migrationPath, this._getStubPath(), { variable: 'd' }, - config.variables || {} + this.config.variables || {} ); + return migrationPath; } _absoluteConfigDirs() { diff --git a/lib/seed/Seeder.js b/lib/seed/Seeder.js index 285e24166d..c94cb2da7f 100644 --- a/lib/seed/Seeder.js +++ b/lib/seed/Seeder.js @@ -42,7 +42,8 @@ class Seeder { if (!name) throw new Error('A name must be specified for the generated seed'); await this._ensureFolder(config); - await this._writeNewSeed(name); + const seedPath = await this._writeNewSeed(name); + return seedPath; } // Lists all available seed files as a sorted array. @@ -121,13 +122,15 @@ class Seeder { // Write a new seed to disk, using the config and generated filename, // passing any `variables` given in the config to the template. - _writeNewSeed(name) { - return writeJsFileUsingTemplate( - this._getNewStubFilePath(name), + async _writeNewSeed(name) { + const seedPath = this._getNewStubFilePath(name); + await writeJsFileUsingTemplate( + seedPath, this._getStubPath(), { variable: 'd' }, this.config.variables || {} ); + return seedPath; } // Runs a batch of seed files. diff --git a/test/cli/cli-test-utils.js b/test/cli/cli-test-utils.js index 7a1c81a98e..3345872c2c 100644 --- a/test/cli/cli-test-utils.js +++ b/test/cli/cli-test-utils.js @@ -29,15 +29,40 @@ function migrationStubOptionSetup(fileHelper) { return { migrationGlobPath }; } -function expectMigrationMatchesStub(stubPath, migrationGlobPath, fileHelper) { +function seedStubOptionSetup(fileHelper) { + const seedGlobPath = 'test/jake-util/knexfile_seeds/*.js'; + + fileHelper.registerGlobForCleanup(seedGlobPath); + fileHelper.createFile( + process.cwd() + '/knexfile.js', + ` + module.exports = { + development: { + client: 'sqlite3', + connection: { + filename: __dirname + '/test/jake-util/test.sqlite3', + }, + seeds: { + directory: __dirname + '/test/jake-util/knexfile_seeds', + }, + } + }; + `, + { isPathAbsolute: true } + ); + + return { seedGlobPath }; +} + +function expectContentMatchesStub(stubPath, globPath, fileHelper) { // accepts full or relative stub path const relativeStubPath = stubPath.replace('test/jake-util/', ''); const stubContent = fileHelper.getFileTextContent(relativeStubPath); - const [migrationContent] = fileHelper.getFileGlobTextContent( - migrationGlobPath + const [content] = fileHelper.getFileGlobTextContent( + globPath ); - expect(migrationContent).equals(stubContent); + expect(content).equals(stubContent); } function setupFileHelper() { @@ -51,7 +76,8 @@ function setupFileHelper() { } module.exports = { - expectMigrationMatchesStub, + expectContentMatchesStub, migrationStubOptionSetup, + seedStubOptionSetup, setupFileHelper, }; diff --git a/test/cli/migrate-make.spec.js b/test/cli/migrate-make.spec.js index 8557eeecd2..5b3f0e7a49 100644 --- a/test/cli/migrate-make.spec.js +++ b/test/cli/migrate-make.spec.js @@ -7,7 +7,7 @@ const { expect } = require('chai'); const KNEX = path.normalize(__dirname + '/../../bin/cli.js'); const { migrationStubOptionSetup, - expectMigrationMatchesStub, + expectContentMatchesStub, setupFileHelper, } = require('./cli-test-utils'); @@ -252,7 +252,7 @@ development: { 'test/jake-util/knexfile_migrations/*_somename.js' ); expect(fileCount).to.equal(1); - expectMigrationMatchesStub(stubPath, migrationGlobPath, fileHelper); + expectContentMatchesStub(stubPath, migrationGlobPath, fileHelper); }); it('Create a new migration with stub parameter in knexfile', async () => { @@ -273,7 +273,7 @@ development: { const stubName = 'table.stub'; const stubPath = `test/jake-util/knexfile-stubs/${stubName}`; expect(fileCount).to.equal(1); - expectMigrationMatchesStub(stubPath, migrationGlobPath, fileHelper); + expectContentMatchesStub(stubPath, migrationGlobPath, fileHelper); }); it('Create a new migration with --stub in config.migrations.directory', async () => { @@ -293,7 +293,7 @@ development: { 'test/jake-util/knexfile_migrations/*_somename.js' ); expect(fileCount).to.equal(1); - expectMigrationMatchesStub(stubPath, migrationGlobPath, fileHelper); + expectContentMatchesStub(stubPath, migrationGlobPath, fileHelper); }); it('Create a new migration with --stub when file does not exist', async () => { diff --git a/test/cli/seed-make.spec.js b/test/cli/seed-make.spec.js new file mode 100644 index 0000000000..996383eef4 --- /dev/null +++ b/test/cli/seed-make.spec.js @@ -0,0 +1,349 @@ +'use strict'; + +const path = require('path'); +const { execCommand } = require('cli-testlab'); +const { expect } = require('chai'); + +const KNEX = path.normalize(__dirname + '/../../bin/cli.js'); + +const NODE = 'node'; +// To enable debug change to: +// const NODE = 'node --inspect-brk'; + +const { + seedStubOptionSetup, + expectContentMatchesStub, + setupFileHelper, +} = require('./cli-test-utils'); + +describe('seed:make', () => { + describe('-x option: make seed using a specific extension', () => { + /** + * @type FileTestHelper + */ + let fileHelper; + beforeEach(() => { + fileHelper = setupFileHelper(); + }); + + afterEach(() => { + fileHelper.cleanup(); + }); + + before(() => { + process.env.KNEX_PATH = '../knex.js'; + }); + + it('Creates new seed with js knexfile passed', async () => { + fileHelper.registerGlobForCleanup( + 'test/jake-util/knexfile_seeds/somename.js' + ); + + await execCommand( + `${NODE} ${KNEX} seed:make somename --knexfile=test/jake-util/knexfile/knexfile.js --knexpath=../knex.js`, + { + expectedOutput: 'Created seed file', + } + ); + + const fileCount = fileHelper.fileGlobExists( + 'test/jake-util/knexfile_seeds/somename.js' + ); + expect(fileCount).to.equal(1); + }); + + it('Creates new seed with ts knexfile passed', async () => { + fileHelper.registerGlobForCleanup( + 'test/jake-util/knexfile_seeds/somename.ts' + ); + await execCommand( + `${NODE} ${KNEX} seed:make somename --knexfile=test/jake-util/knexfile-ts/knexfile.ts --knexpath=../knex.js`, + { + expectedOutput: 'Created seed file', + } + ); + + const fileCount = fileHelper.fileGlobExists( + 'test/jake-util/knexfile_seeds/somename.ts' + ); + expect(fileCount).to.equal(1); + }); + + it('Creates new seed with default knexfile', async () => { + fileHelper.registerGlobForCleanup( + 'test/jake-util/knexfile_seeds/somename.js' + ); + fileHelper.createFile( + process.cwd() + '/knexfile.js', + ` +module.exports = { + client: 'sqlite3', + connection: { + filename: __dirname + '/test/jake-util/test.sqlite3', + }, + seeds: { + directory: __dirname + '/test/jake-util/knexfile_seeds', + }, +}; + `, + { isPathAbsolute: true } + ); + await execCommand( + `${NODE} ${KNEX} seed:make somename --knexpath=../knex.js`, + { + expectedOutput: 'Created seed file', + } + ); + + const fileCount = fileHelper.fileGlobExists( + 'test/jake-util/knexfile_seeds/somename.js' + ); + expect(fileCount).to.equal(1); + }); + + it('Creates new seed with default ts knexfile', async () => { + fileHelper.registerGlobForCleanup( + 'test/jake-util/knexfile_seeds/somename.ts' + ); + fileHelper.createFile( + process.cwd() + '/knexfile.ts', + ` +module.exports = { + client: 'sqlite3', + connection: { + filename: __dirname + '/test/jake-util/test.sqlite3', + }, + seeds: { + directory: __dirname + '/test/jake-util/knexfile_seeds', + }, +}; + `, + { isPathAbsolute: true } + ); + await execCommand( + `${NODE} ${KNEX} seed:make somename --knexpath=../knex.js`, + { + expectedOutput: 'Created seed file', + } + ); + + const fileCount = fileHelper.fileGlobExists( + 'test/jake-util/knexfile_seeds/somename.ts' + ); + expect(fileCount).to.equal(1); + }); + + it('Creates new seed with ts extension using -x switch', async () => { + fileHelper.registerGlobForCleanup( + 'test/jake-util/knexfile_seeds/somename.ts' + ); + await execCommand( + `${NODE} ${KNEX} seed:make somename -x ts --knexfile=test/jake-util/knexfile/knexfile.js --knexpath=../knex.js`, + { + expectedOutput: 'Created seed file', + } + ); + + const fileCount = fileHelper.fileGlobExists( + 'test/jake-util/knexfile_seeds/somename.ts' + ); + expect(fileCount).to.equal(1); + }); + + it('Creates new seed with ts extension using "extension" knexfile param', async () => { + fileHelper.registerGlobForCleanup( + 'test/jake-util/knexfile_seeds/somename.ts' + ); + fileHelper.createFile( + process.cwd() + '/knexfile.js', + ` +module.exports = { + client: 'sqlite3', + connection: { + filename: __dirname + '/test/jake-util/test.sqlite3', + }, + seeds: { + extension: 'ts', + directory: __dirname + '/test/jake-util/knexfile_seeds', + }, +}; + `, + { isPathAbsolute: true } + ); + await execCommand( + `${NODE} ${KNEX} seed:make somename --knexpath=../knex.js`, + { + expectedOutput: 'Created seed file', + } + ); + + const fileCount = fileHelper.fileGlobExists( + 'test/jake-util/knexfile_seeds/somename.ts' + ); + expect(fileCount).to.equal(1); + }); + + it('Creates new seed with ts extension using environment "extension" knexfile param', async () => { + fileHelper.registerGlobForCleanup( + 'test/jake-util/knexfile_seeds/somename.ts' + ); + fileHelper.createFile( + process.cwd() + '/knexfile.js', + ` +module.exports = { +development: { + client: 'sqlite3', + connection: { + filename: __dirname + '/test/jake-util/test.sqlite3', + }, + seeds: { + extension: 'ts', + directory: __dirname + '/test/jake-util/knexfile_seeds', + }, + } +}; + `, + { isPathAbsolute: true } + ); + await execCommand( + `${NODE} ${KNEX} seed:make somename --knexpath=../knex.js`, + { + expectedOutput: 'Created seed file', + } + ); + + const fileCount = fileHelper.fileGlobExists( + 'test/jake-util/knexfile_seeds/somename.ts' + ); + expect(fileCount).to.equal(1); + }); + }); + + describe('--stub option: make seed using specific stub file', () => { + /** + * @type FileTestHelper + */ + let fileHelper; + beforeEach(() => { + fileHelper = setupFileHelper(); + }); + + afterEach(() => { + fileHelper.cleanup(); + }); + + before(() => { + process.env.KNEX_PATH = '../knex.js'; + }); + + it('Creates a new seed using --stub relative to the knexfile', async () => { + const { seedGlobPath } = seedStubOptionSetup(fileHelper); + const stubPath = 'test/jake-util/knexfile-stubs/seed.stub'; + + await execCommand( + `${NODE} ${KNEX} seed:make somename --stub ${stubPath} --knexpath=../knex.js`, + { + expectedOutput: 'Created seed file', + } + ); + + const fileCount = fileHelper.fileGlobExists( + 'test/jake-util/knexfile_seeds/somename.js' + ); + expect(fileCount).to.equal(1); + expectContentMatchesStub(stubPath, seedGlobPath, fileHelper); + }); + + it('Creates a new seed with stub parameter in knexfile', async () => { + const seedGlobPath = 'test/jake-util/knexfile-stubs/somename.js'; + fileHelper.registerGlobForCleanup(seedGlobPath); + + await execCommand( + `${NODE} ${KNEX} seed:make somename --knexfile=test/jake-util/knexfile-stubs/knexfile.js --knexpath=../knex.js`, + { + expectedOutput: 'Created seed file', + } + ); + + const fileCount = fileHelper.fileGlobExists( + 'test/jake-util/knexfile-stubs/somename.js' + ); + + const stubName = 'seed.stub'; + const stubPath = `test/jake-util/knexfile-stubs/${stubName}`; + expect(fileCount).to.equal(1); + expectContentMatchesStub(stubPath, seedGlobPath, fileHelper); + }); + + it('Creates a new seed with --stub in config.seeds.directory', async () => { + const { seedGlobPath } = seedStubOptionSetup(fileHelper); + + const stubName = 'seed2.stub'; + const stubPath = `test/jake-util/knexfile_seeds/${stubName}`; + + await execCommand( + `${NODE} ${KNEX} seed:make somename --stub ${stubName} --knexpath=../knex.js`, + { + expectedOutput: 'Created seed file', + } + ); + + const fileCount = fileHelper.fileGlobExists( + 'test/jake-util/knexfile_seeds/somename.js' + ); + expect(fileCount).to.equal(1); + expectContentMatchesStub(stubPath, seedGlobPath, fileHelper); + }); + + it('fails to create a new seed with --stub when file does not exist', async () => { + seedStubOptionSetup(fileHelper); + const stubName = 'non-existat.stub'; + await execCommand( + `${NODE} ${KNEX} seed:make somename --stub ${stubName} --knexpath=../knex.js`, + { + expectedErrorMessage: 'ENOENT:', + } + ); + }); + + it('fails to create a new seed with --stub when file does not exist', async () => { + seedStubOptionSetup(fileHelper); + const stubPath = '/path/non-existat.stub'; + await execCommand( + `${NODE} ${KNEX} seed:make somename --stub ${stubPath} --knexpath=../knex.js`, + { + expectedErrorMessage: 'ENOENT:', + } + ); + }); + + it('fails to create a seed with --stub when config.seeds.directory not defined', async () => { + fileHelper.createFile( + process.cwd() + '/knexfile.js', + ` + module.exports = { + development: { + client: 'sqlite3', + connection: { + filename: __dirname + '/test/jake-util/test.sqlite3', + }, + seeds: { + // directory not defined + }, + } + }; + `, + { isPathAbsolute: true } + ); + + const stubName = 'table-foreign.stub'; + await execCommand( + `${NODE} ${KNEX} seed:make somename --stub ${stubName} --knexpath=../knex.js`, + { + expectedErrorMessage: + 'config.seeds.directory in knexfile must be defined', + } + ); + }); + }); +}); diff --git a/test/index.js b/test/index.js index 0a9111719a..6f1a722632 100644 --- a/test/index.js +++ b/test/index.js @@ -86,5 +86,6 @@ describe('CLI tests', function() { this.timeout(process.env.KNEX_TEST_TIMEOUT || 5000); require('./cli/knexfile-test.spec'); require('./cli/migrate-make.spec'); + require('./cli/seed-make.spec'); require('./cli/version.spec'); }); diff --git a/test/jake-util/knexfile-stubs/knexfile.js b/test/jake-util/knexfile-stubs/knexfile.js index 872a2cf6d8..3b8cb7e16d 100644 --- a/test/jake-util/knexfile-stubs/knexfile.js +++ b/test/jake-util/knexfile-stubs/knexfile.js @@ -7,4 +7,8 @@ module.exports = { directory: __dirname + '/../knexfile-stubs', stub: 'table.stub', }, + seeds: { + directory: __dirname + '/../knexfile-stubs', + stub: 'seed.stub' + }, }; diff --git a/test/jake-util/knexfile-stubs/seed.stub b/test/jake-util/knexfile-stubs/seed.stub new file mode 100644 index 0000000000..0ac77c852e --- /dev/null +++ b/test/jake-util/knexfile-stubs/seed.stub @@ -0,0 +1,14 @@ +const tableName = 'table_name'; + +exports.seed = (knex) => { + // Deletes ALL existing entries + return knex(`tbl_${tableName}`).del() + .then(() => { + // Inserts seed entries + return knex(`tbl_${tableName}`).insert([ + {id: 1, colName: 'rowValue1'}, + {id: 2, colName: 'rowValue2'}, + {id: 3, colName: 'rowValue3'} + ]); + }); +}; diff --git a/test/jake-util/knexfile-ts/knexfile.ts b/test/jake-util/knexfile-ts/knexfile.ts index dcb3e598d8..5149c621c1 100644 --- a/test/jake-util/knexfile-ts/knexfile.ts +++ b/test/jake-util/knexfile-ts/knexfile.ts @@ -6,4 +6,7 @@ module.exports = { migrations: { directory: __dirname + '/../knexfile_migrations', }, + seeds: { + directory: __dirname + '/../knexfile_seeds', + }, }; diff --git a/test/jake-util/knexfile/knexfile.js b/test/jake-util/knexfile/knexfile.js index dcb3e598d8..b2157b952f 100644 --- a/test/jake-util/knexfile/knexfile.js +++ b/test/jake-util/knexfile/knexfile.js @@ -6,4 +6,7 @@ module.exports = { migrations: { directory: __dirname + '/../knexfile_migrations', }, + seeds: { + directory: __dirname + '/../knexfile_seeds', + } }; diff --git a/test/jake-util/knexfile_seeds/seed2.stub b/test/jake-util/knexfile_seeds/seed2.stub new file mode 100644 index 0000000000..943eb21d84 --- /dev/null +++ b/test/jake-util/knexfile_seeds/seed2.stub @@ -0,0 +1,6 @@ +const tableName = 'table_name'; + +exports.seed = (knex) => { + // Deletes ALL existing entries + return knex(`tbl_${tableName}`).del(); +};