WIP - Fix objection and model registry

This commit is contained in:
Francisco Gaona
2025-12-27 06:08:25 +01:00
parent 516e132611
commit f4143ab106
3 changed files with 266 additions and 2 deletions

View File

@@ -5,6 +5,7 @@ import { BaseModel } from './base.model';
import { ModelRegistry } from './model.registry'; import { ModelRegistry } from './model.registry';
import { ObjectMetadata } from './dynamic-model.factory'; import { ObjectMetadata } from './dynamic-model.factory';
import { TenantDatabaseService } from '../../tenant/tenant-database.service'; import { TenantDatabaseService } from '../../tenant/tenant-database.service';
import { UserModel, RoleModel, PermissionModel } from './system-models';
/** /**
* Service to manage dynamic models for a specific tenant * Service to manage dynamic models for a specific tenant
@@ -21,11 +22,30 @@ export class ModelService {
*/ */
getTenantRegistry(tenantId: string): ModelRegistry { getTenantRegistry(tenantId: string): ModelRegistry {
if (!this.tenantRegistries.has(tenantId)) { 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)!; 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 * Create and register a model for a tenant
*/ */
@@ -60,6 +80,24 @@ export class ModelService {
): Promise<ModelClass<BaseModel>> { ): Promise<ModelClass<BaseModel>> {
const knex = await this.tenantDbService.getTenantKnexById(tenantId); const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const model = this.getModel(tenantId, objectApiName); 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); return model.bindKnex(knex);
} }
@@ -78,4 +116,69 @@ export class ModelService {
const registry = this.getTenantRegistry(tenantId); const registry = this.getTenantRegistry(tenantId);
return registry.getAllModelNames(); 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<ObjectMetadata>,
visited: Set<string> = new Set(),
): Promise<void> {
// 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;
}
}
} }

View File

@@ -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' },
},
};
}
}

View File

@@ -347,6 +347,67 @@ export class ObjectService {
return typeMap[frontendType] || 'TEXT'; 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<void> {
// Provide a metadata fetcher function that the ModelService can use
const fetchMetadata = async (apiName: string): Promise<ObjectMetadata> => {
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 // Runtime endpoints - CRUD operations
async getRecords( async getRecords(
tenantId: string, tenantId: string,
@@ -362,6 +423,9 @@ export class ObjectService {
const tableName = this.getTableName(objectApiName); 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 to use the Objection model if available
try { try {
const Model = this.modelService.getModel(resolvedTenantId, objectApiName); const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
@@ -476,6 +540,9 @@ export class ObjectService {
const tableName = this.getTableName(objectApiName); 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 to use the Objection model if available
try { try {
const Model = this.modelService.getModel(resolvedTenantId, objectApiName); const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
@@ -570,6 +637,9 @@ export class ObjectService {
// Verify object exists // Verify object exists
await this.getObjectDefinition(tenantId, objectApiName); 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 to use the Objection model if available
try { try {
const Model = this.modelService.getModel(resolvedTenantId, objectApiName); const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
@@ -621,6 +691,9 @@ export class ObjectService {
const tableName = this.getTableName(objectApiName); 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 to use the Objection model if available
try { try {
const Model = this.modelService.getModel(resolvedTenantId, objectApiName); const Model = this.modelService.getModel(resolvedTenantId, objectApiName);
@@ -662,6 +735,9 @@ export class ObjectService {
const tableName = this.getTableName(objectApiName); 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 to use the Objection model if available
try { try {
const Model = this.modelService.getModel(resolvedTenantId, objectApiName); const Model = this.modelService.getModel(resolvedTenantId, objectApiName);