appwrite-utils-cli
Version:
Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.
645 lines (644 loc) • 34.9 kB
JavaScript
import inquirer from "inquirer";
import chalk from "chalk";
import { join } from "node:path";
import { Query } from "node-appwrite";
import { MessageFormatter } from "../../shared/messageFormatter.js";
import { ConfirmationDialogs } from "../../shared/confirmationDialogs.js";
import { SelectionDialogs } from "../../shared/selectionDialogs.js";
import { logger } from "../../shared/logging.js";
import { fetchAllDatabases } from "../../databases/methods.js";
import { listBuckets } from "../../storage/methods.js";
import { getFunction, downloadLatestFunctionDeployment } from "../../functions/methods.js";
import { wipeTableRows } from "../../collections/wipeOperations.js";
export const databaseCommands = {
async syncDb(cli) {
MessageFormatter.progress("Pushing local configuration to Appwrite...", { prefix: "Database" });
try {
// Initialize controller
await cli.controller.init();
// Get available and configured databases
const availableDatabases = await fetchAllDatabases(cli.controller.database);
const configuredDatabases = cli.controller.config?.databases || [];
// Get local collections for selection
const localCollections = cli.getLocalCollections();
// Push operations always use local configuration as source of truth
// Select databases
const selectedDatabaseIds = await SelectionDialogs.selectDatabases(availableDatabases, configuredDatabases, { showSelectAll: false, allowNewOnly: false, defaultSelected: [] });
if (selectedDatabaseIds.length === 0) {
MessageFormatter.warning("No databases selected. Skipping database sync.", { prefix: "Database" });
return;
}
// Select tables/collections for each database using the existing method
const tableSelectionsMap = new Map();
const availableTablesMap = new Map();
for (const databaseId of selectedDatabaseIds) {
const database = availableDatabases.find(db => db.$id === databaseId);
// Use the existing selectCollectionsAndTables method
const selectedCollections = await cli.selectCollectionsAndTables(database, cli.controller.database, chalk.blue(`Select collections/tables to push to "${database.name}":`), true, // multiSelect
true, // prefer local
true // shouldFilterByDatabase
);
// Map selected collections to table IDs
const selectedTableIds = selectedCollections.map((c) => c.$id || c.id);
// Store selections
tableSelectionsMap.set(databaseId, selectedTableIds);
availableTablesMap.set(databaseId, selectedCollections);
if (selectedCollections.length === 0) {
MessageFormatter.warning(`No collections selected for database "${database.name}". Skipping.`, { prefix: "Database" });
continue;
}
}
// Ask if user wants to select buckets
const { selectBuckets } = await inquirer.prompt([
{
type: "confirm",
name: "selectBuckets",
message: "Do you want to select storage buckets to sync as well?",
default: false,
},
]);
let bucketSelections = [];
if (selectBuckets) {
// Get available and configured buckets
try {
const availableBucketsResponse = await listBuckets(cli.controller.storage);
const availableBuckets = availableBucketsResponse.buckets || [];
const configuredBuckets = cli.controller.config?.buckets || [];
if (availableBuckets.length === 0) {
MessageFormatter.warning("No storage buckets available in remote instance.", { prefix: "Database" });
}
else {
// Select buckets using SelectionDialogs
const selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(selectedDatabaseIds, availableBuckets, configuredBuckets, { showSelectAll: false, groupByDatabase: true, defaultSelected: [] });
if (selectedBucketIds.length > 0) {
// Create BucketSelection objects
bucketSelections = SelectionDialogs.createBucketSelection(selectedBucketIds, availableBuckets, configuredBuckets, availableDatabases);
MessageFormatter.info(`Selected ${bucketSelections.length} storage bucket(s)`, { prefix: "Database" });
}
}
}
catch (error) {
MessageFormatter.warning("Failed to fetch storage buckets. Continuing with databases only.", { prefix: "Database" });
logger.warn("Storage bucket fetch failed during syncDb", { error: error instanceof Error ? error.message : String(error) });
}
}
// Create DatabaseSelection objects
const databaseSelections = SelectionDialogs.createDatabaseSelection(selectedDatabaseIds, availableDatabases, tableSelectionsMap, configuredDatabases, availableTablesMap);
// Show confirmation summary
const selectionSummary = SelectionDialogs.createSyncSelectionSummary(databaseSelections, bucketSelections);
const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'push');
if (!confirmed) {
MessageFormatter.info("Push operation cancelled by user", { prefix: "Database" });
return;
}
// Perform selective push using the controller
MessageFormatter.progress("Starting selective push...", { prefix: "Database" });
await cli.controller.selectivePush(databaseSelections, bucketSelections);
MessageFormatter.success("\n✅ All database configurations pushed successfully!", { prefix: "Database" });
// Then handle functions if requested
const { syncFunctions } = await inquirer.prompt([
{
type: "confirm",
name: "syncFunctions",
message: "Do you want to push local functions to remote?",
default: false,
},
]);
if (syncFunctions && cli.controller.config?.functions?.length) {
const functions = await cli.selectFunctions(chalk.blue("Select local functions to push:"), true, true // prefer local
);
for (const func of functions) {
try {
await cli.controller.deployFunction(func.name);
MessageFormatter.success(`Function ${func.name} deployed successfully`, { prefix: "Functions" });
}
catch (error) {
MessageFormatter.error(`Failed to deploy function ${func.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Functions" });
}
}
}
MessageFormatter.success("Local configuration push completed successfully!", { prefix: "Database" });
}
catch (error) {
MessageFormatter.error("Failed to push local configuration", error instanceof Error ? error : new Error(String(error)), { prefix: "Database" });
throw error;
}
},
async synchronizeConfigurations(cli) {
MessageFormatter.progress("Synchronizing configurations...", { prefix: "Config" });
await cli.controller.init();
// Sync databases, collections, and buckets
const { syncDatabases } = await inquirer.prompt([
{
type: "confirm",
name: "syncDatabases",
message: "Do you want to synchronize databases, collections, and their buckets?",
default: true,
},
]);
if (syncDatabases) {
const remoteDatabases = await fetchAllDatabases(cli.controller.database);
// First, prepare the combined database list for bucket configuration
const localDatabases = cli.controller.config?.databases || [];
const allDatabases = [
...localDatabases,
...remoteDatabases.filter((rd) => !localDatabases.some((ld) => ld.name === rd.name)),
];
// Configure buckets FIRST to get user selections before writing config
MessageFormatter.progress("Configuring storage buckets...", { prefix: "Buckets" });
const configWithBuckets = await cli.configureBuckets({
...cli.controller.config,
databases: allDatabases,
});
// Update controller config with bucket selections
cli.controller.config = configWithBuckets;
// Now synchronize configurations with the updated config that includes bucket selections
MessageFormatter.progress("Pulling collections and generating collection files...", { prefix: "Collections" });
await cli.controller.synchronizeConfigurations(remoteDatabases, configWithBuckets);
}
// Then sync functions
const { syncFunctions } = await inquirer.prompt([
{
type: "confirm",
name: "syncFunctions",
message: "Do you want to synchronize functions?",
default: true,
},
]);
if (syncFunctions) {
const remoteFunctions = await cli.controller.listAllFunctions();
const localFunctions = cli.controller.config?.functions || [];
const allFunctions = [
...remoteFunctions,
...localFunctions.filter((f) => !remoteFunctions.some((rf) => rf.$id === f.$id)),
];
for (const func of allFunctions) {
const hasLocal = localFunctions.some((lf) => lf.$id === func.$id);
const hasRemote = remoteFunctions.some((rf) => rf.$id === func.$id);
if (hasLocal && hasRemote) {
// Function exists in both local and remote
const { preference } = await inquirer.prompt([
{
type: "list",
name: "preference",
message: `Function "${func.name}" exists both locally and remotely. What would you like to do?`,
choices: [
{ name: "Keep local version (deploy to remote)", value: "local" },
{ name: "Use remote version (download)", value: "remote" },
{ name: "Update config only", value: "config" },
{ name: "Skip this function", value: "skip" },
],
},
]);
if (preference === "local") {
await cli.controller.deployFunction(func.name);
}
else if (preference === "remote") {
await downloadLatestFunctionDeployment(cli.controller.appwriteServer, func.$id, join(cli.controller.getAppwriteFolderPath(), "functions"));
}
else if (preference === "config") {
// Update config with remote function details
const remoteFunction = await getFunction(cli.controller.appwriteServer, func.$id);
const newFunction = {
$id: remoteFunction.$id,
name: remoteFunction.name,
runtime: remoteFunction.runtime,
execute: remoteFunction.execute || [],
events: remoteFunction.events || [],
schedule: remoteFunction.schedule || "",
timeout: remoteFunction.timeout || 15,
enabled: remoteFunction.enabled !== false,
logging: remoteFunction.logging !== false,
entrypoint: remoteFunction.entrypoint || "src/index.ts",
commands: remoteFunction.commands || "npm install",
scopes: remoteFunction.scopes || [],
installationId: remoteFunction.installationId,
providerRepositoryId: remoteFunction.providerRepositoryId,
providerBranch: remoteFunction.providerBranch,
providerSilentMode: remoteFunction.providerSilentMode,
providerRootDirectory: remoteFunction.providerRootDirectory,
specification: remoteFunction.specification,
};
const existingIndex = cli.controller.config.functions.findIndex((f) => f.$id === remoteFunction.$id);
if (existingIndex >= 0) {
cli.controller.config.functions[existingIndex] = newFunction;
}
else {
cli.controller.config.functions.push(newFunction);
}
MessageFormatter.success(`Updated config for function: ${func.name}`, { prefix: "Functions" });
}
}
else if (hasLocal) {
// Function exists only locally
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: `Function "${func.name}" exists only locally. What would you like to do?`,
choices: [
{ name: "Deploy to remote", value: "deploy" },
{ name: "Skip this function", value: "skip" },
],
},
]);
if (action === "deploy") {
await cli.controller.deployFunction(func.name);
}
}
else if (hasRemote) {
// Function exists only remotely
const { action } = await inquirer.prompt([
{
type: "list",
name: "action",
message: `Function "${func.name}" exists only remotely. What would you like to do?`,
choices: [
{ name: "Update config only", value: "config" },
{ name: "Download locally", value: "download" },
{ name: "Skip this function", value: "skip" },
],
},
]);
if (action === "download") {
await downloadLatestFunctionDeployment(cli.controller.appwriteServer, func.$id, join(cli.controller.getAppwriteFolderPath(), "functions"));
}
else if (action === "config") {
const remoteFunction = await getFunction(cli.controller.appwriteServer, func.$id);
const newFunction = {
$id: remoteFunction.$id,
name: remoteFunction.name,
runtime: remoteFunction.runtime,
execute: remoteFunction.execute || [],
events: remoteFunction.events || [],
schedule: remoteFunction.schedule || "",
timeout: remoteFunction.timeout || 15,
enabled: remoteFunction.enabled !== false,
logging: remoteFunction.logging !== false,
entrypoint: remoteFunction.entrypoint || "src/index.ts",
commands: remoteFunction.commands || "npm install",
scopes: remoteFunction.scopes || [],
installationId: remoteFunction.installationId,
providerRepositoryId: remoteFunction.providerRepositoryId,
providerBranch: remoteFunction.providerBranch,
providerSilentMode: remoteFunction.providerSilentMode,
providerRootDirectory: remoteFunction.providerRootDirectory,
specification: remoteFunction.specification,
};
cli.controller.config.functions =
cli.controller.config.functions || [];
cli.controller.config.functions.push(newFunction);
MessageFormatter.success(`Added config for remote function: ${func.name}`, { prefix: "Functions" });
}
}
}
}
MessageFormatter.success("✨ Configurations synchronized successfully!", { prefix: "Config" });
},
async backupDatabase(cli) {
if (!cli.controller.database || !cli.controller.storage) {
throw new Error("Database or Storage is not initialized, is the config file correct & created?");
}
try {
// STEP 1: Select tracking database
MessageFormatter.info("Step 1/5: Select tracking database", { prefix: "Backup" });
const trackingDb = await this.selectTrackingDatabase(cli);
// STEP 2: Ensure backup tracking table exists
MessageFormatter.info("Step 2/5: Initializing backup tracking", { prefix: "Backup" });
await this.ensureBackupTrackingTable(cli, trackingDb);
// STEP 3: Select backup scope
MessageFormatter.info("Step 3/5: Select backup scope", { prefix: "Backup" });
const scope = await this.selectBackupScope(cli);
// STEP 4: Show confirmation
MessageFormatter.info("Step 4/5: Confirm backup plan", { prefix: "Backup" });
const confirmed = await this.confirmBackupPlan(scope);
if (!confirmed) {
MessageFormatter.info("Backup cancelled by user", { prefix: "Backup" });
return;
}
// STEP 5: Execute unified backup
MessageFormatter.info("Step 5/5: Executing backup", { prefix: "Backup" });
await this.executeUnifiedBackup(cli, trackingDb, scope);
MessageFormatter.success("Backup operation completed successfully", { prefix: "Backup" });
}
catch (error) {
MessageFormatter.error("Backup operation failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Backup" });
throw error;
}
},
// Helper method: Select tracking database
async selectTrackingDatabase(cli) {
const databases = await fetchAllDatabases(cli.controller.database);
const { trackingDatabaseId } = await inquirer.prompt([
{
type: "list",
name: "trackingDatabaseId",
message: "Select database to store backup metadata:",
choices: databases.map(db => ({
name: `${db.name} (${db.$id})`,
value: db.$id
}))
}
]);
MessageFormatter.info(`Using ${trackingDatabaseId} for backup tracking`, { prefix: "Backup" });
return trackingDatabaseId;
},
// Helper method: Ensure backup tracking table exists
async ensureBackupTrackingTable(cli, trackingDatabaseId) {
const { createCentralizedBackupTrackingTable } = await import("../../backups/tracking/centralizedTracking.js");
const adapter = cli.controller.adapter;
await createCentralizedBackupTrackingTable(adapter, trackingDatabaseId);
MessageFormatter.success("Backup tracking table ready", { prefix: "Backup" });
},
// Helper method: Select backup scope
async selectBackupScope(cli) {
const { scopeType } = await inquirer.prompt([
{
type: "list",
name: "scopeType",
message: "What would you like to backup?",
choices: [
{ name: "Comprehensive (ALL databases + ALL buckets)", value: "comprehensive" },
{ name: "Selective databases (choose specific databases)", value: "selective-databases" },
{ name: "Selective collections (choose specific collections)", value: "selective-collections" }
]
}
]);
if (scopeType === "comprehensive") {
return { type: "comprehensive" };
}
if (scopeType === "selective-databases") {
const databases = await fetchAllDatabases(cli.controller.database);
const selectedDatabases = await cli.selectDatabases(databases, "Select databases to backup:");
const { includeBuckets } = await inquirer.prompt([
{
type: "confirm",
name: "includeBuckets",
message: "Include storage buckets in backup?",
default: false
}
]);
let selectedBuckets = [];
if (includeBuckets) {
const buckets = await listBuckets(cli.controller.storage);
const { bucketIds } = await inquirer.prompt([
{
type: "checkbox",
name: "bucketIds",
message: "Select buckets to backup:",
choices: buckets.buckets.map((b) => ({
name: `${b.name} (${b.$id})`,
value: b.$id
}))
}
]);
selectedBuckets = bucketIds;
}
return {
type: "selective-databases",
databases: selectedDatabases,
buckets: selectedBuckets
};
}
if (scopeType === "selective-collections") {
const databases = await fetchAllDatabases(cli.controller.database);
const selectedDatabase = await cli.selectDatabases(databases, "Select database containing collections:", false // single selection
);
if (!selectedDatabase || selectedDatabase.length === 0) {
throw new Error("No database selected");
}
const db = selectedDatabase[0];
const collections = await cli.selectCollectionsAndTables(db, cli.controller.database, "Select collections to backup:", true, true, true);
return {
type: "selective-collections",
databaseId: db.$id,
databaseName: db.name,
collections: collections
};
}
throw new Error("Invalid backup scope selected");
},
// Helper method: Confirm backup plan
async confirmBackupPlan(scope) {
let summary = "\n" + chalk.bold("Backup Plan Summary:") + "\n";
if (scope.type === "comprehensive") {
summary += " • ALL databases\n";
summary += " • ALL storage buckets\n";
}
else if (scope.type === "selective-databases") {
summary += ` • ${scope.databases.length} selected databases\n`;
if (scope.buckets.length > 0) {
summary += ` • ${scope.buckets.length} selected buckets\n`;
}
}
else if (scope.type === "selective-collections") {
summary += ` • Database: ${scope.databaseName}\n`;
summary += ` • ${scope.collections.length} selected collections\n`;
}
console.log(summary);
const { confirmed } = await inquirer.prompt([
{
type: "confirm",
name: "confirmed",
message: "Proceed with backup?",
default: true
}
]);
return confirmed;
},
// Helper method: Execute unified backup
async executeUnifiedBackup(cli, trackingDatabaseId, scope) {
if (scope.type === "comprehensive") {
const { comprehensiveBackup } = await import("../../backups/operations/comprehensiveBackup.js");
await comprehensiveBackup(cli.controller.config, cli.controller.database, cli.controller.storage, cli.controller.adapter, {
trackingDatabaseId,
backupFormat: 'zip',
parallelDownloads: 10,
onProgress: (message) => {
MessageFormatter.progress(message, { prefix: "Backup" });
}
});
}
else if (scope.type === "selective-databases") {
// Backup each selected database
for (const db of scope.databases) {
MessageFormatter.progress(`Backing up database: ${db.name}`, { prefix: "Backup" });
await cli.controller.backupDatabase(db);
}
// Backup selected buckets if any
for (const bucketId of scope.buckets) {
MessageFormatter.progress(`Backing up bucket: ${bucketId}`, { prefix: "Backup" });
const { backupBucket } = await import("../../backups/operations/bucketBackup.js");
await backupBucket(cli.controller.storage, bucketId, "appwrite-backups", { parallelDownloads: 10 });
}
}
else if (scope.type === "selective-collections") {
const { backupCollections } = await import("../../backups/operations/collectionBackup.js");
await backupCollections(cli.controller.config, cli.controller.database, cli.controller.storage, cli.controller.adapter, {
trackingDatabaseId,
databaseId: scope.databaseId,
collectionIds: scope.collections.map((c) => c.$id || c.id),
backupFormat: 'zip',
onProgress: (message) => {
MessageFormatter.progress(message, { prefix: "Backup" });
}
});
}
},
async wipeDatabase(cli) {
if (!cli.controller.database || !cli.controller.storage) {
throw new Error("Database or Storage is not initialized, is the config file correct & created?");
}
const databases = await fetchAllDatabases(cli.controller.database);
const storage = await listBuckets(cli.controller.storage);
const selectedDatabases = await cli.selectDatabases(databases, "Select databases to wipe:");
const { selectedStorage } = await inquirer.prompt([
{
type: "checkbox",
name: "selectedStorage",
message: "Select storage buckets to wipe:",
choices: storage.buckets.map((s) => ({ name: s.name, value: s.$id })),
},
]);
const { wipeUsers } = await inquirer.prompt([
{
type: "confirm",
name: "wipeUsers",
message: "Do you want to wipe users as well?",
default: false,
},
]);
const databaseNames = selectedDatabases.map((db) => db.name);
const confirmed = await ConfirmationDialogs.confirmDatabaseWipe(databaseNames, {
includeStorage: selectedStorage.length > 0,
includeUsers: wipeUsers
});
if (confirmed) {
MessageFormatter.info("Starting wipe operation...", { prefix: "Wipe" });
for (const db of selectedDatabases) {
await cli.controller.wipeDatabase(db);
}
for (const bucketId of selectedStorage) {
await cli.controller.wipeDocumentStorage(bucketId);
}
if (wipeUsers) {
await cli.controller.wipeUsers();
}
MessageFormatter.success("Wipe operation completed", { prefix: "Wipe" });
}
else {
MessageFormatter.info("Wipe operation cancelled", { prefix: "Wipe" });
}
},
async wipeCollections(cli) {
if (!cli.controller.database) {
throw new Error("Database is not initialized, is the config file correct & created?");
}
const databases = await fetchAllDatabases(cli.controller.database);
const selectedDatabases = await cli.selectDatabases(databases, "Select the database(s) containing the collections to wipe:", true);
for (const database of selectedDatabases) {
const collections = await cli.selectCollectionsAndTables(database, cli.controller.database, `Select collections/tables to wipe from ${database.name}:`, true, undefined, true);
const collectionNames = collections.map((c) => c.name);
const confirmed = await ConfirmationDialogs.confirmCollectionWipe(database.name, collectionNames);
if (confirmed) {
MessageFormatter.info(`Wiping selected collections from ${database.name}...`, { prefix: "Wipe" });
for (const collection of collections) {
await cli.controller.wipeCollection(database, collection);
MessageFormatter.success(`Collection ${collection.name} wiped successfully`, { prefix: "Wipe" });
}
}
else {
MessageFormatter.info(`Wipe operation cancelled for ${database.name}`, { prefix: "Wipe" });
}
}
MessageFormatter.success("Wipe collections operation completed", { prefix: "Wipe" });
},
async wipeTablesData(cli) {
const controller = cli.controller;
if (!controller?.adapter) {
throw new Error("Database adapter is not initialized. TablesDB operations require adapter support.");
}
try {
// Step 1: Select database (single selection for clearer UX)
const databases = await fetchAllDatabases(controller.database);
if (!databases || databases.length === 0) {
MessageFormatter.warning("No databases found", { prefix: "Wipe" });
return;
}
const { selectedDatabase } = await inquirer.prompt([
{
type: "list",
name: "selectedDatabase",
message: "Select database containing tables to wipe:",
choices: databases.map((db) => ({
name: `${db.name} (${db.$id})`,
value: db
}))
}
]);
const database = selectedDatabase;
// Step 2: Get available tables
const adapter = controller.adapter;
const tablesResponse = await adapter.listTables({
databaseId: database.$id,
queries: [Query.limit(500)]
});
const availableTables = tablesResponse.tables || [];
if (availableTables.length === 0) {
MessageFormatter.warning(`No tables found in database: ${database.name}`, { prefix: "Wipe" });
return;
}
// Step 3: Select tables using existing SelectionDialogs
const selectedTableIds = await SelectionDialogs.selectTablesForDatabase(database.$id, database.name, availableTables, [], // No configured tables context needed for wipe
{
showSelectAll: true,
allowNewOnly: false,
defaultSelected: []
});
if (selectedTableIds.length === 0) {
MessageFormatter.warning("No tables selected. Operation cancelled.", { prefix: "Wipe" });
return;
}
// Step 4: Show confirmation with table details
const selectedTables = availableTables.filter((t) => selectedTableIds.includes(t.$id));
const tableNames = selectedTables.map((t) => t.name);
console.log(chalk.yellow.bold("\n⚠️ WARNING: Table Row Wipe Operation"));
console.log(chalk.yellow("This will delete ALL ROWS from the selected tables."));
console.log(chalk.yellow("The table structures will remain intact.\n"));
console.log(chalk.cyan("Database:"), chalk.white(database.name));
console.log(chalk.cyan("Tables to wipe:"));
tableNames.forEach((name) => console.log(chalk.white(` • ${name}`)));
console.log();
const { confirmed } = await inquirer.prompt([
{
type: "confirm",
name: "confirmed",
message: chalk.red.bold("Are you ABSOLUTELY SURE you want to wipe these table rows?"),
default: false
}
]);
if (!confirmed) {
MessageFormatter.info("Wipe operation cancelled by user", { prefix: "Wipe" });
return;
}
// Step 5: Execute wipe using existing wipeTableRows function
MessageFormatter.progress("Starting table row wipe operation...", { prefix: "Wipe" });
for (const table of selectedTables) {
try {
MessageFormatter.info(`Wiping rows from table: ${table.name}`, { prefix: "Wipe" });
// Use existing wipeTableRows from wipeOperations.ts
await wipeTableRows(adapter, database.$id, table.$id);
MessageFormatter.success(`Successfully wiped rows from table: ${table.name}`, { prefix: "Wipe" });
}
catch (error) {
MessageFormatter.error(`Failed to wipe table ${table.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Wipe" });
}
}
MessageFormatter.success(`Wipe operation completed for ${selectedTables.length} table(s)`, { prefix: "Wipe" });
}
catch (error) {
MessageFormatter.error("Table wipe operation failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Wipe" });
throw error;
}
}
};