@catalystlabs/awm
Version:
Appwrite Migration Tool - Schema management and code generation for Appwrite databases
249 lines (206 loc) ⢠7.78 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
/**
* AWM Zod Schema Generator
* Generates Zod validation schemas from Appwrite schema
*/
class ZodGenerator {
constructor(schemaPath = './appwrite.schema') {
this.schemaPath = schemaPath;
this.collections = [];
}
async parseSchema() {
const content = await fs.readFile(this.schemaPath, 'utf-8');
const lines = content.split('\n');
let currentCollection = null;
let inCollection = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Skip comments and empty lines
if (!line || line.startsWith('//') || line.startsWith('/*')) continue;
// Start of collection
if (line.startsWith('collection ')) {
const name = line.split(' ')[1].replace('{', '').trim();
currentCollection = {
name,
attributes: [],
indexes: [],
uniques: []
};
inCollection = true;
continue;
}
// End of collection
if (inCollection && line === '}') {
this.collections.push(currentCollection);
currentCollection = null;
inCollection = false;
continue;
}
// Parse attributes
if (inCollection && currentCollection && !line.startsWith('@@')) {
const attrMatch = line.match(/^(\w+)\s+(String|Int|Float|Boolean|DateTime)(\[\])?\s*(.*)/);
if (attrMatch) {
const [_, name, type, isArray, decorators] = attrMatch;
const attribute = {
name,
type,
isArray: !!isArray,
required: decorators.includes('@required'),
unique: decorators.includes('@unique'),
default: this.extractDefault(decorators),
size: this.extractSize(decorators),
isRelationship: decorators.includes('@relationship')
};
currentCollection.attributes.push(attribute);
}
}
}
}
extractDefault(decorators) {
const defaultMatch = decorators.match(/@default\((.*?)\)/);
if (!defaultMatch) return null;
const value = defaultMatch[1];
if (value === 'now') return 'now';
if (value === 'false') return false;
if (value === 'true') return true;
if (value === 'null') return null;
if (!isNaN(value)) return Number(value);
return value;
}
extractSize(decorators) {
const sizeMatch = decorators.match(/@size\((\d+)\)/);
return sizeMatch ? parseInt(sizeMatch[1]) : null;
}
mapToZodType(type, isArray, size, defaultValue) {
let zodType;
switch (type) {
case 'String':
zodType = 'z.string()';
if (size) zodType = `z.string().max(${size})`;
break;
case 'Int':
zodType = 'z.number().int()';
break;
case 'Float':
zodType = 'z.number()';
break;
case 'Boolean':
zodType = 'z.boolean()';
break;
case 'DateTime':
zodType = 'z.union([z.string().datetime(), z.date()])';
break;
default:
zodType = 'z.any()';
}
if (isArray) {
zodType = `z.array(${zodType})`;
}
if (defaultValue !== null && defaultValue !== undefined) {
if (defaultValue === 'now') {
zodType += '.default(() => new Date())';
} else if (typeof defaultValue === 'string') {
zodType += `.default('${defaultValue}')`;
} else {
zodType += `.default(${defaultValue})`;
}
}
return zodType;
}
generateZodSchemas() {
let output = `// Generated by AWM Zod Generator
// DO NOT EDIT - This file is auto-generated from appwrite.schema
import { z } from 'zod';
`;
// Generate base document schema
output += `// Base Appwrite document schema
const BaseDocumentSchema = z.object({
$id: z.string().optional(),
$createdAt: z.string().datetime().optional(),
$updatedAt: z.string().datetime().optional(),
$permissions: z.array(z.string()).optional(),
$databaseId: z.string().optional(),
$collectionId: z.string().optional(),
});
`;
// Generate schemas for each collection
for (const collection of this.collections) {
const schemaName = `${this.toPascalCase(collection.name)}Schema`;
// Create schema
output += `// ${collection.name} collection\n`;
output += `export const ${schemaName} = BaseDocumentSchema.extend({\n`;
for (const attr of collection.attributes) {
if (attr.isRelationship) {
// Relationships are just string IDs
const zodType = attr.required ? 'z.string()' : 'z.string().optional()';
output += ` ${attr.name}: ${zodType}, // relationship\n`;
} else {
const zodType = this.mapToZodType(attr.type, attr.isArray, attr.size, attr.default);
const optional = attr.required ? '' : '.optional()';
output += ` ${attr.name}: ${zodType}${optional},\n`;
}
}
output += `});\n\n`;
// Create type from schema
output += `export type ${this.toPascalCase(collection.name)} = z.infer<typeof ${schemaName}>;\n\n`;
// Create input schema (without auto-generated fields)
output += `export const ${schemaName}Input = ${schemaName}.omit({\n`;
output += ` $id: true,\n`;
output += ` $createdAt: true,\n`;
output += ` $updatedAt: true,\n`;
output += ` $permissions: true,\n`;
output += ` $databaseId: true,\n`;
output += ` $collectionId: true,\n`;
output += `});\n\n`;
output += `export type ${this.toPascalCase(collection.name)}Input = z.infer<typeof ${schemaName}Input>;\n\n`;
}
// Generate validation helpers
output += `// Validation helpers\n`;
output += `export const validators = {\n`;
for (const collection of this.collections) {
const pascalCase = this.toPascalCase(collection.name);
output += ` ${collection.name}: {\n`;
output += ` parse: (data: unknown) => ${pascalCase}Schema.parse(data),\n`;
output += ` safeParse: (data: unknown) => ${pascalCase}Schema.safeParse(data),\n`;
output += ` parseInput: (data: unknown) => ${pascalCase}SchemaInput.parse(data),\n`;
output += ` safeParseInput: (data: unknown) => ${pascalCase}SchemaInput.safeParse(data),\n`;
output += ` },\n`;
}
output += `};\n`;
return output;
}
toPascalCase(str) {
return str.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
}
async generate(outputPath = './schemas/appwrite.schemas.ts') {
console.log('š Parsing schema...');
await this.parseSchema();
console.log(`š Found ${this.collections.length} collections`);
const zodSchemas = this.generateZodSchemas();
// Ensure directory exists
const dir = path.dirname(outputPath);
await fs.mkdir(dir, { recursive: true });
// Write Zod schemas file
await fs.writeFile(outputPath, zodSchemas);
console.log(`ā
Zod schemas generated: ${outputPath}`);
return { collections: this.collections.length, outputPath };
}
}
// CLI usage
if (import.meta.url === `file://${process.argv[1]}`) {
const generator = new ZodGenerator(process.argv[2] || './appwrite.schema');
const outputPath = process.argv[3] || './schemas/appwrite.schemas.ts';
generator.generate(outputPath)
.then(result => {
console.log(`\n⨠Successfully generated Zod schemas for ${result.collections} collections`);
})
.catch(error => {
console.error('ā Error generating schemas:', error);
process.exit(1);
});
}
export default ZodGenerator;