@wearesage/schema
Version:
A flexible schema definition and validation system for TypeScript with multi-database support
468 lines (392 loc) • 14.5 kB
text/typescript
/**
* 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');
}
`;
}
}