diff --git a/backend/src/object/models/model.service.ts b/backend/src/object/models/model.service.ts index efbf349..6b87979 100644 --- a/backend/src/object/models/model.service.ts +++ b/backend/src/object/models/model.service.ts @@ -5,6 +5,7 @@ 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 @@ -21,11 +22,30 @@ export class ModelService { */ getTenantRegistry(tenantId: string): ModelRegistry { if (!this.tenantRegistries.has(tenantId)) { - this.tenantRegistries.set(tenantId, new ModelRegistry()); + 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 */ @@ -60,6 +80,24 @@ export class ModelService { ): 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); } @@ -78,4 +116,69 @@ export class ModelService { 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}` + ); + } + } + } + + // 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; + } + } } diff --git a/backend/src/object/models/system-models.ts b/backend/src/object/models/system-models.ts new file mode 100644 index 0000000..d1a4e65 --- /dev/null +++ b/backend/src/object/models/system-models.ts @@ -0,0 +1,85 @@ +import { Model } from 'objection'; + +/** + * Simplified User model for use in dynamic object relations + * This version doesn't include complex relationMappings to avoid modelPath issues + */ +export class UserModel extends Model { + static tableName = 'users'; + static objectApiName = 'User'; + + id!: string; + email!: string; + firstName?: string; + lastName?: string; + name?: string; + isActive!: boolean; + createdAt!: Date; + updatedAt!: Date; + + static get jsonSchema() { + return { + type: 'object', + required: ['email'], + properties: { + id: { type: 'string' }, + email: { type: 'string', format: 'email' }, + firstName: { type: 'string' }, + lastName: { type: 'string' }, + name: { type: 'string' }, + isActive: { type: 'boolean' }, + }, + }; + } + + // No relationMappings to avoid modelPath resolution issues + // These simplified models are only used for lookup relations from dynamic models +} + +/** + * Simplified Role model for use in dynamic object relations + */ +export class RoleModel extends Model { + static tableName = 'roles'; + static objectApiName = 'Role'; + + id!: string; + name!: string; + description?: string; + + static get jsonSchema() { + return { + type: 'object', + required: ['name'], + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + description: { type: 'string' }, + }, + }; + } +} + +/** + * Simplified Permission model for use in dynamic object relations + */ +export class PermissionModel extends Model { + static tableName = 'permissions'; + static objectApiName = 'Permission'; + + id!: string; + name!: string; + description?: string; + + static get jsonSchema() { + return { + type: 'object', + required: ['name'], + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + description: { type: 'string' }, + }, + }; + } +} diff --git a/backend/src/object/object.service.ts b/backend/src/object/object.service.ts index 009aa15..2ed66f3 100644 --- a/backend/src/object/object.service.ts +++ b/backend/src/object/object.service.ts @@ -347,6 +347,67 @@ export class ObjectService { return typeMap[frontendType] || 'TEXT'; } + /** + * Ensure a model is registered for the given object. + * Delegates to ModelService which handles creating the model and all its dependencies. + */ + private async ensureModelRegistered( + tenantId: string, + objectApiName: string, + ): Promise { + // Provide a metadata fetcher function that the ModelService can use + const fetchMetadata = async (apiName: string): Promise => { + const objectDef = await this.getObjectDefinition(tenantId, apiName); + const tableName = this.getTableName(apiName); + + // Build relations from lookup fields, but only for models that exist + const lookupFields = objectDef.fields.filter((f: any) => + f.type === 'LOOKUP' && f.referenceObject + ); + + // Filter to only include relations where we can successfully resolve the target + const validRelations: any[] = []; + for (const field of lookupFields) { + // Check if the referenced object will be available + // We'll let the recursive registration attempt it, but won't include failed ones + validRelations.push({ + name: field.apiName.replace(/Id$/, '').toLowerCase(), + type: 'belongsTo' as const, + targetObjectApiName: field.referenceObject, + fromColumn: field.apiName, + toColumn: 'id', + }); + } + + return { + apiName, + tableName, + fields: objectDef.fields.map((f: any) => ({ + apiName: f.apiName, + label: f.label, + type: f.type, + isRequired: f.isRequired, + isUnique: f.isUnique, + referenceObject: f.referenceObject, + })), + relations: validRelations, + }; + }; + + // Let the ModelService handle recursive model creation + try { + await this.modelService.ensureModelWithDependencies( + tenantId, + objectApiName, + fetchMetadata, + ); + } catch (error) { + this.logger.warn( + `Failed to ensure model for ${objectApiName}: ${error.message}. Will fall back to manual hydration.`, + ); + } + } + // Runtime endpoints - CRUD operations async getRecords( tenantId: string, @@ -362,6 +423,9 @@ export class ObjectService { const tableName = this.getTableName(objectApiName); + // Ensure model is registered before attempting to use it + await this.ensureModelRegistered(resolvedTenantId, objectApiName); + // Try to use the Objection model if available try { const Model = this.modelService.getModel(resolvedTenantId, objectApiName); @@ -402,7 +466,7 @@ export class ObjectService { } catch (error) { this.logger.warn(`Could not use Objection model for ${objectApiName}, falling back to manual join: ${error.message}`); } - + // Fallback to manual data hydration let query = knex(tableName); @@ -476,6 +540,9 @@ export class ObjectService { const tableName = this.getTableName(objectApiName); + // Ensure model is registered before attempting to use it + await this.ensureModelRegistered(resolvedTenantId, objectApiName); + // Try to use the Objection model if available try { const Model = this.modelService.getModel(resolvedTenantId, objectApiName); @@ -570,6 +637,9 @@ export class ObjectService { // Verify object exists await this.getObjectDefinition(tenantId, objectApiName); + // Ensure model is registered before attempting to use it + await this.ensureModelRegistered(resolvedTenantId, objectApiName); + // Try to use the Objection model if available try { const Model = this.modelService.getModel(resolvedTenantId, objectApiName); @@ -621,6 +691,9 @@ export class ObjectService { const tableName = this.getTableName(objectApiName); + // Ensure model is registered before attempting to use it + await this.ensureModelRegistered(resolvedTenantId, objectApiName); + // Try to use the Objection model if available try { const Model = this.modelService.getModel(resolvedTenantId, objectApiName); @@ -662,6 +735,9 @@ export class ObjectService { const tableName = this.getTableName(objectApiName); + // Ensure model is registered before attempting to use it + await this.ensureModelRegistered(resolvedTenantId, objectApiName); + // Try to use the Objection model if available try { const Model = this.modelService.getModel(resolvedTenantId, objectApiName);