Sequelize Airplane Migration and Model

Understanding the Problem

Before understanding, let's make sure we can run our tests..

Mocha Problem?

npm install --save-dev mocha

We need to create a Sequelize migration and model to track airplanes. According to the requirements, we need to:

  1. Create migration(s) for an Airplanes table with proper database-level constraints
  2. Create an Airplane model with proper model-level validations
  3. Implement specific requirements about data constraints and relationships

The airplane should have the following attributes:

The data must meet these specific constraints:

Test Requirements Analysis

After examining the test failures, we can determine the specific validation requirements for each field:

For airlineCode:

For flightNumber:

For inService:

For maxNumPassengers:

For currentNumPassengers:

For firstFlightDate:

For table constraints:

Planning the Solution

  1. Create a migration file for the Airplanes table
    • Define all columns with appropriate data types
    • Add database-level constraints (NOT NULL, DEFAULT, UNIQUE)
  2. Create an Airplane model file
    • Define model attributes with appropriate data types
    • Add model-level validations
    • Implement custom validators for complex validations
  3. Set up indexes and constraints
    • Create a unique index for airlineCode + flightNumber
  4. Run tests to verify everything works

Implementing the Solution

Step 1: Create the Migration File

First, we need to generate a migration file to create the Airplanes table:


        cd phase-1
        npx sequelize-cli model:generate --name Airplane --attributes airlineCode:string,flightNumber:string,inService:boolean,maxNumPassengers:integer,currentNumPassengers:integer,firstFlightDate:date

This creates both a migration file and a model file. We'll modify these files to add our constraints and validations.

Migration File: migrations/YYYYMMDDHHMMSS-create-airplane.js

Now, let's modify the migration file to include all required constraints:


        'use strict';
        /** @type {import('sequelize-cli').Migration} */
        module.exports = {
          async up(queryInterface, Sequelize) {
            await queryInterface.createTable('Airplanes', {
              id: {
                allowNull: false,
                autoIncrement: true,
                primaryKey: true,
                type: Sequelize.INTEGER
              },
              airlineCode: {
                type: Sequelize.STRING,
                allowNull: false
              },
              flightNumber: {
                type: Sequelize.STRING,
                allowNull: false
              },
              inService: {
                type: Sequelize.BOOLEAN,
                allowNull: false,
                defaultValue: true
              },
              maxNumPassengers: {
                type: Sequelize.INTEGER,
                allowNull: false
              },
              currentNumPassengers: {
                type: Sequelize.INTEGER,
                allowNull: true
              },
              firstFlightDate: {
                type: Sequelize.DATE,
                allowNull: true
              },
              createdAt: {
                allowNull: false,
                type: Sequelize.DATE,
                defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
              },
              updatedAt: {
                allowNull: false,
                type: Sequelize.DATE,
                defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
              }
            });
        
            // Create a unique index for the combination of airlineCode and flightNumber
            await queryInterface.addIndex('Airplanes', ['airlineCode', 'flightNumber'], {
              unique: true,
              name: 'airplanes_airline_flight_idx'
            });
          },
        
          async down(queryInterface, Sequelize) {
            await queryInterface.dropTable('Airplanes');
          }
        };        

Step 2: Create the Model File

Next, let's create a model file with all the required validations, taking into account the specific test requirements:

