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.
491 lines (490 loc) • 20.9 kB
JavaScript
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
import path from "node:path";
import { ulid } from "ulidx";
import { MessageFormatter } from "./shared/messageFormatter.js";
import { detectAppwriteVersionCached, fetchServerVersion, isVersionAtLeast } from "./utils/versionDetection.js";
import { loadAppwriteProjectConfig, findAppwriteProjectConfig, isTablesDBProject } from "./utils/projectConfig.js";
import { findYamlConfig, generateYamlConfigTemplate } from "./config/yamlConfig.js";
import { loadYamlConfig } from "./config/yamlConfig.js";
import { hasSessionAuth } from "./utils/sessionAuth.js";
/**
* Get terminology configuration based on API mode
*/
export function getTerminologyConfig(useTables) {
return useTables
? {
container: "table",
containerName: "Table",
fields: "columns",
fieldName: "Column",
security: "rowSecurity",
schemaRef: "table.schema.json",
items: "rows"
}
: {
container: "collection",
containerName: "Collection",
fields: "attributes",
fieldName: "Attribute",
security: "documentSecurity",
schemaRef: "collection.schema.json",
items: "documents"
};
}
/**
* Detect API mode using multiple detection sources
* Priority: appwrite.json > server version > default (collections)
*/
export async function detectApiMode(basePath) {
let useTables = false;
let detectionSource = "default";
let serverVersion;
try {
// Priority 1: Check for existing appwrite.json project config
const projectConfigPath = findAppwriteProjectConfig(basePath);
if (projectConfigPath) {
const projectConfig = loadAppwriteProjectConfig(projectConfigPath);
if (projectConfig) {
useTables = isTablesDBProject(projectConfig);
detectionSource = "appwrite.json";
MessageFormatter.info(`Detected ${useTables ? 'TablesDB' : 'Collections'} project from ${projectConfigPath}`, { prefix: "Setup" });
return {
apiMode: useTables ? 'tablesdb' : 'legacy',
useTables,
detectionSource
};
}
}
// Priority 2: Try reading existing YAML config for version detection
const yamlPath = findYamlConfig(basePath);
if (yamlPath) {
const cfg = await loadYamlConfig(yamlPath);
if (cfg) {
const endpoint = cfg.appwriteEndpoint;
const projectId = cfg.appwriteProject;
if (hasSessionAuth(endpoint, projectId)) {
MessageFormatter.info("Using session authentication for version detection", { prefix: "Setup" });
}
const ver = await fetchServerVersion(endpoint);
serverVersion = ver || undefined;
if (isVersionAtLeast(ver || undefined, '1.8.0')) {
useTables = true;
detectionSource = "server-version";
MessageFormatter.info(`Detected TablesDB support (Appwrite ${ver})`, { prefix: "Setup" });
}
else {
MessageFormatter.info(`Using Collections API (Appwrite ${ver || 'unknown'})`, { prefix: "Setup" });
}
return {
apiMode: useTables ? 'tablesdb' : 'legacy',
useTables,
detectionSource,
serverVersion
};
}
}
}
catch (error) {
MessageFormatter.warning(`Version detection failed, defaulting to Collections API: ${error instanceof Error ? error.message : String(error)}`, { prefix: "Setup" });
}
// Default to Collections API
return {
apiMode: 'legacy',
useTables: false,
detectionSource: 'default'
};
}
/**
* Create directory structure for Appwrite project
*/
export function createProjectDirectories(basePath, useTables) {
const appwriteFolder = path.join(basePath, ".appwrite");
const containerName = useTables ? "tables" : "collections";
const containerFolder = path.join(appwriteFolder, containerName);
const schemaFolder = path.join(appwriteFolder, "schemas");
const yamlSchemaFolder = path.join(appwriteFolder, ".yaml_schemas");
const dataFolder = path.join(appwriteFolder, "importData");
// Create all directories
for (const dir of [appwriteFolder, containerFolder, schemaFolder, yamlSchemaFolder, dataFolder]) {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
return {
appwriteFolder,
containerFolder,
schemaFolder,
yamlSchemaFolder,
dataFolder
};
}
/**
* Create example YAML schema file with correct terminology
*/
export function createExampleSchema(containerFolder, terminology) {
const yamlExample = `# yaml-language-server: $schema=../.yaml_schemas/${terminology.schemaRef}
# Example ${terminology.containerName} Definition
name: Example${terminology.containerName}
id: example_${terminology.container}_${Date.now()}
${terminology.security}: false
enabled: true
permissions:
- permission: read
target: any
- permission: create
target: users
- permission: update
target: users
- permission: delete
target: users
${terminology.fields}:
- key: title
type: string
size: 255
required: true
description: "The title of the item"
- key: description
type: string
size: 1000
required: false
description: "A longer description"
- key: isActive
type: boolean
required: false
default: true${terminology.container === 'table' ? `
- key: uniqueCode
type: string
size: 50
required: false
unique: true
description: "Unique identifier code (TablesDB feature)"` : ''}
indexes:
- key: title_search
type: fulltext
attributes:
- title
importDefs: []
`;
const examplePath = path.join(containerFolder, `Example${terminology.containerName}.yaml`);
writeFileSync(examplePath, yamlExample);
MessageFormatter.info(`Created example ${terminology.container} definition with ${terminology.fields} terminology`, { prefix: "Setup" });
return examplePath;
}
/**
* Create JSON schema for YAML validation
*/
export function createYamlValidationSchema(yamlSchemaFolder, useTables) {
const schemaFileName = useTables ? "table.schema.json" : "collection.schema.json";
const containerType = useTables ? "Table" : "Collection";
const fieldsName = useTables ? "columns" : "attributes";
const fieldsDescription = useTables ? "Table columns (fields)" : "Collection attributes (fields)";
const securityField = useTables ? "rowSecurity" : "documentSecurity";
const securityDescription = useTables ? "Enable row-level permissions" : "Enable document-level permissions";
const itemType = useTables ? "row" : "document";
const schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": `https://appwrite-utils.dev/schemas/${schemaFileName}`,
"title": `Appwrite ${containerType} Definition`,
"description": `Schema for defining Appwrite ${useTables ? 'tables' : 'collections'} in YAML${useTables ? ' (TablesDB API)' : ''}`,
"type": "object",
"properties": {
"name": {
"type": "string",
"description": `The name of the ${useTables ? 'table' : 'collection'}`
},
"id": {
"type": "string",
"description": `The ID of the ${useTables ? 'table' : 'collection'} (optional, auto-generated if not provided)`,
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$"
},
[securityField]: {
"type": "boolean",
"default": false,
"description": securityDescription
},
"enabled": {
"type": "boolean",
"default": true,
"description": `Whether the ${useTables ? 'table' : 'collection'} is enabled`
},
"permissions": {
"type": "array",
"description": `${containerType}-level permissions`,
"items": {
"type": "object",
"properties": {
"permission": {
"type": "string",
"enum": ["read", "create", "update", "delete"],
"description": "The permission type"
},
"target": {
"type": "string",
"description": "Permission target (e.g., 'any', 'users', 'users/verified', 'label:admin')"
}
},
"required": ["permission", "target"],
"additionalProperties": false
}
},
[fieldsName]: {
"type": "array",
"description": fieldsDescription,
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": `${useTables ? 'Column' : 'Attribute'} name`,
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$"
},
"type": {
"type": "string",
"enum": ["string", "integer", "double", "boolean", "datetime", "email", "ip", "url", "enum", "relationship"],
"description": `${useTables ? 'Column' : 'Attribute'} data type`
},
"size": {
"type": "number",
"description": `Maximum size for string ${useTables ? 'columns' : 'attributes'}`,
"minimum": 1,
"maximum": 1073741824
},
"required": {
"type": "boolean",
"default": false,
"description": `Whether the ${useTables ? 'column' : 'attribute'} is required`
},
"array": {
"type": "boolean",
"default": false,
"description": `Whether the ${useTables ? 'column' : 'attribute'} is an array`
},
// Encryption flag for string types
"encrypt": {
"type": "boolean",
"default": false,
"description": `Enable encryption for string ${useTables ? 'columns' : 'attributes'}`
},
...(useTables ? {
"unique": {
"type": "boolean",
"default": false,
"description": "Whether the column values must be unique (TablesDB feature)"
}
} : {}),
"default": {
"description": `Default value for the ${useTables ? 'column' : 'attribute'}`
},
"description": {
"type": "string",
"description": `${useTables ? 'Column' : 'Attribute'} description`
},
"min": {
"type": "number",
"description": `Minimum value for numeric ${useTables ? 'columns' : 'attributes'}`
},
"max": {
"type": "number",
"description": `Maximum value for numeric ${useTables ? 'columns' : 'attributes'}`
},
"elements": {
"type": "array",
"items": {
"type": "string"
},
"description": `Allowed values for enum ${useTables ? 'columns' : 'attributes'}`
},
"relatedCollection": {
"type": "string",
"description": `Related ${useTables ? 'table' : 'collection'} name for relationship ${useTables ? 'columns' : 'attributes'}`
},
"relationType": {
"type": "string",
"enum": ["oneToOne", "oneToMany", "manyToOne", "manyToMany"],
"description": "Type of relationship"
},
"twoWay": {
"type": "boolean",
"description": "Whether the relationship is bidirectional"
},
"twoWayKey": {
"type": "string",
"description": "Key name for the reverse relationship"
},
"onDelete": {
"type": "string",
"enum": ["cascade", "restrict", "setNull"],
"description": `Action to take when related ${itemType} is deleted`
},
"side": {
"type": "string",
"enum": ["parent", "child"],
"description": "Side of the relationship"
}
},
"required": ["key", "type"],
"additionalProperties": false,
"allOf": [
{
"if": {
"properties": { "type": { "const": "enum" } }
},
"then": {
"required": ["elements"]
}
},
{
"if": {
"properties": { "type": { "const": "relationship" } }
},
"then": {
"required": ["relatedCollection", "relationType"]
}
}
]
}
},
"indexes": {
"type": "array",
"description": `Database indexes for the ${useTables ? 'table' : 'collection'}`,
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Index name"
},
"type": {
"type": "string",
"enum": ["key", "fulltext", "unique"],
"description": "Index type"
},
"attributes": {
"type": "array",
"items": {
"type": "string"
},
"description": `${useTables ? 'Columns' : 'Attributes'} to index`,
"minItems": 1
},
"orders": {
"type": "array",
"items": {
"type": "string",
"enum": ["ASC", "DESC"]
},
"description": `Sort order for each ${useTables ? 'column' : 'attribute'}`
}
},
"required": ["key", "type", "attributes"],
"additionalProperties": false
}
},
"importDefs": {
"type": "array",
"description": "Import definitions for data migration",
"default": []
}
},
"required": ["name"],
"additionalProperties": false
};
const schemaPath = path.join(yamlSchemaFolder, schemaFileName);
writeFileSync(schemaPath, JSON.stringify(schema, null, 2));
return schemaPath;
}
/**
* Initialize a new Appwrite project with correct directory structure and terminology
*/
export async function initProject(basePath, forceApiMode) {
const projectPath = basePath || process.cwd();
// Detect API mode
const detection = forceApiMode
? {
apiMode: forceApiMode,
useTables: forceApiMode === 'tablesdb',
detectionSource: 'forced',
}
: await detectApiMode(projectPath);
const { useTables, detectionSource } = detection;
const terminology = getTerminologyConfig(useTables);
// Create directory structure
const dirs = createProjectDirectories(projectPath, useTables);
// Generate YAML config
const configPath = path.join(dirs.appwriteFolder, "config.yaml");
generateYamlConfigTemplate(configPath);
// Create example schema file
createExampleSchema(dirs.containerFolder, terminology);
// Create JSON validation schema
createYamlValidationSchema(dirs.yamlSchemaFolder, useTables);
// Success messages
const containerType = useTables ? "TablesDB" : "Collections";
MessageFormatter.success(`Created YAML config and setup files/directories in .appwrite/ folder.`, { prefix: "Setup" });
MessageFormatter.info(`Project configured for ${containerType} API (${detectionSource} detection)`, { prefix: "Setup" });
MessageFormatter.info("You can now configure your project in .appwrite/config.yaml", { prefix: "Setup" });
MessageFormatter.info(`${terminology.containerName}s can be defined in .appwrite/${terminology.container}s/ as .yaml files`, { prefix: "Setup" });
MessageFormatter.info("Schemas will be generated in .appwrite/schemas/", { prefix: "Setup" });
MessageFormatter.info("Import data can be placed in .appwrite/importData/", { prefix: "Setup" });
if (useTables) {
MessageFormatter.info("TablesDB features: unique constraints, enhanced performance, row-level security", { prefix: "Setup" });
}
}
/**
* Create a new collection or table schema file
*/
export async function createSchema(name, basePath, forceApiMode) {
const projectPath = basePath || process.cwd();
// Detect API mode
const detection = forceApiMode
? {
apiMode: forceApiMode,
useTables: forceApiMode === 'tablesdb',
detectionSource: 'forced',
}
: await detectApiMode(projectPath);
const { useTables } = detection;
const terminology = getTerminologyConfig(useTables);
// Find or create container directory
const appwriteFolder = path.join(projectPath, ".appwrite");
const containerFolder = path.join(appwriteFolder, useTables ? "tables" : "collections");
if (!existsSync(containerFolder)) {
mkdirSync(containerFolder, { recursive: true });
}
// Create YAML schema file
const yamlSchema = `# yaml-language-server: $schema=../.yaml_schemas/${terminology.schemaRef}
# ${terminology.containerName} Definition: ${name}
name: ${name}
id: ${ulid()}
${terminology.security}: false
enabled: true
permissions:
- permission: read
target: any
- permission: create
target: users
- permission: update
target: users
- permission: delete
target: users
${terminology.fields}:
# Add your ${terminology.fields} here
# Example:
# - key: title
# type: string
# size: 255
# required: true
# description: "The title of the item"
indexes:
# Add your indexes here
# Example:
# - key: title_search
# type: fulltext
# attributes:
# - title
importDefs: []
`;
const schemaPath = path.join(containerFolder, `${name}.yaml`);
writeFileSync(schemaPath, yamlSchema);
MessageFormatter.success(`Created ${terminology.container} schema: ${schemaPath}`, { prefix: "Setup" });
MessageFormatter.info(`Add your ${terminology.fields} to define the ${terminology.container} structure`, { prefix: "Setup" });
}