204 lines
6.5 KiB
TypeScript
204 lines
6.5 KiB
TypeScript
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<string, ModelRegistry>();
|
|
|
|
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<ModelClass<BaseModel>> {
|
|
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<BaseModel> {
|
|
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<ModelClass<BaseModel>> {
|
|
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<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}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|