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.

429 lines (428 loc) 16.4 kB
import yaml from "js-yaml"; import { Decimal } from "decimal.js"; // Extreme values that Appwrite may return, which should be treated as undefined const EXTREME_MIN_INTEGER = -9223372036854776000; const EXTREME_MAX_INTEGER = 9223372036854776000; const EXTREME_MIN_FLOAT = -1.7976931348623157e+308; const EXTREME_MAX_FLOAT = 1.7976931348623157e+308; /** * Type guard to check if an attribute has min/max properties */ const hasMinMaxProperties = (yamlAttr) => { return yamlAttr.type === 'integer' || yamlAttr.type === 'double' || yamlAttr.type === 'float'; }; /** * Normalizes min/max values for integer and float attributes using Decimal.js for precision * Validates that min < max and handles extreme database values */ const normalizeMinMaxValues = (yamlAttr) => { if (!hasMinMaxProperties(yamlAttr)) { return {}; } const { type, min, max, key } = yamlAttr; let normalizedMin = min; let normalizedMax = max; // Handle min value - only filter out extreme database values if (normalizedMin !== undefined && normalizedMin !== null) { const minValue = Number(normalizedMin); const originalMin = normalizedMin; // Check if it's an extreme database value (but don't filter out large numbers) if (type === 'integer') { if (minValue === EXTREME_MIN_INTEGER) { console.debug(`Min value normalized to undefined for attribute '${yamlAttr.key}': extreme database value`); normalizedMin = undefined; } } else { // float/double if (minValue === EXTREME_MIN_FLOAT) { console.debug(`Min value normalized to undefined for attribute '${yamlAttr.key}': extreme database value`); normalizedMin = undefined; } } } // Handle max value - only filter out extreme database values if (normalizedMax !== undefined && normalizedMax !== null) { const maxValue = Number(normalizedMax); const originalMax = normalizedMax; // Check if it's an extreme database value (but don't filter out large numbers) if (type === 'integer') { if (maxValue === EXTREME_MAX_INTEGER) { console.debug(`Max value normalized to undefined for attribute '${yamlAttr.key}': extreme database value`); normalizedMax = undefined; } } else { // float/double if (maxValue === EXTREME_MAX_FLOAT) { console.debug(`Max value normalized to undefined for attribute '${yamlAttr.key}': extreme database value`); normalizedMax = undefined; } } } // Validate that min < max using Decimal.js for safe comparison if (normalizedMin !== undefined && normalizedMax !== undefined && normalizedMin !== null && normalizedMax !== null) { try { const minDecimal = new Decimal(normalizedMin.toString()); const maxDecimal = new Decimal(normalizedMax.toString()); if (minDecimal.greaterThanOrEqualTo(maxDecimal)) { // Swap values to ensure min < max (graceful handling) console.warn(`Swapping min/max values for attribute '${yamlAttr.key}' to fix validation: min (${normalizedMin}) must be less than max (${normalizedMax})`); const temp = normalizedMin; normalizedMin = normalizedMax; normalizedMax = temp; } } catch (error) { console.error(`Error comparing min/max values for attribute '${yamlAttr.key}':`, error); // If Decimal comparison fails, set both to undefined to avoid API errors normalizedMin = undefined; normalizedMax = undefined; } } return { min: normalizedMin, max: normalizedMax }; }; /** * Converts a Collection object to YAML format with proper schema reference * Supports both collection and table terminology based on configuration */ export function collectionToYaml(collection, config = { useTableTerminology: false, entityType: 'collection', schemaPath: "../.yaml_schemas/collection.schema.json" }) { const schemaPath = config.schemaPath || (config.useTableTerminology ? "../.yaml_schemas/table.schema.json" : "../.yaml_schemas/collection.schema.json"); // Convert Collection to YamlCollectionData format const yamlData = { name: collection.name, id: collection.$id, enabled: collection.enabled, }; // Use appropriate security field based on terminology if (config.useTableTerminology) { yamlData.rowSecurity = collection.documentSecurity; } else { yamlData.documentSecurity = collection.documentSecurity; } // Convert permissions if (collection.$permissions && collection.$permissions.length > 0) { yamlData.permissions = collection.$permissions.map(p => ({ permission: p.permission, target: p.target })); } // Convert attributes/columns based on terminology if (collection.attributes && collection.attributes.length > 0) { const attributeArray = collection.attributes.map(attr => { const yamlAttr = { key: attr.key, type: attr.type, }; // Add optional properties only if they exist (safely access with 'in' operator) if ('size' in attr && attr.size !== undefined) yamlAttr.size = attr.size; if (attr.required !== undefined) yamlAttr.required = attr.required; if (attr.array !== undefined) yamlAttr.array = attr.array; // Always include encrypt field for string attributes (default to false) if (attr.type === 'string') { yamlAttr.encrypt = ('encrypt' in attr && attr.encrypt === true) ? true : false; } if ('xdefault' in attr && attr.xdefault !== undefined) yamlAttr.default = attr.xdefault; // Normalize min/max values using Decimal.js precision if ('min' in attr || 'max' in attr) { const { min, max } = normalizeMinMaxValues(attr); if (min !== undefined) yamlAttr.min = min; if (max !== undefined) yamlAttr.max = max; } if ('elements' in attr && attr.elements !== undefined) yamlAttr.elements = attr.elements; if ('relatedCollection' in attr && attr.relatedCollection !== undefined) yamlAttr.relatedCollection = attr.relatedCollection; if ('relationType' in attr && attr.relationType !== undefined) yamlAttr.relationType = attr.relationType; if ('twoWay' in attr && attr.twoWay !== undefined) yamlAttr.twoWay = attr.twoWay; if ('twoWayKey' in attr && attr.twoWayKey !== undefined) yamlAttr.twoWayKey = attr.twoWayKey; if ('onDelete' in attr && attr.onDelete !== undefined) yamlAttr.onDelete = attr.onDelete; if ('side' in attr && attr.side !== undefined) yamlAttr.side = attr.side; return yamlAttr; }); // Use appropriate terminology if (config.useTableTerminology) { yamlData.columns = attributeArray; } else { yamlData.attributes = attributeArray; } } // Convert indexes with appropriate field references if (collection.indexes && collection.indexes.length > 0) { yamlData.indexes = collection.indexes.map(idx => { const indexData = { key: idx.key, type: idx.type, ...(idx.orders && idx.orders.length > 0 ? { orders: idx.orders } : {}) }; // Use appropriate field terminology for index references if (config.useTableTerminology) { indexData.columns = idx.attributes; } else { indexData.attributes = idx.attributes; } return indexData; }); } // Add import definitions if they exist if (collection.importDefs && collection.importDefs.length > 0) { yamlData.importDefs = collection.importDefs; } else { yamlData.importDefs = []; } // Generate YAML with schema reference const yamlContent = yaml.dump(yamlData, { indent: 2, lineWidth: 120, sortKeys: false, quotingType: '"', forceQuotes: false, }); // Determine the appropriate schema comment based on configuration const entityType = config.useTableTerminology ? 'Table' : 'Collection'; return `# yaml-language-server: $schema=${schemaPath} # ${entityType} Definition: ${collection.name} ${yamlContent}`; } /** * Sanitizes a collection name for use as a filename */ export function sanitizeFilename(name) { return name.replace(/[^a-zA-Z0-9_-]/g, '_'); } /** * Generates the filename for a collection/table YAML file */ export function getCollectionYamlFilename(collection, useTableTerminology = false) { return `${sanitizeFilename(collection.name)}.yaml`; } /** * Converts column terminology back to attribute terminology for loading */ export function normalizeYamlData(yamlData) { const normalized = { ...yamlData }; // Convert columns to attributes if present if (yamlData.columns && !yamlData.attributes) { normalized.attributes = yamlData.columns.map(col => ({ ...col, // Convert table-specific fields back to collection terminology relatedCollection: col.relatedTable || col.relatedCollection, })); delete normalized.columns; } // Normalize index field references if (normalized.indexes) { normalized.indexes = normalized.indexes.map(idx => ({ ...idx, attributes: idx.columns || idx.attributes, // Remove columns field after normalization columns: undefined })); } // Normalize security fields - prefer documentSecurity for consistency if (yamlData.rowSecurity !== undefined && yamlData.documentSecurity === undefined) { normalized.documentSecurity = yamlData.rowSecurity; delete normalized.rowSecurity; } return normalized; } /** * Determines if YAML data uses table terminology */ export function usesTableTerminology(yamlData) { return !!(yamlData.columns && yamlData.columns.length > 0) || !!(yamlData.indexes?.some(idx => !!idx.columns)) || yamlData.rowSecurity !== undefined; } /** * Converts between attribute and column terminology */ export function convertTerminology(yamlData, toTableTerminology) { if (toTableTerminology) { // Convert attributes to columns const converted = { ...yamlData }; if (yamlData.attributes) { converted.columns = yamlData.attributes.map(attr => ({ ...attr, relatedTable: attr.relatedCollection, relatedCollection: undefined })); delete converted.attributes; } // Convert index references if (converted.indexes) { converted.indexes = converted.indexes.map(idx => ({ ...idx, columns: idx.attributes, attributes: idx.attributes // Keep both for compatibility })); } // Convert security field if (yamlData.documentSecurity !== undefined && yamlData.rowSecurity === undefined) { converted.rowSecurity = yamlData.documentSecurity; delete converted.documentSecurity; } return converted; } else { // Convert columns to attributes (normalize) return normalizeYamlData(yamlData); } } /** * Generates a template YAML file for collections or tables */ export function generateYamlTemplate(entityName, config) { const entityType = config.useTableTerminology ? 'table' : 'collection'; const fieldsKey = config.useTableTerminology ? 'columns' : 'attributes'; const relationKey = config.useTableTerminology ? 'relatedTable' : 'relatedCollection'; const indexFieldsKey = config.useTableTerminology ? 'columns' : 'attributes'; // Build template dynamically to handle computed property names const fieldsArray = [ { key: "id", type: "string", required: true, size: 36 }, { key: "name", type: "string", required: true, size: 255 }, { key: "description", type: "string", required: false, size: 1000 }, { key: "isActive", type: "boolean", required: true, default: true }, { key: "createdAt", type: "datetime", required: true }, { key: "tags", type: "string", array: true, required: false }, { key: "categoryId", type: "relationship", relationType: "manyToOne", [relationKey]: "Categories", required: false, onDelete: "setNull" } ]; const indexesArray = [ { key: "name_index", type: "key", [indexFieldsKey]: ["name"] }, { key: "active_created_index", type: "key", [indexFieldsKey]: ["isActive", "createdAt"], orders: ["asc", "desc"] }, { key: "name_fulltext", type: "fulltext", [indexFieldsKey]: ["name", "description"] } ]; const template = { name: entityName, id: entityName.toLowerCase().replace(/\s+/g, '_'), enabled: true, permissions: [ { permission: "read", target: "users" }, { permission: "create", target: "users" }, { permission: "update", target: "users" }, { permission: "delete", target: "users" } ], importDefs: [] }; // Use appropriate security field based on terminology if (config.useTableTerminology) { template.rowSecurity = false; } else { template.documentSecurity = false; } // Assign fields with correct property name template[fieldsKey] = fieldsArray; template.indexes = indexesArray; // Generate YAML content const yamlContent = yaml.dump(template, { indent: 2, lineWidth: 120, sortKeys: false, quotingType: '"', forceQuotes: false, }); // Add schema reference and documentation const schemaPath = config.schemaPath || (config.useTableTerminology ? "../.yaml_schemas/table.schema.json" : "../.yaml_schemas/collection.schema.json"); const documentation = config.useTableTerminology ? `# Table Definition: ${entityName}\n# This file defines a table for the new TablesDB API\n#\n# Key differences from Collections:\n# - Uses 'columns' instead of 'attributes'\n# - Uses 'relatedTable' instead of 'relatedCollection'\n# - Indexes reference 'columns' instead of 'attributes'\n#\n` : `# Collection Definition: ${entityName}\n# This file defines a collection for the legacy Databases API\n#\n# Note: For new projects, consider using TablesDB API with table definitions\n#\n`; return `# yaml-language-server: $schema=${schemaPath}\n${documentation}${yamlContent}`; } /** * Generates example YAML files for both collection and table formats */ export function generateExampleYamls(entityName) { const collectionYaml = generateYamlTemplate(entityName, { useTableTerminology: false, entityType: 'collection' }); const tableYaml = generateYamlTemplate(entityName, { useTableTerminology: true, entityType: 'table' }); return { collection: collectionYaml, table: tableYaml }; }