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
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,
@@ -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.
*
@@ -11,7 +12,7 @@ module.exports = {
*/
},

async down (queryInterface, Sequelize) {
async down (queryInterface<%= isTypescriptProject ? ': QueryInterface' : '' %>, Sequelize<%= isTypescriptProject ? ': typeof DataTypes' : '' %>) {
/**
* Add reverting commands here.
*
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 ? ',' : '' %>
@@ -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.
*
@@ -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.
*
2 changes: 1 addition & 1 deletion src/commands/init.js
Original file line number Diff line number Diff line change
@@ -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) {
15 changes: 15 additions & 0 deletions src/helpers/import-helper.js
Original file line number Diff line number Diff line change
@@ -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.
@@ -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
@@ -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, '/') + "'",
50 changes: 44 additions & 6 deletions src/helpers/model-helper.js
Original file line number Diff line number Diff line change
@@ -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 ]+)))*\}$/;

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 =
@@ -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(', '),
};
}
}
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';
@@ -45,7 +46,7 @@ module.exports = {
},

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

addFileExtension(basename, options) {
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) {
@@ -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) {
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();

8 changes: 4 additions & 4 deletions test/init.test.js
Original file line number Diff line number Diff line change
@@ -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'"
20 changes: 13 additions & 7 deletions test/model/create.test.js
Original file line number Diff line number Diff line change
@@ -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));
}
);