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.

302 lines (301 loc) 15.7 kB
import { Query } from "node-appwrite"; import { attributeSchema, parseAttribute, } from "appwrite-utils"; import { nameToIdMapping, enqueueOperation } from "./queue.js"; import { tryAwaitWithRetry } from "../utils/helperFunctions.js"; const attributesSame = (databaseAttribute, configAttribute) => { const attributesToCheck = [ "key", "type", "array", "encrypted", "required", "size", "min", "max", "xdefault", "elements", "relationType", "twoWay", "twoWayKey", "onDelete", "relatedCollection", ]; return attributesToCheck.every((attr) => { // Special handling for min/max values if (attr === "min" || attr === "max") { const dbValue = databaseAttribute[attr]; const configValue = configAttribute[attr]; // Use type-specific default values when comparing if (databaseAttribute.type === "integer") { const defaultMin = attr === "min" ? -2147483647 : undefined; const defaultMax = attr === "max" ? 2147483647 : undefined; return (dbValue ?? defaultMin) === (configValue ?? defaultMax); } if (databaseAttribute.type === "float") { const defaultMin = attr === "min" ? -2147483647 : undefined; const defaultMax = attr === "max" ? 2147483647 : undefined; return (dbValue ?? defaultMin) === (configValue ?? defaultMax); } } // Check if both objects have the attribute const dbHasAttr = attr in databaseAttribute; const configHasAttr = attr in configAttribute; // If both have the attribute, compare values if (dbHasAttr && configHasAttr) { const dbValue = databaseAttribute[attr]; const configValue = configAttribute[attr]; // Consider undefined and null as equivalent if ((dbValue === undefined || dbValue === null) && (configValue === undefined || configValue === null)) { return true; } return dbValue === configValue; } // If neither has the attribute, consider it the same if (!dbHasAttr && !configHasAttr) { return true; } // If one has the attribute and the other doesn't, check if it's undefined or null if (dbHasAttr && !configHasAttr) { const dbValue = databaseAttribute[attr]; return dbValue === undefined || dbValue === null; } if (!dbHasAttr && configHasAttr) { const configValue = configAttribute[attr]; return configValue === undefined || configValue === null; } // If we reach here, the attributes are different return false; }); }; export const createOrUpdateAttribute = async (db, dbId, collection, attribute) => { let action = "create"; let foundAttribute; const updateEnabled = true; let finalAttribute = attribute; try { const collectionAttr = collection.attributes.find( // @ts-expect-error (attr) => attr.key === attribute.key); foundAttribute = parseAttribute(collectionAttr); } catch (error) { foundAttribute = undefined; } if (foundAttribute && attributesSame(foundAttribute, attribute) && updateEnabled) { finalAttribute = { ...attribute, ...foundAttribute, }; action = "update"; } else if (foundAttribute && !attributesSame(foundAttribute, attribute) && updateEnabled) { console.log(`Updating attribute with same key ${attribute.key} but different values`); finalAttribute = attribute; action = "update"; } else if (!updateEnabled && foundAttribute && !attributesSame(foundAttribute, attribute)) { await db.deleteAttribute(dbId, collection.$id, attribute.key); console.log(`Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`); return; } console.log(`${action}-ing attribute: ${finalAttribute.key}`); // Relationship attribute logic with adjustments let collectionFoundViaRelatedCollection; let relatedCollectionId; if (finalAttribute.type === "relationship") { if (nameToIdMapping.has(finalAttribute.relatedCollection)) { relatedCollectionId = nameToIdMapping.get(finalAttribute.relatedCollection); try { collectionFoundViaRelatedCollection = await db.getCollection(dbId, relatedCollectionId); } catch (e) { console.log(`Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`); collectionFoundViaRelatedCollection = undefined; } } else { const collectionsPulled = await db.listCollections(dbId, [ Query.equal("name", finalAttribute.relatedCollection), ]); if (collectionsPulled.total > 0) { collectionFoundViaRelatedCollection = collectionsPulled.collections[0]; relatedCollectionId = collectionFoundViaRelatedCollection.$id; nameToIdMapping.set(finalAttribute.relatedCollection, relatedCollectionId); } } if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) { console.log(`Enqueueing operation for attribute: ${finalAttribute.key}`); enqueueOperation({ type: "attribute", collectionId: collection.$id, collection: collection, attribute, dependencies: [finalAttribute.relatedCollection], }); return; } } finalAttribute = attributeSchema.parse(finalAttribute); console.log(`Final Attribute: ${JSON.stringify(finalAttribute)}`); switch (finalAttribute.type) { case "string": if (action === "create") { await tryAwaitWithRetry(async () => await db.createStringAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.size, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false, finalAttribute.encrypted)); } else { await tryAwaitWithRetry(async () => await db.updateStringAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null)); } break; case "integer": if (action === "create") { if (finalAttribute.min && BigInt(finalAttribute.min) === BigInt(-9223372036854776000)) { delete finalAttribute.min; } if (finalAttribute.max && BigInt(finalAttribute.max) === BigInt(9223372036854776000)) { delete finalAttribute.max; } await tryAwaitWithRetry(async () => await db.createIntegerAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.min || -2147483647, finalAttribute.max || 2147483647, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false)); } else { if (finalAttribute.min && BigInt(finalAttribute.min) === BigInt(-9223372036854776000)) { delete finalAttribute.min; } if (finalAttribute.max && BigInt(finalAttribute.max) === BigInt(9223372036854776000)) { delete finalAttribute.max; } await tryAwaitWithRetry(async () => await db.updateIntegerAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.min || -2147483647, finalAttribute.max || 2147483647, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null)); } break; case "float": if (action === "create") { await tryAwaitWithRetry(async () => await db.createFloatAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.min || -2147483647, finalAttribute.max || 2147483647, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false)); } else { await tryAwaitWithRetry(async () => await db.updateFloatAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.min || -2147483647, finalAttribute.max || 2147483647, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null)); } break; case "boolean": if (action === "create") { await tryAwaitWithRetry(async () => await db.createBooleanAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false)); } else { await tryAwaitWithRetry(async () => await db.updateBooleanAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null)); } break; case "datetime": if (action === "create") { await tryAwaitWithRetry(async () => await db.createDatetimeAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false)); } else { await tryAwaitWithRetry(async () => await db.updateDatetimeAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null)); } break; case "email": if (action === "create") { await tryAwaitWithRetry(async () => await db.createEmailAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false)); } else { await tryAwaitWithRetry(async () => await db.updateEmailAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null)); } break; case "ip": if (action === "create") { await tryAwaitWithRetry(async () => await db.createIpAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false)); } else { await tryAwaitWithRetry(async () => await db.updateIpAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null)); } break; case "url": if (action === "create") { await tryAwaitWithRetry(async () => await db.createUrlAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false)); } else { await tryAwaitWithRetry(async () => await db.updateUrlAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null)); } break; case "enum": if (action === "create") { await tryAwaitWithRetry(async () => await db.createEnumAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.elements, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null, finalAttribute.array || false)); } else { await tryAwaitWithRetry(async () => await db.updateEnumAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.elements, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required ? finalAttribute.xdefault : null)); } break; case "relationship": if (action === "create") { await tryAwaitWithRetry(async () => await db.createRelationshipAttribute(dbId, collection.$id, relatedCollectionId, finalAttribute.relationType, finalAttribute.twoWay, finalAttribute.key, finalAttribute.twoWayKey, finalAttribute.onDelete)); } else { await tryAwaitWithRetry(async () => await db.updateRelationshipAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.onDelete)); } break; default: console.error("Invalid attribute type"); break; } }; export const createUpdateCollectionAttributes = async (db, dbId, collection, attributes) => { console.log(`Creating/Updating attributes for collection: ${collection.name}`); const batchSize = 3; // Size of each batch for (let i = 0; i < attributes.length; i += batchSize) { // Slice the attributes array to get a batch of at most batchSize elements const batch = attributes.slice(i, i + batchSize); const attributePromises = batch.map((attribute) => createOrUpdateAttribute(db, dbId, collection, attribute)); // Await the completion of all promises in the current batch const results = await Promise.allSettled(attributePromises); results.forEach((result) => { if (result.status === "rejected") { console.error("An attribute promise was rejected:", result.reason); } }); } console.log(`Finished creating/updating attributes for collection: ${collection.name}`); };