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.

416 lines (415 loc) 15.7 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 } from "../config/yamlConfig.js"; import yaml from "js-yaml"; import { z } from "zod"; import { MessageFormatter } from "../shared/messageFormatter.js"; /** * Recursively searches for configuration files starting from the given directory. * Priority: 1) YAML configs in .appwrite directories, 2) appwriteConfig.ts files in subdirectories * @param dir The directory to start the search from. * @returns The directory path where the config was found, suitable for passing to loadConfig(). */ export const findAppwriteConfig = (dir) => { // First try to find YAML config (already searches recursively for .appwrite dirs) const yamlConfig = findYamlConfig(dir); if (yamlConfig) { // Return the directory containing the config file return path.dirname(yamlConfig); } // Fall back to TypeScript config search const tsConfigPath = findAppwriteConfigTS(dir); if (tsConfigPath) { return path.dirname(tsConfigPath); } return null; }; const shouldIgnoreDirectory = (dirName) => { const ignoredDirs = [ 'node_modules', 'dist', 'build', 'coverage', '.next', '.nuxt', '.cache', '.git', '.svn', '.hg', '__pycache__', '.pytest_cache', '.mypy_cache', 'venv', '.venv', 'env', '.env', 'target', 'out', 'bin', 'obj', '.vs', '.vscode', '.idea', 'temp', 'tmp', '.tmp', 'logs', 'log', '.DS_Store', 'Thumbs.db' ]; return ignoredDirs.includes(dirName) || dirName.startsWith('.git') || dirName.startsWith('node_modules') || (dirName.startsWith('.') && dirName !== '.appwrite'); }; const findAppwriteConfigTS = (dir, depth = 0) => { // Limit search depth to prevent infinite recursion if (depth > 10) { return null; } if (shouldIgnoreDirectory(path.basename(dir))) { return null; } try { const entries = fs.readdirSync(dir, { withFileTypes: true }); // First check current directory for appwriteConfig.ts for (const entry of entries) { if (entry.isFile() && entry.name === "appwriteConfig.ts") { return path.join(dir, entry.name); } } // Then search subdirectories for (const entry of entries) { if (entry.isDirectory() && !shouldIgnoreDirectory(entry.name)) { const result = findAppwriteConfigTS(path.join(dir, entry.name), depth + 1); if (result) return result; } } } catch (error) { // Ignore directory access errors } return null; }; /** * Loads the Appwrite configuration and returns both config and the path where it was found. * @param configDir The directory to search for config files. * @returns Object containing the config and the actual path where it was found. */ export const loadConfigWithPath = async (configDir) => { let config = null; let actualConfigPath = null; // 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 = await loadYamlConfig(yamlPath); actualConfigPath = yamlPath; break; } } } else { // Original logic: search for .appwrite directories const yamlConfigPath = findYamlConfig(configDir); if (yamlConfigPath) { config = 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"); } // Determine collections directory based on actual config file location let collectionsDir; const configFileDir = path.dirname(actualConfigPath); // Check if config is in .appwrite directory if (configFileDir.endsWith('.appwrite')) { collectionsDir = path.join(configFileDir, "collections"); } else { // Config is in root or other directory collectionsDir = path.join(configFileDir, "collections"); } // Load collections if they exist if (fs.existsSync(collectionsDir)) { const unregister = register(); // Register tsx for collections try { const collectionFiles = fs.readdirSync(collectionsDir); config.collections = []; for (const file of collectionFiles) { if (file === "index.ts") { continue; } const filePath = path.join(collectionsDir, file); // Handle YAML collections if (file.endsWith('.yaml') || file.endsWith('.yml')) { const collection = loadYamlCollection(filePath); if (collection) { config.collections.push(collection); } continue; } // Handle TypeScript collections if (file.endsWith('.ts')) { const fileUrl = pathToFileURL(filePath).href; const collectionModule = (await import(fileUrl)); const collection = collectionModule.default?.default || collectionModule.default || collectionModule; if (collection) { // Ensure importDefs are properly loaded if (collectionModule.importDefs || collection.importDefs) { collection.importDefs = collectionModule.importDefs || collection.importDefs; } config.collections.push(collection); } } } } finally { unregister(); // Unregister tsx when done } } return { config, actualConfigPath }; }; /** * 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. * @returns The loaded Appwrite configuration including collections. */ export const loadConfig = async (configDir) => { let config = null; let actualConfigPath = null; // First try to find and load YAML config const yamlConfigPath = findYamlConfig(configDir); if (yamlConfigPath) { config = 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"); } // Determine collections directory based on actual config file location let collectionsDir; if (actualConfigPath) { const configFileDir = path.dirname(actualConfigPath); // Check if config is in .appwrite directory if (configFileDir.endsWith('.appwrite')) { collectionsDir = path.join(configFileDir, "collections"); } else { // Config is in root or other directory collectionsDir = path.join(configFileDir, "collections"); } } else { // Fallback to original behavior if no actual config path found collectionsDir = path.join(configDir, "collections"); } // Load collections if they exist if (fs.existsSync(collectionsDir)) { const unregister = register(); // Register tsx for collections try { const collectionFiles = fs.readdirSync(collectionsDir); config.collections = []; for (const file of collectionFiles) { if (file === "index.ts") { continue; } const filePath = path.join(collectionsDir, file); // Handle YAML collections if (file.endsWith('.yaml') || file.endsWith('.yml')) { const collection = loadYamlCollection(filePath); if (collection) { config.collections.push(collection); } continue; } // Handle TypeScript collections if (file.endsWith('.ts')) { const fileUrl = pathToFileURL(filePath).href; const collectionModule = (await import(fileUrl)); const collection = collectionModule.default?.default || collectionModule.default || collectionModule; if (collection) { // Ensure importDefs are properly loaded if (collectionModule.importDefs || collection.importDefs) { collection.importDefs = collectionModule.importDefs || collection.importDefs; } config.collections.push(collection); } } } } finally { unregister(); // Unregister tsx when done } } else { config.collections = config.collections || []; } // Log successful config loading if (actualConfigPath) { MessageFormatter.success(`Loaded config from: ${actualConfigPath}`, { prefix: "Config" }); } return config; }; export const findFunctionsDir = (dir, depth = 0) => { // Limit search depth to prevent infinite recursion if (depth > 5) { return null; } if (shouldIgnoreDirectory(path.basename(dir))) { return null; } try { const files = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of files) { if (!entry.isDirectory() || shouldIgnoreDirectory(entry.name)) { continue; } if (entry.name === "functions") { return path.join(dir, entry.name); } const result = findFunctionsDir(path.join(dir, entry.name), depth + 1); if (result) return result; } } catch (error) { // Ignore directory access errors } return null; }; // YAML Collection Schema const YamlCollectionSchema = z.object({ name: z.string(), id: z.string().optional(), documentSecurity: z.boolean().default(false), enabled: z.boolean().default(true), permissions: z.array(z.object({ permission: z.string(), target: z.string() })).optional().default([]), attributes: z.array(z.object({ key: z.string(), type: z.string(), size: z.number().optional(), required: z.boolean().default(false), array: z.boolean().optional(), default: z.any().optional(), description: z.string().optional(), min: z.number().optional(), max: z.number().optional(), elements: z.array(z.string()).optional(), relatedCollection: z.string().optional(), relationType: z.string().optional(), twoWay: z.boolean().optional(), twoWayKey: z.string().optional(), onDelete: z.string().optional(), side: z.string().optional() })).optional().default([]), indexes: z.array(z.object({ key: z.string(), type: z.string(), attributes: z.array(z.string()), orders: z.array(z.string()).optional() })).optional().default([]), importDefs: z.array(z.any()).optional().default([]) }); const loadYamlCollection = (filePath) => { try { const fileContent = fs.readFileSync(filePath, "utf8"); const yamlData = yaml.load(fileContent); const parsedCollection = YamlCollectionSchema.parse(yamlData); // Convert YAML collection to CollectionCreate format const collection = { name: parsedCollection.name, $id: parsedCollection.id || parsedCollection.name.toLowerCase().replace(/\s+/g, '_'), documentSecurity: parsedCollection.documentSecurity, enabled: parsedCollection.enabled, $permissions: parsedCollection.permissions.map(p => ({ permission: p.permission, target: p.target })), attributes: parsedCollection.attributes.map(attr => ({ key: attr.key, type: attr.type, size: attr.size, required: attr.required, array: attr.array, xdefault: attr.default, description: attr.description, min: attr.min, max: attr.max, elements: attr.elements, relatedCollection: attr.relatedCollection, relationType: attr.relationType, twoWay: attr.twoWay, twoWayKey: attr.twoWayKey, onDelete: attr.onDelete, side: attr.side })), indexes: parsedCollection.indexes.map(idx => ({ key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders })), importDefs: parsedCollection.importDefs }; return collection; } catch (error) { console.error(`Error loading YAML collection from ${filePath}:`, error); return null; } };