2024-12-06 13:24:26 +00:00
// @ts-check
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
///////////////////////// - Update Table Function - ////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
const fs = require ( "fs" ) ;
const varDatabaseDbHandler = require ( "./varDatabaseDbHandler" ) ;
const defaultFieldsRegexp =
/^id$|^uuid$|^date_created$|^date_created_code$|^date_created_timestamp$|^date_updated$|^date_updated_code$|^date_updated_timestamp$/ ;
const generateColumnDescription = require ( "./generateColumnDescription" ) ;
const dbHandler = require ( "./dbHandler" ) ;
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/ * *
* Update table function
* === === === === === === === === === === === === === === === === === === === === === === === === === ===
* @ param { object } params - Single object params
* @ param { string } params . dbFullName - Database full name => "datasquirel_user_4394_db_name"
* @ param { string } params . tableName - Table Name ( slug )
* @ param { import ( "../../types" ) . DSQL _TableSchemaType } params . tableSchema - Table Name ( slug )
* @ param { string } [ params . tableNameFull ] - Table Name ( slug )
* @ param { import ( "../../types" ) . DSQL _FieldSchemaType [ ] } params . tableInfoArray - Table Info Array
* @ param { number | string | null } [ params . userId ] - User ID
* @ param { import ( "../../types" ) . DSQL _DatabaseSchemaType [ ] } params . dbSchema - Single post
* @ param { import ( "../../types" ) . DSQL _IndexSchemaType [ ] } [ params . tableIndexes ] - Table Indexes
* @ param { boolean } [ params . clone ] - Is this a newly cloned table ?
* @ param { number } [ params . tableIndex ] - The number index of the table in the dbSchema array
* @ param { boolean } [ params . childDb ] - The number index of the table in the dbSchema array
* @ param { any } [ params . recordedDbEntry ] - The database object as recorded in ` user_databases ` table
* /
module . exports = async function updateTable ( {
dbFullName ,
tableName ,
tableInfoArray ,
userId ,
dbSchema ,
tableIndexes ,
tableSchema ,
clone ,
childDb ,
tableIndex ,
tableNameFull ,
recordedDbEntry ,
} ) {
/ * *
* Initialize
* === === === === === === === === === === === === === ===
* @ description Initial setup
* /
2024-12-08 08:57:48 +00:00
/** @type {any[]} */
let errorLogs = [ ] ;
2024-12-06 13:24:26 +00:00
/ * *
* @ description Initialize table info array . This value will be
* changing depending on if a field is renamed or not .
* /
let upToDateTableFieldsArray = tableInfoArray ;
/ * *
* Handle Table updates
*
* @ description Try to undate table , catch error if anything goes wrong
* /
try {
/ * *
* @ type { string [ ] }
* @ description Table update query string array
* /
const updateTableQueryArray = [ ] ;
/ * *
* @ type { string [ ] }
* @ description Constriants query string array
* /
const constraintsQueryArray = [ ] ;
/ * *
* @ description Push the query initial value
* /
updateTableQueryArray . push ( ` ALTER TABLE \` ${ tableName } \` ` ) ;
////////////////////////////////////////
////////////////////////////////////////
////////////////////////////////////////
if ( childDb ) {
try {
if ( ! recordedDbEntry ) {
throw new Error ( "Recorded Db entry not found!" ) ;
}
const existingTable = await varDatabaseDbHandler ( {
database : "datasquirel" ,
queryString : ` SELECT * FROM user_database_tables WHERE db_id = ? AND table_slug = ? ` ,
queryValuesArray : [ recordedDbEntry . id , tableName ] ,
} ) ;
/** @type {import("../../types").MYSQL_user_database_tables_table_def} */
const table = existingTable ? . [ 0 ] ;
if ( ! table ? . id ) {
const newTableEntry = await dbHandler ( {
query : ` INSERT INTO user_database_tables SET ? ` ,
values : {
user _id : recordedDbEntry . user _id ,
db _id : recordedDbEntry . id ,
db _slug : recordedDbEntry . db _slug ,
table _name : tableNameFull ,
table _slug : tableName ,
child _table : tableSchema ? . childTable ? "1" : null ,
child _table _parent _database :
tableSchema ? . childTableDbFullName || null ,
child _table _parent _table :
tableSchema . childTableName || null ,
date _created : Date ( ) ,
date _created _code : Date . now ( ) ,
date _updated : Date ( ) ,
date _updated _code : Date . now ( ) ,
} ,
database : "datasquirel" ,
} ) ;
}
} catch ( error ) { }
}
////////////////////////////////////////
////////////////////////////////////////
////////////////////////////////////////
/ * *
* @ type { import ( "../../types" ) . DSQL _MYSQL _SHOW _INDEXES _Type [ ] }
* @ description All indexes from MYSQL db
* / / / @ ts - ignore
const allExistingIndexes = await varDatabaseDbHandler ( {
queryString : ` SHOW INDEXES FROM \` ${ tableName } \` ` ,
database : dbFullName ,
} ) ;
/ * *
* @ type { import ( "../../types" ) . DSQL _MYSQL _SHOW _COLUMNS _Type [ ] }
* @ description All columns from MYSQL db
* / / / @ ts - ignore
const allExistingColumns = await varDatabaseDbHandler ( {
queryString : ` SHOW COLUMNS FROM \` ${ tableName } \` ` ,
database : dbFullName ,
} ) ;
////////////////////////////////////////
/ * *
* @ type { string [ ] }
* @ description Updated column names Array
* /
const updatedColumnsArray = [ ] ;
////////////////////////////////////////
////////////////////////////////////////
////////////////////////////////////////
/ * *
* @ description Iterate through every existing column
* /
for ( let e = 0 ; e < allExistingColumns . length ; e ++ ) {
const { Field } = allExistingColumns [ e ] ;
if ( Field . match ( defaultFieldsRegexp ) ) continue ;
/ * *
* @ description This finds out whether the fieldName corresponds with the MSQL Field name
* if the fildName doesn ' t match any MYSQL Field name , the field is deleted .
* /
let existingEntry = upToDateTableFieldsArray . filter (
( column ) =>
column . fieldName === Field || column . originName === Field
) ;
if ( existingEntry && existingEntry [ 0 ] ) {
/ * *
* @ description Check if Field name has been updated
* /
if (
existingEntry [ 0 ] . updatedField &&
existingEntry [ 0 ] . fieldName
) {
updatedColumnsArray . push ( existingEntry [ 0 ] . fieldName ) ;
const renameColumn = await varDatabaseDbHandler ( {
queryString : ` ALTER TABLE ${ tableName } RENAME COLUMN \` ${ existingEntry [ 0 ] . originName } \` TO \` ${ existingEntry [ 0 ] . fieldName } \` ` ,
database : dbFullName ,
} ) ;
console . log (
` Column Renamed from " ${ existingEntry [ 0 ] . originName } " to " ${ existingEntry [ 0 ] . fieldName } " `
) ;
/ * *
* Update Db Schema
* === === === === === === === === === === === === === === === === ===
* @ description Update Db Schema after renaming column
* /
try {
const userSchemaData = dbSchema ;
const targetDbIndex = userSchemaData . findIndex (
( db ) => db . dbFullName === dbFullName
) ;
const targetTableIndex = userSchemaData [
targetDbIndex
] . tables . findIndex (
( table ) => table . tableName === tableName
) ;
const targetFieldIndex = userSchemaData [
targetDbIndex
] . tables [ targetTableIndex ] . fields . findIndex (
( field ) =>
field . fieldName === existingEntry [ 0 ] . fieldName
) ;
delete userSchemaData [ targetDbIndex ] . tables [
targetTableIndex
] . fields [ targetFieldIndex ] [ "originName" ] ;
delete userSchemaData [ targetDbIndex ] . tables [
targetTableIndex
] . fields [ targetFieldIndex ] [ "updatedField" ] ;
/ * *
* @ description Set New Table Fields Array
* /
upToDateTableFieldsArray =
userSchemaData [ targetDbIndex ] . tables [
targetTableIndex
] . fields ;
fs . writeFileSync (
` ${ String (
process . env . DSQL _USER _DB _SCHEMA _PATH
) } / user - $ { userId } / main . json ` ,
JSON . stringify ( userSchemaData ) ,
"utf8"
) ;
} catch ( /** @type {any} */ error ) {
console . log ( "Update table error =>" , error . message ) ;
}
////////////////////////////////////////
}
////////////////////////////////////////
continue ;
////////////////////////////////////////
} else {
await varDatabaseDbHandler ( {
queryString : ` ALTER TABLE ${ tableName } DROP COLUMN \` ${ Field } \` ` ,
database : dbFullName ,
} ) ;
}
}
////////////////////////////////////////
////////////////////////////////////////
////////////////////////////////////////
/ * *
* Handle MYSQL Table Indexes
* === === === === === === === === === === === === === === === === ===
* @ description Iterate through each table index ( if available )
* and perform operations
* /
for ( let f = 0 ; f < allExistingIndexes . length ; f ++ ) {
const { Key _name , Index _comment } = allExistingIndexes [ f ] ;
/ * *
* @ description Check if this index was specifically created
* by datasquirel
* /
if ( Index _comment ? . match ( /schema_index/ ) ) {
try {
const existingKeyInSchema = tableIndexes ? . filter (
( indexObject ) => indexObject . alias === Key _name
) ;
if ( ! existingKeyInSchema ? . [ 0 ] )
throw new Error (
` This Index( ${ Key _name } ) Has been Deleted! `
) ;
} catch ( error ) {
/ * *
* @ description Drop Index : This happens when the MYSQL index is not
* present in the datasquirel DB schema
* /
await varDatabaseDbHandler ( {
queryString : ` ALTER TABLE ${ tableName } DROP INDEX \` ${ Key _name } \` ` ,
database : dbFullName ,
} ) ;
}
}
}
/ * *
* Handle DATASQUIREL Table Indexes
* === === === === === === === === === === === === === === === === ===
* @ description Iterate through each datasquirel schema
* table index ( if available ) , and perform operations
* /
if ( tableIndexes && tableIndexes [ 0 ] ) {
for ( let g = 0 ; g < tableIndexes . length ; g ++ ) {
const { indexType , indexName , indexTableFields , alias } =
tableIndexes [ g ] ;
if ( ! alias ? . match ( /./ ) ) continue ;
/ * *
* @ description Check for existing Index in MYSQL db
* /
try {
const existingKeyInDb = allExistingIndexes . filter (
( indexObject ) => indexObject . Key _name === alias
) ;
if ( ! existingKeyInDb [ 0 ] )
throw new Error ( "This Index Does not Exist" ) ;
} catch ( error ) {
/ * *
* @ description Create new index if determined that it
* doesn ' t exist in MYSQL db
* /
await varDatabaseDbHandler ( {
queryString : ` CREATE ${
indexType ? . match ( /fullText/i ) ? " FULLTEXT" : ""
} INDEX \ ` ${ alias } \` ON ${ tableName } ( ${ indexTableFields
? . map ( ( nm ) => nm . value )
. map ( ( nm ) => ` \` ${ nm } \` ` )
. join ( "," ) } ) COMMENT 'schema_index' ` ,
database : dbFullName ,
} ) ;
}
}
}
////////////////////////////////////////
////////////////////////////////////////
////////////////////////////////////////
/ * *
* Handle MYSQL Foreign Keys
* === === === === === === === === === === === === === === === === ===
* @ description Iterate through each datasquirel schema
* table index ( if available ) , and perform operations
* /
/ * *
* @ description All MSQL Foreign Keys
* @ type { import ( "../../types" ) . DSQL _MYSQL _FOREIGN _KEYS _Type [ ] | null }
* / / / @ ts - ignore
const allForeignKeys = await varDatabaseDbHandler ( {
queryString : ` SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = ' ${ dbFullName } ' AND TABLE_NAME=' ${ tableName } ' AND CONSTRAINT_TYPE='FOREIGN KEY' ` ,
database : dbFullName ,
} ) ;
if ( allForeignKeys ) {
for ( let c = 0 ; c < allForeignKeys . length ; c ++ ) {
const { CONSTRAINT _NAME } = allForeignKeys [ c ] ;
/ * *
* @ description Skip if Key is the PRIMARY Key
* /
if ( CONSTRAINT _NAME . match ( /PRIMARY/ ) ) continue ;
/ * *
* @ description Drop all foreign Keys to avoid MYSQL errors when adding / updating
* Foreign keys
* /
const dropForeignKey = await varDatabaseDbHandler ( {
queryString : ` ALTER TABLE ${ tableName } DROP FOREIGN KEY \` ${ CONSTRAINT _NAME } \` ` ,
database : dbFullName ,
} ) ;
}
}
////////////////////////////////////////
////////////////////////////////////////
////////////////////////////////////////
/ * *
* Handle DATASQUIREL schema fields for current table
* === === === === === === === === === === === === === === === === ===
* @ description Iterate through each field object and
* perform operations
* /
for ( let i = 0 ; i < upToDateTableFieldsArray . length ; i ++ ) {
const column = upToDateTableFieldsArray [ i ] ;
const prevColumn = upToDateTableFieldsArray [ i - 1 ] ;
const nextColumn = upToDateTableFieldsArray [ i + 1 ] ;
const {
fieldName ,
dataType ,
nullValue ,
primaryKey ,
autoIncrement ,
defaultValue ,
defaultValueLiteral ,
foreignKey ,
updatedField ,
} = column ;
////////////////////////////////////////
/ * *
* @ description Skip default fields
* /
if ( fieldName ? . match ( /^id$|^date_/ ) ) continue ;
/ * *
* @ description Skip columns that have been updated recently
* /
// if (updatedColumnsArray.includes(fieldName)) continue;
////////////////////////////////////////
let updateText = "" ;
////////////////////////////////////////
/** @type {any} */
let existingColumnIndex ;
/ * *
* @ description Existing MYSQL field object
* /
let existingColumn =
allExistingColumns && allExistingColumns [ 0 ]
? allExistingColumns . filter ( ( _column , _index ) => {
if ( _column . Field === fieldName ) {
existingColumnIndex = _index ;
return true ;
}
} )
: null ;
/ * *
* @ description Construct SQL text snippet for this field
* /
let { fieldEntryText } = generateColumnDescription ( {
columnData : column ,
} ) ;
/ * *
* @ description Modify Column ( Field ) if it already exists
* in MYSQL database
* /
if ( existingColumn && existingColumn [ 0 ] ? . Field ) {
const { Field , Type , Null , Key , Default , Extra } =
existingColumn [ 0 ] ;
let isColumnReordered = i < existingColumnIndex ;
if (
Field === fieldName &&
! isColumnReordered &&
dataType ? . toUpperCase ( ) === Type . toUpperCase ( )
) {
updateText += ` MODIFY COLUMN ${ fieldEntryText } ` ;
// continue;
} else {
if ( userId ) {
updateText += ` MODIFY COLUMN ${ fieldEntryText } ${
isColumnReordered
? prevColumn ? . fieldName
? " AFTER `" + prevColumn . fieldName + "`"
: nextColumn ? . fieldName
? " BEFORE `" + nextColumn . fieldName + "`"
: ""
: ""
} ` ;
} else {
updateText += ` MODIFY COLUMN ${ fieldEntryText } ` ;
}
}
} else if ( prevColumn && prevColumn . fieldName ) {
/ * *
* @ description Add new Column AFTER previous column , if
* previous column exists
* /
updateText += ` ADD COLUMN ${ fieldEntryText } AFTER \` ${ prevColumn . fieldName } \` ` ;
} else if ( nextColumn && nextColumn . fieldName ) {
/ * *
* @ description Add new Column BEFORE next column , if
* next column exists
* /
updateText += ` ADD COLUMN ${ fieldEntryText } BEFORE \` ${ nextColumn . fieldName } \` ` ;
} else {
/ * *
* @ description Append new column to the end of existing columns
* /
updateText += ` ADD COLUMN ${ fieldEntryText } ` ;
}
////////////////////////////////////////
/ * *
* @ description Pust SQL code snippet to updateTableQueryArray Array
* Add a comma ( , ) to separate from the next snippet
* /
updateTableQueryArray . push ( updateText + "," ) ;
////////////////////////////////////////
////////////////////////////////////////
////////////////////////////////////////
/ * *
* @ description Handle foreing keys if available , and if there is no
* "clone" boolean = true
* /
if ( ! clone && foreignKey ) {
const {
destinationTableName ,
destinationTableColumnName ,
cascadeDelete ,
cascadeUpdate ,
foreignKeyName ,
} = foreignKey ;
2024-12-08 08:57:48 +00:00
const foreinKeyText = ` ADD CONSTRAINT \` ${ foreignKeyName } \` FOREIGN KEY ( \` ${ fieldName } \` ) REFERENCES \` ${ destinationTableName } \` ( \` ${ destinationTableColumnName } \` ) ${
2024-12-06 13:24:26 +00:00
cascadeDelete ? " ON DELETE CASCADE" : ""
} $ { cascadeUpdate ? " ON UPDATE CASCADE" : "" } ` ;
// const foreinKeyText = `ADD CONSTRAINT \`${foreignKeyName}\` FOREIGN KEY (${fieldName}) REFERENCES ${destinationTableName}(${destinationTableColumnName})${cascadeDelete ? " ON DELETE CASCADE" : ""}${cascadeUpdate ? " ON UPDATE CASCADE" : ""}` + ",";
const finalQueryString = ` ALTER TABLE \` ${ tableName } \` ${ foreinKeyText } ` ;
const addForeignKey = await varDatabaseDbHandler ( {
database : dbFullName ,
queryString : finalQueryString ,
} ) ;
2024-12-08 08:57:48 +00:00
if ( ! addForeignKey ? . serverStatus ) {
errorLogs . push ( addForeignKey ) ;
}
2024-12-06 13:24:26 +00:00
}
////////////////////////////////////////
}
/ * *
* @ description Construct final SQL query by combning all SQL snippets in
* updateTableQueryArray Arry , and trimming the final comma ( , )
* /
const updateTableQuery = updateTableQueryArray
. join ( " " )
. replace ( /,$/ , "" ) ;
////////////////////////////////////////
/ * *
* @ description Check if SQL snippets array has more than 1 entries
* This is because 1 entry means "ALTER TABLE table_name" only , without any
* Alter directives like "ADD COLUMN" or "MODIFY COLUMN"
* /
if ( updateTableQueryArray . length > 1 ) {
const updateTable = await varDatabaseDbHandler ( {
queryString : updateTableQuery ,
database : dbFullName ,
} ) ;
return updateTable ;
} else {
/ * *
* @ description If only 1 SQL snippet is left in updateTableQueryArray , this
* means that no updates have been made to the table
* /
return "No Changes Made to Table" ;
}
////////////////////////////////////////
////////////////////////////////////////
////////////////////////////////////////
} catch ( /** @type {any} */ error ) {
console . log ( 'Error in "updateTable" shell function =>' , error . message ) ;
return "Error in Updating Table" ;
}
} ;
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////