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.
267 lines (266 loc) • 11.8 kB
JavaScript
import {} from "node-appwrite";
import { parseAttribute, } from "appwrite-utils";
import { nameToIdMapping, enqueueOperation } from "./operationQueue.js";
import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
import chalk from "chalk";
import pLimit from "p-limit";
// Concurrency limits for different operations
const attributeLimit = pLimit(3); // Low limit for attribute operations
const queryLimit = pLimit(25); // Higher limit for read operations
export 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 === "double" || 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, options = {}) => {
const { updateEnabled = true, useQueue = true, verbose = false } = options;
let action = "create";
let foundAttribute;
let finalAttribute = attribute;
try {
const collectionAttr = collection.attributes.find((attr) => attr.key === attribute.key);
foundAttribute = parseAttribute(collectionAttr);
if (verbose) {
console.log(`Found attribute: ${JSON.stringify(foundAttribute)}`);
}
}
catch (error) {
foundAttribute = undefined;
}
if (foundAttribute &&
attributesSame(foundAttribute, attribute) &&
updateEnabled) {
if (verbose) {
console.log(chalk.green(`✓ Attribute ${attribute.key} is up to date`));
}
return;
}
if (foundAttribute) {
action = "update";
if (verbose) {
console.log(chalk.yellow(`⚠ Updating attribute ${attribute.key}`));
}
}
else {
if (verbose) {
console.log(chalk.blue(`+ Creating attribute ${attribute.key}`));
}
}
// Handle relationship attributes with nameToIdMapping
if (attribute.type === "relationship" && attribute.relatedCollection) {
const relatedCollectionId = nameToIdMapping.get(attribute.relatedCollection);
if (relatedCollectionId) {
finalAttribute = {
...attribute,
relatedCollection: relatedCollectionId,
};
}
}
// Handle BigInt values for integer, double and float types
if (attribute.type === "integer" || attribute.type === "double" || attribute.type === "float") {
if (typeof finalAttribute.min === "bigint") {
finalAttribute.min = Number(finalAttribute.min);
}
if (typeof finalAttribute.max === "bigint") {
finalAttribute.max = Number(finalAttribute.max);
}
}
const queuedOperation = {
type: "attribute",
collectionId: collection.$id,
attribute: finalAttribute,
collection,
};
const executeOperation = async () => {
await attributeLimit(async () => {
if (action === "update" && foundAttribute) {
// Delete existing attribute first
await tryAwaitWithRetry(async () => {
await db.deleteAttribute(dbId, collection.$id, attribute.key);
});
await delay(250);
}
// Create attribute based on type
switch (finalAttribute.type) {
case "string":
await tryAwaitWithRetry(async () => {
await db.createStringAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.size, finalAttribute.required, finalAttribute.xdefault, finalAttribute.array, finalAttribute.encrypted);
});
break;
case "integer":
await tryAwaitWithRetry(async () => {
await db.createIntegerAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required, finalAttribute.min, finalAttribute.max, finalAttribute.xdefault, finalAttribute.array);
});
break;
case "double":
case "float": // Backward compatibility
await tryAwaitWithRetry(async () => {
await db.createFloatAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required, finalAttribute.min, finalAttribute.max, finalAttribute.xdefault, finalAttribute.array);
});
break;
case "boolean":
await tryAwaitWithRetry(async () => {
await db.createBooleanAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required, finalAttribute.xdefault, finalAttribute.array);
});
break;
case "datetime":
await tryAwaitWithRetry(async () => {
await db.createDatetimeAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required, finalAttribute.xdefault, finalAttribute.array);
});
break;
case "email":
await tryAwaitWithRetry(async () => {
await db.createEmailAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required, finalAttribute.xdefault, finalAttribute.array);
});
break;
case "ip":
await tryAwaitWithRetry(async () => {
await db.createIpAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required, finalAttribute.xdefault, finalAttribute.array);
});
break;
case "url":
await tryAwaitWithRetry(async () => {
await db.createUrlAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required, finalAttribute.xdefault, finalAttribute.array);
});
break;
case "enum":
await tryAwaitWithRetry(async () => {
await db.createEnumAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.elements, finalAttribute.required, finalAttribute.xdefault, finalAttribute.array);
});
break;
case "relationship":
await tryAwaitWithRetry(async () => {
await db.createRelationshipAttribute(dbId, collection.$id, finalAttribute.relatedCollection, finalAttribute.relationType, finalAttribute.twoWay, finalAttribute.key, finalAttribute.twoWayKey, finalAttribute.onDelete);
});
break;
default:
throw new Error(`Unknown attribute type: ${finalAttribute.type}`);
}
});
};
if (useQueue) {
enqueueOperation(queuedOperation);
}
else {
await executeOperation();
}
};
export const createUpdateCollectionAttributes = async (db, dbId, collection, collectionConfig, options = {}) => {
if (!collectionConfig.attributes)
return;
const { verbose = false } = options;
if (verbose) {
console.log(chalk.blue(`Processing ${collectionConfig.attributes.length} attributes for collection ${collection.name}`));
}
for (const attribute of collectionConfig.attributes) {
try {
await createOrUpdateAttribute(db, dbId, collection, attribute, options);
if (verbose) {
console.log(chalk.green(`✓ Processed attribute ${attribute.key}`));
}
// Add delay between attribute operations to prevent rate limiting
await delay(250);
}
catch (error) {
console.error(chalk.red(`❌ Failed to process attribute ${attribute.key}:`), error);
throw error;
}
}
};
export const deleteObsoleteAttributes = async (db, dbId, collection, collectionConfig, options = {}) => {
const { useQueue = true, verbose = false } = options;
const configAttributes = collectionConfig.attributes || [];
const configAttributeKeys = new Set(configAttributes.map(attr => attr.key));
// Find attributes that exist in the database but not in the config
const obsoleteAttributes = collection.attributes.filter((attr) => !configAttributeKeys.has(attr.key));
if (obsoleteAttributes.length === 0) {
return;
}
if (verbose) {
console.log(chalk.yellow(`🗑️ Removing ${obsoleteAttributes.length} obsolete attributes from collection ${collection.name}`));
}
for (const attr of obsoleteAttributes) {
const queuedOperation = {
type: "attribute",
collectionId: collection.$id,
attribute: { key: attr.key, type: "delete" },
collection,
};
const executeOperation = async () => {
await attributeLimit(() => tryAwaitWithRetry(async () => {
await db.deleteAttribute(dbId, collection.$id, attr.key);
}));
};
if (useQueue) {
enqueueOperation(queuedOperation);
}
else {
await executeOperation();
await delay(250);
}
if (verbose) {
console.log(chalk.gray(`🗑️ Deleted obsolete attribute ${attr.key}`));
}
}
};