feat: Improve field handling in BaseModel and Field decorator; ensure getter reliability and remove shadowing properties

This commit is contained in:
anonpenguin 2025-06-19 13:44:08 +03:00
parent 3a22a655b2
commit abb9734b36
2 changed files with 113 additions and 42 deletions

View File

@ -5,8 +5,6 @@ import { QueryBuilder } from '../query/QueryBuilder';
export abstract class BaseModel { export abstract class BaseModel {
// Instance properties // Instance properties
public id: string = ''; public id: string = '';
public createdAt: number = 0;
public updatedAt: number = 0;
public _loadedRelations: Map<string, any> = new Map(); public _loadedRelations: Map<string, any> = new Map();
protected _isDirty: boolean = false; protected _isDirty: boolean = false;
protected _isNew: boolean = true; protected _isNew: boolean = true;
@ -50,28 +48,30 @@ export abstract class BaseModel {
} }
} }
// Ensure Field getters work by fixing them after construction // Remove any instance properties that might shadow prototype getters
this.fixFieldGetters(); this.cleanupShadowingProperties();
} }
private fixFieldGetters(): void { private cleanupShadowingProperties(): void {
const modelClass = this.constructor as typeof BaseModel; const modelClass = this.constructor as typeof BaseModel;
// For each field, ensure the getter works by overriding it if necessary // For each field, ensure no instance properties are shadowing prototype getters
for (const [fieldName] of modelClass.fields) { for (const [fieldName] of modelClass.fields) {
const privateKey = `_${fieldName}`; // If there's an instance property, remove it and create a working getter
const currentValue = (this as any)[fieldName]; if (this.hasOwnProperty(fieldName)) {
const privateValue = (this as any)[privateKey]; delete (this as any)[fieldName];
// If getter returns undefined but private value exists, fix the getter // Define a working getter directly on the instance
if (currentValue === undefined && privateValue !== undefined) {
// Override the getter for this instance
Object.defineProperty(this, fieldName, { Object.defineProperty(this, fieldName, {
get() { get: () => {
return this[privateKey]; const privateKey = `_${fieldName}`;
return (this as any)[privateKey];
}, },
set(value) { set: (value: any) => {
this[privateKey] = value; const privateKey = `_${fieldName}`;
(this as any)[privateKey] = value;
this.markFieldAsModified(fieldName);
}, },
enumerable: true, enumerable: true,
configurable: true configurable: true
@ -87,13 +87,18 @@ export abstract class BaseModel {
if (this._isNew) { if (this._isNew) {
await this.beforeCreate(); await this.beforeCreate();
// Clean up any instance properties created by hooks
this.cleanupShadowingProperties();
// Generate ID if not provided // Generate ID if not provided
if (!this.id) { if (!this.id) {
this.id = this.generateId(); this.id = this.generateId();
} }
this.createdAt = Date.now(); // Set timestamps using Field setters
this.updatedAt = this.createdAt; const now = Date.now();
this.setFieldValue('createdAt', now);
this.setFieldValue('updatedAt', now);
// Save to database (will be implemented when database manager is ready) // Save to database (will be implemented when database manager is ready)
await this._saveToDatabase(); await this._saveToDatabase();
@ -102,10 +107,14 @@ export abstract class BaseModel {
this.clearModifications(); this.clearModifications();
await this.afterCreate(); await this.afterCreate();
// Clean up any shadowing properties created during save
this.cleanupShadowingProperties();
} else if (this._isDirty) { } else if (this._isDirty) {
await this.beforeUpdate(); await this.beforeUpdate();
this.updatedAt = Date.now(); // Set timestamp using Field setter
this.setFieldValue('updatedAt', Date.now());
// Update in database // Update in database
await this._updateInDatabase(); await this._updateInDatabase();
@ -113,6 +122,9 @@ export abstract class BaseModel {
this.clearModifications(); this.clearModifications();
await this.afterUpdate(); await this.afterUpdate();
// Clean up any shadowing properties created during save
this.cleanupShadowingProperties();
} }
return this; return this;
@ -348,19 +360,17 @@ export abstract class BaseModel {
const result: any = {}; const result: any = {};
const modelClass = this.constructor as typeof BaseModel; const modelClass = this.constructor as typeof BaseModel;
// Include all field values using their getters, with fallback to private keys // Include all field values using private keys (more reliable than getters)
for (const [fieldName] of modelClass.fields) { for (const [fieldName] of modelClass.fields) {
let value = (this as any)[fieldName]; const privateKey = `_${fieldName}`;
if (value === undefined) { const value = (this as any)[privateKey];
value = (this as any)[`_${fieldName}`]; if (value !== undefined) {
result[fieldName] = value;
} }
result[fieldName] = value;
} }
// Include basic properties // Include basic properties
result.id = this.id; result.id = this.id;
result.createdAt = this.createdAt;
result.updatedAt = this.updatedAt;
// Include loaded relations // Include loaded relations
this._loadedRelations.forEach((value, key) => { this._loadedRelations.forEach((value, key) => {
@ -393,22 +403,11 @@ export abstract class BaseModel {
const errors: string[] = []; const errors: string[] = [];
const modelClass = this.constructor as typeof BaseModel; const modelClass = this.constructor as typeof BaseModel;
// Debug property descriptors for User class
if (modelClass.name === 'User') {
const usernameDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), 'username');
const emailDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), 'email');
console.log('Username descriptor:', usernameDescriptor);
console.log('Email descriptor:', emailDescriptor);
console.log('Instance private keys:', Object.keys(this).filter(k => k.startsWith('_')));
}
// Validate each field // Validate each field using private keys (more reliable)
for (const [fieldName, fieldConfig] of modelClass.fields) { for (const [fieldName, fieldConfig] of modelClass.fields) {
// Try to get value via getter first, fallback to private key if getter fails const privateKey = `_${fieldName}`;
let value = (this as any)[fieldName]; const value = (this as any)[privateKey];
if (value === undefined) {
value = (this as any)[`_${fieldName}`];
}
const fieldErrors = this.validateField(fieldName, value, fieldConfig); const fieldErrors = this.validateField(fieldName, value, fieldConfig);
errors.push(...fieldErrors); errors.push(...fieldErrors);
} }
@ -558,6 +557,63 @@ export abstract class BaseModel {
this._isDirty = false; this._isDirty = false;
} }
// Reliable field access methods that bypass problematic getters
getFieldValue(fieldName: string): any {
// Always ensure this field's getter works properly
this.ensureFieldGetter(fieldName);
const privateKey = `_${fieldName}`;
return (this as any)[privateKey];
}
private ensureFieldGetter(fieldName: string): void {
// If there's a shadowing instance property, remove it and create a working getter
if (this.hasOwnProperty(fieldName)) {
delete (this as any)[fieldName];
// Define a working getter directly on the instance
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);
},
enumerable: true,
configurable: true
});
}
}
setFieldValue(fieldName: string, value: any): void {
// Try to use the Field decorator's setter first
try {
(this as any)[fieldName] = value;
} catch (error) {
// Fallback to setting private key directly
const privateKey = `_${fieldName}`;
(this as any)[privateKey] = value;
this.markFieldAsModified(fieldName);
}
}
getAllFieldValues(): Record<string, any> {
const modelClass = this.constructor as typeof BaseModel;
const values: Record<string, any> = {};
for (const [fieldName] of modelClass.fields) {
const value = this.getFieldValue(fieldName);
if (value !== undefined) {
values[fieldName] = value;
}
}
return values;
}
// Database operations integrated with DatabaseManager // Database operations integrated with DatabaseManager
private async _saveToDatabase(): Promise<void> { private async _saveToDatabase(): Promise<void> {
const framework = this.getFrameworkInstance(); const framework = this.getFrameworkInstance();

View File

@ -21,9 +21,24 @@ export function Field(config: FieldConfig) {
// Store the current descriptor (if any) - for future use // Store the current descriptor (if any) - for future use
const _currentDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey); const _currentDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);
// Define property with robust delegation to BaseModel methods
Object.defineProperty(target, propertyKey, { Object.defineProperty(target, propertyKey, {
get() { get() {
// Explicitly construct the private key to avoid closure issues // Check for shadowing instance property and remove it
if (this.hasOwnProperty && this.hasOwnProperty(propertyKey)) {
const descriptor = Object.getOwnPropertyDescriptor(this, propertyKey);
if (descriptor && !descriptor.get) {
// Remove shadowing value property
delete this[propertyKey];
}
}
// Use the reliable getFieldValue method if available, otherwise fallback to private key
if (this.getFieldValue && typeof this.getFieldValue === 'function') {
return this.getFieldValue(propertyKey);
}
// Fallback to direct private key access
const key = `_${propertyKey}`; const key = `_${propertyKey}`;
return this[key]; return this[key];
}, },