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.

359 lines (358 loc) 17.4 kB
import path from "path"; import fs from "fs"; import {} from "appwrite-utils"; import { register } from "tsx/esm/api"; // Import the register function import { pathToFileURL } from "node:url"; import chalk from "chalk"; import { findYamlConfig, loadYamlConfig, loadYamlConfigWithSession, extractSessionOptionsFromConfig } from "../config/yamlConfig.js"; import { detectAppwriteVersionCached, fetchServerVersion, isVersionAtLeast } from "./versionDetection.js"; import { MessageFormatter } from "../shared/messageFormatter.js"; import { validateCollectionsTablesConfig, reportValidationResults } from "../config/configValidation.js"; import { resolveCollectionsDir, resolveTablesDir } from "./pathResolvers.js"; import { findAppwriteConfig, findAppwriteConfigTS, findFunctionsDir, discoverCollections, discoverTables, discoverLegacyDirectory } from "./configDiscovery.js"; /** * Helper function to create session preservation options from session data * @param sessionCookie The session cookie string * @param email Optional email associated with the session * @param expiresAt Optional expiration timestamp * @returns SessionPreservationOptions object */ export function createSessionPreservation(sessionCookie, email, expiresAt) { return { sessionCookie, authMethod: "session", sessionMetadata: { ...(email && { email }), ...(expiresAt && { expiresAt }) } }; } // Re-export config discovery functions for backward compatibility export { findAppwriteConfig, findFunctionsDir } from "./configDiscovery.js"; /** * Loads the Appwrite configuration and returns both config and the path where it was found. * @param configDir The directory to search for config files. * @param options Loading options including validation settings and session preservation. * @returns Object containing the config, path, and validation results. */ export const loadConfigWithPath = async (configDir, options = {}) => { const { validate = true, strictMode = false, reportValidation = true } = options; let config = null; let actualConfigPath = null; // Convert session preservation options to YAML format const yamlSessionOptions = options.preserveAuth ? { sessionCookie: options.preserveAuth.sessionCookie, authMethod: options.preserveAuth.authMethod, sessionMetadata: options.preserveAuth.sessionMetadata, } : undefined; // Check if we're given the .appwrite directory directly if (configDir.endsWith('.appwrite')) { // Look for config files directly in this directory const possibleYamlFiles = ['config.yaml', 'config.yml', 'appwriteConfig.yaml', 'appwriteConfig.yml']; for (const fileName of possibleYamlFiles) { const yamlPath = path.join(configDir, fileName); if (fs.existsSync(yamlPath)) { config = yamlSessionOptions ? await loadYamlConfigWithSession(yamlPath, yamlSessionOptions) : await loadYamlConfig(yamlPath); actualConfigPath = yamlPath; break; } } } else { // Original logic: search for .appwrite directories const yamlConfigPath = findYamlConfig(configDir); if (yamlConfigPath) { config = yamlSessionOptions ? await loadYamlConfigWithSession(yamlConfigPath, yamlSessionOptions) : await loadYamlConfig(yamlConfigPath); actualConfigPath = yamlConfigPath; } } // Fall back to TypeScript config if YAML not found or failed to load if (!config) { const configPath = path.join(configDir, "appwriteConfig.ts"); // Only try to load TypeScript config if the file exists if (fs.existsSync(configPath)) { const unregister = register(); // Register tsx enhancement try { const configUrl = pathToFileURL(configPath).href; const configModule = (await import(configUrl)); config = configModule.default?.default || configModule.default || configModule; if (!config) { throw new Error("Failed to load config"); } actualConfigPath = configPath; } finally { unregister(); // Unregister tsx when done } } } if (!config || !actualConfigPath) { throw new Error("No valid configuration found"); } // Preserve session authentication if provided // This allows maintaining session context when config is reloaded during CLI operations if (options.preserveAuth) { const { sessionCookie, authMethod, sessionMetadata } = options.preserveAuth; // Inject session cookie into the loaded config if (sessionCookie) { config.sessionCookie = sessionCookie; } // Set or override authentication method preference if (authMethod) { config.authMethod = authMethod; } // Merge session metadata (email, expiration, etc.) with existing metadata if (sessionMetadata) { config.sessionMetadata = { ...config.sessionMetadata, ...sessionMetadata }; } // Auto-detect authentication method if not explicitly provided // If we have a session cookie but no auth method specified, prefer session auth if (!authMethod && sessionCookie) { config.authMethod = "session"; } } // Enhanced dual folder support: Load from BOTH collections/ AND tables/ directories const configFileDir = path.dirname(actualConfigPath); // Look for collections/tables directories in the same directory as the config file const collectionsDir = resolveCollectionsDir(configFileDir); const tablesDir = resolveTablesDir(configFileDir); // Initialize collections array config.collections = []; // Load from collections/ directory first (higher priority) const collectionsResult = await discoverCollections(collectionsDir); config.collections.push(...collectionsResult.collections); // Load from tables/ directory second (lower priority, check for conflicts) const tablesResult = await discoverTables(tablesDir, collectionsResult.loadedNames); config.collections.push(...tablesResult.tables); // Combine conflicts from both discovery operations const allConflicts = [...collectionsResult.conflicts, ...tablesResult.conflicts]; // Report conflicts if any if (allConflicts.length > 0) { MessageFormatter.warning(`Found ${allConflicts.length} naming conflicts between collections/ and tables/`, { prefix: "Config" }); allConflicts.forEach(conflict => { MessageFormatter.info(` - '${conflict.name}': ${conflict.source1} (used) vs ${conflict.source2} (skipped)`, { prefix: "Config" }); }); } // Fallback: If neither directory exists, try legacy single-directory detection if (!fs.existsSync(collectionsDir) && !fs.existsSync(tablesDir)) { // Determine directory (collections or tables) based on server version / API mode let dirName = "collections"; try { const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey); if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) { dirName = 'tables'; } else { // Try health version if not provided const ver = await fetchServerVersion(config.appwriteEndpoint); if (isVersionAtLeast(ver || undefined, '1.8.0')) dirName = 'tables'; } } catch { } const legacyItems = await discoverLegacyDirectory(configFileDir, dirName); config.collections.push(...legacyItems); } // Ensure array exists even if empty config.collections = config.collections || []; // Log the final result const allCollections = config.collections || []; const fromCollectionsDir = allCollections.filter((c) => !c._isFromTablesDir).length; const fromTablesDir = allCollections.filter((c) => c._isFromTablesDir).length; const totalLoaded = allCollections.length; if (totalLoaded > 0) { if (fromTablesDir > 0) { MessageFormatter.success(`Successfully loaded ${totalLoaded} items total: ${fromCollectionsDir} from collections/ and ${fromTablesDir} from tables/`, { prefix: "Config" }); } else { MessageFormatter.success(`Successfully loaded ${totalLoaded} collections from collections/`, { prefix: "Config" }); } } // Validate configuration if requested let validation; if (validate) { validation = validateCollectionsTablesConfig(config); // In strict mode, treat warnings as errors if (strictMode && validation.warnings.length > 0) { const strictValidation = { ...validation, isValid: false, errors: [...validation.errors, ...validation.warnings.map(w => ({ ...w, severity: "error" }))], warnings: [] }; validation = strictValidation; } // Report validation results if requested if (reportValidation) { reportValidationResults(validation, { verbose: true }); } // Throw error if validation fails in strict mode if (strictMode && !validation.isValid) { throw new Error(`Configuration validation failed in strict mode. Found ${validation.errors.length} validation errors.`); } } return { config, actualConfigPath, validation }; }; /** * Loads the Appwrite configuration and all collection configurations from a specified directory. * Supports both YAML and TypeScript config formats with backward compatibility. * @param configDir The directory containing the config file and collections folder. * @param options Loading options including validation settings and session preservation. * @returns The loaded Appwrite configuration including collections. */ export const loadConfig = async (configDir, options = {}) => { const { validate = false, strictMode = false, reportValidation = false } = options; let config = null; let actualConfigPath = null; // Convert session preservation options to YAML format const yamlSessionOptions = options.preserveAuth ? { sessionCookie: options.preserveAuth.sessionCookie, authMethod: options.preserveAuth.authMethod, sessionMetadata: options.preserveAuth.sessionMetadata, } : undefined; // First try to find and load YAML config const yamlConfigPath = findYamlConfig(configDir); if (yamlConfigPath) { config = yamlSessionOptions ? await loadYamlConfigWithSession(yamlConfigPath, yamlSessionOptions) : await loadYamlConfig(yamlConfigPath); actualConfigPath = yamlConfigPath; } // Fall back to TypeScript config if YAML not found or failed to load if (!config) { const configPath = path.join(configDir, "appwriteConfig.ts"); // Only try to load TypeScript config if the file exists if (fs.existsSync(configPath)) { const unregister = register(); // Register tsx enhancement try { const configUrl = pathToFileURL(configPath).href; const configModule = (await import(configUrl)); config = configModule.default?.default || configModule.default || configModule; if (!config) { throw new Error("Failed to load config"); } actualConfigPath = configPath; } finally { unregister(); // Unregister tsx when done } } } if (!config) { throw new Error("No valid configuration found"); } // Preserve session authentication if provided // This allows maintaining session context when config is reloaded during CLI operations if (options.preserveAuth) { const { sessionCookie, authMethod, sessionMetadata } = options.preserveAuth; // Inject session cookie into the loaded config if (sessionCookie) { config.sessionCookie = sessionCookie; } // Set or override authentication method preference if (authMethod) { config.authMethod = authMethod; } // Merge session metadata (email, expiration, etc.) with existing metadata if (sessionMetadata) { config.sessionMetadata = { ...config.sessionMetadata, ...sessionMetadata }; } // Auto-detect authentication method if not explicitly provided // If we have a session cookie but no auth method specified, prefer session auth if (!authMethod && sessionCookie) { config.authMethod = "session"; } } // Enhanced dual folder support: Load from BOTH collections/ AND tables/ directories const configFileDir = actualConfigPath ? path.dirname(actualConfigPath) : configDir; // Look for collections/tables directories in the same directory as the config file const collectionsDir = resolveCollectionsDir(configFileDir); const tablesDir = resolveTablesDir(configFileDir); // Initialize collections array config.collections = []; // Load from collections/ directory first (higher priority) const collectionsResult = await discoverCollections(collectionsDir); config.collections.push(...collectionsResult.collections); // Load from tables/ directory second (lower priority, check for conflicts) const tablesResult = await discoverTables(tablesDir, collectionsResult.loadedNames); config.collections.push(...tablesResult.tables); // Combine conflicts from both discovery operations const allConflicts = [...collectionsResult.conflicts, ...tablesResult.conflicts]; // Report conflicts if any if (allConflicts.length > 0) { MessageFormatter.warning(`Found ${allConflicts.length} naming conflicts between collections/ and tables/`, { prefix: "Config" }); allConflicts.forEach(conflict => { MessageFormatter.info(` - '${conflict.name}': ${conflict.source1} (used) vs ${conflict.source2} (skipped)`, { prefix: "Config" }); }); } // Fallback: If neither directory exists, try legacy single-directory detection if (!fs.existsSync(collectionsDir) && !fs.existsSync(tablesDir)) { // Determine directory (collections or tables) based on server version / API mode let dirName = "collections"; try { const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey); if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) { dirName = 'tables'; } else { const ver = await fetchServerVersion(config.appwriteEndpoint); if (isVersionAtLeast(ver || undefined, '1.8.0')) dirName = 'tables'; } } catch { } const legacyItems = await discoverLegacyDirectory(configFileDir, dirName); config.collections.push(...legacyItems); } // Ensure array exists even if empty config.collections = config.collections || []; // Log the final result const allCollections = config.collections || []; const fromCollectionsDir = allCollections.filter((c) => !c._isFromTablesDir).length; const fromTablesDir = allCollections.filter((c) => c._isFromTablesDir).length; const totalLoaded = allCollections.length; if (totalLoaded > 0) { if (fromTablesDir > 0) { MessageFormatter.success(`Successfully loaded ${totalLoaded} items total: ${fromCollectionsDir} from collections/ and ${fromTablesDir} from tables/`, { prefix: "Config" }); } else { MessageFormatter.success(`Successfully loaded ${totalLoaded} collections from collections/`, { prefix: "Config" }); } } // Log successful config loading if (actualConfigPath) { MessageFormatter.success(`Loaded config from: ${actualConfigPath}`, { prefix: "Config" }); } // Validate configuration if requested if (validate) { let validation = validateCollectionsTablesConfig(config); // In strict mode, treat warnings as errors if (strictMode && validation.warnings.length > 0) { validation = { ...validation, isValid: false, errors: [...validation.errors, ...validation.warnings.map(w => ({ ...w, severity: "error" }))], warnings: [] }; } // Report validation results if requested if (reportValidation) { reportValidationResults(validation, { verbose: true }); } // Throw error if validation fails in strict mode if (strictMode && !validation.isValid) { throw new Error(`Configuration validation failed in strict mode. Found ${validation.errors.length} validation errors.`); } } return config; };