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
JavaScript
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();
}
}