@krishnapawar/kp-mysql-models
Version:
`kp-mysql-models` is a lightweight, easy-to-use, and promise-based Node.js ORM tool designed for MySQL. It simplifies database interactions by offering features like model-based query building, CRUD operations, and support for advanced queries such as joi
563 lines (479 loc) • 20.6 kB
JavaScript
const fs = require('fs');
const path = require('path');
const DataTypes = {
STRING: "VARCHAR(255)",
TEXT: "TEXT",
DATE: "DATETIME",
INTEGER: "INT(11)",
FLOAT: "FLOAT",
BOOLEAN: "TINYINT(1)",
BIGINT: "BIGINT",
JSON: "JSON",
ENUM: (values) => `ENUM(${values.map((v) => `'${v}'`).join(", ")})`, // Only for ENUM
UUID: "CHAR(36)", // UUID type
DECIMAL: (precision, scale) => `DECIMAL(${precision}, ${scale})`, // For precision decimals
};
// Generates the SQL `CREATE TABLE` query
function generateCreateTableQuery(tableName, schema, options = {}) {
if (!tableName || typeof tableName !== "string") {
throw new Error("Table name must be a valid string.");
}
if (!schema || typeof schema !== "object") {
throw new Error("Schema must be an object.");
}
let query = `CREATE TABLE IF NOT EXISTS \`${tableName}\` (\n`;
const columns = [];
const constraints = [];
const indexes = [];
const uniqueConstraints = []; // For multiple columns uniqueness
// Check if "id" exists, if not create an auto-increment primary key
if (!schema.id) {
let idData= {id:{
type: "INTEGER",
primaryKey: true,
autoIncrement: true,
allowNull: false
}}
Object.assign(idData, schema);
schema = idData;
}
// Loop through the schema and generate column definitions
for (const [columnName, columnDef] of Object.entries(schema)) {
const {
type, allowNull, unique, primaryKey, defaultValue, references,
values, check, autoIncrement, precision, scale
} = columnDef;
// Ensure the type is valid
let columnType;
if (type === "ENUM") {
columnType = DataTypes[type](values || []);
} else if (type === "DECIMAL") {
columnType = DataTypes[type](precision, scale);
} else {
columnType = DataTypes[type] || type;
}
// Create the column definition
let columnDefStr = ` \`${columnName}\` ${columnType}`;
if(columnName == "timestamps" && columnDef == true){
columnDefStr = `createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`;
}
// Apply the NOT NULL constraint if not explicitly set
if (allowNull === false) columnDefStr += " NOT NULL";
else if (allowNull === true) columnDefStr += " NULL"; // Optional: allows explicit NULL setting
// Handle UNIQUE constraints
if (unique && !primaryKey) {
uniqueConstraints.push(` UNIQUE (\`${columnName}\`)`);
}
// Handle PRIMARY KEY
if (primaryKey) columnDefStr += " PRIMARY KEY";
// Handle AUTO_INCREMENT for MySQL or equivalent
if (autoIncrement) columnDefStr += " AUTO_INCREMENT";
// Handle DEFAULT value (for expressions or literals)
if (defaultValue) {
if (typeof defaultValue === "string" && defaultValue.toLowerCase() === "current_timestamp") {
columnDefStr += ` DEFAULT CURRENT_TIMESTAMP`; // Handle `CURRENT_TIMESTAMP`
} else {
columnDefStr += ` DEFAULT '${defaultValue}'`; // Handle other defaults like `0`, `'N/A'`, etc.
}
}
// Add CHECK constraints
if (check) columnDefStr += ` CHECK (${check})`;
columns.push(columnDefStr);
// Add foreign key constraint if references are specified
if (references) {
const { table, field, onDelete, onUpdate } = references;
let foreignKeyStr = ` FOREIGN KEY (\`${columnName}\`) REFERENCES \`${table}\`(\`${field}\`)`;
if (onDelete) foreignKeyStr += ` ON DELETE ${onDelete}`;
if (onUpdate) foreignKeyStr += ` ON UPDATE ${onUpdate}`;
constraints.push(foreignKeyStr);
}
// Optionally, handle indexes (e.g., for unique columns)
if (unique && !primaryKey) {
indexes.push(` INDEX \`${columnName}_unique_index\` (\`${columnName}\`)`);
}
// Automatically add index for primary key
if (primaryKey) {
indexes.push(` INDEX \`${columnName}_primary_key_index\` (\`${columnName}\`)`);
}
}
// Handle composite unique constraints (multiple columns)
if (uniqueConstraints.length > 0) {
query += uniqueConstraints.join(",\n") + ",\n";
}
// Handle auto-created `timestamps` if the option is enabled
if (options.timestamps) {
const { createdAt = "createdAt", updatedAt = "updatedAt" } = options.timestamps;
columns.push(
` \`${createdAt}\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP`,
` \`${updatedAt}\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`
);
}
// Finalize the table columns
query += columns.join(",\n");
// Add constraints (e.g., foreign keys) if they exist
if (constraints.length > 0) {
query += ",\n" + constraints.join(",\n");
}
// Add indexes if any were defined
if (indexes.length > 0) {
query += ",\n" + indexes.join(",\n");
}
query += "\n);";
return query;
}
function migrateSchema(tableName, schema) {
const migrationsFolderPath = "migrations";
if (!fs.existsSync(migrationsFolderPath)) {
fs.mkdirSync(migrationsFolderPath, { recursive: true });
}
const jsonFilePath = path.join(migrationsFolderPath, `${tableName}.json`);
if (!fs.existsSync(jsonFilePath)) {
fs.writeFileSync(jsonFilePath, JSON.stringify({ [tableName]: schema }, null, 2));
console.log("Schema saved to file.");
return generateCreateTableQuery(tableName, schema);
}
const existingSchemas = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8'));
const existingSchema = existingSchemas[tableName] || {};
const alterQueries = [];
// Check for added or modified fields
for (const [columnName, columnDef] of Object.entries(schema)) {
const existingColumn = existingSchema[columnName];
if (!existingColumn) {
// New column
if(columnName == "timestamps" && columnDef == true){
alterQueries.push(`ADD COLUMN createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ADD COLUMN updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`);
continue;
}
if(schema[columnName].references){
alterQueries.push(generateAlterQuery('references',{[columnName]:schema[columnName]}));
continue;
}
alterQueries.push(generateAlterQuery('add',{[columnName]:schema[columnName]}));
} else {
// Compare existing column properties
const type =
typeof columnDef.type === "function"
? columnDef.type(columnDef.values || [])
: DataTypes[columnDef.type] || columnDef.type;
const existingType =
typeof existingColumn.type === "function"
? existingColumn.type(existingColumn.values || [])
: DataTypes[existingColumn.type] || existingColumn.type;
// ---------------------------------------------------------
const allowNull = columnDef.allowNull !== false ? "" : " NOT NULL";
const existingAllowNull = existingColumn.allowNull !== false ? "" : " NOT NULL";
// ---------------------------------------------------------
const unique = columnDef.unique ? " UNIQUE" : "";
const existingUnique = existingColumn.unique ? " UNIQUE" : "";
// ---------------------------------------------------------
const defaultValue = columnDef.defaultValue
? ` DEFAULT ${columnDef.defaultValue}`
: "";
const existingDefaultValue = existingColumn.defaultValue
? ` DEFAULT ${existingColumn.defaultValue}`
: "";
// ---------------------------------------------------------
const after = columnDef.after ? ` AFTER ${columnDef.after}` : "";
const existingDefaultAfter = existingColumn.after
? ` AFTER ${existingColumn.after}`
: "";
// ---------------------------------------------------------
const rename = columnDef.rename ? columnDef.rename : "";
const existingRename = existingColumn.rename
? existingColumn.rename
: "";
// -----------------------------------------------------------
const references = columnDef.references && columnDef.references.table ? columnDef.references.table : "";
const existingReferences = existingColumn.references && existingColumn.references.table
? existingColumn.references.table
: "";
// -----------------------------------------------------------
const referencesField = columnDef.references && columnDef.references.field ? columnDef.references.field : "";
const existingReferencesField = existingColumn.references && existingColumn.references.field
? existingColumn.references.field
: "";
// -----------------------------------------------------------
if (
type !== existingType ||
allowNull !== existingAllowNull ||
unique !== existingUnique ||
defaultValue !== existingDefaultValue ||
after !== existingDefaultAfter ||
rename !== existingRename ||
references !== existingReferences ||
referencesField !== existingReferencesField
) {
if(rename !== existingRename){
alterQueries.push(generateAlterQuery('rename',{[columnName]:schema[columnName]},{[columnName]:existingSchema[columnName]}));
continue;
}
if (references !== existingReferences ||
referencesField !== existingReferencesField) {
if (references !== '') {
schema[columnName].modify = true;
alterQueries.push(generateAlterQuery('references', { [columnName]: schema[columnName] },{ [columnName]: existingSchema[columnName] }));
continue;
}
}
alterQueries.push(generateAlterQuery('modify',{[columnName]:schema[columnName]}));
}
}
}
const dropQueries =[];
// Check for removed fields
for (const columnName of Object.keys(existingSchema)) {
if (!schema[columnName]) {
if(columnName == 'timestamps'){
dropQueries.push(`DROP COLUMN createdAt, DROP COLUMN updatedAt`);
continue;
}
if(existingSchema[columnName].references){
dropQueries.push(generateAlterQuery('dropreferences',columnName));
continue;
}
dropQueries.push(generateAlterQuery('drop',columnName));
}
}
// Save updated schema
existingSchemas[tableName] = schema;
fs.writeFileSync(jsonFilePath, JSON.stringify(existingSchemas, null, 2));
let sql = `ALTER TABLE ${tableName} `;
const queryArr = [];
// Add drop column queries
if (dropQueries.length) {
queryArr.push(...dropQueries);
}
// Add alter column queries
if (alterQueries.length) {
queryArr.push(...alterQueries);
}
if(queryArr.length > 0){
sql += queryArr.join(", ");
return sql;
}
return false;
}
function generateAlterQuery(action, config,trackConfig={}) {
let query = "";
switch (action.toLowerCase()) {
case "add":
query = generateAddQuery(config);
break;
case "modify":
query = generateModifyQuery(config);
break;
case "drop":
query = generateDropQuery(config);
break;
case "rename":
query = generateRenameQuery(config,trackConfig);
break;
case "addconstraint":
query = generateAddConstraintQuery(config);
break;
case "dropconstraint":
query = generateDropConstraintQuery(config);
break;
case "references":
query = generateForeignKeyConstraint(config);
break;
case "dropreferences":
query = dropForeignKeyConstraint(config);
break;
default:
throw new Error("Invalid action. Use 'add', 'modify', 'drop', 'rename', 'addConstraint', or 'dropConstraint'.");
}
return query;
}
function generateAddQuery(config) {
const fields = Object.keys(config).filter(key => key !== "tableName" && key !== "name" && !isConstraint(config[key]));
if (fields.length === 0) {
throw new Error("For 'add', fields and their types are required.");
}
const addColumns = fields.map(field => {
const fieldConfig = config[field];
validateFieldConfig(fieldConfig);
return buildColumnDefinition(fieldConfig, field);
}).join(", ");
let query = ` ADD COLUMN ${addColumns}`;
const constraints = Object.keys(config).filter(key => isConstraint(config[key]));
if (constraints.length > 0) {
const constraintQueries = constraints.map(constraint => {
const constraintConfig = config[constraint];
return buildConstraintDefinition(constraintConfig, constraint);
}).join(", ");
query += `, ${constraintQueries}`;
}
return query;
}
function generateModifyQuery(config) {
const fields = Object.keys(config).filter(key => key !== "tableName" && key !== "name" && !isConstraint(config[key]));
if (fields.length === 0) {
throw new Error("For 'modify', fields and their types are required.");
}
const modifyColumns = fields.map(field => {
const fieldConfig = config[field];
validateFieldConfig(fieldConfig);
return buildColumnDefinition(fieldConfig, field);
}).join(", ");
return ` MODIFY COLUMN ${modifyColumns}`;
}
function generateDropQuery(config) {
if (!config || config.length === 0) {
throw new Error("For 'drop', field names are required.");
}
if(Array.isArray(config)){
return ` DROP COLUMN ${config.join(", ")}`;
}
return ` DROP COLUMN ${config}`;
}
function generateDropConstraintQuery(config) {
if (!config) {
throw new Error("For 'dropConstraint', name is required.");
}
if(Array.isArray(config)){
return ` DROP CONSTRAINT ${config.join(", ")}`;
}
return ` DROP CONSTRAINT ${config}`;
}
function generateRenameQuery(config,trackConfig) {
const field = Object.keys(config)
if (!config[field].rename || !config[field].type) {
throw new Error("For 'rename', field, newField, and type are required.");
}
let query = ` CHANGE ${trackConfig[field]?.rename ?? field} ${config[field].rename} ${config[field].type}`;
if (config[field].defaultValue !== undefined) {
query += ` DEFAULT ${typeof config[field].defaultValue === "string" ? `'${config[field].defaultValue}'` : config[field].defaultValue}`;
}
if (config[field].allowNull === false) {
query += ` NOT NULL`;
}
return query;
}
function generateAddConstraintQuery(config) {
const field = Object.keys(config)
if (!field || !config[field].type) {
throw new Error("For 'addConstraint', name and type are required.");
}
let query = ` ADD CONSTRAINT ${field} ${config[field].type}`;
if (config[field].type.toLowerCase() === "foreign key") {
if (!config[field].fields || !config[field].references) {
throw new Error("For 'foreign key', fields and references are required.");
}
query += ` (${config[field].fields.join(", ")}) REFERENCES ${config[field].references.table} (${config[field].references.field})`;
if (config[field].onDelete) {
query += ` ON DELETE ${config[field].onDelete}`;
}
if (config[field].onUpdate) {
query += ` ON UPDATE ${config[field].onUpdate}`;
}
}
return query;
}
function generateForeignKeyConstraint(config) {
if (!config || typeof config !== "object") {
throw new Error("Config must be a valid object.");
}
const fields = Object.keys(config);
if (fields.length === 0) {
throw new Error("Config must have at least one field.");
}
const statements = [];
fields.forEach((field) => {
const fieldConfig = config[field];
// Validate required properties
if (!fieldConfig.type) {
throw new Error(`Missing 'type' property for field: ${field}`);
}
if (!fieldConfig.references || !fieldConfig.references.table || !fieldConfig.references.field) {
throw new Error(
`Missing required 'references' properties for field: ${field}`
);
}
if(!fieldConfig.modify){
const addColumnStr = `ADD COLUMN \`${field}\` ${fieldConfig.type}`;
statements.push(addColumnStr);
}else{
const dropForeignKeyStr = `DROP FOREIGN KEY \`${field}_fk\``;
statements.push(dropForeignKeyStr);
}
// Add the foreign key constraint
let foreignKeyStr = `ADD FOREIGN KEY (\`${field}\`) REFERENCES \`${fieldConfig.references.table}\` (\`${fieldConfig.references.field}\`)`;
if (fieldConfig.onDelete) {
foreignKeyStr += ` ON DELETE ${fieldConfig.onDelete}`;
}
if (fieldConfig.onUpdate) {
foreignKeyStr += ` ON UPDATE ${fieldConfig.onUpdate}`;
}
statements.push(foreignKeyStr);
});
return statements.join(", ");
}
function dropForeignKeyConstraint(config) {
if (!config) {
throw new Error("BothconstraintName are required.");
}
return ` DROP FOREIGN KEY ${config}`;
}
function validateFieldConfig(fieldConfig) {
if (!fieldConfig.type) {
throw new Error("Each field must have a type.");
}
}
function isConstraint(fieldConfig) {
return fieldConfig.type && fieldConfig.type.toLowerCase() === "foreign key";
}
function buildColumnDefinition(fieldConfig, fieldName) {
let columnDef = `${fieldName} `;
let type = fieldConfig.type.toUpperCase();
let dType = DataTypes[type];
if (type === "ENUM") {
columnDef += dType(fieldConfig.values || []);
} else if (type === "DECIMAL") {
columnDef += dType(fieldConfig.precision, fieldConfig.scale);
} else {
columnDef += dType || type;
}
if (fieldConfig.length) {
columnDef += `(${fieldConfig.length})`;
}
if (fieldConfig.allowNull === false) {
columnDef += ` NOT NULL`;
}
if (fieldConfig.unique) {
columnDef += ` UNIQUE`;
}
if (fieldConfig.defaultValue !== undefined) {
columnDef += ` DEFAULT ${typeof fieldConfig.defaultValue === "string" ? `'${fieldConfig.defaultValue}'` : fieldConfig.defaultValue}`;
}
if (fieldConfig.autoIncrement) {
columnDef += ` AUTO_INCREMENT`;
}
if (fieldConfig.characterSet) {
columnDef += ` CHARACTER SET ${fieldConfig.characterSet}`;
}
if (fieldConfig.collation) {
columnDef += ` COLLATE ${fieldConfig.collation}`;
}
if (fieldConfig.after) {
columnDef += ` AFTER ${fieldConfig.after}`;
}
return columnDef;
}
function buildConstraintDefinition(constraintConfig, constraintName) {
let constraintDef = `${constraintName} ${constraintConfig.type}`;
if (constraintConfig.type.toLowerCase() === "foreign key") {
if (!constraintConfig.fields || !constraintConfig.references) {
throw new Error("For 'foreign key', fields and references are required.");
}
constraintDef += ` (${constraintConfig.fields.join(", ")}) REFERENCES ${constraintConfig.references.table} (${constraintConfig.references.field})`;
if (constraintConfig.onDelete) {
constraintDef += ` ON DELETE ${constraintConfig.onDelete}`;
}
if (constraintConfig.onUpdate) {
constraintDef += ` ON UPDATE ${constraintConfig.onUpdate}`;
}
}
return constraintDef;
}
module.exports = {migrateSchema};