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.
323 lines (322 loc) • 12.3 kB
JavaScript
import fs from "fs";
import path from "path";
import { findUp } from "find-up";
import { MessageFormatter } from "../../shared/messageFormatter.js";
import { shouldIgnoreDirectory } from "../../utils/directoryUtils.js";
/**
* Service for discovering Appwrite configuration files and collection/table definitions.
*
* Uses find-up for intelligent searching with git repository boundary detection:
* 1. Finds .git directory to establish repo root boundary
* 2. Searches UP from current directory to repo root
* 3. Searches DOWN recursively within repo root
*
* Search Priority:
* 1. YAML configs (.appwrite/config.yaml, .appwrite/config.yml, etc.)
* 2. JSON configs (appwrite.config.json, appwrite.json)
* 3. TypeScript configs (appwriteConfig.ts)
*/
export class ConfigDiscoveryService {
/**
* YAML configuration file names to search for
*/
YAML_FILENAMES = [
".appwrite/config.yaml",
".appwrite/config.yml",
".appwrite/appwriteConfig.yaml",
".appwrite/appwriteConfig.yml",
"appwrite.yaml",
"appwrite.yml",
];
/**
* JSON configuration file names to search for
*/
JSON_FILENAMES = ["appwrite.config.json", "appwrite.json"];
/**
* TypeScript configuration file names to search for
*/
TS_FILENAMES = ["appwriteConfig.ts"];
/**
* Finds the git repository root directory
* @param startDir The directory to start searching from
* @returns Path to the repository root, or startDir if no .git found
*/
async findRepoRoot(startDir) {
const gitDir = await findUp(".git", {
cwd: startDir,
type: "directory",
});
return gitDir ? path.dirname(gitDir) : startDir;
}
/**
* Recursively searches downward for files matching patterns
* @param dir Directory to search in
* @param patterns File patterns to match
* @param maxDepth Maximum depth to search
* @param currentDepth Current recursion depth
* @returns First matching file path or null
*/
async searchDownward(dir, patterns, maxDepth = 5, currentDepth = 0) {
if (currentDepth > maxDepth)
return null;
if (shouldIgnoreDirectory(path.basename(dir)))
return null;
try {
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
// Check current directory for matches
for (const pattern of patterns) {
const fullPath = path.join(dir, pattern);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
// Recurse into subdirectories
for (const entry of entries) {
if (entry.isDirectory() && !shouldIgnoreDirectory(entry.name)) {
const result = await this.searchDownward(path.join(dir, entry.name), patterns, maxDepth, currentDepth + 1);
if (result)
return result;
}
}
}
catch (error) {
// Ignore permission errors
}
return null;
}
/**
* Finds any configuration file with configurable priority
* @param startDir The directory to start searching from
* @param preferJson If true, prioritizes appwrite.config.json over YAML (default: false)
* @returns Path to the configuration file or null if not found
*
* Default priority: YAML → JSON → TypeScript
* With preferJson=true: JSON → YAML → TypeScript
*/
async findConfig(startDir, preferJson = false) {
// Find repo root to establish boundary
const repoRoot = await this.findRepoRoot(startDir);
if (preferJson) {
// Try JSON first when --appwrite-config flag is used
const jsonConfig = await this.findProjectConfig(startDir, repoRoot);
if (jsonConfig)
return jsonConfig;
// Try YAML second
const yamlConfig = await this.findYamlConfig(startDir, repoRoot);
if (yamlConfig)
return yamlConfig;
// Try TypeScript last (lowest priority)
const tsConfig = await this.findTypeScriptConfig(startDir, repoRoot);
if (tsConfig)
return tsConfig;
}
else {
// Default priority: YAML → JSON → TypeScript
const yamlConfig = await this.findYamlConfig(startDir, repoRoot);
if (yamlConfig)
return yamlConfig;
const jsonConfig = await this.findProjectConfig(startDir, repoRoot);
if (jsonConfig)
return jsonConfig;
const tsConfig = await this.findTypeScriptConfig(startDir, repoRoot);
if (tsConfig)
return tsConfig;
}
return null;
}
/**
* Finds YAML configuration files
* Searches UP to repo root, then DOWN from repo root
* @param startDir The directory to start searching from
* @param repoRoot The repository root boundary
* @returns Path to the YAML config file or null if not found
*/
async findYamlConfig(startDir, repoRoot) {
const boundary = repoRoot || (await this.findRepoRoot(startDir));
// Search UP to repo root
const upwardResult = await findUp(this.YAML_FILENAMES, {
cwd: startDir,
stopAt: boundary,
});
if (upwardResult)
return upwardResult;
// Search DOWN from repo root
return await this.searchDownward(boundary, this.YAML_FILENAMES);
}
/**
* Finds JSON project configuration files (appwrite.config.json, appwrite.json)
* @param startDir The directory to start searching from
* @param repoRoot The repository root boundary
* @returns Path to the JSON config file or null if not found
*/
async findProjectConfig(startDir, repoRoot) {
const boundary = repoRoot || (await this.findRepoRoot(startDir));
// Search UP to repo root
const upwardResult = await findUp(this.JSON_FILENAMES, {
cwd: startDir,
stopAt: boundary,
});
if (upwardResult)
return upwardResult;
// Search DOWN from repo root
return await this.searchDownward(boundary, this.JSON_FILENAMES);
}
/**
* Finds TypeScript configuration files (appwriteConfig.ts)
* @param startDir The directory to start searching from
* @param repoRoot The repository root boundary
* @returns Path to the TypeScript config file or null if not found
*/
async findTypeScriptConfig(startDir, repoRoot) {
const boundary = repoRoot || (await this.findRepoRoot(startDir));
// Search UP to repo root
const upwardResult = await findUp(this.TS_FILENAMES, {
cwd: startDir,
stopAt: boundary,
});
if (upwardResult)
return upwardResult;
// Search DOWN from repo root
return await this.searchDownward(boundary, this.TS_FILENAMES);
}
/**
* Discovers collection YAML files in a collections/ directory
* @param collectionsDir Path to the collections directory
* @returns Discovery result with file paths
*/
async discoverCollections(collectionsDir) {
if (!fs.existsSync(collectionsDir)) {
return {
found: false,
type: "none",
files: [],
};
}
try {
const files = fs.readdirSync(collectionsDir);
const collectionFiles = files.filter((file) => (file.endsWith(".yaml") ||
file.endsWith(".yml") ||
file.endsWith(".ts")) &&
file !== "index.ts");
if (collectionFiles.length === 0) {
return {
found: false,
type: "none",
files: [],
};
}
MessageFormatter.success(`Discovered ${collectionFiles.length} collection file(s) in ${collectionsDir}`, { prefix: "Discovery" });
return {
found: true,
path: collectionsDir,
type: "yaml",
files: collectionFiles,
};
}
catch (error) {
MessageFormatter.error(`Error discovering collections in ${collectionsDir}`, error instanceof Error ? error : undefined, { prefix: "Discovery" });
return {
found: false,
type: "none",
files: [],
};
}
}
/**
* Discovers table YAML files in a tables/ directory
* @param tablesDir Path to the tables directory
* @returns Discovery result with file paths
*/
async discoverTables(tablesDir) {
if (!fs.existsSync(tablesDir)) {
return {
found: false,
type: "none",
files: [],
};
}
try {
const files = fs.readdirSync(tablesDir);
const tableFiles = files.filter((file) => (file.endsWith(".yaml") ||
file.endsWith(".yml") ||
file.endsWith(".ts")) &&
file !== "index.ts");
if (tableFiles.length === 0) {
return {
found: false,
type: "none",
files: [],
};
}
MessageFormatter.success(`Discovered ${tableFiles.length} table file(s) in ${tablesDir}`, { prefix: "Discovery" });
return {
found: true,
path: tablesDir,
type: "yaml",
files: tableFiles,
};
}
catch (error) {
MessageFormatter.error(`Error discovering tables in ${tablesDir}`, error instanceof Error ? error : undefined, { prefix: "Discovery" });
return {
found: false,
type: "none",
files: [],
};
}
}
/**
* Finds the .appwrite configuration directory
* @param startDir The directory to start searching from
* @returns Path to .appwrite directory or null if not found
*/
async findAppwriteDirectory(startDir) {
const repoRoot = await this.findRepoRoot(startDir);
// Search UP to repo root
const upwardResult = await findUp(".appwrite", {
cwd: startDir,
type: "directory",
stopAt: repoRoot,
});
if (upwardResult)
return upwardResult;
// Search DOWN from repo root
return await this.searchDownward(repoRoot, [".appwrite"]);
}
/**
* Finds the functions directory
* @param startDir The directory to start searching from
* @returns Path to functions directory or null if not found
*/
async findFunctionsDirectory(startDir) {
const repoRoot = await this.findRepoRoot(startDir);
// Search UP to repo root
const upwardResult = await findUp("functions", {
cwd: startDir,
type: "directory",
stopAt: repoRoot,
});
if (upwardResult)
return upwardResult;
// Search DOWN from repo root
return await this.searchDownward(repoRoot, ["functions"]);
}
/**
* Gets a summary of all discoverable configuration files
* Useful for debugging configuration issues
* @param startDir The directory to start searching from
* @returns Object containing paths to all discovered config types
*/
async getConfigurationSummary(startDir) {
const repoRoot = await this.findRepoRoot(startDir);
return {
yaml: await this.findYamlConfig(startDir, repoRoot),
typescript: await this.findTypeScriptConfig(startDir, repoRoot),
json: await this.findProjectConfig(startDir, repoRoot),
appwriteDirectory: await this.findAppwriteDirectory(startDir),
functionsDirectory: await this.findFunctionsDirectory(startDir),
selectedConfig: await this.findConfig(startDir),
repoRoot,
};
}
}