Created
April 29, 2022 17:32
-
-
Save luisvonmuller/1371fca74a77cb3a10808043eefe6c32 to your computer and use it in GitHub Desktop.
Meu singleton top zera q tem que chamar o constructor e dps await no exec :)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
CreateTableCommand, | |
CreateTableCommandInput, | |
DeleteTableCommand, | |
DeleteTableInput, | |
DescribeTableCommand, | |
DescribeTableInput, | |
ScanCommand, | |
ScanCommandInput, | |
} from '@aws-sdk/client-dynamodb'; | |
import dynamoClient from './database/dynamodb-client-config'; | |
/* Folder, Path, Files, inter OS compatibility, etc */ | |
import { statSync, existsSync, readdirSync } from 'fs'; | |
import { sep } from 'path'; | |
import { EOL } from 'os'; | |
import { unmarshall } from '@aws-sdk/util-dynamodb'; | |
import { PutCommand, PutCommandInput } from '@aws-sdk/lib-dynamodb'; | |
type FailSafe<T> = never | T; | |
type OldTableData = { | |
dynamosTableName: string; | |
oldData: any[]; | |
}; | |
class InitDynamoTables { | |
/* Path, folder, and others */ | |
databaseFolderPath?: string; | |
tablesDirectories: string[]; | |
totalCreatedTables = 0; | |
/* Array of CreateTableCommandInput (If found) */ | |
createTablesScripts: any[]; | |
/* Back-Up if new table disagrees with the old one */ | |
oldTableDataToRewind: OldTableData[] = []; | |
/* Who we're searching for that sould have the Dynamos CreateTableCommandInput commando beeing exported */ | |
private scriptName = 'create-table.js'; | |
/** | |
* ## Instanciate by mapping the create files and others. | |
* ### Right after, call the object.exec() to behave (with await before it). | |
* #### What this constructor actually does? | |
* ------------------------------------------- | |
* This will first map the folders/files. When no Path (the only argument accepted) is provided, | |
* we will behave with our common project structure | |
* reading from 'database' folder. | |
* @param {string} desideredPath - If you wan't to initialize and search for scripts on other folders then the default, feel free to pass the new databaseFolderPath here! | |
* ### Extra Information | |
* You just need to provide the folder name itself, without any "/" ou "\\", it will append and get from where where running. | |
* Imagine that you (Oh.. you don't say 🤦♂️) right where this file is located. So... if the folder will be "dynamo" just pass dynamo that the Class itself will build the rest. | |
* ------------------------------------------- | |
* TODO: If theres other folders inside the folder that are not create statements, it maybe behave a little weird. By default its ignoring "seed" and "utils". Feel free to implement it as a second arg... | |
* | |
* */ | |
constructor(desideredPath?: string) { | |
this.databaseFolderPath = | |
__dirname + sep + (desideredPath ?? 'database' + sep); | |
try { | |
this.checkDir(); | |
this.checkForCreatingTableFiles(); | |
} catch (e) { | |
console.trace( | |
'\x1b[31m%s\x1b[0m', // Give the Red Color that all We love when seeing errors. | |
`🚩 InitDynamoTables => ${e}`, | |
); | |
} | |
} | |
/** ------------------------------------- | |
* ## Assure that the Database Dir Exists | |
* -------------------------------------- */ | |
checkDir(): FailSafe<void> { | |
if (!existsSync(this.databaseFolderPath)) { | |
throw new Error( | |
`The File wich should contain declarative table statements do not exit, the "Database Folder Path" provided was: ${this.databaseFolderPath}`, | |
); | |
} | |
return; | |
} | |
/** -------------------------------------------------------------------------------------------------------------------- | |
* ## Assure that the we just wan't the right folders, and each folder contains the "creat-table.ts" script to work with | |
* --------------------------------------------------------------------------------------------------------------------- */ | |
checkForCreatingTableFiles(): FailSafe<void> { | |
const tablesDirectoriesReader = readdirSync(this.databaseFolderPath); | |
const tablesDirectories = tablesDirectoriesReader | |
.filter((folder: string) => { | |
if (statSync(this.databaseFolderPath + folder).isDirectory()) { | |
// Remove common folders that arent usefull, like: Seed and Utils (that sometimes are there) - Just to get a Clean Array. | |
if (folder !== 'seed' && folder !== 'utils') { | |
// Check if the folder contains a "create-table" file inside it •‿• - And If not, it will yells at you ¯\_(ツ)_/¯ | |
try { | |
if ( | |
statSync( | |
this.databaseFolderPath + folder + sep + this.scriptName, | |
).isFile() | |
) { | |
return folder; | |
} | |
} catch (e) { | |
/* Comm'on bro, just write the script with the "create-table.ts" */ | |
throw new Error( | |
` 😡 Bad nomenclature inside database create table folder: ${ | |
this.databaseFolderPath + folder + EOL | |
} [👌 PRO-TIP 💪] name the file that contains the Create table as: "create-table.ts" ¯\_(ツ)_/¯`, | |
); | |
} | |
} | |
} | |
}) | |
.map((folder: string) => this.databaseFolderPath + folder); | |
/* We got some tables to work with! (▰˘◡˘▰) Yey! */ | |
if (tablesDirectories.length > 0) { | |
this.tablesDirectories = tablesDirectories; | |
return; | |
} | |
/* We didn't got any folder that should contain a table inside it... (⋟﹏⋞) */ | |
throw new Error( | |
"We didn't found any table inside the path provided to search for table creating statements.", | |
); | |
} | |
/* What really do stuff */ | |
async exec(): Promise<any> { | |
try { | |
await this.importCreateTableScripts(); | |
await this.createDynamoTable(); | |
console.log( | |
'\x1b[32m%s\x1b[0m', | |
`(✿◠‿◠) We are done setting up the Dynamo Tables for this microsservice!${EOL} (˘︶˘).。.:*♡ We created/re-created: ${this.totalCreatedTables} tables (Async sometimes shows zero, dont worry)!`, | |
); | |
} catch (e) { | |
console.trace( | |
'\x1b[31m%s\x1b[0m', | |
`[ERROR] 🚩 InitDynamoTables => ${e}`, | |
); | |
} | |
} | |
/** | |
* # Dynamic Import | |
* ## This function execute a Dynamic import of the Create Table Scripts. | |
*/ | |
async importCreateTableScripts(): Promise<FailSafe<void>> { | |
try { | |
const modulesListing = this.tablesDirectories.map( | |
async (folder: string) => { | |
// This Create an array of promisses, this should be handle downawards... | |
return await import(folder + sep + this.scriptName); | |
}, | |
); | |
// Lets resolve this Array of imports. | |
this.createTablesScripts = await Promise.all(modulesListing).then( | |
(modules) => modules, | |
); | |
} catch (error) { | |
throw new Error( | |
`🚩 There was some problem while importing the Create Table CreateTableCommandInput ¯\_(ツ)_/¯ ${EOL} | |
[👌 PRO-TIP 💪] Check if the the file (create-table.ts) Exports as default the variable that contains the CreateTableCommandInput. ${EOL} ${error}.`, | |
); | |
} | |
} | |
async createDynamoTable(): Promise<any> { | |
try { | |
this.createTablesScripts.forEach(async (table) => { | |
const tableCreateComand = table.default; // Rearrange since its a default export. | |
const shouldCreate: boolean = await this.checkIfTableExists( | |
tableCreateComand.TableName, | |
tableCreateComand, | |
); | |
if (shouldCreate) { | |
try { | |
const newTableCommand = new CreateTableCommand(tableCreateComand); | |
await dynamoClient.send(newTableCommand); | |
const dataToRewind = this.oldTableDataToRewind.filter( | |
(data) => data?.dynamosTableName == tableCreateComand.TableName, | |
); | |
if (dataToRewind.length > 0) await this.rewindData(dataToRewind[0]); | |
this.totalCreatedTables = this.totalCreatedTables + 1; | |
} catch (error) { | |
throw new Error( | |
`🚩 There was some problem while importing the Create Table CreateTableCommandInput ¯\_(ツ)_/¯ ${EOL} [👌 PRO-TIP 💪] Check ur syntax and try to create it first by running the script yourself. ${EOL} Actually, also it can be that we weren't able to restore some data, by mismatching or missing SGI's and others. ${EOL} Extra Information: ${EOL} ${error}`, | |
); | |
} finally { | |
dynamoClient.destroy(); | |
} | |
} | |
return; | |
}); | |
} catch (error) { | |
throw new Error( | |
`Error over iterating the tables that we should create (or not). Heres more info: ${EOL} ${error}`, | |
); | |
} | |
} | |
async rewindData(data: OldTableData): Promise<void> { | |
try { | |
data.oldData.forEach(async (dataRow) => { | |
const insertParams: PutCommandInput = { | |
TableName: data.dynamosTableName, | |
Item: dataRow, | |
}; | |
const command = new PutCommand(insertParams); | |
await dynamoClient.send(command); | |
return; | |
}); | |
} catch (error) { | |
throw new Error( | |
`We weren't able to Rewind the data for the Table: ${data.dynamosTableName}`, | |
); | |
} finally { | |
dynamoClient.destroy(); | |
} | |
} | |
async checkIfTableExists( | |
tableName: string, | |
newTableCommand: CreateTableCommandInput, | |
): Promise<boolean> { | |
const DescribeTableInput: DescribeTableInput = { | |
TableName: tableName, | |
}; | |
const describeTableCommand = new DescribeTableCommand(DescribeTableInput); | |
try { | |
// We don't want everything, but lets get everything by now. | |
const completeMetaAndTableDesc = await dynamoClient.send( | |
describeTableCommand, | |
); | |
// Get what matters for now. | |
const tableData = completeMetaAndTableDesc.Table; | |
const checkTable = await this.checkTableDescription( | |
newTableCommand, | |
tableData, | |
); // Return on how to behave if the table exists. | |
return checkTable; | |
} catch (_err) { | |
return true; // The table don't exist. (True = Lets creat it on parent function) | |
} | |
} | |
/** | |
* ## Check Table Implementation 👌 | |
* ### This function will check if the Keys matches, SGIs, and other definitions to assure the need to recreate. 🚩 | |
* # 🚩 Atention for the information Below! 🚩 | |
* ### If they don't matches the structure, backup data and then recreate and later on repopulate the table with the <bold>old data</bold> and also beeing able to achieve new indexes / others. | |
*/ | |
async checkTableDescription(newTableCommand, tableData): Promise<boolean> { | |
/* Global Secondaries Indexes to check */ | |
const newSGI = newTableCommand.GlobalSecondaryIndexes ?? 0; | |
const oldSGI = tableData.GlobalSecondaryIndexes ?? 0; | |
const newSGILength = newSGI.length; | |
const oldSGILength = oldSGI.length; | |
/** ======================================================================================================= | |
* ! Since objects points to memory addresses... Lets stringfy to compare if its properties are matching. | | |
* ======================================================================================================== */ | |
/* Atribute Definitions to check */ | |
const newAttrDef = JSON.stringify(newTableCommand.AttributeDefinitions); | |
const oldAttrDef = JSON.stringify(tableData.AttributeDefinitions); | |
/* Key Schema Indexes to check */ | |
const newKeySchemaDef = JSON.stringify(newTableCommand.KeySchema); | |
const oldKeySchemaDef = JSON.stringify(tableData.KeySchema); | |
if ( | |
newSGILength == oldSGILength && | |
newAttrDef == oldAttrDef && | |
newKeySchemaDef == oldKeySchemaDef | |
) { | |
return false; // No need to recreate | |
} else { | |
/* Lets back-up the table Data */ | |
const oldData = await this.getOldTableData(newTableCommand.TableName); | |
/* Push to the array to repopulate latter on */ | |
this.oldTableDataToRewind.push({ | |
dynamosTableName: newTableCommand.TableName, | |
oldData: oldData, | |
}); | |
await this.deleteTableParams(newTableCommand.TableName); | |
return true; // Need to Recreate the Table, the data is totally safe bro. U can chill out bro (☞゚ヮ゚)☞ | |
} | |
} | |
/** ------------------------------------------------------------------------------------------------------- | |
* # Back up the Data . | |
* ## This could take some time! We will query all the data with Dynamos Scan ( 눈_눈, yeah, I know) | |
* ### Explation (☞゚ヮ゚)☞ | |
* Since a table could handle multiple PK's, theres no other way, or if it is, show me! ☜(゚ヮ゚☜) | |
* -------------------------------------------------------------------------------------------------------- | |
*/ | |
async getOldTableData(tableName: string): Promise<any[]> { | |
try { | |
const scanParams: ScanCommandInput = { | |
TableName: tableName, | |
}; | |
const scanCommand = new ScanCommand(scanParams); | |
const result = await dynamoClient.send(scanCommand); | |
return (result.Items || []).map((element) => { | |
return unmarshall(element); | |
}); | |
} catch (error) { | |
throw new Error( | |
`We weren't able to retrieve the old data. We'll not execute any further for this table. ${EOL} ${error}`, | |
); | |
} finally { | |
dynamoClient.destroy(); | |
} | |
} | |
async deleteTableParams(tableName: string): Promise<boolean> { | |
try { | |
const deleteTableParams: DeleteTableInput = { | |
TableName: tableName, | |
}; | |
const deleteCommand = new DeleteTableCommand(deleteTableParams); | |
await dynamoClient.send(deleteCommand); | |
return; | |
} catch (error) { | |
throw new Error( | |
`We weren't been able to Delete the Table, heres more info about why: ${EOL} ${error}`, | |
); | |
} finally { | |
dynamoClient.destroy(); | |
} | |
} | |
} | |
export default InitDynamoTables; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment