@megaorm/cli
Version:
This package allows you to communicate with MegaORM via commands directly from the command line interface (CLI).
466 lines (465 loc) • 21.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SeederHandler = exports.SeederHandlerError = void 0;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = require("path");
const seeder_1 = require("@megaorm/seeder");
const builder_1 = require("@megaorm/builder");
const test_1 = require("@megaorm/test");
/**
* Custom error class for handling errors related to the seeding operations.
*/
class SeederHandlerError extends Error {
}
exports.SeederHandlerError = SeederHandlerError;
/**
* Checks if the provided name matches the pattern for a valid seeder file.
*
* @param name - The name to check if it matches a seeder file pattern.
* @returns `true` if the name matches the seeder file pattern (e.g., `01_seed_users_table.js`), otherwise `false`.
*/
function isFile(name) {
// Pattern example: 01_seed_users_table.js
return /^[0-9]+_seed_[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*_table\.(ts|js)$/.test(name);
}
/**
* Checks if the provided name matches the pattern for a valid TypeScript output file (either a map or declaration file).
*
* @param name - The name to check if it matches a TypeScript output file pattern.
* @returns `true` if the name matches the pattern for a `.js.map` or `.d.ts` file, otherwise `false`.
*/
function isMappedFile(name) {
// Pattern example: 01_seed_users_table.js.map or .d.ts
return /^[0-9]+_seed_[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*_table(\.js\.map|\.d\.ts)$/.test(name);
}
/**
* Retrieves the last seeder file number in the specified directory.
*
* This function scans for existing seeder files in the given path and extracts
* the highest seeder file number. If no files are found, it defaults to returning `0`.
*
* @param path The directory path where the seeder files are located.
* @returns Promise that resolves with the last available file number, if no file found resolves with `0`.
* @throws `SeederHandlerError` if the provided path is empty or invalid.
*/
function lastNumber(path) {
return new Promise((resolve, reject) => {
promises_1.default.readdir(path)
.then((result) => {
// resolve with 0
if ((0, test_1.isEmptyArr)(result))
return resolve(0);
// filter map files
const files = result.filter((name) => !isMappedFile(name));
// validate each file
for (const file of files) {
if (isFile(file))
continue;
return reject(new SeederHandlerError(`Invalid seeder file: ${String(file)}`));
}
// Find the highest existing file number
const match = files
.pop()
.match(/^([0-9]+)_seed_[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*_table\.(ts|js)$/);
return resolve(Number(match[1]));
})
.catch((e) => reject(new SeederHandlerError(e.message)));
});
}
/**
* Renames seeder files by updating their numbering sequence after a deletion.
*
* This function is used internally to ensure that the numbering of seeder files
* remains consistent after a user deletes one or more files. Each group of paths
* may include related files, such as:
* - JavaScript files (.js)
* - TypeScript files (.ts)
* - source map files (.js.map)
*
* @param groups An array of arrays, where each inner array contains paths to seeder files that need to be renamed.
* @returns Promise that resolves when all files have been successfully renamed.
* @throws if the renaming operation fails.
*/
function rename(groups) {
return new Promise((resolve, reject) => {
if ((0, test_1.isEmptyArr)(groups))
return resolve();
const promises = [];
groups.forEach((group, index) => {
group.forEach((path) => {
const newNumber = (index + 1).toString().padStart(2, '0');
const newName = `${newNumber}${(0, path_1.basename)(path).replace(/^[0-9]+/, '')}`;
const newPath = (0, path_1.dirname)(path).concat('/', newName);
promises.push(promises_1.default.rename(path, newPath));
});
});
return Promise.all(promises)
.then(() => resolve())
.catch((error) => reject(new SeederHandlerError(error.message)));
});
}
/**
* Groups seeder file paths by their numeric prefix. This function organizes seeder
* files that share the same numeric prefix into arrays
*
* @param paths - An array of file paths representing seeder files.
* @returns An array of arrays, where each inner array represents a group of files that share the same numeric prefix.
*/
function group(paths) {
const storage = {};
paths.forEach((path) => {
// prettier-ignore
const regex = /([0-9]+)_seed_[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)*_table/;
const number = path.match(regex)[1];
if (!storage[number])
storage[number] = [];
storage[number].push(path);
});
// Return paths in the correct order
return Object.keys(storage)
.sort((a, b) => parseInt(a) - parseInt(b))
.map((key) => storage[key]);
}
/**
* Safely deletes multiple seeder files at the specified paths.
*
* This function calls `unlink` for each path provided in the array, attempting to
* remove each corresponding file.
*
* @param paths An array of strings representing the absolute paths of the seeder files to be deleted.
* @returns A promise that resolves when all files are removed.
*/
function unlink(paths) {
return new Promise((resolve, reject) => {
return Promise.all(paths.map((path) => promises_1.default.unlink(path)))
.then(resolve)
.catch(reject);
});
}
/**
* Creates a seeder class name based on the table name.
*
* @param name The snake_case table name.
* @returns A seeder class name.
*/
function toClassName(name) {
return (name
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('') + 'TableSeeder');
}
/**
* Creates a seeder file name based on the provided name, number, and extension.
*
* @param name The snake_case table name.
* @param number The file number.
* @param ext The file extension.
* @returns A seeder file name.
*/
function toFileName(name, number, ext) {
return `${(number + 1)
.toString()
.padStart(2, '0')}_seed_${name}_table.${ext}`;
}
/**
* Returns an `s` if the array length is not 1, for pluralization purposes.
*
* @param paths - An array whose length determines the suffix.
* @returns - Returns an empty string if the array length is 1, otherwise `s`.
*/
function sfx(paths) {
return paths.length === 1 ? '' : 's';
}
/**
* Manages seeder files, including their creation, execution, and cleanup. This class
* provides functionality for adding, seeding, clearing, and removing seeder files, allowing
* for efficient population and cleanup of initial data in database tables.
*
*/
class SeederHandler {
/**
* Instantiates a new SeederHandler with the specified database builder.
*
* @param builder The database builder instance used for constructing seeder queries.
* @throws `SeederHandlerError` if the provided builder is not a valid instance of `MegaBuilder`.
*/
constructor(builder) {
if (!(0, test_1.isChildOf)(builder, builder_1.MegaBuilder)) {
throw new SeederHandlerError(`Invalid builder: ${String(builder)}`);
}
this.builder = builder;
this.assets = (0, path_1.resolve)(__dirname, '../../assets');
}
/**
* Collects the absolute paths of seeder files from the specified directory.
*
* @param path The directory path from which to collect seeder files.
* @param map A boolean indicating whether to include seeder map files (e.g., `.js.map`) in the resulting paths.
* @returns A promise that resolves with an array of absolute paths to valid seeder files.
* @throws `SeederHandlerError` if there is an issue collecting the files or if an invalid seeder file is encountered.
*/
collectPaths(path, map) {
return new Promise((resolve, reject) => {
promises_1.default.readdir(path)
.then((result) => {
// check if the directory is empty
if ((0, test_1.isEmptyArr)(result))
return resolve([]);
// filter map files
const files = map
? result
: result.filter((name) => !isMappedFile(name));
// validate each file
for (const file of files) {
if (!isFile(file)) {
if (map && isMappedFile(file))
continue;
return reject(new SeederHandlerError(`Invalid seeder file: ${String(file)}`));
}
}
return resolve(files.map((file) => (0, path_1.resolve)(path, file)));
})
.catch((e) => reject(new SeederHandlerError(e.message)));
});
}
/**
* Collects exported seeder instances from the specified file paths.
*
* This method attempts to require each file listed in the provided array of
* paths. It expects each file to export either a default instance of `MegaSeeder`
* or a named seeder instance. If any of the files do not conform to these
* expectations, an error will be thrown.
*
* ### Behavior
* - If no paths are provided or if no valid seeders are found, a
* `SeederHandlerError` is thrown with a relevant message.
* - For each path, the method tries to:
* - Import the module using `require`.
* - Check if the imported module is a valid `MegaSeeder` instance.
* - If the module exports a named seeder, it will check for the first
* property and validate it as a `MegaSeeder`.
*
* @param paths An array absolute paths to your seeder files.
* @returns An array of `MegaSeeder` instances collected from the specified file paths.
* @throws `SeederHandlerError` if no seeders are found or if an invalid seeder is encountered in any of the specified paths.
*/
collectSeeders(paths) {
if ((0, test_1.isEmptyArr)(paths)) {
throw new SeederHandlerError(`Ops! No seeder found`);
}
const seeders = [];
for (const path of paths) {
let module;
try {
module = require(path);
}
catch (error) {
throw new SeederHandlerError(error.message);
}
// Check if module exports a valid default seeder
if ((0, test_1.isChildOf)(module, seeder_1.MegaSeeder)) {
seeders.push(module);
continue;
}
// Check if module exports named seeders
if ((0, test_1.isObj)(module) && (0, test_1.hasLength)(module, 1)) {
const key = Object.keys(module)[0];
const namedGenerator = module[key];
if ((0, test_1.isChildOf)(namedGenerator, seeder_1.MegaSeeder)) {
seeders.push(namedGenerator);
continue;
}
}
throw new SeederHandlerError(`Invalid seeder in: ${path}`);
}
return seeders;
}
/**
* Clears data from a specific table or all tables based on available seeders.
* If a table name is provided, only that table is cleared. If not, all tables
* associated with seeders in the specified path are cleared.
*
* @param path The directory path containing seeder files.
* @param table (Optional) The name of the table to clear.
* @returns A promise resolving with a message indicating the number of tables cleared.
*
*/
clear(path, table) {
return new Promise((resolve, reject) => {
if (!(0, test_1.isStr)(path)) {
return reject(new SeederHandlerError(`Invalid path: ${String(path)}`));
}
if ((0, test_1.isDefined)(table) && !(0, test_1.isSnakeCase)(table)) {
return reject(new SeederHandlerError(`Invalid table: ${String(table)}`));
}
this.collectPaths(path, false)
.then((paths) => {
if ((0, test_1.isDefined)(table)) {
const seeder = this.collectSeeders(paths).find((seeder) => seeder.get.table() === table);
if ((0, test_1.isUndefined)(seeder)) {
return reject(new SeederHandlerError(`No seeder found for table: ${String(table)}`));
}
return seeder.set
.builder(this.builder)
.clear()
.then(() => resolve(`Table cleared successfully`))
.catch((error) => reject(new SeederHandlerError(`Failed to clear table: ${error.message}`)));
}
const seeders = this.collectSeeders(paths).reverse();
const exec = (index = 0, count = 0) => {
if (index >= seeders.length) {
return resolve(`${count}/${paths.length} table${sfx(paths)} cleared`);
}
return seeders[index].set
.builder(this.builder)
.clear()
.then(() => exec(index + 1, count + 1))
.catch((error) => {
return reject(new SeederHandlerError(`${count}/${paths.length} table${sfx(paths)} cleared: ${error.message}`));
});
};
exec();
})
.catch(reject);
});
}
/**
* Seeds data into a specific table or all tables based on available seeders.
* If a table name is provided, only that table is seeded. If not, all tables
* associated with seeders in the specified path are seeded.
*
* @param path The directory path containing seeder files.
* @param table (Optional) The name of the table to seed.
* @returns A promise resolving with a message indicating the number of tables seeded.
*
*/
seed(path, table) {
return new Promise((resolve, reject) => {
if (!(0, test_1.isStr)(path)) {
return reject(new SeederHandlerError(`Invalid path: ${String(path)}`));
}
if ((0, test_1.isDefined)(table) && !(0, test_1.isSnakeCase)(table)) {
return reject(new SeederHandlerError(`Invalid table: ${String(table)}`));
}
this.collectPaths(path, false)
.then((paths) => {
if ((0, test_1.isDefined)(table)) {
const seeder = this.collectSeeders(paths).find((seeder) => seeder.get.table() === table);
if ((0, test_1.isUndefined)(seeder)) {
return reject(new SeederHandlerError(`No seeder found for table: ${String(table)}`));
}
return seeder.set
.builder(this.builder)
.seed()
.then(() => resolve(`Table seeded successfully`))
.catch((error) => reject(new SeederHandlerError(`Failed to seed table: ${error.message}`)));
}
const seeders = this.collectSeeders(paths);
const exec = (index = 0, count = 0) => {
if (index >= seeders.length) {
return resolve(`${count}/${paths.length} table${sfx(paths)} seeded`);
}
return seeders[index].set
.builder(this.builder)
.seed()
.then(() => exec(index + 1, count + 1))
.catch((error) => {
return reject(new SeederHandlerError(`${count}/${paths.length} table${sfx(paths)} seeded: ${error.message}`));
});
};
exec();
})
.catch(reject);
});
}
/**
* Creates a new seeder file in the specified path.
*
* This method creates a new file for the specified table name in the given directory path.
* You can choose to create a TypeScript file or a JavaScript file based on the provided boolean flag.
*
* @param name The snake_case table name for which the seeder is created.
* @param path The directory path where the seeder file will be created.
* @param ts A boolean indicating whether to create a TypeScript file (`true`) or a JavaScript file (`false`).
* @returns A promise that resolves with a success message indicating the path of the created seeder file.
* @throws `SeederHandlerError` If the table name is invalid, the path is empty, or if any errors occur during file operations.
* @note The table name must be a snake_case table name, e.g., users, category_product.
*/
add(name, path, ts) {
return new Promise((resolve, reject) => {
if (!(0, test_1.isSnakeCase)(name)) {
return reject(new SeederHandlerError(`Invalid table name: ${String(name)}`));
}
if (!(0, test_1.isFullStr)(path)) {
return reject(new SeederHandlerError(`Invalid path: ${String(path)}`));
}
if (!(0, test_1.isBool)(ts))
ts = false;
const ext = ts ? 'ts' : 'js';
const layout = ts ? 'ts/seeder.txt' : 'js/seeder.txt';
const content = (0, path_1.resolve)(this.assets, layout);
promises_1.default.readFile(content, 'utf-8')
.then((template) => {
lastNumber(path)
.then((number) => {
const className = toClassName(name);
const fileName = toFileName(name, number, ext);
const filePath = (0, path_1.resolve)(path, fileName);
const fileContent = template
.replace(/\[className\]/g, className)
.replace(/\[tableName\]/g, name)
.replace(/\[fileName\]/g, fileName);
// Create seeder file
promises_1.default.writeFile(filePath, fileContent, 'utf-8')
.then(() => resolve(`Seeder added in: ${filePath}`))
.catch((e) => reject(new SeederHandlerError(e.message)));
})
.catch((e) => reject(new SeederHandlerError(e.message)));
})
.catch((e) => reject(new SeederHandlerError(e.message)));
});
}
/**
* Removes the seeder associated with the given table name from the specified folder path.
*
* This method first removes the seeder file for the specified table,
* and finally updates the numbering of each remaining seeder file in the folder.
*
* @param name The snake_case table name whose associated seeder is to be removed.
* @param path The directory path where the seeder files are located.
* @returns A promise that resolves with a success message indicating how many seeder files were removed.
* @throws `SeederHandlerError` If the table name is invalid, the path is empty.
* @note The table name must be a snake_case table name, e.g., users, category_product.
*/
remove(name, path) {
return new Promise((resolve, reject) => {
if (!(0, test_1.isSnakeCase)(name)) {
return reject(new SeederHandlerError(`Invalid table name: ${String(name)}`));
}
if (!(0, test_1.isFullStr)(path)) {
return reject(new SeederHandlerError(`Invalid path: ${String(path)}`));
}
return this.collectPaths(path, true)
.then((paths) => {
const regex = new RegExp(`[0-9]+_seed_${name}_table\.(ts|js|js\.map|d\.ts)$`);
const unlinkPaths = paths.filter((path) => regex.test(path));
const renamePaths = group(paths.filter((path) => !regex.test(path)));
if (unlinkPaths.length === 0) {
return Promise.reject(new SeederHandlerError(`No seeder found for table: ${name}`));
}
// prettier-ignore
const message = `${unlinkPaths.length} seeder file${sfx(unlinkPaths)} removed in:\n${unlinkPaths.join('\n')}`;
return unlink(unlinkPaths)
.then(() => rename(renamePaths))
.then(() => resolve(message))
.catch(reject);
})
.catch(reject);
});
}
}
exports.SeederHandler = SeederHandler;
//# sourceMappingURL=SeederHandler.js.map