This small module is designed to make Sequelize migrations files easier to manage by abstracting away boilerplate code that is used repeatedly and providing a builder object to construct the configuration.
// Manual Sequelize Migration
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
firstName: {
type: Sequelize.STRING,
allowNull: false,
},
lastName: {
type: Sequelize.STRING,
allowNull: false,
},
email: {
type: Sequelize.STRING,
unique: true,
allowNull: false,
},
password: {
type: Sequelize.STRING,
allowNull: false,
},
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Users')
}
}
// Sequelize Builder
const FieldBuilder = require('./utils/migration.utils');
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Users', {
id: new FieldBuilder().primaryKey(),
firstName: new FieldBuilder()
.string()
.allowNull()
.output(),
lastName: new FieldBuilder()
.string()
.allowNull()
.output(),
email: new FieldBuilder()
.email()
.allowNull()
.unique()
.output(),
password: new FieldBuilder().output()
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Users');
}
};
The module is available via either directly targeting the github repository in your package.json
or by setting up a custom registery using the .npmrc
file.
If you are going to install the github repository directly, you can use the following command:
npm install --save-dev https://github.com/tkottke90/sequelize-migration-builder
The module is hosted in Github Packages. To install using npm, first you will need to create a Personal Access Token which will be used by npm to authenticate to Github and download the package.
You can create a token by following the Github Documentation. When creating the token you will specifically need to include the read:packages
permission which allows the token to be used to download from Github.
Next you will need to create a .npmrc
file in your project, add your personal access token and define the where Github can find the module:
echo "//npm.pkg.github.com/:_authToken=<Personal Access Token>" >> /.npmrc
echo "@tkottke90:registry=https://npm.pkg.github.com\n" >> .npmrc
This will let NPM know that when you are looking for packages in the @tkottke90
scope, to look in github packages instead of in NPM. Then install the module using NPM:
npm install --save-dev @tkottke90/[email protected]
To use the module, simply import it into your migration files. You can then create a new instance of the builder for each column you wish to add:
// Sequelize Builder
const FieldBuilder = require('./utils/migration.utils');
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Users', {
id: new FieldBuilder().primaryKey(),
firstName: new FieldBuilder()
.string()
.required()
.output(),
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Users');
}
};
At the core each column should instantiate a new instance of the FieldBuilder
class.
firstName: new FieldBuilder()
Once you have the class, you will have access to all the functions available to that class (see docs below for full list). For example, calling the string()
and optional()
will add the Sequelize.STRING
type and set the optional
property to false which results in a required string field:
firstName: new FieldBuilder()
.string()
.optional()
Finally call the output
method to serialize to output the data to the property;
firstName: new FieldBuilder()
.string()
.optional()
.output()
// Output:
firstName: {
type: Sequelize.STRING
default: "",
allowNull: false
}
Under the hood the main action being taken by the builder is to use Object.assign
to update the internal state of the instance. For this reason, the order of your functions can be important.
I found it tedious to have to manage Sequelize migration implementation by hand since many of the column configurations used the exact same objects in the end. Additionally I wanted to explore the builder pattern on a small scale to better understand it's implementation. I was especially interested in the a la carte style function chaining allowing the developer using the module to build something up by calling specific methods. I have seen this pattern used in other modules like Joi and MomentJS which inspired the investigation.
The following functions configure a column by setting the type
property on the column. Some functions will set additional supporting properties such as the enumColumn
function which will populate an array of accepted values.
Marks a column for storing boolean values.
Usage: FieldBuilder.boolean()
Example:
field: FieldBuilder.boolean();
// Output
// field: {
// type: Sequelize.BOOLEAN
// }
Marks a column for storing date values. Note that this is the simplest date option provided by Sequelize. Other date based types can be found in their documentation.
Usage: FieldBuilder.date()
Example:
field: FieldBuilder.date();
// Output
// field: {
// type: Sequelize.DATE
// }
Marks the column as a string and assigns the isEmail
validation condition. This is one of the many validators provided by sequelize
Usage: FieldBuilder.email()
Example:
field: FieldBuilder.email();
// Output
// field: {
// type: Sequelize.STRING,
// validate: {
// isEmail: true,
// },
// }
Configures a column to accept a set of accepted values. This is a list of strings either as a list of parameters or as an array of strings.
Usage: FieldBuilder.enum(...options: string)
Example:
field: FieldBuilder.enum('pending', 'active', 'complete');
// Output
// field: {
// type: Sequelize.ENUM,
// values: [ 'pending', 'active', 'complete' ],
// }
const status = ['pending', 'active', 'complete']
field: FieldBuilder.enum(status);
// Output
// field: {
// type: Sequelize.ENUM,
// values: [ 'pending', 'active', 'complete' ],
// }
Creates a foreign key constraint to tie this column to the key of another table.
Usage: FieldBuilder.foreignKey(model: string, key?: string)
Example:
// Ties this table to another table called `Addresses`
field: FieldBuilder.foreignKey('Addresses');
// Output
// field: {
// type: Sequelize.INTEGER
// references: {
// model: {
// tableName: 'Addresses'
// }
// key: 'id'
// }
// }
// Ties this table to another table called `Users_Addresses` on the column named `userId`
field: FieldBuilder.foreignKey('Users_Addresses', 'userId);
// Output
// field: {
// type: Sequelize.INTEGER
// references: {
// model: {
// tableName: 'Addresses'
// }
// key: 'userId'
// }
// }
Marks a column for storing JSON data. Full details can be found in the documentation.
Usage: FieldBuilder.json(defaultValue?: Record<string,any>)
Example:
field: FieldBuilder.json();
// Output
// field: {
// type: Sequelize.JSON
// default: {}
// }
Marks a column for various types of numbers.
Usage: FieldBuilder.number(type?: string)
Example:
field: FieldBuilder.number();
// Output
// field: {
// type: Sequelize.INTEGER
// }
In addition to paramter-less, the function can also take a single parameter to assign various other number data types. The list here is selected based on the Sequelize documentation by selecting the configurations with the most support. If you wish to use another type of number column, use the custom input function.
Type | Output |
---|---|
'bigint' | Sequelize.BIGINT |
'double' | Sequelize.DOUBLE |
'float' | Sequelize.FLOAT |
'integer' | Sequelize.INTEGER |
'smallint' | Sequelize.SMALLINT |
Example:
field: FieldBuilder.number('double');
// Output
// field: {
// type: Sequelize.DOUBLE
// }
Configures the column as the primary key for the record. This includes setting the auto increment flag and identifying the column as the primary key.
Usage: FieldBuilder.primaryKey()
Example:
field: FieldBuilder.primaryKey();
// Output
// field: {
// allowNull: false,
// autoIncrement: true,
// primaryKey: true,
// type: Sequelize.INTEGER,
// }
Marks a column for storing smaller strings. Per the documentation, Sequelize will create a VARCHAR
type with a 255 character max.
Usage: FieldBuilder.string()
Example:
field: FieldBuilder.string();
// Output
// field: {
// type: Sequelize.STRING
// }
Marks a column for storing larger strings. Depending on your database, Sequelize will use that flavor syntax to generate the field. The max size will depend on the database and it is recommended that you review the documentation.
Usage: FieldBuilder.text()
Example:
field: FieldBuilder.text();
// Output
// field: {
// type: Sequelize.TEXT
// }
The following functions are available on the FieldBuilder class instance and will apply specific configurations to the field
Allows for custom configuration not provided by this tool. This function will throw an error if you attempt to pass a non-object.
Usage: FieldBuilder.customInput(input: Record<string, any>)
Example:
// Add a JSONB field to a PostgreSQL table
FieldBuilder.customInput({ type: Sequelize.JSONB });
// Output
// {
// type: Sequelize.JSONB
// }
// Invalid: Passing a non-object
FieldBuilder.customInput(true);
// Output
// Error: Invalid custom input
Configures a default value for the field. Note that this is not type checked against the fields type
value.
Usage: FieldBuilder.defaultValue(value: any)
Example:
// Set default value on a string column to `None`
FieldBuilder
.string()
.defaultValue('None');
// Output
// {
// type: Sequelize.JSONB
// }
// Invalid: Parameter-less call
FieldBuilder.defaultValue();
// Output
// Error: Default value must be defined
Adds the notEmpty
validation flag to the fields validate configuration.
Usage: FieldBuilder.nonEmptyString()
Example:
field: new FieldBuilder().nonEmptyString();
// Output
// field: {
// validate: {
// notEmpty: true
// }
// }
Sets the field as allowing NULL as a value. Depending on your SQL dialect, you may or may not need this. Check your dialect documentation for more details.
Example:
field: new FieldBuilder().optional();
// Output
// field: {
// allowNull: true
// }
Sets the field as requiring a non-null value. Depending on your SQL dialect, you may or may not need this. Check your dialect documentation for more details.
Example:
field: new FieldBuilder().required();
// Output
// field: {
// allowNull: false
// }
This is a special static method that I have added to the builder to further remove the boilerplate of having to setup tables. In each of my tables I have included an active, created_at, and updated_at columns. This method can be used to apply those columns to your table if you would like.
Usage: FieldBuilder.statusColumns()
Example:
up(queryInstance, Sequelize) {
await queryInterface.createTable('Users', {
id: new FieldBuilder().primaryKey().output();
...FieldBuilder.statusColumns()
});
}
// Output:
// {
// id: {
// allowNull: false,
// autoIncrement: true,
// primaryKey: true,
// type: Sequelize.INTEGER,
// },
// active: { type: Sequelize.BOOLEAN },
// created_at: { type: Sequelize.DATE },
// updated_at: { type: Sequelize.DATE }
// }
If you wish to rename the columns, you an do so using normal Javascript techniques like destructuring:
const { active, created_at, updated_at } = FieldBuilder.statusColumns();
up(queryInstance, Sequelize) {
await queryInterface.createTable('Users', {
id: new FieldBuilder().primaryKey();
createdTimestamp: created_at,
updatedTimestamp: updated_at
});
}
The unique operator allows you to identify a column as unique to the table. This will be set as a constraint in your SQL database and will be enforced by the database.
There are 2 forms of uniqueness. The first one is column level uniqueness. To implement column level uniqueness, simply call the unique
function:
field: new FieldBuilder().unique();
// Output:
// {
// unique: true
// }
Alternatively you can create named constraints by passing a string constraint name:
field: new FieldBuilder().unique('user_info');
// Output:
// {
// unique: 'user_info'
// }
Named constraints can come in handy when you are building out tables that require that multiple pieces of information are unique in conjunction with each other:
up(queryInstance, Sequelize) {
await queryInterface.createTable('Users', {
id: new FieldBuilder().primaryKey();
name: new FieldBuilder().string().unique('user_info')
email: new FieldBuilder().email().unique('user_info')
});
}
// Output
// {
// id: {
// allowNull: false,
// autoIncrement: true,
// primaryKey: true,
// type: Sequelize.INTEGER,
// },
// name: {
// type: Sequelize.STRING,
// default: '',
// unique: 'user_info'
// },
// email: {
// type: Sequelize.STRING,
// validate: {
// isEmail: true
// },
// unique: 'user_info'
// }
// }
If you have feedback, don't hesitate to open an issue and I will get to it when I have time.