Files

297 lines
9.2 KiB
JavaScript

'use strict'
/*
* This base migration attempts to create an 1:1 schema with the schema on the
* current production server. The schema **DOES NOT MATCH** the schema created
* by Sequelize from the models, because it does not match production's schema!
* The next migration fixes this.
*
* An oddity here is that **some** models use snake_case and some camelCase, and
* some use the "underscore: true" option, whereas some don't.
* As a result, **some** tables have createdAt and some created_at, etc.
* Some foreign key associations also use underscores, whereas some use camelCase.
*
* Another change should address this change as a migration and model update.
*
* Edit (2024-01-22): renamed camelCase column names to snake_case (which is used
* in the production db).
*/
module.exports = {
up: async (query, Sequelize) => {
// first create tables, then add relations
const createTimestampColumns = ({ camelCase }) => {
const timestampType = {
type: Sequelize.DATE,
allowNull: false,
}
const fieldNames = camelCase ? ['created_at', 'updated_at'] : ['created_at', 'updated_at']
/*
* Map the fieldNames array into an array of objects
* where the field name is the key and timetampType is the value
* ['foo', 'bar'] --> [ {foo: timestampType, bar: timestampType} ]
*/
return fieldNames.reduce(
(opts, name) => ({
...opts,
[name]: timestampType,
}),
{},
)
}
// since associations are created afterwards, we can do all of these
// concurrently. query.createTable returns promises so we gather them all
// into an array to start them all at the same time, then wait for finish
// with Promise.all
const tableCreationPromises = [
query.createTable('configurations', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: Sequelize.STRING,
content: Sequelize.JSONB,
active: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
// sequelize timestamps
...createTimestampColumns({ camelCase: false }),
// foreign keys for associations
review_question_set1_id: Sequelize.INTEGER,
review_question_set2_id: Sequelize.INTEGER,
registration_question_set_id: Sequelize.INTEGER,
}),
query.createTable('groups', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
group_name: Sequelize.STRING,
// sequelize timestamps
...createTimestampColumns({ camelCase: false }),
}),
query.createTable('memberships', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
role: Sequelize.STRING,
// sequelize timestamps
...createTimestampColumns({ camelCase: false }),
// foreign keys for associations
student_number: Sequelize.STRING,
}),
query.createTable('registration_question_sets', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: Sequelize.STRING,
questions: Sequelize.JSONB,
// sequelize timestamps
...createTimestampColumns({ camelCase: false }),
}),
query.createTable('registrations', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
preferred_topics: Sequelize.JSONB,
questions: Sequelize.JSONB,
// sequelize timestamps
...createTimestampColumns({ camelCase: false }),
// foreign keys for associations
configuration_id: Sequelize.INTEGER,
student_student_number: Sequelize.STRING(255),
}),
query.createTable('review_question_sets', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: Sequelize.STRING,
questions: Sequelize.JSONB,
// sequelize timestamps
...createTimestampColumns({ camelCase: false }),
}),
query.createTable('topic_dates', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
dates: Sequelize.JSONB,
// sequelize timestamps
...createTimestampColumns({ camelCase: false }),
}),
query.createTable('topics', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
active: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
content: Sequelize.JSONB,
acronym: Sequelize.STRING,
secret_id: Sequelize.STRING,
// sequelize timestamps
...createTimestampColumns({ camelCase: false }),
}),
query.createTable('users', {
student_number: {
type: Sequelize.STRING,
primaryKey: true,
},
username: Sequelize.STRING,
first_names: Sequelize.STRING,
last_name: Sequelize.STRING,
email: Sequelize.STRING,
admin: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
// sequelize timestamps
...createTimestampColumns({ camelCase: false }),
}),
]
// wait for table creation
await Promise.all(tableCreationPromises)
// tables created, add association
/*
* Sequelize's model constrains, by default, add
* - ON UPDATE CASCADE
* - ON DELETE SET NULL
* However, the production schema has one relation with ON DELETE CASCADE...
*/
const defaultOnDeleteAndUpdate = {
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
}
const addForeignKey = (
constraintName,
targetTable,
referencedTable,
onDeleteAndUpdate = defaultOnDeleteAndUpdate,
) => {
const [table, field] = targetTable
const [foreignTable, foreignField] = referencedTable
return query.addConstraint(table, {
fields: [field],
name: constraintName,
type: 'FOREIGN KEY',
references: {
table: foreignTable,
field: foreignField,
},
onUpdate: onDeleteAndUpdate.onUpdate,
onDelete: onDeleteAndUpdate.onDelete,
})
}
// Foreign key names have been plucked directly from the database schema
// dump
await addForeignKey(
'configurations_registration_question_set_id_fkey',
['configurations', 'registration_question_set_id'],
['registration_question_sets', 'id'],
)
await addForeignKey(
'configurations_review_question_set1_id_fkey',
['configurations', 'review_question_set1_id'],
['review_question_sets', 'id'],
)
await addForeignKey(
'configurations_review_question_set2_id_fkey',
['configurations', 'review_question_set2_id'],
['review_question_sets', 'id'],
)
await addForeignKey('memberships_id_fkey', ['memberships', 'id'], ['groups', 'id'], {
onUpdate: 'CASCADE',
onDelete: 'CASCADE', // Production schema's single constraint oddity.
})
await addForeignKey(
'registrations_configuration_id_fkey',
['registrations', 'configuration_id'],
['configurations', 'id'],
)
// Important: migration must return promise which, when resolved, marks the
// completion of all query operations. Here we just return the last awaited
// promise since all the other awaits have been done before this one.
return await addForeignKey(
'registrations_student_student_number _fkey',
['registrations', 'student_student_number'],
['users', 'student_number'],
)
},
down: async (query) => {
// Since this is the base migration, rolling back this migration should
// delete all of the tables and their associations.
// Start with associations so that we can delete tables without constraint
// violations.
const constraints = [
{
table: 'configurations',
constraint: 'configurations_registration_question_set_id_fkey',
},
{
table: 'configurations',
constraint: 'configurations_review_question_set1_id_fkey',
},
{
table: 'configurations',
constraint: 'configurations_review_question_set2_id_fkey',
},
{ table: 'memberships', constraint: 'memberships_id_fkey' },
{
table: 'registrations',
constraint: 'registrations_configuration_id_fkey',
},
{
table: 'registrations',
constraint: 'registrations_student_student_number _fkey',
},
]
const removeConstraintPromises = constraints.map(({ table, constraint }) =>
query.removeConstraint(table, constraint),
)
await Promise.all(removeConstraintPromises)
// Constraints removed, remove tables
const tableNames = [
'configurations',
'groups',
'memberships',
'registration_question_sets',
'registrations',
'review_question_sets',
'topic_dates',
'topics',
'users',
]
return await Promise.all(tableNames.map((tableName) => query.dropTable(tableName)))
},
}