import { Injectable, Logger } from '@nestjs/common'; import { Knex } from 'knex'; import { ModelClass } from 'objection'; import { BaseModel } from './base.model'; import { ModelRegistry } from './model.registry'; import { ObjectMetadata } from './dynamic-model.factory'; import { TenantDatabaseService } from '../../tenant/tenant-database.service'; import { UserModel, RoleModel, PermissionModel } from './system-models'; /** * Service to manage dynamic models for a specific tenant */ @Injectable() export class ModelService { private readonly logger = new Logger(ModelService.name); private tenantRegistries = new Map(); constructor(private tenantDbService: TenantDatabaseService) {} /** * Get or create a registry for a tenant */ getTenantRegistry(tenantId: string): ModelRegistry { if (!this.tenantRegistries.has(tenantId)) { const registry = new ModelRegistry(); // Register system models that are defined as static Objection models this.registerSystemModels(registry); this.tenantRegistries.set(tenantId, registry); } return this.tenantRegistries.get(tenantId)!; } /** * Register static system models in the registry * Uses simplified models without complex relationMappings to avoid modelPath issues */ private registerSystemModels(registry: ModelRegistry): void { // Register system models by their API name (used in referenceObject fields) // These are simplified versions without relationMappings to avoid dependency issues registry.registerModel('User', UserModel as any); registry.registerModel('Role', RoleModel as any); registry.registerModel('Permission', PermissionModel as any); this.logger.debug('Registered system models: User, Role, Permission'); } /** * Create and register a model for a tenant */ async createModelForObject( tenantId: string, objectMetadata: ObjectMetadata, ): Promise> { const registry = this.getTenantRegistry(tenantId); const model = registry.createAndRegisterModel(objectMetadata); this.logger.log( `Registered model for ${objectMetadata.apiName} in tenant ${tenantId}`, ); return model; } /** * Get a model for a tenant and object */ getModel(tenantId: string, objectApiName: string): ModelClass { const registry = this.getTenantRegistry(tenantId); return registry.getModel(objectApiName); } /** * Get a bound model (with knex connection) for a tenant and object */ async getBoundModel( tenantId: string, objectApiName: string, ): Promise> { const knex = await this.tenantDbService.getTenantKnexById(tenantId); const model = this.getModel(tenantId, objectApiName); // Bind knex to the model and also to all models in the registry // This ensures system models also have knex bound when they're used in relations const registry = this.getTenantRegistry(tenantId); const allModels = registry.getAllModelNames(); // Bind knex to all models to ensure relations work for (const modelName of allModels) { try { const m = registry.getModel(modelName); if (m && !m.knex()) { m.knex(knex); } } catch (error) { // Ignore errors for models that don't need binding } } return model.bindKnex(knex); } /** * Check if a model exists for a tenant */ hasModel(tenantId: string, objectApiName: string): boolean { const registry = this.getTenantRegistry(tenantId); return registry.hasModel(objectApiName); } /** * Get all model names for a tenant */ getAllModelNames(tenantId: string): string[] { const registry = this.getTenantRegistry(tenantId); return registry.getAllModelNames(); } /** * Ensure a model is registered with all its dependencies. * This method handles recursive model creation for related objects. * * @param tenantId - The tenant ID * @param objectApiName - The object API name to ensure registration for * @param fetchMetadata - Callback function to fetch object metadata (provided by ObjectService) * @param visited - Set to track visited models and prevent infinite loops */ async ensureModelWithDependencies( tenantId: string, objectApiName: string, fetchMetadata: (apiName: string) => Promise, visited: Set = new Set(), ): Promise { // Prevent infinite recursion if (visited.has(objectApiName)) { return; } visited.add(objectApiName); // Check if model already exists if (this.hasModel(tenantId, objectApiName)) { return; } try { // Fetch the object metadata const objectMetadata = await fetchMetadata(objectApiName); // Extract lookup fields to find dependencies const lookupFields = objectMetadata.fields.filter( f => f.type === 'LOOKUP' && f.referenceObject ); // Recursively ensure all dependent models are registered first for (const field of lookupFields) { if (field.referenceObject) { try { await this.ensureModelWithDependencies( tenantId, field.referenceObject, fetchMetadata, visited, ); } catch (error) { // If related object doesn't exist (e.g., system tables), skip it this.logger.debug( `Skipping registration of related model ${field.referenceObject}: ${error.message}` ); } } } if (objectMetadata.relations) { for (const relation of objectMetadata.relations) { if (relation.targetObjectApiName) { try { await this.ensureModelWithDependencies( tenantId, relation.targetObjectApiName, fetchMetadata, visited, ); } catch (error) { this.logger.debug( `Skipping registration of related model ${relation.targetObjectApiName}: ${error.message}` ); } } } } // Now create and register this model (all dependencies are ready) await this.createModelForObject(tenantId, objectMetadata); this.logger.log(`Registered model for ${objectApiName} in tenant ${tenantId}`); } catch (error) { this.logger.warn( `Failed to ensure model for ${objectApiName}: ${error.message}` ); throw error; } } }