WIP - Fix objection and model registry
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
backend/src/object/models/system-models.ts
Normal file
85
backend/src/object/models/system-models.ts
Normal 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' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user