feat: Enhance model registration and validation with default configurations and improved error handling

This commit is contained in:
anonpenguin 2025-07-09 19:43:39 +03:00
parent f183015f47
commit 0ebdb7cbbf
5 changed files with 132 additions and 42 deletions

View File

@ -6,7 +6,7 @@ export class ModelRegistry {
private static models: Map<string, typeof BaseModel> = new Map();
private static configs: Map<string, ModelConfig> = new Map();
static register(name: string, modelClass: typeof BaseModel, config: ModelConfig): void {
static register(name: string, modelClass: typeof BaseModel, config: ModelConfig = {}): void {
this.models.set(name, modelClass);
this.configs.set(name, config);

View File

@ -20,11 +20,14 @@ export abstract class BaseModel {
static hooks: Map<string, Function[]> = new Map();
constructor(data: any = {}) {
console.log(`[DEBUG] Constructing ${this.constructor.name} with data:`, data);
// Generate ID first
this.id = this.generateId();
// Apply field defaults first
this.applyFieldDefaults();
console.log(`[DEBUG] After applying defaults, instance properties:`, Object.getOwnPropertyNames(this));
// Then apply provided data, but only for properties that are explicitly provided
if (data && typeof data === 'object') {
@ -35,14 +38,21 @@ export abstract class BaseModel {
key !== '_isNew' &&
data[key] !== undefined
) {
// Always set directly - the Field decorator's setter will handle validation and transformation
try {
(this as any)[key] = data[key];
} catch (error) {
console.error(`Error setting field ${key}:`, error);
// If Field setter fails, set the private key directly
// Check if this is a field defined in the model
const modelClass = this.constructor as typeof BaseModel;
if (modelClass.fields && modelClass.fields.has(key)) {
// For model fields, store in private field
const privateKey = `_${key}`;
(this as any)[privateKey] = data[key];
console.log(`[DEBUG] Set private field ${privateKey} = ${data[key]}`);
} else {
// For non-field properties, set directly
try {
(this as any)[key] = data[key];
console.log(`[DEBUG] Set property ${key} = ${data[key]}`);
} catch (error) {
console.error(`Error setting property ${key}:`, error);
}
}
}
});
@ -55,26 +65,36 @@ export abstract class BaseModel {
// Remove any instance properties that might shadow prototype getters
this.cleanupShadowingProperties();
console.log(`[DEBUG] After cleanup, instance properties:`, Object.getOwnPropertyNames(this));
}
private cleanupShadowingProperties(): void {
const modelClass = this.constructor as typeof BaseModel;
// For each field, ensure no instance properties are shadowing prototype getters
// For each field, ensure proper getters and setters
for (const [fieldName] of modelClass.fields) {
// If there's an instance property, remove it and create a working getter
const privateKey = `_${fieldName}`;
let existingValue = (this as any)[privateKey];
// If there's an instance property, remove it and preserve its value
if (this.hasOwnProperty(fieldName)) {
const _oldValue = (this as any)[fieldName];
delete (this as any)[fieldName];
// Use the instance property value if the private field doesn't exist
if (existingValue === undefined) {
existingValue = _oldValue;
(this as any)[privateKey] = _oldValue;
}
}
// Define a working getter directly on the instance
// Always ensure the field has proper getters/setters
if (!this.hasOwnProperty(fieldName)) {
Object.defineProperty(this, fieldName, {
get: () => {
const privateKey = `_${fieldName}`;
return (this as any)[privateKey];
},
set: (value: any) => {
const privateKey = `_${fieldName}`;
(this as any)[privateKey] = value;
this.markFieldAsModified(fieldName);
},
@ -440,10 +460,20 @@ export abstract class BaseModel {
const errors: string[] = [];
const modelClass = this.constructor as typeof BaseModel;
// Validate each field using private keys (more reliable)
console.log(`[DEBUG] Validating model ${modelClass.name}`);
console.log(`[DEBUG] Available fields:`, Array.from(modelClass.fields.keys()));
console.log(`[DEBUG] Instance properties:`, Object.getOwnPropertyNames(this));
// Validate each field using getter values (more reliable)
for (const [fieldName, fieldConfig] of modelClass.fields) {
const privateKey = `_${fieldName}`;
const value = (this as any)[privateKey];
const privateValue = (this as any)[privateKey];
const propertyValue = (this as any)[fieldName];
// Use the property value (getter) if available, otherwise use private value
const value = propertyValue !== undefined ? propertyValue : privateValue;
console.log(`[DEBUG] Field ${fieldName}: privateKey=${privateKey}, privateValue=${privateValue}, propertyValue=${propertyValue}, finalValue=${value}, config=`, fieldConfig);
const fieldErrors = await this.validateField(fieldName, value, fieldConfig);
errors.push(...fieldErrors);
@ -452,6 +482,7 @@ export abstract class BaseModel {
const result = { valid: errors.length === 0, errors };
if (!result.valid) {
console.log(`[DEBUG] Validation failed:`, errors);
throw new ValidationError(errors);
}
@ -900,7 +931,9 @@ export abstract class BaseModel {
}
static query<T extends BaseModel>(this: typeof BaseModel & (new (data?: any) => T)): any {
const { QueryBuilder } = require('../query/QueryBuilder');
// Import dynamically to avoid circular dependency
const QueryBuilderModule = require('../query/QueryBuilder');
const QueryBuilder = QueryBuilderModule.QueryBuilder;
return new QueryBuilder(this);
}

View File

@ -51,28 +51,11 @@ class BlogAPIServer {
// Logging
this.app.use((req, res, next) => {
console.log(`[${this.nodeId}] ${new Date().toISOString()} ${req.method} ${req.path}`);
if (req.method === 'POST' && req.body) {
console.log(`[${this.nodeId}] Request body:`, JSON.stringify(req.body, null, 2));
}
next();
});
// Error handling
this.app.use(
(error: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(`[${this.nodeId}] Error:`, error);
if (error instanceof ValidationError) {
return res.status(400).json({
error: error.message,
field: error.field,
nodeId: this.nodeId,
});
}
res.status(500).json({
error: 'Internal server error',
nodeId: this.nodeId,
});
},
);
}
private setupRoutes() {
@ -101,20 +84,47 @@ class BlogAPIServer {
this.setupPostRoutes();
this.setupCommentRoutes();
this.setupMetricsRoutes();
// Error handling middleware must be defined after all routes
this.app.use(
(error: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(`[${this.nodeId}] Error:`, error);
if (error instanceof ValidationError) {
return res.status(400).json({
error: error.message,
field: error.field,
nodeId: this.nodeId,
});
}
res.status(500).json({
error: 'Internal server error',
nodeId: this.nodeId,
});
},
);
}
private setupUserRoutes() {
// Create user
this.app.post('/api/users', async (req, res, next) => {
try {
console.log(`[${this.nodeId}] Received user creation request:`, JSON.stringify(req.body, null, 2));
const sanitizedData = BlogValidation.sanitizeUserInput(req.body);
console.log(`[${this.nodeId}] Sanitized user data:`, JSON.stringify(sanitizedData, null, 2));
BlogValidation.validateUser(sanitizedData);
console.log(`[${this.nodeId}] User validation passed`);
const user = await User.create(sanitizedData);
console.log(`[${this.nodeId}] User created successfully:`, JSON.stringify(user, null, 2));
console.log(`[${this.nodeId}] Created user: ${user.getFieldValue('username')} (${user.id})`);
console.log(`[${this.nodeId}] Created user: ${user.username} (${user.id})`);
res.status(201).json(user);
} catch (error) {
console.error(`[${this.nodeId}] Error creating user:`, error);
next(error);
}
});
@ -221,14 +231,32 @@ class BlogAPIServer {
// Create category
this.app.post('/api/categories', async (req, res, next) => {
try {
console.log(`[${this.nodeId}] Received category creation request:`, JSON.stringify(req.body, null, 2));
const sanitizedData = BlogValidation.sanitizeCategoryInput(req.body);
console.log(`[${this.nodeId}] Sanitized category data:`, JSON.stringify(sanitizedData, null, 2));
// Generate slug if not provided
if (!sanitizedData.slug && sanitizedData.name) {
sanitizedData.slug = sanitizedData.name
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/--+/g, '-')
.replace(/^-|-$/g, '');
console.log(`[${this.nodeId}] Generated slug: ${sanitizedData.slug}`);
}
BlogValidation.validateCategory(sanitizedData);
console.log(`[${this.nodeId}] Category validation passed`);
const category = await Category.create(sanitizedData);
console.log(`[${this.nodeId}] Category created successfully:`, JSON.stringify(category, null, 2));
console.log(`[${this.nodeId}] Created category: ${category.getFieldValue('name')} (${category.id})`);
console.log(`[${this.nodeId}] Created category: ${category.name} (${category.id})`);
res.status(201).json(category);
} catch (error) {
console.error(`[${this.nodeId}] Error creating category:`, error);
next(error);
}
});
@ -276,7 +304,7 @@ class BlogAPIServer {
const post = await Post.create(sanitizedData);
console.log(`[${this.nodeId}] Created post: ${post.getFieldValue('title')} (${post.id})`);
console.log(`[${this.nodeId}] Created post: ${post.title} (${post.id})`);
res.status(201).json(post);
} catch (error) {
next(error);

View File

@ -200,12 +200,18 @@ export class Category extends BaseModel {
@BeforeCreate()
generateSlug() {
console.log(`[DEBUG] generateSlug called for category: ${this.name}`);
console.log(`[DEBUG] Current slug: ${this.slug}`);
if (!this.slug && this.name) {
this.slug = this.name
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '');
.replace(/[^a-z0-9-]/g, '')
.replace(/--+/g, '-')
.replace(/^-|-$/g, '');
console.log(`[DEBUG] Generated slug: ${this.slug}`);
}
console.log(`[DEBUG] Final slug: ${this.slug}`);
}
}
@ -411,6 +417,7 @@ export interface CreateUserRequest {
export interface CreateCategoryRequest {
name: string;
slug?: string;
description?: string;
color?: string;
}
@ -445,3 +452,24 @@ export interface UpdatePostRequest {
// Initialize field configurations after all models are defined
setupFieldConfigurations();
// Ensure static properties are set properly (for Docker environment)
UserProfile.modelName = 'UserProfile';
UserProfile.storeType = 'docstore';
UserProfile.scope = 'global';
User.modelName = 'User';
User.storeType = 'docstore';
User.scope = 'global';
Category.modelName = 'Category';
Category.storeType = 'docstore';
Category.scope = 'global';
Post.modelName = 'Post';
Post.storeType = 'docstore';
Post.scope = 'user';
Comment.modelName = 'Comment';
Comment.storeType = 'docstore';
Comment.scope = 'user';

View File

@ -176,8 +176,8 @@ export class BlogValidation {
static sanitizeUserInput(data: CreateUserRequest): CreateUserRequest {
return {
username: this.sanitizeString(data.username),
email: this.sanitizeString(data.email.toLowerCase()),
username: data.username ? this.sanitizeString(data.username) : '',
email: data.email ? this.sanitizeString(data.email.toLowerCase()) : '',
displayName: data.displayName ? this.sanitizeString(data.displayName) : undefined,
avatar: data.avatar ? this.sanitizeString(data.avatar) : undefined,
roles: data.roles ? this.sanitizeArray(data.roles) : undefined
@ -186,7 +186,8 @@ export class BlogValidation {
static sanitizeCategoryInput(data: CreateCategoryRequest): CreateCategoryRequest {
return {
name: this.sanitizeString(data.name),
name: data.name ? this.sanitizeString(data.name) : '',
slug: data.slug ? this.sanitizeString(data.slug) : undefined,
description: data.description ? this.sanitizeString(data.description) : undefined,
color: data.color ? this.sanitizeString(data.color) : undefined
};