feat: Enhance model registration and validation with default configurations and improved error handling
This commit is contained in:
parent
f183015f47
commit
0ebdb7cbbf
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user