UNPKG

@wearesage/schema

Version:

A flexible schema definition and validation system for TypeScript with multi-database support

468 lines (392 loc) 14.5 kB
/** * Service Generator - Automatically generates services from decorated entities */ import "reflect-metadata"; export interface EntityMetadata { name: string; constructor: Function; properties: PropertyMetadata[]; relationships: RelationshipMetadata[]; indexes: IndexMetadata[]; timestamps: TimestampMetadata[]; validation: ValidationMetadata[]; auth?: AuthMetadata; neo4j?: Neo4jMetadata; } export interface PropertyMetadata { name: string; type: any; options: any; isId: boolean; isRequired: boolean; validation: ValidationRule[]; } export interface RelationshipMetadata { name: string; type: 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many'; target: Function; inverse?: string; required?: boolean; neo4j?: any; mongodb?: any; postgresql?: any; } export interface IndexMetadata { property: string; options: any; } export interface TimestampMetadata { property: string; onCreate?: boolean; onUpdate?: boolean; } export interface ValidationRule { type: string; options: any; message: string; } export interface ValidationMetadata { property: string; rules: ValidationRule[]; } export interface AuthMetadata { permissions: string[]; roles?: string[]; } export interface Neo4jMetadata { labels: string[]; relationships: Record<string, any>; } /** * Extracts metadata from a decorated entity class */ export class MetadataExtractor { static extractEntityMetadata(entityClass: Function): EntityMetadata { const name = entityClass.name; const properties = this.extractProperties(entityClass); const relationships = this.extractRelationships(entityClass); const indexes = this.extractIndexes(entityClass); const timestamps = this.extractTimestamps(entityClass); const validation = this.extractValidation(entityClass); const auth = this.extractAuth(entityClass); const neo4j = this.extractNeo4j(entityClass); return { name, constructor: entityClass, properties, relationships, indexes, timestamps, validation, auth, neo4j }; } private static extractProperties(entityClass: Function): PropertyMetadata[] { const propertyNames: string[] = Reflect.getMetadata("entity:properties", entityClass) || []; return propertyNames.map(name => { const isId = Reflect.getMetadata("property:id", entityClass.prototype, name) || false; const options = Reflect.getMetadata("property:options", entityClass.prototype, name) || {}; const type = Reflect.getMetadata("design:type", entityClass.prototype, name); const validation = Reflect.getMetadata("validation:rules", entityClass.prototype, name) || []; return { name, type, options, isId, isRequired: options.required || false, validation }; }); } private static extractRelationships(entityClass: Function): RelationshipMetadata[] { const relationshipNames: string[] = Reflect.getMetadata("entity:relationships", entityClass) || []; return relationshipNames.map(name => { const type = Reflect.getMetadata("relationship:type", entityClass.prototype, name); const options = Reflect.getMetadata("relationship:options", entityClass.prototype, name); const neo4j = Reflect.getMetadata("neo4j:relationship", entityClass.prototype, name); const mongodb = Reflect.getMetadata("mongodb:relationship", entityClass.prototype, name); const postgresql = Reflect.getMetadata("postgresql:relationship", entityClass.prototype, name); return { name, type, target: options?.target, inverse: options?.inverse, required: options?.required, neo4j, mongodb, postgresql }; }); } private static extractIndexes(entityClass: Function): IndexMetadata[] { const indexNames: string[] = Reflect.getMetadata("entity:indexes", entityClass) || []; return indexNames.map(property => { const options = Reflect.getMetadata("index:options", entityClass.prototype, property); return { property, options }; }); } private static extractTimestamps(entityClass: Function): TimestampMetadata[] { const timestampNames: string[] = Reflect.getMetadata("entity:timestamps", entityClass) || []; return timestampNames.map(property => { const options = Reflect.getMetadata("timestamp:options", entityClass.prototype, property); return { property, onCreate: options?.onCreate, onUpdate: options?.onUpdate }; }); } private static extractValidation(entityClass: Function): ValidationMetadata[] { const propertyNames: string[] = Reflect.getMetadata("entity:properties", entityClass) || []; return propertyNames .map(property => { const rules = Reflect.getMetadata("validation:rules", entityClass.prototype, property) || []; return { property, rules }; }) .filter(v => v.rules.length > 0); } private static extractAuth(entityClass: Function): AuthMetadata | undefined { return Reflect.getMetadata("auth:options", entityClass); } private static extractNeo4j(entityClass: Function): Neo4jMetadata | undefined { const labels = Reflect.getMetadata("neo4j:labels", entityClass); if (!labels) return undefined; const relationshipNames: string[] = Reflect.getMetadata("entity:relationships", entityClass) || []; const relationships: Record<string, any> = {}; relationshipNames.forEach(name => { const neo4jRel = Reflect.getMetadata("neo4j:relationship", entityClass.prototype, name); if (neo4jRel) { relationships[name] = neo4jRel; } }); return { labels, relationships }; } } /** * Base generated service interface */ export interface GeneratedService { entityName: string; metadata: EntityMetadata; } /** * Generates services from entity metadata */ export class ServiceGenerator { /** * Generate a complete service for an entity */ static generateService(entityClass: Function): GeneratedService { const metadata = MetadataExtractor.extractEntityMetadata(entityClass); return { entityName: metadata.name, metadata }; } /** * Generate CRUD operations based on entity metadata */ static generateCRUDOperations(metadata: EntityMetadata): any { const idProperty = metadata.properties.find(p => p.isId); const requiredProperties = metadata.properties.filter(p => p.isRequired && !p.isId); return { // Create operation create: { requiredFields: requiredProperties.map(p => p.name), validation: metadata.validation, auth: metadata.auth }, // Read operations findById: { idField: idProperty?.name || 'id', auth: metadata.auth }, findAll: { indexedFields: metadata.indexes.map(i => i.property), auth: metadata.auth }, // Update operation update: { idField: idProperty?.name || 'id', validation: metadata.validation, timestamps: metadata.timestamps.filter(t => t.onUpdate), auth: metadata.auth }, // Delete operation delete: { idField: idProperty?.name || 'id', auth: metadata.auth } }; } /** * Generate relationship traversal operations */ static generateRelationshipOperations(metadata: EntityMetadata): any { const operations: any = {}; metadata.relationships.forEach(rel => { const operationName = `get${rel.name.charAt(0).toUpperCase() + rel.name.slice(1)}`; operations[operationName] = { type: rel.type, target: rel.target.name, neo4j: rel.neo4j, auth: metadata.auth }; // For spatial navigation (if this is Space entity) if (metadata.name === 'Space' && rel.name === 'childSpaces') { operations.enterSpace = { type: 'spatial_navigation', target: rel.target.name, neo4j: rel.neo4j, auth: metadata.auth }; } if (metadata.name === 'Space' && rel.name === 'parentSpace') { operations.exitSpace = { type: 'spatial_navigation', target: rel.target.name, neo4j: rel.neo4j, auth: metadata.auth }; } }); return operations; } /** * Generate Neo4j-specific operations */ static generateNeo4jOperations(metadata: EntityMetadata): any { if (!metadata.neo4j) return {}; const operations: any = { // Node operations createNode: { labels: metadata.neo4j.labels, properties: metadata.properties.map(p => p.name), auth: metadata.auth }, findNode: { labels: metadata.neo4j.labels, indexes: metadata.indexes.map(i => i.property), auth: metadata.auth }, // Relationship operations createRelationship: {}, findRelationships: {}, traverseGraph: {} }; // Add relationship-specific operations Object.entries(metadata.neo4j.relationships).forEach(([name, config]) => { operations.createRelationship[name] = config; operations.findRelationships[name] = config; operations.traverseGraph[name] = config; }); return operations; } /** * Generate complete service class code */ static generateServiceClass(entityClass: Function, options: { includePermissions?: boolean; verbose?: boolean } = {}): string { const metadata = MetadataExtractor.extractEntityMetadata(entityClass); const crud = this.generateCRUDOperations(metadata); const relationships = this.generateRelationshipOperations(metadata); const neo4j = this.generateNeo4jOperations(metadata); return ` /** * Generated service for ${metadata.name} entity * Auto-generated from entity decorators - DO NOT EDIT MANUALLY */ export class ${metadata.name}Service implements GeneratedService { entityName = '${metadata.name}'; metadata = ${JSON.stringify(metadata, null, 2)}; // CRUD Operations ${this.generateCRUDMethods(crud, metadata.auth, options)} // Relationship Operations ${this.generateRelationshipMethods(relationships, metadata.auth, options)} // Neo4j Operations ${this.generateNeo4jMethods(neo4j, options)} } `; } private static generateCRUDMethods(crud: any, auth: any, options: any): string { const generatePermissionCheck = (methodName: string) => { if (!options.includePermissions || !auth?.permissions) { return '// No permission checks required'; } return ` // Permission check if (!context?.user?.permissions?.some((p: string) => ${JSON.stringify(auth.permissions)}.includes(p))) { throw new Error('Insufficient permissions. Required: ${auth.permissions.join(', ')}'); }`; }; return ` async create(data: any, context?: { user?: { permissions?: string[] } }): Promise<any> { ${generatePermissionCheck('create')} // Validate required fields: ${crud.create.requiredFields.join(', ')} // Apply validation rules throw new Error('Implementation needed - connect to your database adapter'); } async findById(id: string, context?: { user?: { permissions?: string[] } }): Promise<any> { ${generatePermissionCheck('findById')} // Find by ${crud.findById.idField} throw new Error('Implementation needed - connect to your database adapter'); } async findAll(filters?: any, context?: { user?: { permissions?: string[] } }): Promise<any[]> { ${generatePermissionCheck('findAll')} // Use indexed fields: ${crud.findAll.indexedFields.join(', ')} throw new Error('Implementation needed - connect to your database adapter'); } async update(id: string, data: any, context?: { user?: { permissions?: string[] } }): Promise<any> { ${generatePermissionCheck('update')} // Update by ${crud.update.idField} // Update timestamps: ${crud.update.timestamps.map((t: any) => t.property).join(', ')} throw new Error('Implementation needed - connect to your database adapter'); } async delete(id: string, context?: { user?: { permissions?: string[] } }): Promise<boolean> { ${generatePermissionCheck('delete')} // Delete by ${crud.delete.idField} throw new Error('Implementation needed - connect to your database adapter'); } `; } private static generateRelationshipMethods(relationships: any, auth: any, options: any): string { const generatePermissionCheck = () => { if (!options.includePermissions || !auth?.permissions) { return '// No permission checks required'; } return ` // Permission check if (!context?.user?.permissions?.some((p: string) => ${JSON.stringify(auth.permissions)}.includes(p))) { throw new Error('Insufficient permissions. Required: ${auth.permissions.join(', ')}'); }`; }; return Object.entries(relationships) .map(([name, config]: [string, any]) => ` async ${name}(id: string, context?: { user?: { permissions?: string[] } }): Promise<any> { ${generatePermissionCheck()} // ${config.type} relationship to ${config.target} // Neo4j relationship: ${JSON.stringify(config.neo4j)} throw new Error('Implementation needed - connect to your database adapter'); }`) .join('\n'); } private static generateNeo4jMethods(neo4j: any, options: any): string { if (!neo4j.createNode) return ''; return ` async createNode(data: any, context?: { user?: { permissions?: string[] } }): Promise<any> { // Create node with labels: ${neo4j.createNode.labels.join(', ')} // Properties: ${neo4j.createNode.properties.join(', ')} throw new Error('Implementation needed - connect to your Neo4j adapter'); } async findNode(filters: any, context?: { user?: { permissions?: string[] } }): Promise<any> { // Find node with labels: ${neo4j.findNode.labels.join(', ')} // Use indexes: ${neo4j.findNode.indexes.join(', ')} throw new Error('Implementation needed - connect to your Neo4j adapter'); } async traverseGraph(fromId: string, relationshipType: string, depth?: number, context?: { user?: { permissions?: string[] } }): Promise<any[]> { // Traverse graph relationships throw new Error('Implementation needed - connect to your Neo4j adapter'); } `; } }