adonis-odm
Version:
A comprehensive MongoDB ODM for AdonisJS with Lucid-style API, type-safe relationships, embedded documents, and transaction support
148 lines (147 loc) • 6.08 kB
JavaScript
/**
* AttributeManager - Handles attribute getting, setting, and management
*
* This class encapsulates all the logic for managing model attributes,
* including getting, setting, filling, and merging attributes.
*/
export class AttributeManager {
model;
constructor(model) {
this.model = model;
}
/**
* Fill model with attributes
*/
fill(attributes) {
for (const [key, value] of Object.entries(attributes)) {
const metadata = this.model.constructor.getMetadata();
const columnOptions = metadata.columns.get(key);
// Skip reference fields (virtual properties) and computed properties
if (columnOptions?.isReference || columnOptions?.isComputed) {
continue;
}
let processedValue = value;
if (columnOptions?.deserialize) {
processedValue = columnOptions.deserialize(value);
}
// If this is a column with a property descriptor, use the private key
if (columnOptions && !columnOptions.isReference && !columnOptions.isComputed) {
const privateKey = `_${key}`;
this.model[privateKey] = processedValue;
}
else {
;
this.model[key] = processedValue;
}
}
return this.model;
}
/**
* Merge new attributes into the model
*/
merge(attributes) {
for (const [key, value] of Object.entries(attributes)) {
const metadata = this.model.constructor.getMetadata();
const columnOptions = metadata.columns.get(key);
// Skip reference fields (virtual properties) and computed properties
if (columnOptions?.isReference || columnOptions?.isComputed) {
continue;
}
if (this.getAttribute(key) !== value) {
this.setAttribute(key, value);
// setAttribute already handles dirty tracking, but we need to ensure it's set
if (this.model.$isPersisted) {
this.model.$dirty[key] = value;
}
}
}
return this.model;
}
/**
* Get an attribute value
*/
getAttribute(key) {
const metadata = this.model.constructor.getMetadata();
const columnOptions = metadata.columns.get(key);
// If this is a column with metadata (and not a reference), use the property getter
// which will access the private key through the property descriptor
if (columnOptions && !columnOptions.isReference) {
return this.model[key];
}
// For properties without metadata or reference fields, access directly
return this.model[key];
}
/**
* Set an attribute value
*/
setAttribute(key, value) {
const metadata = this.model.constructor.getMetadata();
const columnOptions = metadata.columns.get(key);
// Skip setting reference fields (virtual properties) and computed properties - they have getters only
if (columnOptions?.isReference || columnOptions?.isComputed) {
return;
}
if (columnOptions?.deserialize) {
value = columnOptions.deserialize(value);
}
// If this is a column with a property descriptor, use the private key
if (columnOptions) {
const privateKey = `_${key}`;
const oldValue = this.model[privateKey];
this.model[privateKey] = value;
// Track dirty attributes if the model is persisted and value changed
if (this.model.$isPersisted && oldValue !== value) {
this.model.$dirty[key] = value;
}
}
else {
// For properties without column metadata, check if it's a getter-only property
const descriptor = Object.getOwnPropertyDescriptor(this.model, key) ||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this.model), key);
if (descriptor && descriptor.get && !descriptor.set) {
// This is a getter-only property, skip setting it
return;
}
// Set directly for properties without metadata
const oldValue = this.model[key];
this.model[key] = value;
// Track dirty attributes if the model is persisted and value changed
if (this.model.$isPersisted && oldValue !== value) {
this.model.$dirty[key] = value;
}
}
}
/**
* Get dirty attributes
* Returns attributes with database column names (snake_case) for database operations
*/
getDirtyAttributes() {
const metadata = this.model.constructor.getMetadata();
const namingStrategy = this.model.constructor.namingStrategy;
const dirtyAttributes = {};
// Convert property names to database column names and apply serialization
for (const [propertyName, value] of Object.entries(this.model.$dirty)) {
const columnOptions = metadata.columns.get(propertyName);
// Skip reference fields (virtual properties) and computed properties
if (columnOptions?.isReference || columnOptions?.isComputed) {
continue;
}
// Convert property name to database column name using naming strategy
const dbColumnName = namingStrategy.columnName(this.model.constructor, propertyName);
// Apply serialization if defined for this column
let serializedValue = value;
if (columnOptions?.serialize) {
serializedValue = columnOptions.serialize(value);
}
dirtyAttributes[dbColumnName] = serializedValue;
}
return dirtyAttributes;
}
/**
* Sync original values with current values
*/
syncOriginal() {
this.model.$original = { ...this.model.toDocument() };
this.model.$dirty = {};
}
}