Skip to content

Instantly share code, notes, and snippets.

@neonexus
Last active December 23, 2022 03:07
Show Gist options
  • Save neonexus/4f37181f0eff57d7e1c1bef80ba07e42 to your computer and use it in GitHub Desktop.
Save neonexus/4f37181f0eff57d7e1c1bef80ba07e42 to your computer and use it in GitHub Desktop.
Sails Schema Validation
module.exports = {
sync: true, // this is a synchronous helper
friendlyName: 'Object For Each',
description: 'Run a cb() on every item in an object.',
inputs: {
obj: {
type: 'ref',
required: true
},
cb: {
type: 'ref',
required: true
},
context: {
type: 'ref'
}
},
exits: {},
fn: function(inputs){
for (let item in inputs.obj) {
// eslint-disable-next-line no-prototype-builtins
if (inputs.obj.hasOwnProperty(item)) {
if (!inputs.context) {
inputs.cb(inputs.obj[item], item);
} else {
inputs.cb.call(inputs.context, inputs.obj[item], item);
}
}
}
}
};
// Use this for subheaders: http://patorjk.com/software/taag/#p=display&c=c&f=Calvin%20S&t=header
const fs = require('fs');
module.exports.bootstrap = function(next) {
// Check if we need to validate our schema
if (sails.config.models.migrate === 'safe') { // aka PROD
let waitingToFinish = 0;
sails.helpers.objForEach(sails.models, (model, modelName) => {
if (model.tableName !== 'archive') {
waitingToFinish++;
sails.getDatastore().sendNativeQuery('SHOW COLUMNS FROM `' + model.tableName + '`', (err, columns) => {
if (err) {
console.error(err);
return console.error('Table "' + model.tableName + '" does not seem to exist.');
}
let i; // we use this to make sure there aren't too many columns in the database
let continueOn = true;
function notWhatWasExpected(column, foundAt, expected) {
continueOn = false;
console.error('Column "' + column + '" for "' + modelName + '" is type "' + columns.rows[foundAt].Type + '", expected it to be a(n) ' + expected);
}
// check if the model field exists in the database
for (let column in model.schema) {
// verify this isn't a special/magical property or a collection
// eslint-disable-next-line no-prototype-builtins
if (model.schema.hasOwnProperty(column) && !model.schema[column].collection) {
let foundAt = null;
for (i = 0; i < columns.rows.length; i++) {
if (columns.rows[i].Field === column) {
foundAt = i;
break;
}
}
/***
* ┌─┐┌─┐┬ ┬ ┬┌┬┐┌┐┌ ┬┌─┐ ┌┬┐┬┌─┐┌─┐┬┌┐┌┌─┐
* │ │ ││ │ │││││││ │└─┐ ││││└─┐└─┐│││││ ┬
* └─┘└─┘┴─┘└─┘┴ ┴┘└┘ ┴└─┘ ┴ ┴┴└─┘└─┘┴┘└┘└─┘
*/
if (foundAt === null) {
console.error('Column "' + column + '" does not exist in database for "' + modelName + '"');
continueOn = false;
break;
}
// validate the types of columns are correct in the database
switch (model.schema[column].type) {
/***
* ┌┐┌┬ ┬┌┬┐┌┐ ┌─┐┬─┐
* ││││ ││││├┴┐├┤ ├┬┘
* ┘└┘└─┘┴ ┴└─┘└─┘┴└─
*/
case 'number':
if (model.schema[column].autoMigrations.columnType === '_numberkey') {
/***
* ┬┌─┐ ┬┌┬┐ ┬ ┬┌┐┌┌─┐┬┌─┐┌┐┌┌─┐┌┬┐┌─┐
* │└─┐ │ │ │ ││││└─┐││ ┬│││├┤ ││ ┌┘
* ┴└─┘ ┴ ┴ └─┘┘└┘└─┘┴└─┘┘└┘└─┘─┴┘ o
*/
if (columns.rows[foundAt].Type !== 'int(11) unsigned') {
notWhatWasExpected(column, foundAt, 'int(11) unsigned');
}
/***
* ┌┬┐┌─┐┌─┐┌─┐ ┬┌┬┐ ┬┌┐┌┌─┐┬─┐┌─┐┌┬┐┌─┐┌┐┌┌┬┐┌─┐
* │││ │├┤ └─┐ │ │ │││││ ├┬┘├┤ │││├┤ │││ │ ┌┘
* ─┴┘└─┘└─┘└─┘ ┴ ┴ ┴┘└┘└─┘┴└─└─┘┴ ┴└─┘┘└┘ ┴ o
*/
if (model.schema[column].autoMigrations.autoIncrement && columns.rows[foundAt].Extra !== 'auto_increment') {
notWhatWasExpected(column, foundAt, 'int(11) unsigned (auto_increment)');
}
} else {
/***
* ┌┬┐┌─┐┌┬┐┌─┐┬ ┬┌─┐ ┌┬┐┬┌─┐┌─┐┬┌┐┌┌─┐ ┌─┐┌─┐┬ ┬ ┬┌┬┐┌┐┌ ┌┬┐┬ ┬┌─┐┌─┐
* ││││ │ ││├┤ │ │└─┐ ││││└─┐└─┐│││││ ┬ │ │ ││ │ │││││││ │ └┬┘├─┘├┤
* ┴ ┴└─┘─┴┘└─┘┴─┘ ┴└─┘ ┴ ┴┴└─┘└─┘┴┘└┘└─┘ └─┘└─┘┴─┘└─┘┴ ┴┘└┘ ┴ ┴ ┴ └─┘
*/
if (model.schema[column].autoMigrations.columnType === '_number') {
notWhatWasExpected(column, foundAt, 'MODEL IS MISSING COLUMN TYPE');
}
/***
* ┌┬┐┌─┐ ┌┬┐┬ ┬┌─┐┌─┐┌─┐ ┌┬┐┌─┐┌┬┐┌─┐┬ ┬┌─┐
* │││ │ │ └┬┘├─┘├┤ └─┐ │││├─┤ │ │ ├─┤ ┌┘
* ─┴┘└─┘ ┴ ┴ ┴ └─┘└─┘ ┴ ┴┴ ┴ ┴ └─┘┴ ┴ o
*/
if (model.schema[column].autoMigrations.columnType !== '_number' && model.schema[column].autoMigrations.columnType !== columns.rows[foundAt].Type) {
notWhatWasExpected(column, foundAt, model.schema[column].autoMigrations.columnType);
}
}
break;
/***
* ┌─┐┌┬┐┬─┐┬┌┐┌┌─┐
* └─┐ │ ├┬┘│││││ ┬
* └─┘ ┴ ┴└─┴┘└┘└─┘
*/
case 'string':
/***
* ┬┌─┐ ┌┬┐┌─┐┌─┐┌─┐┬ ┬┬ ┌┬┐ ┌─┐┌┬┐┬─┐┬┌┐┌┌─┐ ┌─┐┬┌─┐┌─┐┌─┐
* │└─┐ ││├┤ ├┤ ├─┤│ ││ │ └─┐ │ ├┬┘│││││ ┬ └─┐│┌─┘├┤ ┌┘
* ┴└─┘ ─┴┘└─┘└ ┴ ┴└─┘┴─┘┴ └─┘ ┴ ┴└─┴┘└┘└─┘ └─┘┴└─┘└─┘ o
*/
if (model.schema[column].autoMigrations.columnType === '_string' && columns.rows[foundAt].Type !== 'varchar(191)') {
notWhatWasExpected(column, foundAt, 'varchar(191)');
}
/***
* ┌┬┐┌─┐ ┌┬┐┬ ┬┌─┐┌─┐┌─┐ ┌┬┐┌─┐┌┬┐┌─┐┬ ┬┌─┐
* │││ │ │ └┬┘├─┘├┤ └─┐ │││├─┤ │ │ ├─┤ ┌┘
* ─┴┘└─┘ ┴ ┴ ┴ └─┘└─┘ ┴ ┴┴ ┴ ┴ └─┘┴ ┴ o
*/
if (model.schema[column].autoMigrations.columnType.substr(0, 7) === 'varchar' && model.schema[column].autoMigrations.columnType !== columns.rows[foundAt].Type) {
notWhatWasExpected(column, foundAt, model.schema[column].autoMigrations.columnType);
}
/***
* ┬┌─┐ ┌┬┐┌─┐┌┬┐┌─┐┌┬┐┬┌┬┐┌─┐┌─┐
* │└─┐ ││├─┤ │ ├┤ │ ││││├┤ ┌┘
* ┴└─┘ ─┴┘┴ ┴ ┴ └─┘ ┴ ┴┴ ┴└─┘ o
*/
if (model.schema[column].autoMigrations.columnType === 'datetime' && columns.rows[foundAt].Type !== 'datetime') {
notWhatWasExpected(column, foundAt, 'datetime');
}
/***
* ┬┌─┐ ┌┬┐┌─┐┌┬┐┌─┐┌─┐
* │└─┐ ││├─┤ │ ├┤ ┌┘
* ┴└─┘ ─┴┘┴ ┴ ┴ └─┘ o
*/
if (model.schema[column].autoMigrations.columnType === 'date' && columns.rows[foundAt].Type !== 'date') {
notWhatWasExpected(column, foundAt, 'date');
}
/***
* ┬┌─┐ ┌┬┐┬┌┬┐┌─┐┌─┐
* │└─┐ │ ││││├┤ ┌┘
* ┴└─┘ ┴ ┴┴ ┴└─┘ o
*/
if (model.schema[column].autoMigrations.columnType === 'time' && columns.rows[foundAt].Type !== 'time') {
notWhatWasExpected(column, foundAt, 'time');
}
break;
/***
* ┌┐ ┌─┐┌─┐┬ ┌─┐┌─┐┌┐┌
* ├┴┐│ ││ ││ ├┤ ├─┤│││
* └─┘└─┘└─┘┴─┘└─┘┴ ┴┘└┘
*/
case 'boolean':
if (columns.rows[foundAt].Type !== 'tinyint(1)') {
notWhatWasExpected(column, foundAt, 'boolean (tinyint)');
}
break;
/***
* ┬┌─┐┌─┐┌┐┌
* │└─┐│ ││││
* └┘└─┘└─┘┘└┘
*/
case 'json':
if (columns.rows[foundAt].Type !== 'longtext') {
notWhatWasExpected(column, foundAt, 'json (longtext)');
}
break;
/***
* ┬─┐┌─┐┌─┐
* ├┬┘├┤ ├┤
* ┴└─└─┘└
*/
case 'ref':
if (model.schema[column].autoMigrations.columnType !== columns.rows[foundAt].Type) {
notWhatWasExpected(column, foundAt, model.schema[column].autoMigrations.columnType);
}
break;
/***
* ┬ ┬┌┐┌┬┌─┌┐┌┌─┐┬ ┬┌┐┌
* │ ││││├┴┐││││ │││││││
* └─┘┘└┘┴ ┴┘└┘└─┘└┴┘┘└┘
*/
default:
continueOn = false;
console.error('Column definition type "' + model.schema[column].type + '" does not have any safety checks! HALTING EXECUTION FOR SAFETY!');
break;
}
/***
* ┬┌─┐ ┬┌┬┐ ┬ ┬┌┐┌┬┌─┐ ┬ ┬┌─┐┌─┐
* │└─┐ │ │ │ ││││││─┼┐│ │├┤ ┌┘
* ┴└─┘ ┴ ┴ └─┘┘└┘┴└─┘└└─┘└─┘ o
*/
if (model.schema[column].autoMigrations.unique === true && columns.rows[foundAt].Key !== 'UNI' && columns.rows[foundAt].Key !== 'PRI') {
console.error('Column "' + column + '" is not marked as "unique" in database schema for "' + modelName + '"');
continueOn = false;
}
/***
* ┬┌─┐ ┬┌┬┐ ┌─┐┬─┐┬┌┬┐┌─┐┬─┐┬ ┬┌─┐
* │└─┐ │ │ ├─┘├┬┘││││├─┤├┬┘└┬┘ ┌┘
* ┴└─┘ ┴ ┴ ┴ ┴└─┴┴ ┴┴ ┴┴└─ ┴ o
*/
if (column === model.primaryKey && columns.rows[foundAt].Key !== 'PRI') {
console.error('Column "' + column + '" is not marked as the "primary key" in database schema for "' + modelName + '"');
continueOn = false;
}
}
}
/***
* ┌┬┐┌─┐┌─┐ ┌┬┐┌─┐┌┐┌┬ ┬ ┌─┐┌─┐┬ ┬ ┬┌┬┐┌┐┌┌─┐ ┬┌┐┌ ┌┬┐┌┐┌─┐
* │ │ ││ │ │││├─┤│││└┬┘ │ │ ││ │ │││││││└─┐ ││││ ││├┴┐┌┘
* ┴ └─┘└─┘ ┴ ┴┴ ┴┘└┘ ┴ └─┘└─┘┴─┘└─┘┴ ┴┘└┘└─┘ ┴┘└┘ ─┴┘└─┘o
*/
for (i = 0; i < columns.rows.length; i++) {
if (!model.schema[columns.rows[i].Field]) {
console.error('Extra column "' + columns.rows[i].Field + '" found for ' + modelName);
continueOn = false;
}
}
if (continueOn) {
waitingToFinish--;
}
});
}
});
let movedOn = false;
(function waitForSchemaChecks() {
setTimeout(() => {
if (waitingToFinish === 0 && movedOn === false) {
// everything has finished, no red flags yet, move on...
movedOn = true;
validateIndexes();
} else if (movedOn !== true) {
waitForSchemaChecks();
}
}, 20);
})();
// something is taking WAY too long... assume the worst
setTimeout(() => {
if (waitingToFinish > 0) {
console.error('The database schema does not appear to match the model definitions.');
process.exit(1);
}
}, 5000);
} else {
// no safety requirement for database modifications, bypass safeties
return next();
}
function validateIndexes() {
let waitingToFinish = 0;
sails.helpers.objForEach(sails.models, (model, modelName) => {
if (model.tableName !== 'archive' && model.associations && model.associations.length) {
model.associations.map((association) => {
waitingToFinish++;
// check this isn't a collection
if (!association.collection) {
sails.getDatastore().sendNativeQuery(
'SELECT * FROM `information_schema`.`KEY_COLUMN_USAGE` WHERE `TABLE_NAME` = \'' + model.tableName
+ '\' AND `COLUMN_NAME` = \'' + association.alias
+ '\' AND `REFERENCED_TABLE_NAME` = \'' + sails.models[association.model].tableName
+ '\'',
(err, foundKeys) => {
if (err) {
console.error(err);
return console.error('I can\'t seem to read the required relationship data. HALTING!');
}
if (!foundKeys.rows[0] || !foundKeys.rows[0]['REFERENCED_COLUMN_NAME'] || foundKeys.rows[0]['REFERENCED_COLUMN_NAME'] !== sails.models[association.model].primaryKey) {
console.error('Column "' + association.alias + '" for "' + modelName + '" does not have a relationship setup');
} else {
waitingToFinish--;
}
}
);
} else {
waitingToFinish--;
}
});
}
});
(function waitForIndexChecks() {
setTimeout(() => {
if (waitingToFinish === 0) {
next();
} else {
waitForIndexChecks();
}
}, 10);
})();
setTimeout(() => {
if (waitingToFinish > 0) {
console.error('The database schema does not appear to match the model definitions.');
process.exit(1);
}
}, 3000);
}
};

Sails Schema Validation

A bootstrap.js script designed to prevent Sails.js from lifting, in the event the database schema does not match the model definitions.

Perfect for preventing AWS ELB from deploying an incompatible version to production (requires using immutable deployments).

This is designed using native queries (MySQL), but likely can be easily adapted for most any adapter.

See https://github.com/neonexus/sails-react-bootstrap-webpack for an example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment