feat: Improve field handling in BaseModel and Field decorator; ensure getter reliability and remove shadowing properties
This commit is contained in:
parent
3a22a655b2
commit
abb9734b36
@ -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();
|
||||||
|
@ -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];
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user