Skip to content

Instantly share code, notes, and snippets.

@luisvonmuller
Created April 29, 2022 17:32
Show Gist options
  • Save luisvonmuller/1371fca74a77cb3a10808043eefe6c32 to your computer and use it in GitHub Desktop.
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 :)
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