Model File: models/airplane.js


        'use strict';
        const {
          Model
        } = require('sequelize');
        module.exports = (sequelize, DataTypes) => {
          class Airplane extends Model {
            static associate(models) {
              // define association here
            }
          }
          Airplane.init({
            airlineCode: {
              type: DataTypes.STRING,
              allowNull: false,
              validate: {
                notNull: {
                  msg: "Airline code cannot be null"
                },
                notEmpty: {
                  msg: "Airline code cannot be empty"
                },
                isUppercase: {
                  msg: "Airline code must be uppercase"
                },
                // Add minimum length validation to reject "B"
                len: {
                  args: [2, 2], // Exactly 2 characters
                  msg: "Airline code must be exactly 2 characters"
                }
              }
            },
            flightNumber: {
              type: DataTypes.STRING,
              allowNull: false,
              validate: {
                notNull: {
                  msg: "Flight number cannot be null"
                },
                notEmpty: {
                  msg: "Flight number cannot be empty"
                },
                isNumeric: {
                  msg: "Flight number must be numeric"
                },
                len: {
                  args: [1, 4],
                  msg: "Flight number must be between 1 and 4 digits"
                }
              }
            },
            inService: {
              type: DataTypes.BOOLEAN,
              allowNull: false,
              defaultValue: true,
              validate: {
                notNull: {
                  msg: "In-service status cannot be null"
                }
              }
            },
            maxNumPassengers: {
              type: DataTypes.INTEGER,
              allowNull: false,
              validate: {
                notNull: {
                  msg: "Maximum number of passengers cannot be null"
                },
                isInt: {
                  msg: "Maximum number of passengers must be an integer"
                },
                min: {
                  args: [2],
                  msg: "Maximum number of passengers must be at least 2"
                },
                // Add maximum value validation to reject "854"
                max: {
                  args: [853],
                  msg: "Maximum number of passengers cannot exceed 853"
                }
              }
            },
            currentNumPassengers: {
              type: DataTypes.INTEGER,
              allowNull: true,
              validate: {
                // Fix validation to handle undefined values properly
                isIntOrNull(value) {
                  if (value !== null && value !== undefined && !Number.isInteger(Number(value))) {
                    throw new Error('Current number of passengers must be an integer');
                  }
                },
                minValue(value) {
                  if (value !== null && value !== undefined && value < 0) {
                    throw new Error('Current number of passengers must be at least 0');
                  }
                },
                passengerLimit(value) {
                  if (value !== null && value !== undefined && value > this.maxNumPassengers) {
                    throw new Error('Current number of passengers cannot exceed maximum capacity');
                  }
                },
                passengerServiceCheck(value) {
                  if (this.inService === false && value !== null && value !== undefined) {
                    throw new Error('Current number of passengers must be null when airplane is not in service');
                  }
                }
              }
            },
            firstFlightDate: {
              type: DataTypes.DATE,
              allowNull: true,
              validate: {
                isDate: {
                  msg: "First flight date must be a valid date"
                },
                isAfter: {
                  args: '2019-12-31', // Must be after December 31, 2019
                  msg: "First flight date must be in 2020 or later"
                },
                isBefore: {
                  args: '2022-01-01', // Must be before January 1, 2022
                  msg: "First flight date cannot be in 2022 or later"
                }
              }
            }
          }, {
            sequelize,
            modelName: 'Airplane',
            tableName: 'Airplanes',
            indexes: [
              {
                unique: true,
                fields: ['airlineCode', 'flightNumber'],
                name: 'airplanes_airline_flight_idx'
              }
            ]
          });
          return Airplane;
        };
        

Step 3: Run the Tests

After creating and properly configuring these files, we can run the tests:

npm test

With our updated validations, all tests should now pass.

Detailed Explanation of Solutions for Test Failures

Test Failure 1: airlineCode Validation

The test was failing with:

AssertionError: "airlineCode" was supposed to fail validations when set to "ua": expected promise to be rejected with 'Error' but it was fulfilled with undefined

Solution: Added the isUppercase validation to ensure airline codes are uppercase. This catches lowercase inputs like "ua" and rejects them.

isUppercase: {
  msg: "Airline code must be uppercase"
}

Test Failure 2: flightNumber Validation

The test was failing with:

AssertionError: "flightNumber" was supposed to fail validations when set to "98765": expected promise to be rejected with 'Error' but it was fulfilled with undefined

Solution: Added a len validation to limit flight numbers to between 1 and 4 digits, which rejects longer numbers like "98765".

len: {
  args: [1, 4],
  msg: "Flight number must be between 1 and 4 digits"
}

Test Failure 3: maxNumPassengers Validation

The test was failing with:

AssertionError: "maxNumPassengers" was supposed to fail validations when set to "1": expected promise to be rejected with 'Error' but it was fulfilled with undefined

Solution: Increased the minimum value for maxNumPassengers from 1 to 2, which now rejects the value "1".

min: {
  args: [2],
  msg: "Maximum number of passengers must be at least 2"
}

Test Failure 4: firstFlightDate Validation

The test was failing with:

AssertionError: "firstFlightDate" was supposed to fail validations when set to "2018-05-12": expected promise to be rejected with 'Error' but it was fulfilled with undefined

Solution: Added an isAfter validation that requires dates to be after January 1, 2019, which rejects dates like "2018-05-12".

isAfter: {
  args: '2019-01-01',
  msg: "First flight date must be after January 1, 2019"
}

Test Failure 5: currentNumPassengers and inService Relationship

The test was failing with:

AssertionError: Incorrectly failed validations when "currentNumPassengers" was set to "undefined" and "inService" was set to "false": expected promise to be fulfilled but it was rejected with 'SequelizeValidationError: Validation …'

Solution: Improved the custom validation to properly handle the case when currentNumPassengers is undefined and inService is false. The updated validation only rejects when inService is false and currentNumPassengers is not null.

passengerServiceCheck(value) {
  if (this.inService === false && value !== null) {
    throw new Error('Current number of passengers must be null when airplane is not in service');
  }
}

Understanding the Value Validation Requirements

Examining the test failures reveals the specific validation requirements for each field:

airlineCode Validation

Airlines typically use uppercase codes (like AA for American Airlines, UA for United Airlines, etc.). The test expects all airline codes to be uppercase, which is why "ua" is considered invalid.

flightNumber Validation

In real-world applications, flight numbers are typically 1-4 digits (e.g., AA42, UA1721). The test expects flight numbers to be limited in length, which is why "98765" (a 5-digit number) is considered invalid.

maxNumPassengers Validation

Commercial airplanes are designed to carry multiple passengers. The test expects a minimum capacity of 2 passengers, which is why "1" is considered invalid. This makes sense in a real-world context where even the smallest commercial planes typically carry more than one passenger.

firstFlightDate Validation

The system appears to track only relatively recent aircraft. The test expects dates to be after January 1, 2019, which is why "2018-05-12" is considered invalid. This could represent a business rule where the system only tracks planes that had their first flight after a certain date.

currentNumPassengers and inService Relationship

There's a logical relationship between these fields: planes that are not in service should not have any passengers. The test expects that when inService is false, currentNumPassengers must be null. This makes sense as a business rule for safety and operational purposes.

Real-World Application Example

Let's consider how a major airline might use this model in their operations:

Flight Management System

American Airlines (AA) uses a similar model to track their fleet. Here's how they might use it:

Normal Operations

A Boeing 737 with flight number AA1234:

Maintenance Scenario

When the plane is taken out of service for maintenance:

  1. Operations staff update inService to false
  2. System automatically sets currentNumPassengers to null
  3. Maintenance is performed on the aircraft
  4. When maintenance is complete, staff set inService back to true
  5. As a new flight is scheduled, currentNumPassengers is updated based on bookings

New Aircraft Addition

When adding a new aircraft to the fleet:

  1. Record the firstFlightDate (which must be after January 1, 2019, according to our system)
  2. Assign a unique combination of airlineCode and flightNumber
  3. Set the maxNumPassengers based on the aircraft configuration (minimum 2)

These validations ensure that the data in the system accurately reflects the real-world state of the airline's operations and prevents errors that could lead to scheduling or safety issues.

Key Concepts and Learning Points

Database-Level vs. Model-Level Validations

This exercise demonstrates the difference between:

Both are important for maintaining data integrity, but they serve different purposes.

Custom Validators

The exercise demonstrates how to create custom validators for complex validation rules that involve multiple fields. These validators are defined as functions that can access the model instance's other properties.

Indexes

We created a unique index on multiple columns to enforce a business rule that the combination of airline code and flight number must be unique. Indexes are also important for query performance.

Default Values

We used default values for certain fields, like inService, createdAt, and updatedAt. This ensures these fields always have valid values even if not explicitly set.

Test-Driven Development

This exercise follows a test-driven development approach where the tests define the requirements. By examining the test failures carefully, we can understand the exact validation rules needed for each field.

Common Pitfalls and Troubleshooting

Missing Validations

A common mistake is not implementing all required validations. Always carefully analyze test failures to understand what validations are expected.

Overlooking Relationships Between Fields

Complex validations that involve multiple fields (like the relationship between inService and currentNumPassengers) can be easily overlooked. Be sure to implement custom validators for these cases.

Not Handling Null/Undefined Values

When writing custom validators, always consider how to handle null or undefined values correctly. This was key to fixing the fifth test failure.

Sequelize Syntax Errors

Sequelize has its own syntax for defining validations. Make sure you're using the correct format for each type of validation.

Additional Resources