diff --git a/src/assets/migrations/create-table.js b/src/assets/migrations/create-table.js index 6a6af8bd1..a4f5361ba 100644 --- a/src/assets/migrations/create-table.js +++ b/src/assets/migrations/create-table.js @@ -1,8 +1,9 @@ 'use strict'; +<%= isTypescriptProject ? `import { QueryInterface, DataTypes } from 'sequelize';` : '' %> /** @type {import('sequelize-cli').Migration} */ module.exports = { - async up (queryInterface, Sequelize) { + async up (queryInterface<%= isTypescriptProject ? ': QueryInterface' : '' %>, Sequelize<%= isTypescriptProject ? ': typeof DataTypes' : '' %>) { await queryInterface.createTable('<%= tableName %>', { id: { allowNull: false, @@ -29,7 +30,7 @@ module.exports = { }); }, - async down (queryInterface, Sequelize) { + async down (queryInterface<%= isTypescriptProject ? ': QueryInterface' : '' %>, Sequelize<%= isTypescriptProject ? ': typeof DataTypes' : '' %>) { await queryInterface.dropTable('<%= tableName %>'); } }; diff --git a/src/assets/migrations/skeleton.js b/src/assets/migrations/skeleton.js index b6e01dea5..c995a6525 100644 --- a/src/assets/migrations/skeleton.js +++ b/src/assets/migrations/skeleton.js @@ -1,8 +1,9 @@ 'use strict'; +<%= isTypescriptProject ? `import { QueryInterface, DataTypes } from 'sequelize';` : '' %> /** @type {import('sequelize-cli').Migration} */ module.exports = { - async up (queryInterface, Sequelize) { + async up (queryInterface<%= isTypescriptProject ? ': QueryInterface' : '' %>, Sequelize<%= isTypescriptProject ? ': typeof DataTypes' : '' %>) { /** * Add altering commands here. * @@ -11,7 +12,7 @@ module.exports = { */ }, - async down (queryInterface, Sequelize) { + async down (queryInterface<%= isTypescriptProject ? ': QueryInterface' : '' %>, Sequelize<%= isTypescriptProject ? ': typeof DataTypes' : '' %>) { /** * Add reverting commands here. * diff --git a/src/assets/models/connection.js b/src/assets/models/connection.js new file mode 100644 index 000000000..bac316a6f --- /dev/null +++ b/src/assets/models/connection.js @@ -0,0 +1,10 @@ +'use strict'; +import { Sequelize } from 'sequelize'; +const env = process.env.NODE_ENV || 'development'; +const config = require(<%= configFile %>)[env]; + +const sequelizeConnection<%= isTypescriptProject ? ': Sequelize' : '' %> = config.config.use_env_variable + ? new Sequelize( process.env[config.config.use_env_variable], config) + : new Sequelize(config.database, config.username, config.password, config); + +module.exports = sequelizeConnection; diff --git a/src/assets/models/index.js b/src/assets/models/index.js deleted file mode 100644 index 7872564f5..000000000 --- a/src/assets/models/index.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const Sequelize = require('sequelize'); -const process = require('process'); -const basename = path.basename(__filename); -const env = process.env.NODE_ENV || 'development'; -const config = require(<%= configFile %>)[env]; -const db = {}; - -let sequelize; -if (config.use_env_variable) { - sequelize = new Sequelize(process.env[config.use_env_variable], config); -} else { - sequelize = new Sequelize(config.database, config.username, config.password, config); -} - -fs - .readdirSync(__dirname) - .filter(file => { - return ( - file.indexOf('.') !== 0 && - file !== basename && - file.slice(-3) === '.js' && - file.indexOf('.test.js') === -1 - ); - }) - .forEach(file => { - const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); - db[model.name] = model; - }); - -Object.keys(db).forEach(modelName => { - if (db[modelName].associate) { - db[modelName].associate(db); - } -}); - -db.sequelize = sequelize; -db.Sequelize = Sequelize; - -module.exports = db; diff --git a/src/assets/models/model.js b/src/assets/models/model.js index 930254e8f..d562f8809 100644 --- a/src/assets/models/model.js +++ b/src/assets/models/model.js @@ -1,20 +1,29 @@ 'use strict'; -const { Model } = require('sequelize'); +import { Model, DataTypes } from 'sequelize'; +<% if (isTypescriptProject) { %> +import sequelize from './connection'; +<% }else{ %> +const sequelize = require('./connection'); +<% } %> -module.exports = (sequelize, DataTypes) => { - class <%= name %> extends Model { - /** - * Helper method for defining associations. - * This method is not a part of Sequelize lifecycle. - * The `models/index` file will call this method automatically. - */ - static associate (models) { - // define association here - } - } +<% if (isTypescriptProject) { %> +export interface <%= name %>Attributes { + <% attributes.forEach(function(attribute, index) { %> + <%= attribute.fieldName %>: <%= attribute.tsType %>; + <% }) %> +} +<% } %> - <%= name %>.init({ +class <%= name %> extends Model<%= isTypescriptProject ? `<${name}Attributes> implements ${name}Attributes` : '' %> { +<% if (isTypescriptProject) { %> + <% attributes.forEach(function(attribute, index) { %> + <%= attribute.fieldName %><%=isTypescriptProject ? `!: ${attribute.tsType}` : '' %>; + <% }) %> +<% } %> +} + +<%= name %>.init({ <% attributes.forEach(function(attribute, index) { %> <%= attribute.fieldName %>: DataTypes.<%= attribute.dataFunction ? `${attribute.dataFunction.toUpperCase()}(DataTypes.${attribute.dataType.toUpperCase()})` : attribute.dataValues ? `${attribute.dataType.toUpperCase()}(${attribute.dataValues})` : attribute.dataType.toUpperCase() %> <%= (Object.keys(attributes).length - 1) > index ? ',' : '' %> @@ -25,5 +34,15 @@ module.exports = (sequelize, DataTypes) => { <%= underscored ? 'underscored: true,' : '' %> }); - return <%= name %>; -}; +// Associations +// <%= name %>.belongsTo(TargetModel, { +// as: 'custom_name', +// foreignKey: { +// name: 'foreign_key_column_name', +// allowNull: false, +// }, +// onDelete: "RESTRICT", +// foreignKeyConstraint: true, +// }); + +export default <%= name %>; diff --git a/src/assets/seeders/skeleton.js b/src/assets/seeders/skeleton.js index 48a68a022..777627aa3 100644 --- a/src/assets/seeders/skeleton.js +++ b/src/assets/seeders/skeleton.js @@ -1,8 +1,9 @@ 'use strict'; +<%= isTypescriptProject ? `import { QueryInterface, DataTypes } from 'sequelize';` : '' %> /** @type {import('sequelize-cli').Migration} */ module.exports = { - async up (queryInterface, Sequelize) { + async up (queryInterface<%= isTypescriptProject ? ': QueryInterface' : '' %>, Sequelize<%= isTypescriptProject ? ': typeof DataTypes' : '' %>) { /** * Add seed commands here. * @@ -14,7 +15,7 @@ module.exports = { */ }, - async down (queryInterface, Sequelize) { + async down (queryInterface<%= isTypescriptProject ? ': QueryInterface' : '' %>, Sequelize<%= isTypescriptProject ? ': typeof DataTypes' : '' %>) { /** * Add commands to revert seed here. * diff --git a/src/commands/init.js b/src/commands/init.js index 6d2d7d819..b9a51e56c 100644 --- a/src/commands/init.js +++ b/src/commands/init.js @@ -52,7 +52,7 @@ function initConfig(args) { function initModels(args) { helpers.init.createModelsFolder(!!args.force); - helpers.init.createModelsIndexFile(!!args.force); + helpers.init.createConnectionFile(!!args.force); } function initMigrations(args) { diff --git a/src/helpers/import-helper.js b/src/helpers/import-helper.js index 71b7a3bcd..1d27193ae 100644 --- a/src/helpers/import-helper.js +++ b/src/helpers/import-helper.js @@ -12,6 +12,19 @@ async function supportsDynamicImport() { } } +function isPackageInstalled(packageName) { + try { + // Try to require the package + require.resolve(packageName); + return true; + } catch (error) { + // If require.resolve throws an error, the package is not installed + return false; + } +} + +const isTypescriptProject = isPackageInstalled('typescript'); + /** * Imports a JSON, CommonJS or ESM module * based on feature detection. @@ -39,4 +52,6 @@ async function importModule(modulePath) { module.exports = { supportsDynamicImport, importModule, + isPackageInstalled, + isTypescriptProject, }; diff --git a/src/helpers/init-helper.js b/src/helpers/init-helper.js index 32b21fdd5..b517856cc 100644 --- a/src/helpers/init-helper.js +++ b/src/helpers/init-helper.js @@ -51,11 +51,11 @@ const init = { createFolder('models', helpers.path.getModelsPath(), force); }, - createModelsIndexFile: (force) => { + createConnectionFile: (force) => { const modelsPath = helpers.path.getModelsPath(); const indexPath = path.resolve( modelsPath, - helpers.path.addFileExtension('index') + helpers.path.addFileExtension('connection') ); if (!helpers.path.existsSync(modelsPath)) { @@ -71,7 +71,7 @@ const init = { helpers.asset.write( indexPath, helpers.template.render( - 'models/index.js', + 'models/connection.js', { configFile: "__dirname + '/" + relativeConfigPath.replace(/\\/g, '/') + "'", diff --git a/src/helpers/model-helper.js b/src/helpers/model-helper.js index 2fb43c9fe..d7ed7e9a0 100644 --- a/src/helpers/model-helper.js +++ b/src/helpers/model-helper.js @@ -2,6 +2,27 @@ import helpers from './index'; const Sequelize = helpers.generic.getSequelize(); const validAttributeFunctionType = ['array', 'enum']; +const typescriptTypesForDbFieldTypes = { + string: 'string', + text: 'string', + uuid: 'string', + CHAR: 'string', + number: 'number', + float: 'number', + integer: 'number', + bigint: 'number', + mediumint: 'number', + tinyint: 'number', + smallint: 'number', + double: 'number', + 'double precision': 'number', + real: 'number', + decimal: 'number', + date: 'data', + now: 'data', + dateonly: 'data', + boolean: 'boolean', +}; /** * Check the given dataType actual exists. @@ -15,6 +36,17 @@ function validateDataType(dataType) { return dataType; } +function getTsTypeForDbColumnType(db_type, attribute_func, values) { + db_type = db_type.toLowerCase(); + if (attribute_func === 'array') { + return `${typescriptTypesForDbFieldTypes[db_type]}[]`; + } else if (attribute_func === 'enum') { + return values.join(' | '); + } + + return typescriptTypesForDbFieldTypes[db_type] || 'any'; +} + function formatAttributes(attribute) { let result; const split = attribute.split(':'); @@ -24,10 +56,13 @@ function formatAttributes(attribute) { fieldName: split[0], dataType: split[1], dataFunction: null, + tsType: getTsTypeForDbColumnType(split[1]), dataValues: null, }; } else if (split.length === 3) { - const validValues = /^\{(,? ?[A-z0-9 ]+)+\}$/; + const validValues = + /^\{((('[A-z0-9 ]+')|("[A-z0-9 ]+")|([A-z0-9 ]+)))(, ?(('[A-z0-9 ]+')|("[A-z0-9 ]+")|([A-z0-9 ]+)))*\}$/; + const isValidFunction = validAttributeFunctionType.indexOf(split[1].toLowerCase()) !== -1; const isValidValue = @@ -40,20 +75,23 @@ function formatAttributes(attribute) { fieldName: split[0], dataType: split[2], dataFunction: split[1], + tsType: getTsTypeForDbColumnType(split[2], split[1]), dataValues: null, }; } if (isValidFunction && !isValidValue && isValidValues) { + const values = split[2] + .replace(/(^\{|\}$)/g, '') + .split(/\s*,\s*/) + .map((s) => (s.startsWith('"') || s.startsWith("'") ? s : `'${s}'`)); + result = { fieldName: split[0], dataType: split[1], + tsType: getTsTypeForDbColumnType(split[2], split[1], values), dataFunction: null, - dataValues: split[2] - .replace(/(^\{|\}$)/g, '') - .split(/\s*,\s*/) - .map((s) => `'${s}'`) - .join(', '), + dataValues: values.join(', '), }; } } diff --git a/src/helpers/path-helper.js b/src/helpers/path-helper.js index e06ca74f7..64e08f237 100644 --- a/src/helpers/path-helper.js +++ b/src/helpers/path-helper.js @@ -1,6 +1,7 @@ import path from 'path'; import fs from 'fs'; import process from 'process'; +import { isTypescriptProject } from './import-helper'; const resolve = require('resolve').sync; import getYArgs from '../core/yargs'; @@ -45,7 +46,7 @@ module.exports = { }, getFileExtension() { - return 'js'; + return isTypescriptProject ? 'ts' : 'js'; }, addFileExtension(basename, options) { diff --git a/src/helpers/template-helper.js b/src/helpers/template-helper.js index 92efc71a0..3d0a04573 100644 --- a/src/helpers/template-helper.js +++ b/src/helpers/template-helper.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import beautify from 'js-beautify'; import helpers from './index'; +import { isTypescriptProject } from './import-helper'; module.exports = { render(path, locals, options) { @@ -14,6 +15,9 @@ module.exports = { ); const template = helpers.asset.read(path); + locals = locals || {}; + locals['isTypescriptProject'] = isTypescriptProject; + let content = _.template(template)(locals || {}); if (options.beautify) { diff --git a/src/sequelize.js b/src/sequelize.js index 7249135c6..ee5807ee4 100644 --- a/src/sequelize.js +++ b/src/sequelize.js @@ -1,6 +1,10 @@ #!/usr/bin/env node import getYArgs from './core/yargs'; +import { isTypescriptProject } from './helpers/import-helper'; + +// enable typescript compatibility if project is based on typescript +if (isTypescriptProject) require('ts-node/register'); const yargs = getYArgs(); diff --git a/test/init.test.js b/test/init.test.js index 518e4dadf..05ccb17a1 100644 --- a/test/init.test.js +++ b/test/init.test.js @@ -37,7 +37,7 @@ const gulp = require('gulp'); 'config/config.json', 'migrations', 'models', - 'models/index.js', + 'models/connection.js', ]); it('creates a custom config folder', (done) => { @@ -86,7 +86,7 @@ const gulp = require('gulp'); .pipe(helpers.teardown(done)); }); - describe('models/index.js', () => { + describe('models/connection.js', () => { it('correctly injects the reference to the default config file', (done) => { gulp .src(Support.resolveSupportPath('tmp')) @@ -96,7 +96,7 @@ const gulp = require('gulp'); helpers.teardown(() => { gulp .src(Support.resolveSupportPath('tmp', 'models')) - .pipe(helpers.readFile('index.js')) + .pipe(helpers.readFile('connection.js')) .pipe( helpers.ensureContent("__dirname + '/../config/config.json'") ) @@ -114,7 +114,7 @@ const gulp = require('gulp'); helpers.teardown(() => { gulp .src(Support.resolveSupportPath('tmp', 'models')) - .pipe(helpers.readFile('index.js')) + .pipe(helpers.readFile('connection.js')) .pipe( helpers.ensureContent( "__dirname + '/../my/configuration-file.json'" diff --git a/test/model/create.test.js b/test/model/create.test.js index ddf38aa33..096fc0d9b 100644 --- a/test/model/create.test.js +++ b/test/model/create.test.js @@ -98,8 +98,8 @@ const _ = require('lodash'); }); }); [ - 'first_name:string,last_name:string,bio:text,role:enum:{Admin,"Guest User"},reviews:array:text', - "first_name:string,last_name:string,bio:text,role:enum:{Admin,'Guest User'},reviews:array:text", + `'first_name:string,last_name:string,bio:text,role:enum:{Admin,"Guest User"},reviews:array:text'`, + `"first_name:string,last_name:string,bio:text,role:enum:{Admin,'Guest User'},reviews:array:text"`, "'first_name:string last_name:string bio:text role:enum:{Admin,Guest User} reviews:array:text'", "'first_name:string, last_name:string, bio:text, role:enum:{Admin, Guest User}, reviews:array:text'", ].forEach((attributes) => { @@ -145,7 +145,9 @@ const _ = require('lodash'); .pipe(helpers.ensureContent('bio: DataTypes.TEXT')) .pipe( helpers.ensureContent( - "role: DataTypes.ENUM('Admin', 'Guest User')" + attributes.includes('"Guest User"') + ? `role: DataTypes.ENUM('Admin', "Guest User")` + : "role: DataTypes.ENUM('Admin', 'Guest User')" ) ) .pipe( @@ -228,7 +230,11 @@ const _ = require('lodash'); ) .pipe( helpers.ensureContent( - "role: {\n type: Sequelize.ENUM('Admin', 'Guest User')\n }," + `role: {\n type: Sequelize.ENUM(${ + attributes.includes('"Guest User"') + ? `'Admin', "Guest User"` + : "'Admin', 'Guest User'" + })\n },` ) ) .pipe( @@ -282,8 +288,8 @@ const _ = require('lodash'); }; const targetContent = attrUnd.underscored - ? "modelName: 'User',\n underscored: true,\n });" - : "modelName: 'User',\n });"; + ? "modelName: 'User',\n underscored: true,\n});" + : "modelName: 'User',\n});"; if (attrUnd.underscored) { flags.underscored = attrUnd.underscored; @@ -298,7 +304,7 @@ const _ = require('lodash'); .src(Support.resolveSupportPath('tmp', 'models')) .pipe(helpers.readFile('user.js')) .pipe(helpers.ensureContent(targetContent)) - .pipe(helpers.ensureContent('static associate')) + .pipe(helpers.ensureContent('// Associations')) .pipe(helpers.teardown(done)); } );