Skip to content

feat: configuration for seamless cli use in a typescript project #1439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/assets/migrations/create-table.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -29,7 +30,7 @@ module.exports = {
});
},

async down (queryInterface, Sequelize) {
async down (queryInterface<%= isTypescriptProject ? ': QueryInterface' : '' %>, Sequelize<%= isTypescriptProject ? ': typeof DataTypes' : '' %>) {
await queryInterface.dropTable('<%= tableName %>');
}
};
5 changes: 3 additions & 2 deletions src/assets/migrations/skeleton.js
Original file line number Diff line number Diff line change
@@ -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.
*
Expand All @@ -11,7 +12,7 @@ module.exports = {
*/
},

async down (queryInterface, Sequelize) {
async down (queryInterface<%= isTypescriptProject ? ': QueryInterface' : '' %>, Sequelize<%= isTypescriptProject ? ': typeof DataTypes' : '' %>) {
/**
* Add reverting commands here.
*
Expand Down
10 changes: 10 additions & 0 deletions src/assets/models/connection.js
Original file line number Diff line number Diff line change
@@ -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;
43 changes: 0 additions & 43 deletions src/assets/models/index.js

This file was deleted.

49 changes: 34 additions & 15 deletions src/assets/models/model.js
Original file line number Diff line number Diff line change
@@ -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 ? ',' : '' %>
Expand All @@ -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 %>;
5 changes: 3 additions & 2 deletions src/assets/seeders/skeleton.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use strict';
<%= isTypescriptProject ? `import { QueryInterface, DataTypes } from 'sequelize';` : '' %>

/** @type {import('sequelize-cli').Migration} */
module.exports = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think also worth export default no?

async up (queryInterface, Sequelize) {
async up (queryInterface<%= isTypescriptProject ? ': QueryInterface' : '' %>, Sequelize<%= isTypescriptProject ? ': typeof DataTypes' : '' %>) {
/**
* Add seed commands here.
*
Expand All @@ -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.
*
Expand Down
2 changes: 1 addition & 1 deletion src/commands/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
15 changes: 15 additions & 0 deletions src/helpers/import-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Copy link

@shlomisas shlomisas Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about having a way to decide it from the CLI? e.g. npx sequelize-cli migration:generate --name XXX --typescript and then inside each *_generate.js provide the isTypescript to the render function?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


/**
* Imports a JSON, CommonJS or ESM module
* based on feature detection.
Expand Down Expand Up @@ -39,4 +52,6 @@ async function importModule(modulePath) {
module.exports = {
supportsDynamicImport,
importModule,
isPackageInstalled,
isTypescriptProject,
};
6 changes: 3 additions & 3 deletions src/helpers/init-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -71,7 +71,7 @@ const init = {
helpers.asset.write(
indexPath,
helpers.template.render(
'models/index.js',
'models/connection.js',
{
configFile:
"__dirname + '/" + relativeConfigPath.replace(/\\/g, '/') + "'",
Expand Down
50 changes: 44 additions & 6 deletions src/helpers/model-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(':');
Expand All @@ -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 ]+)))*\}$/;

Check failure

Code scanning / CodeQL

Inefficient regular expression

This part of the regular expression may cause exponential backtracking on strings starting with '{{ ,' and containing many repetitions of ' ,'.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a false positive, @WikiRik can you please dismiss this alert?

Check warning

Code scanning / CodeQL

Overly permissive regular expression range

Suspicious character range that is equivalent to \[A-Z\\[\\\\]^_`a-z\].

Check warning

Code scanning / CodeQL

Overly permissive regular expression range

Suspicious character range that is equivalent to \[A-Z\\[\\\\]^_`a-z\].

Check warning

Code scanning / CodeQL

Overly permissive regular expression range

Suspicious character range that is equivalent to \[A-Z\\[\\\\]^_`a-z\].

Check warning

Code scanning / CodeQL

Overly permissive regular expression range

Suspicious character range that is equivalent to \[A-Z\\[\\\\]^_`a-z\].

Check warning

Code scanning / CodeQL

Overly permissive regular expression range

Suspicious character range that is equivalent to \[A-Z\\[\\\\]^_`a-z\].

Check warning

Code scanning / CodeQL

Overly permissive regular expression range

Suspicious character range that is equivalent to \[A-Z\\[\\\\]^_`a-z\].

const isValidFunction =
validAttributeFunctionType.indexOf(split[1].toLowerCase()) !== -1;
const isValidValue =
Expand All @@ -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(', '),
};
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/helpers/path-helper.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -45,7 +46,7 @@ module.exports = {
},

getFileExtension() {
return 'js';
return isTypescriptProject ? 'ts' : 'js';
},

addFileExtension(basename, options) {
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/template-helper.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions src/sequelize.js
Original file line number Diff line number Diff line change
@@ -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();

Expand Down
Loading