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.

292 lines (267 loc) 9.98 kB
import { SchemaGenerator } from "../shared/schemaGenerator.js"; import { findYamlConfig } from "../config/yamlConfig.js"; import { Client, Compression, Databases, Query, Storage, type Models, type Permission, } from "node-appwrite"; import { fetchAllCollections } from "../collections/methods.js"; import { fetchAllDatabases } from "../databases/methods.js"; import { CollectionSchema, attributeSchema, type AppwriteConfig, AppwriteConfigSchema, type ConfigDatabases, type Attribute, permissionsSchema, attributesSchema, indexesSchema, parseAttribute, type Runtime, type Specification, } from "appwrite-utils"; import { getDatabaseFromConfig } from "./afterImportActions.js"; import { listBuckets } from "../storage/methods.js"; import { listFunctions, listFunctionDeployments } from "../functions/methods.js"; export class AppwriteToX { config: AppwriteConfig; storage: Storage; updatedConfig: AppwriteConfig; collToAttributeMap = new Map<string, Attribute[]>(); appwriteFolderPath: string; constructor( config: AppwriteConfig, appwriteFolderPath: string, storage: Storage ) { this.config = config; this.updatedConfig = config; this.storage = storage; this.appwriteFolderPath = appwriteFolderPath; this.ensureClientInitialized(); } private ensureClientInitialized() { if (!this.config.appwriteClient) { const client = new Client(); client .setEndpoint(this.config.appwriteEndpoint) .setProject(this.config.appwriteProject) .setKey(this.config.appwriteKey); this.config.appwriteClient = client; } } // Function to parse a single permission string parsePermissionString = (permissionString: string) => { const match = permissionString.match(/^(\w+)\('([^']+)'\)$/); if (!match) { throw new Error(`Invalid permission format: ${permissionString}`); } return { permission: match[1], target: match[2], }; }; // Function to parse an array of permission strings parsePermissionsArray = (permissions: string[]) => { if (permissions.length === 0) { return []; } const parsedPermissions = permissionsSchema.parse(permissions); // Validate the parsed permissions using Zod return parsedPermissions ?? []; }; updateCollectionConfigAttributes = (collection: Models.Collection) => { for (const attribute of collection.attributes) { const attributeMap = this.collToAttributeMap.get( collection.name as string ); const attributeParsed = attributeSchema.parse(attribute); this.collToAttributeMap .get(collection.name as string) ?.push(attributeParsed); } }; async appwriteSync(config: AppwriteConfig, databases?: Models.Database[]) { const db = getDatabaseFromConfig(config); if (!databases) { databases = await fetchAllDatabases(db); } let updatedConfig: AppwriteConfig = { ...config }; // Fetch all buckets const allBuckets = await listBuckets(this.storage); // Loop through each database for (const database of databases) { if (!this.config.useMigrations && database.name.toLowerCase() === "migrations") { continue; } // Match bucket to database const matchedBucket = allBuckets.buckets.find((bucket) => bucket.$id.toLowerCase().includes(database.$id.toLowerCase()) ); if (matchedBucket) { const dbConfig = updatedConfig.databases.find( (db) => db.$id === database.$id ); if (dbConfig) { dbConfig.bucket = { $id: matchedBucket.$id, name: matchedBucket.name, enabled: matchedBucket.enabled, maximumFileSize: matchedBucket.maximumFileSize, allowedFileExtensions: matchedBucket.allowedFileExtensions, compression: matchedBucket.compression as Compression, encryption: matchedBucket.encryption, antivirus: matchedBucket.antivirus, }; } } const collections = await fetchAllCollections(database.$id, db); // Loop through each collection in the current database if (!updatedConfig.collections) { updatedConfig.collections = []; } for (const collection of collections) { console.log(`Processing collection: ${collection.name}`); const existingCollectionIndex = updatedConfig.collections.findIndex( (c) => c.name === collection.name ); // Parse the collection permissions and attributes const collPermissions = this.parsePermissionsArray( collection.$permissions ); const collAttributes = collection.attributes .map((attr: any) => { return parseAttribute(attr); }) .filter((attribute: Attribute) => attribute.type === "relationship" ? attribute.side !== "child" : true ); for (const attribute of collAttributes) { if ( attribute.type === "relationship" && attribute.relatedCollection ) { console.log( `Fetching related collection for ID: ${attribute.relatedCollection}` ); try { const relatedCollectionPulled = await db.getCollection( database.$id, attribute.relatedCollection ); console.log( `Fetched Collection Name: ${relatedCollectionPulled.name}` ); attribute.relatedCollection = relatedCollectionPulled.name; console.log( `Updated attribute.relatedCollection to: ${attribute.relatedCollection}` ); } catch (error) { console.log("Error fetching related collection:", error); } } } this.collToAttributeMap.set(collection.name, collAttributes); const finalIndexes = collection.indexes.map((index: Models.Index) => { return { ...index, orders: index.orders?.filter((order: string) => { return order !== null && order; }), }; }); const collIndexes = indexesSchema.parse(finalIndexes) ?? []; // Prepare the collection object to be added or updated const collToPush = CollectionSchema.parse({ $id: collection.$id, name: collection.name, enabled: collection.enabled, documentSecurity: collection.documentSecurity, $createdAt: collection.$createdAt, $updatedAt: collection.$updatedAt, $permissions: collPermissions.length > 0 ? collPermissions : undefined, indexes: collIndexes.length > 0 ? collIndexes : undefined, attributes: collAttributes.length > 0 ? collAttributes : undefined, }); if (existingCollectionIndex !== -1) { // Update existing collection updatedConfig.collections[existingCollectionIndex] = collToPush; } else { // Add new collection updatedConfig.collections.push(collToPush); } } console.log( `Processed ${collections.length} collections in ${database.name}` ); } // Add unmatched buckets as global buckets const globalBuckets = allBuckets.buckets.filter( (bucket) => !updatedConfig.databases.some( (db) => db.bucket && db.bucket.$id === bucket.$id ) ); updatedConfig.buckets = globalBuckets.map((bucket) => ({ $id: bucket.$id, name: bucket.name, enabled: bucket.enabled, maximumFileSize: bucket.maximumFileSize, allowedFileExtensions: bucket.allowedFileExtensions, compression: bucket.compression as Compression, encryption: bucket.encryption, antivirus: bucket.antivirus, })); const remoteFunctions = await listFunctions(this.config.appwriteClient!, [ Query.limit(1000), ]); this.updatedConfig.functions = remoteFunctions.functions.map( (func) => ({ $id: func.$id, name: func.name, runtime: func.runtime as Runtime, execute: func.execute, events: func.events || [], schedule: func.schedule || "", timeout: func.timeout || 15, enabled: func.enabled !== false, logging: func.logging !== false, entrypoint: func.entrypoint || "src/index.ts", commands: func.commands || "npm install", dirPath: `functions/${func.name}`, specification: func.specification as Specification, }) ); // Make sure to update the config with all changes updatedConfig.functions = this.updatedConfig.functions; this.updatedConfig = updatedConfig; } async toSchemas(databases?: Models.Database[]) { await this.appwriteSync(this.config, databases); const generator = new SchemaGenerator( this.updatedConfig, this.appwriteFolderPath ); // Check if this is a YAML-based project const yamlConfigPath = findYamlConfig(this.appwriteFolderPath); const isYamlProject = !!yamlConfigPath; if (isYamlProject) { console.log("📄 Detected YAML configuration - generating YAML collection definitions"); generator.updateYamlCollections(); await generator.updateConfig(this.updatedConfig, true); } else { console.log("📝 Generating TypeScript collection definitions"); generator.updateTsSchemas(); await generator.updateConfig(this.updatedConfig, false); } generator.generateSchemas(); } }