diff --git a/src/framework/core/ModelRegistry.ts b/src/framework/core/ModelRegistry.ts index fbf0273..b8a0f61 100644 --- a/src/framework/core/ModelRegistry.ts +++ b/src/framework/core/ModelRegistry.ts @@ -6,7 +6,7 @@ export class ModelRegistry { private static models: Map = new Map(); private static configs: Map = 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); diff --git a/src/framework/models/BaseModel.ts b/src/framework/models/BaseModel.ts index 66400ee..a7f2462 100644 --- a/src/framework/models/BaseModel.ts +++ b/src/framework/models/BaseModel.ts @@ -20,11 +20,14 @@ export abstract class BaseModel { static hooks: Map = 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(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); } diff --git a/tests/real-integration/blog-scenario/docker/blog-api-server.ts b/tests/real-integration/blog-scenario/docker/blog-api-server.ts index 747dce3..0e30a7a 100644 --- a/tests/real-integration/blog-scenario/docker/blog-api-server.ts +++ b/tests/real-integration/blog-scenario/docker/blog-api-server.ts @@ -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); diff --git a/tests/real-integration/blog-scenario/models/BlogModels.ts b/tests/real-integration/blog-scenario/models/BlogModels.ts index 93ee741..dbc5b28 100644 --- a/tests/real-integration/blog-scenario/models/BlogModels.ts +++ b/tests/real-integration/blog-scenario/models/BlogModels.ts @@ -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'; diff --git a/tests/real-integration/blog-scenario/models/BlogValidation.ts b/tests/real-integration/blog-scenario/models/BlogValidation.ts index 99e4760..858e900 100644 --- a/tests/real-integration/blog-scenario/models/BlogValidation.ts +++ b/tests/real-integration/blog-scenario/models/BlogValidation.ts @@ -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 };