202 lines
5.6 KiB
TypeScript
202 lines
5.6 KiB
TypeScript
import { ModelClass, JSONSchema, RelationMappings, Model } from 'objection';
|
|
import { BaseModel } from './base.model';
|
|
|
|
export interface FieldDefinition {
|
|
apiName: string;
|
|
label: string;
|
|
type: string;
|
|
isRequired?: boolean;
|
|
isUnique?: boolean;
|
|
referenceObject?: string;
|
|
defaultValue?: string;
|
|
}
|
|
|
|
export interface RelationDefinition {
|
|
name: string;
|
|
type: 'belongsTo' | 'hasMany' | 'hasManyThrough';
|
|
targetObjectApiName: string;
|
|
fromColumn: string;
|
|
toColumn: string;
|
|
}
|
|
|
|
export interface ObjectMetadata {
|
|
apiName: string;
|
|
tableName: string;
|
|
fields: FieldDefinition[];
|
|
relations?: RelationDefinition[];
|
|
}
|
|
|
|
export class DynamicModelFactory {
|
|
/**
|
|
* Get relation name from lookup field API name
|
|
* Converts "ownerId" -> "owner", "customFieldId" -> "customfield"
|
|
*/
|
|
static getRelationName(lookupFieldApiName: string): string {
|
|
return lookupFieldApiName.replace(/Id$/, '').toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Create a dynamic model class from object metadata
|
|
* @param meta Object metadata
|
|
* @param getModel Function to retrieve model classes from registry
|
|
*/
|
|
static createModel(
|
|
meta: ObjectMetadata,
|
|
getModel?: (apiName: string) => ModelClass<any>,
|
|
): ModelClass<any> {
|
|
const { tableName, fields, apiName, relations = [] } = meta;
|
|
|
|
// Build JSON schema properties
|
|
const properties: Record<string, any> = {
|
|
id: { type: 'string' },
|
|
tenantId: { type: 'string' },
|
|
ownerId: { type: 'string' },
|
|
name: { type: 'string' },
|
|
created_at: { type: 'string', format: 'date-time' },
|
|
updated_at: { type: 'string', format: 'date-time' },
|
|
};
|
|
|
|
// Don't require id or tenantId - they'll be set automatically
|
|
const required: string[] = [];
|
|
|
|
// Add custom fields
|
|
for (const field of fields) {
|
|
properties[field.apiName] = this.fieldToJsonSchema(field);
|
|
|
|
// Only mark as required if explicitly required AND not a system field
|
|
const systemFields = ['id', 'tenantId', 'ownerId', 'name', 'created_at', 'updated_at'];
|
|
if (field.isRequired && !systemFields.includes(field.apiName)) {
|
|
required.push(field.apiName);
|
|
}
|
|
}
|
|
|
|
// Build relation mappings from lookup fields
|
|
const lookupFields = fields.filter(f => f.type === 'LOOKUP' && f.referenceObject);
|
|
|
|
// Store lookup fields metadata for later use
|
|
const lookupFieldsInfo = lookupFields.map(f => ({
|
|
apiName: f.apiName,
|
|
relationName: DynamicModelFactory.getRelationName(f.apiName),
|
|
referenceObject: f.referenceObject,
|
|
targetTable: this.getTableName(f.referenceObject),
|
|
}));
|
|
|
|
// Create the dynamic model class extending BaseModel
|
|
class DynamicModel extends BaseModel {
|
|
static tableName = tableName;
|
|
|
|
static objectApiName = apiName;
|
|
|
|
static lookupFields = lookupFieldsInfo;
|
|
|
|
static get relationMappings(): RelationMappings {
|
|
const mappings: RelationMappings = {};
|
|
|
|
// Build relation mappings from lookup fields
|
|
for (const lookupInfo of lookupFieldsInfo) {
|
|
// Use getModel function if provided, otherwise use string reference
|
|
let modelClass: any = lookupInfo.referenceObject;
|
|
|
|
if (getModel) {
|
|
const resolvedModel = getModel(lookupInfo.referenceObject);
|
|
// Only use resolved model if it exists, otherwise skip this relation
|
|
// It will be resolved later when the model is registered
|
|
if (resolvedModel) {
|
|
modelClass = resolvedModel;
|
|
} else {
|
|
// Skip this relation if model not found yet
|
|
continue;
|
|
}
|
|
}
|
|
|
|
mappings[lookupInfo.relationName] = {
|
|
relation: Model.BelongsToOneRelation,
|
|
modelClass,
|
|
join: {
|
|
from: `${tableName}.${lookupInfo.apiName}`,
|
|
to: `${lookupInfo.targetTable}.id`,
|
|
},
|
|
};
|
|
}
|
|
|
|
return mappings;
|
|
}
|
|
|
|
static get jsonSchema() {
|
|
return {
|
|
type: 'object',
|
|
required,
|
|
properties,
|
|
};
|
|
}
|
|
}
|
|
|
|
return DynamicModel as any;
|
|
}
|
|
|
|
/**
|
|
* Convert a field definition to JSON schema property
|
|
*/
|
|
private static fieldToJsonSchema(field: FieldDefinition): Record<string, any> {
|
|
switch (field.type.toUpperCase()) {
|
|
case 'TEXT':
|
|
case 'STRING':
|
|
case 'EMAIL':
|
|
case 'URL':
|
|
case 'PHONE':
|
|
case 'PICKLIST':
|
|
case 'MULTI_PICKLIST':
|
|
return {
|
|
type: 'string',
|
|
...(field.isUnique && { uniqueItems: true }),
|
|
};
|
|
|
|
case 'LONG_TEXT':
|
|
return { type: 'string' };
|
|
|
|
case 'NUMBER':
|
|
case 'DECIMAL':
|
|
case 'CURRENCY':
|
|
case 'PERCENT':
|
|
return {
|
|
type: 'number',
|
|
...(field.isUnique && { uniqueItems: true }),
|
|
};
|
|
|
|
case 'INTEGER':
|
|
return {
|
|
type: 'integer',
|
|
...(field.isUnique && { uniqueItems: true }),
|
|
};
|
|
|
|
case 'BOOLEAN':
|
|
return { type: 'boolean', default: false };
|
|
|
|
case 'DATE':
|
|
return { type: 'string', format: 'date' };
|
|
|
|
case 'DATE_TIME':
|
|
return { type: 'string', format: 'date-time' };
|
|
|
|
case 'LOOKUP':
|
|
case 'BELONGS_TO':
|
|
return { type: 'string' };
|
|
|
|
default:
|
|
return { type: 'string' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get table name from object API name
|
|
*/
|
|
private static getTableName(objectApiName: string): string {
|
|
// Convert PascalCase/camelCase to snake_case and pluralize
|
|
const snakeCase = objectApiName
|
|
.replace(/([A-Z])/g, '_$1')
|
|
.toLowerCase()
|
|
.replace(/^_/, '');
|
|
return snakeCase.endsWith('s') ? snakeCase : `${snakeCase}s`;
|
|
}
|
|
}
|