Files
twenty/packages/twenty-cli/src/utils/schema-validator.ts
T
Félix Malfait 30a2164980 First Application POC (#14382)
Quick proof of concept for twenty-apps + twenty-cli, with local
development / hot reload

Let's discuss it!



https://github.com/user-attachments/assets/c6789936-cd5f-4110-a265-863a6ac1af2d
2025-09-10 15:12:38 +02:00

121 lines
3.3 KiB
TypeScript

import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import * as fs from 'fs-extra';
import * as path from 'path';
export class SchemaValidationError extends Error {
constructor(
message: string,
public readonly errors: any[],
public readonly filePath?: string,
) {
super(message);
this.name = 'SchemaValidationError';
}
}
export class SchemaValidator {
private ajv: Ajv;
private schemasLoaded = false;
constructor() {
this.ajv = new Ajv({
allErrors: true,
verbose: true,
strict: false,
});
addFormats(this.ajv);
}
private async loadSchemas(): Promise<void> {
if (this.schemasLoaded) return;
const schemasDir = path.join(__dirname, '../../schemas');
try {
// Load agent schema
const agentSchemaPath = path.join(schemasDir, 'agent.schema.json');
const agentSchema = await fs.readJson(agentSchemaPath);
this.ajv.addSchema(agentSchema, 'agent');
// Load app manifest schema
const appSchemaPath = path.join(schemasDir, 'app-manifest.schema.json');
const appSchema = await fs.readJson(appSchemaPath);
this.ajv.addSchema(appSchema, 'app-manifest');
this.schemasLoaded = true;
} catch {
// Gracefully handle missing schemas in development
console.warn('Warning: Could not load JSON schemas for validation');
this.schemasLoaded = true; // Prevent retry
}
}
async validateAgent(agent: any, filePath?: string): Promise<void> {
await this.loadSchemas();
const validate = this.ajv.getSchema('agent');
if (!validate) {
// Schema not available, skip validation
return;
}
const valid = validate(agent);
if (!valid) {
const errorMessages = this.formatErrors(validate.errors || []);
throw new SchemaValidationError(
`Agent validation failed:\n${errorMessages}`,
validate.errors || [],
filePath,
);
}
}
async validateAppManifest(manifest: any, filePath?: string): Promise<void> {
await this.loadSchemas();
const validate = this.ajv.getSchema('app-manifest');
if (!validate) {
// Schema not available, skip validation
return;
}
const valid = validate(manifest);
if (!valid) {
const errorMessages = this.formatErrors(validate.errors || []);
throw new SchemaValidationError(
`App manifest validation failed:\n${errorMessages}`,
validate.errors || [],
filePath,
);
}
}
private formatErrors(errors: any[]): string {
return errors
.map((error) => {
const path = error.instancePath || 'root';
const message = error.message;
const value =
error.data !== undefined
? ` (got: ${JSON.stringify(error.data)})`
: '';
return `${path}: ${message}${value}`;
})
.join('\n');
}
// Get schema URLs for $schema references
static getSchemaUrls() {
return {
agent:
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json',
appManifest:
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json',
};
}
}
// Singleton instance
export const schemaValidator = new SchemaValidator();