UNPKG

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.

1,069 lines (991 loc) 38.4 kB
import { converterFunctions, tryAwaitWithRetry } from "appwrite-utils"; import { Client, Databases, IndexType, Query, Storage, Users, type Models, } from "node-appwrite"; import { InputFile } from "node-appwrite/file"; import { getAppwriteClient } from "../utils/helperFunctions.js"; // Legacy attribute helpers retained only for local-to-local flows if needed import { parseAttribute } from "appwrite-utils"; import chalk from "chalk"; import { fetchAllCollections } from "../collections/methods.js"; import { MessageFormatter } from "../shared/messageFormatter.js"; import { LegacyAdapter } from "../adapters/LegacyAdapter.js"; import { ProgressManager } from "../shared/progressManager.js"; import { getClient, getAdapter } from "../utils/getClientFromConfig.js"; import { diffTableColumns } from "../collections/tableOperations.js"; import { mapToCreateAttributeParams } from "../shared/attributeMapper.js"; import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js"; export interface TransferOptions { fromDb: Models.Database | undefined; targetDb: Models.Database | undefined; isRemote: boolean; collections?: string[]; transferEndpoint?: string; transferProject?: string; transferKey?: string; sourceBucket?: Models.Bucket; targetBucket?: Models.Bucket; transferUsers?: boolean; } export const transferStorageLocalToLocal = async ( storage: Storage, fromBucketId: string, toBucketId: string ) => { MessageFormatter.info( `Transferring files from ${fromBucketId} to ${toBucketId}`, { prefix: "Transfer" } ); let lastFileId: string | undefined; let fromFiles = await tryAwaitWithRetry( async () => await storage.listFiles(fromBucketId, [Query.limit(100)]) ); const allFromFiles = fromFiles.files; let numberOfFiles = 0; const downloadFileWithRetry = async (bucketId: string, fileId: string) => { let attempts = 3; while (attempts > 0) { try { return await storage.getFileDownload(bucketId, fileId); } catch (error) { MessageFormatter.error( `Error downloading file ${fileId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" } ); attempts--; if (attempts === 0) throw error; } } }; if (fromFiles.files.length < 100) { for (const file of allFromFiles) { const fileData = await tryAwaitWithRetry( async () => await downloadFileWithRetry(file.bucketId, file.$id) ); if (!fileData) { MessageFormatter.error( `Error downloading file ${file.$id}`, undefined, { prefix: "Transfer" } ); continue; } const fileToCreate = InputFile.fromBuffer( new Uint8Array(fileData), file.name ); MessageFormatter.progress(`Creating file: ${file.name}`, { prefix: "Transfer", }); try { await tryAwaitWithRetry( async () => await storage.createFile( toBucketId, file.$id, fileToCreate, file.$permissions ) ); } catch (error: any) { // File already exists, so we can skip it continue; } numberOfFiles++; } } else { lastFileId = fromFiles.files[fromFiles.files.length - 1].$id; while (lastFileId) { const files = await tryAwaitWithRetry( async () => await storage.listFiles(fromBucketId, [ Query.limit(100), Query.cursorAfter(lastFileId!), ]) ); allFromFiles.push(...files.files); if (files.files.length < 100) { lastFileId = undefined; } else { lastFileId = files.files[files.files.length - 1].$id; } } for (const file of allFromFiles) { const fileData = await tryAwaitWithRetry( async () => await downloadFileWithRetry(file.bucketId, file.$id) ); if (!fileData) { MessageFormatter.error( `Error downloading file ${file.$id}`, undefined, { prefix: "Transfer" } ); continue; } const fileToCreate = InputFile.fromBuffer( new Uint8Array(fileData), file.name ); try { await tryAwaitWithRetry( async () => await storage.createFile( toBucketId, file.$id, fileToCreate, file.$permissions ) ); } catch (error: any) { // File already exists, so we can skip it MessageFormatter.warning( `File ${file.$id} already exists, skipping...`, { prefix: "Transfer" } ); continue; } numberOfFiles++; } } MessageFormatter.success( `Transferred ${numberOfFiles} files from ${fromBucketId} to ${toBucketId}`, { prefix: "Transfer" } ); }; export const transferStorageLocalToRemote = async ( localStorage: Storage, endpoint: string, projectId: string, apiKey: string, fromBucketId: string, toBucketId: string ) => { MessageFormatter.info( `Transferring files from current storage ${fromBucketId} to ${endpoint} bucket ${toBucketId}`, { prefix: "Transfer" } ); const client = getAppwriteClient(endpoint, projectId, apiKey); const remoteStorage = new Storage(client); let numberOfFiles = 0; let lastFileId: string | undefined; let fromFiles = await tryAwaitWithRetry( async () => await localStorage.listFiles(fromBucketId, [Query.limit(100)]) ); const allFromFiles = fromFiles.files; if (fromFiles.files.length === 100) { lastFileId = fromFiles.files[fromFiles.files.length - 1].$id; while (lastFileId) { const files = await tryAwaitWithRetry( async () => await localStorage.listFiles(fromBucketId, [ Query.limit(100), Query.cursorAfter(lastFileId!), ]) ); allFromFiles.push(...files.files); if (files.files.length < 100) { break; } lastFileId = files.files[files.files.length - 1].$id; } } for (const file of allFromFiles) { const fileData = await tryAwaitWithRetry( async () => await localStorage.getFileDownload(file.bucketId, file.$id) ); const fileToCreate = InputFile.fromBuffer( new Uint8Array(fileData), file.name ); try { await tryAwaitWithRetry( async () => await remoteStorage.createFile( toBucketId, file.$id, fileToCreate, file.$permissions ) ); } catch (error: any) { // File already exists, so we can skip it MessageFormatter.warning(`File ${file.$id} already exists, skipping...`, { prefix: "Transfer", }); continue; } numberOfFiles++; } MessageFormatter.success( `Transferred ${numberOfFiles} files from ${fromBucketId} to ${toBucketId}`, { prefix: "Transfer" } ); }; // Document transfer functions moved to collections/methods.ts with enhanced UX // Remote document transfer functions moved to collections/methods.ts with enhanced UX /** * Transfers all collections and documents from one local database to another local database. * * @param {Databases} localDb - The local database instance. * @param {string} fromDbId - The ID of the source database. * @param {string} targetDbId - The ID of the target database. * @return {Promise<void>} A promise that resolves when the transfer is complete. */ export const transferDatabaseLocalToLocal = async ( localDb: Databases, fromDbId: string, targetDbId: string ) => { MessageFormatter.info( `Starting database transfer from ${fromDbId} to ${targetDbId}`, { prefix: "Transfer" } ); // Get all collections from source database const sourceCollections = await fetchAllCollections(fromDbId, localDb); MessageFormatter.info( `Found ${sourceCollections.length} collections in source database`, { prefix: "Transfer" } ); // Process each collection for (const collection of sourceCollections) { MessageFormatter.processing( `Processing collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" } ); try { // Create or update collection in target let targetCollection: Models.Collection; const existingCollection = await tryAwaitWithRetry(async () => localDb.listCollections(targetDbId, [ Query.equal("$id", collection.$id), ]) ); if (existingCollection.collections.length > 0) { targetCollection = existingCollection.collections[0]; MessageFormatter.info( `Collection ${collection.name} exists in target database`, { prefix: "Transfer" } ); // Update collection if needed if ( targetCollection.name !== collection.name || targetCollection.$permissions !== collection.$permissions || targetCollection.documentSecurity !== collection.documentSecurity || targetCollection.enabled !== collection.enabled ) { targetCollection = await tryAwaitWithRetry(async () => localDb.updateCollection( targetDbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled ) ); MessageFormatter.success( `Collection ${collection.name} updated`, { prefix: "Transfer" } ); } } else { MessageFormatter.progress( `Creating collection ${collection.name} in target database...`, { prefix: "Transfer" } ); targetCollection = await tryAwaitWithRetry(async () => localDb.createCollection( targetDbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled ) ); } // Create attributes via local adapter (wrap the existing client) const localAdapter: DatabaseAdapter = new LegacyAdapter((localDb as any).client); MessageFormatter.info(`Creating attributes for ${collection.name} via adapter...`, { prefix: 'Transfer' }); const uniformAttrs = collection.attributes.map((attr) => parseAttribute(attr as any)); const nonRel = uniformAttrs.filter((a: any) => a.type !== 'relationship'); for (const attr of nonRel) { const params = mapToCreateAttributeParams(attr as any, { databaseId: targetDbId, tableId: targetCollection.$id }); await localAdapter.createAttribute(params); await new Promise((r) => setTimeout(r, 150)); } // Wait for attributes to become available for (const attr of nonRel) { const maxWait = 60000; const start = Date.now(); let lastStatus = ''; while (Date.now() - start < maxWait) { try { const tableRes = await localAdapter.getTable({ databaseId: targetDbId, tableId: targetCollection.$id }); const attrs = (tableRes as any).attributes || (tableRes as any).columns || []; const found = attrs.find((a: any) => a.key === attr.key); if (found) { if (found.status === 'available') break; if (found.status === 'failed' || found.status === 'stuck') { throw new Error(found.error || `Attribute ${attr.key} failed`); } lastStatus = found.status; } await new Promise((r) => setTimeout(r, 2000)); } catch { await new Promise((r) => setTimeout(r, 2000)); } } if (Date.now() - start >= maxWait) { MessageFormatter.warning(`Attribute ${attr.key} did not become available within 60s (last: ${lastStatus})`, { prefix: 'Transfer' }); } } // Relationship attributes const rels = uniformAttrs.filter((a: any) => a.type === 'relationship'); for (const attr of rels) { const params = mapToCreateAttributeParams(attr as any, { databaseId: targetDbId, tableId: targetCollection.$id }); await localAdapter.createAttribute(params); await new Promise((r) => setTimeout(r, 150)); } // Handle indexes via adapter (create or update) for (const idx of collection.indexes) { try { await localAdapter.createIndex({ databaseId: targetDbId, tableId: targetCollection.$id, key: (idx as any).key, type: (idx as any).type, attributes: (idx as any).attributes, orders: (idx as any).orders || [] }); await new Promise((r) => setTimeout(r, 150)); MessageFormatter.success(`Index ${(idx as any).key} created`, { prefix: 'Transfer' }); } catch (e) { // Try update path by deleting and recreating if necessary try { await localAdapter.deleteIndex({ databaseId: targetDbId, tableId: targetCollection.$id, key: (idx as any).key }); await localAdapter.createIndex({ databaseId: targetDbId, tableId: targetCollection.$id, key: (idx as any).key, type: (idx as any).type, attributes: (idx as any).attributes, orders: (idx as any).orders || [] }); await new Promise((r) => setTimeout(r, 150)); MessageFormatter.info(`Index ${(idx as any).key} recreated`, { prefix: 'Transfer' }); } catch (e2) { MessageFormatter.error(`Failed to ensure index ${(idx as any).key}`, e2 instanceof Error ? e2 : new Error(String(e2)), { prefix: 'Transfer' }); } } } // Transfer documents const { transferDocumentsBetweenDbsLocalToLocal } = await import( "../collections/methods.js" ); await transferDocumentsBetweenDbsLocalToLocal( localDb, fromDbId, targetDbId, collection.$id, targetCollection.$id ); } catch (error) { MessageFormatter.error( `Error processing collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" } ); } } }; export const transferDatabaseLocalToRemote = async ( localDb: Databases, endpoint: string, projectId: string, apiKey: string, fromDbId: string, toDbId: string ) => { const client = getAppwriteClient(endpoint, projectId, apiKey); const remoteDb = new Databases(client); // Get all collections from source database const sourceCollections = await fetchAllCollections(fromDbId, localDb); MessageFormatter.info( `Found ${sourceCollections.length} collections in source database`, { prefix: "Transfer" } ); // Process each collection for (const collection of sourceCollections) { MessageFormatter.processing( `Processing collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" } ); try { // Create or update collection in target let targetCollection: Models.Collection; const existingCollection = await tryAwaitWithRetry(async () => remoteDb.listCollections(toDbId, [Query.equal("$id", collection.$id)]) ); if (existingCollection.collections.length > 0) { targetCollection = existingCollection.collections[0]; MessageFormatter.info( `Collection ${collection.name} exists in remote database`, { prefix: "Transfer" } ); // Update collection if needed if ( targetCollection.name !== collection.name || targetCollection.$permissions !== collection.$permissions || targetCollection.documentSecurity !== collection.documentSecurity || targetCollection.enabled !== collection.enabled ) { targetCollection = await tryAwaitWithRetry(async () => remoteDb.updateCollection( toDbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled ) ); MessageFormatter.success( `Collection ${collection.name} updated`, { prefix: "Transfer" } ); } } else { MessageFormatter.progress( `Creating collection ${collection.name} in remote database...`, { prefix: "Transfer" } ); targetCollection = await tryAwaitWithRetry(async () => remoteDb.createCollection( toDbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled ) ); } // Create/Update attributes via adapter (prefer adapter for remote) const { adapter: remoteAdapter } = await getAdapter(endpoint, projectId, apiKey, 'auto'); MessageFormatter.info(`Creating attributes for ${collection.name} via adapter...`, { prefix: 'Transfer' }); const uniformAttrs = collection.attributes.map((attr) => parseAttribute(attr as any)); const nonRel = uniformAttrs.filter((a: any) => a.type !== 'relationship'); if (nonRel.length > 0) { const tableInfo = await (remoteAdapter as DatabaseAdapter).getTable({ databaseId: toDbId, tableId: collection.$id }); const existingCols: any[] = (tableInfo as any).columns || (tableInfo as any).attributes || []; const { toCreate, toUpdate } = diffTableColumns(existingCols, nonRel as any); for (const a of toUpdate) { const p = mapToCreateAttributeParams(a as any, { databaseId: toDbId, tableId: collection.$id }); await (remoteAdapter as DatabaseAdapter).updateAttribute(p as any); await new Promise((r)=>setTimeout(r,150)); } for (const a of toCreate) { const p = mapToCreateAttributeParams(a as any, { databaseId: toDbId, tableId: collection.$id }); await (remoteAdapter as DatabaseAdapter).createAttribute(p); await new Promise((r)=>setTimeout(r,150)); } } // Wait for non-relationship attributes to become available for (const attr of nonRel) { const maxWait = 60000; const start = Date.now(); let lastStatus = ''; while (Date.now() - start < maxWait) { try { const tableRes = await (remoteAdapter as DatabaseAdapter).getTable({ databaseId: toDbId, tableId: collection.$id }); const attrs = (tableRes as any).attributes || (tableRes as any).columns || []; const found = attrs.find((a: any) => a.key === attr.key); if (found) { if (found.status === 'available') break; if (found.status === 'failed' || found.status === 'stuck') { throw new Error(found.error || `Attribute ${attr.key} failed`); } lastStatus = found.status; } await new Promise((r) => setTimeout(r, 2000)); } catch { await new Promise((r) => setTimeout(r, 2000)); } } if (Date.now() - start >= maxWait) { MessageFormatter.warning(`Attribute ${attr.key} did not become available within 60s (last: ${lastStatus})`, { prefix: 'Transfer' }); } } // Relationship attributes const rels = uniformAttrs.filter((a: any) => a.type === 'relationship'); if (rels.length > 0) { const tableInfo2 = await (remoteAdapter as DatabaseAdapter).getTable({ databaseId: toDbId, tableId: collection.$id }); const existingCols2: any[] = (tableInfo2 as any).columns || (tableInfo2 as any).attributes || []; const { toCreate: rCreate, toUpdate: rUpdate } = diffTableColumns(existingCols2, rels as any); for (const a of rUpdate) { const p = mapToCreateAttributeParams(a as any, { databaseId: toDbId, tableId: collection.$id }); await (remoteAdapter as DatabaseAdapter).updateAttribute(p as any); await new Promise((r)=>setTimeout(r,150)); } for (const a of rCreate) { const p = mapToCreateAttributeParams(a as any, { databaseId: toDbId, tableId: collection.$id }); await (remoteAdapter as DatabaseAdapter).createAttribute(p); await new Promise((r)=>setTimeout(r,150)); } } // Handle indexes with enhanced status checking MessageFormatter.info( `Creating indexes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" } ); // Create indexes via adapter for (const idx of (collection.indexes as any[]) || []) { try { await (remoteAdapter as DatabaseAdapter).createIndex({ databaseId: toDbId, tableId: collection.$id, key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders || [] }); await new Promise((r) => setTimeout(r, 150)); } catch (e) { MessageFormatter.error(`Failed to create index ${idx.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Transfer' }); } } // Transfer documents const { transferDocumentsBetweenDbsLocalToRemote } = await import( "../collections/methods.js" ); await transferDocumentsBetweenDbsLocalToRemote( localDb, endpoint, projectId, apiKey, fromDbId, toDbId, collection.$id, targetCollection.$id ); } catch (error) { MessageFormatter.error( `Error processing collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" } ); } } }; export const transferUsersLocalToRemote = async ( localUsers: Users, endpoint: string, projectId: string, apiKey: string ) => { MessageFormatter.info( "Starting user transfer to remote instance...", { prefix: "Transfer" } ); const client = getClient(endpoint, projectId, apiKey); const remoteUsers = new Users(client); let totalTransferred = 0; let lastId: string | undefined; while (true) { const queries = [Query.limit(100)]; if (lastId) { queries.push(Query.cursorAfter(lastId)); } const usersList = await tryAwaitWithRetry(async () => localUsers.list(queries) ); if (usersList.users.length === 0) { break; } for (const user of usersList.users) { try { // Check if user already exists in remote let remoteUser: Models.User<Models.Preferences> | undefined; try { remoteUser = await tryAwaitWithRetry(async () => remoteUsers.get(user.$id) ); // If user exists, update only the differences if (remoteUser) { MessageFormatter.info( `User ${user.$id} exists, checking for updates...`, { prefix: "Transfer" } ); let hasUpdates = false; // Update name if different if (remoteUser.name !== user.name) { await tryAwaitWithRetry(async () => remoteUsers.updateName(user.$id, user.name) ); MessageFormatter.success( `Updated name for user ${user.$id}`, { prefix: "Transfer" } ); hasUpdates = true; } // Update email if different if (remoteUser.email !== user.email) { await tryAwaitWithRetry(async () => remoteUsers.updateEmail(user.$id, user.email) ); MessageFormatter.success( `Updated email for user ${user.$id}`, { prefix: "Transfer" } ); hasUpdates = true; } // Update phone if different const normalizedLocalPhone = user.phone ? converterFunctions.convertPhoneStringToUSInternational(user.phone) : undefined; if (remoteUser.phone !== normalizedLocalPhone) { if (normalizedLocalPhone) { await tryAwaitWithRetry(async () => remoteUsers.updatePhone(user.$id, normalizedLocalPhone) ); } MessageFormatter.success( `Updated phone for user ${user.$id}`, { prefix: "Transfer" } ); hasUpdates = true; } // Update preferences if different if (JSON.stringify(remoteUser.prefs) !== JSON.stringify(user.prefs)) { await tryAwaitWithRetry(async () => remoteUsers.updatePrefs(user.$id, user.prefs) ); MessageFormatter.success( `Updated preferences for user ${user.$id}`, { prefix: "Transfer" } ); hasUpdates = true; } // Update labels if different if (JSON.stringify(remoteUser.labels) !== JSON.stringify(user.labels)) { await tryAwaitWithRetry(async () => remoteUsers.updateLabels(user.$id, user.labels) ); MessageFormatter.success( `Updated labels for user ${user.$id}`, { prefix: "Transfer" } ); hasUpdates = true; } // Update email verification if different if (remoteUser.emailVerification !== user.emailVerification) { await tryAwaitWithRetry(async () => remoteUsers.updateEmailVerification(user.$id, user.emailVerification) ); MessageFormatter.success( `Updated email verification for user ${user.$id}`, { prefix: "Transfer" } ); hasUpdates = true; } // Update phone verification if different if (remoteUser.phoneVerification !== user.phoneVerification) { await tryAwaitWithRetry(async () => remoteUsers.updatePhoneVerification(user.$id, user.phoneVerification) ); MessageFormatter.success( `Updated phone verification for user ${user.$id}`, { prefix: "Transfer" } ); hasUpdates = true; } // Update status if different if (remoteUser.status !== user.status) { await tryAwaitWithRetry(async () => remoteUsers.updateStatus(user.$id, user.status) ); MessageFormatter.success( `Updated status for user ${user.$id}`, { prefix: "Transfer" } ); hasUpdates = true; } if (!hasUpdates) { MessageFormatter.info( `User ${user.$id} is already up to date, skipping...`, { prefix: "Transfer" } ); } else { totalTransferred++; MessageFormatter.success( `Updated user ${user.$id}`, { prefix: "Transfer" } ); } continue; } } catch (error: any) { // User doesn't exist, proceed with creation } const phone = user.phone ? converterFunctions.convertPhoneStringToUSInternational(user.phone) : undefined; // Handle user creation based on hash type if (user.hash && user.password) { // User has a hashed password - recreate with proper hash method const hashType = user.hash.toLowerCase(); const hashedPassword = user.password; // This is already hashed const hashOptions = (user.hashOptions as Record<string, any>) || {}; try { switch (hashType) { case "argon2": await tryAwaitWithRetry(async () => remoteUsers.createArgon2User( user.$id, user.email, hashedPassword, user.name ) ); break; case "bcrypt": await tryAwaitWithRetry(async () => remoteUsers.createBcryptUser( user.$id, user.email, hashedPassword, user.name ) ); break; case "scrypt": // Scrypt requires additional parameters from hashOptions const salt = typeof hashOptions.salt === "string" ? hashOptions.salt : ""; const costCpu = typeof hashOptions.costCpu === "number" ? hashOptions.costCpu : 32768; const costMemory = typeof hashOptions.costMemory === "number" ? hashOptions.costMemory : 14; const costParallel = typeof hashOptions.costParallel === "number" ? hashOptions.costParallel : 1; const length = typeof hashOptions.length === "number" ? hashOptions.length : 64; // Warn if using default values due to missing hash options if ( !hashOptions.salt || typeof hashOptions.costCpu !== "number" ) { MessageFormatter.warning( `User ${user.$id}: Using default Scrypt parameters due to missing hashOptions`, { prefix: "Transfer" } ); } await tryAwaitWithRetry(async () => remoteUsers.createScryptUser( user.$id, user.email, hashedPassword, salt, costCpu, costMemory, costParallel, length, user.name ) ); break; case "scryptmodified": // Scrypt Modified (Firebase) requires salt, separator, and signer key const modSalt = typeof hashOptions.salt === "string" ? hashOptions.salt : ""; const saltSeparator = typeof hashOptions.saltSeparator === "string" ? hashOptions.saltSeparator : ""; const signerKey = typeof hashOptions.signerKey === "string" ? hashOptions.signerKey : ""; // Warn if critical parameters are missing if ( !hashOptions.salt || !hashOptions.saltSeparator || !hashOptions.signerKey ) { MessageFormatter.warning( `User ${user.$id}: Missing critical Scrypt Modified parameters in hashOptions`, { prefix: "Transfer" } ); } await tryAwaitWithRetry(async () => remoteUsers.createScryptModifiedUser( user.$id, user.email, hashedPassword, modSalt, saltSeparator, signerKey, user.name ) ); break; case "md5": await tryAwaitWithRetry(async () => remoteUsers.createMD5User( user.$id, user.email, hashedPassword, user.name ) ); break; case "sha": case "sha1": case "sha256": case "sha512": // SHA variants - determine version from hash type const getPasswordHashVersion = (hash: string) => { switch (hash.toLowerCase()) { case "sha1": return "sha1" as any; case "sha256": return "sha256" as any; case "sha512": return "sha512" as any; default: return "sha256" as any; // Default to SHA256 } }; await tryAwaitWithRetry(async () => remoteUsers.createSHAUser( user.$id, user.email, hashedPassword, getPasswordHashVersion(hashType), user.name ) ); break; case "phpass": await tryAwaitWithRetry(async () => remoteUsers.createPHPassUser( user.$id, user.email, hashedPassword, user.name ) ); break; default: MessageFormatter.warning( `Unknown hash type '${hashType}' for user ${user.$id}, falling back to Argon2`, { prefix: "Transfer" } ); await tryAwaitWithRetry(async () => remoteUsers.createArgon2User( user.$id, user.email, hashedPassword, user.name ) ); break; } MessageFormatter.success( `User ${user.$id} created with preserved ${hashType} password`, { prefix: "Transfer" } ); } catch (error) { MessageFormatter.warning( `Failed to create user ${user.$id} with ${hashType} hash, trying with temporary password`, { prefix: "Transfer" } ); // Fallback to creating user with temporary password await tryAwaitWithRetry(async () => remoteUsers.create( user.$id, user.email, phone, `changeMe${user.email}`, user.name ) ); MessageFormatter.warning( `User ${user.$id} created with temporary password - password reset required`, { prefix: "Transfer" } ); } } else { // No hash or password - create with temporary password const tempPassword = user.password || `changeMe${user.email}`; await tryAwaitWithRetry(async () => remoteUsers.create( user.$id, user.email, phone, tempPassword, user.name ) ); if (!user.password) { MessageFormatter.warning( `User ${user.$id} created with temporary password - password reset required`, { prefix: "Transfer" } ); } } // Update phone, labels, and other attributes for newly created users if (phone) { await tryAwaitWithRetry(async () => remoteUsers.updatePhone(user.$id, phone) ); } if (user.labels && user.labels.length > 0) { await tryAwaitWithRetry(async () => remoteUsers.updateLabels(user.$id, user.labels) ); } // Update user preferences and status for newly created users await tryAwaitWithRetry(async () => remoteUsers.updatePrefs(user.$id, user.prefs) ); if (user.emailVerification) { await tryAwaitWithRetry(async () => remoteUsers.updateEmailVerification(user.$id, true) ); } else { await tryAwaitWithRetry(async () => remoteUsers.updateEmailVerification(user.$id, false) ); } if (user.phoneVerification) { await tryAwaitWithRetry(async () => remoteUsers.updatePhoneVerification(user.$id, true) ); } if (user.status === false) { await tryAwaitWithRetry(async () => remoteUsers.updateStatus(user.$id, false) ); } totalTransferred++; MessageFormatter.success( `Transferred user ${user.$id}`, { prefix: "Transfer" } ); } catch (error) { MessageFormatter.error( `Failed to transfer user ${user.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" } ); } } if (usersList.users.length < 100) { break; } lastId = usersList.users[usersList.users.length - 1].$id; } MessageFormatter.success( `Successfully transferred ${totalTransferred} users`, { prefix: "Transfer" } ); };