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.

210 lines (209 loc) 10.1 kB
import { SchemaGenerator } from "../shared/schemaGenerator.js"; import { findYamlConfig } from "../config/yamlConfig.js"; import { Client, Compression, Databases, Query, Storage, } from "node-appwrite"; import { fetchAllCollections } from "../collections/methods.js"; import { fetchAllDatabases } from "../databases/methods.js"; import { CollectionSchema, attributeSchema, AppwriteConfigSchema, permissionsSchema, attributesSchema, indexesSchema, parseAttribute, } 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; storage; updatedConfig; collToAttributeMap = new Map(); appwriteFolderPath; constructor(config, appwriteFolderPath, storage) { this.config = config; this.updatedConfig = config; this.storage = storage; this.appwriteFolderPath = appwriteFolderPath; this.ensureClientInitialized(); } 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) => { 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) => { if (permissions.length === 0) { return []; } const parsedPermissions = permissionsSchema.parse(permissions); // Validate the parsed permissions using Zod return parsedPermissions ?? []; }; updateCollectionConfigAttributes = (collection) => { for (const attribute of collection.attributes) { const attributeMap = this.collToAttributeMap.get(collection.name); const attributeParsed = attributeSchema.parse(attribute); this.collToAttributeMap .get(collection.name) ?.push(attributeParsed); } }; async appwriteSync(config, databases) { const db = getDatabaseFromConfig(config); if (!databases) { databases = await fetchAllDatabases(db); } let updatedConfig = { ...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, 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) => { return parseAttribute(attr); }) .filter((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) => { return { ...index, orders: index.orders?.filter((order) => { 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, 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, 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, })); // Make sure to update the config with all changes updatedConfig.functions = this.updatedConfig.functions; this.updatedConfig = updatedConfig; } async toSchemas(databases) { 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(); } }