adonis-odm
Version:
A comprehensive MongoDB ODM for AdonisJS with Lucid-style API, type-safe relationships, embedded documents, and transaction support
217 lines (216 loc) • 8.23 kB
JavaScript
import { DateTime } from 'luxon';
import { Decimal128 } from 'mongodb';
/**
* SerializationManager - Handles model serialization and deserialization
*
* This class encapsulates all the logic for converting models to/from
* different formats like JSON, MongoDB documents, etc.
*/
export class SerializationManager {
model;
constructor(model) {
this.model = model;
}
/**
* Convert model to MongoDB document format
*/
toDocument() {
const metadata = this.model.constructor.getMetadata();
const document = {};
// Get all column metadata
for (const [key, columnOptions] of metadata.columns) {
// Skip reference fields (virtual properties) and computed properties
if (columnOptions.isReference || columnOptions.isComputed) {
continue;
}
const value = this.getAttributeValue(key);
if (value !== undefined) {
// Apply naming strategy to convert property name to database column name
const dbColumnName = this.model.constructor.namingStrategy.columnName(this.model.constructor, key);
// Serialize the value for database storage
const serializedValue = this.serializeValue(value, key);
document[dbColumnName] = serializedValue;
}
}
// Handle _id field specially
if (this.model._id !== undefined) {
document._id = this.model._id;
}
return document;
}
/**
* Convert model to JSON format
*/
toJSON() {
const metadata = this.model.constructor.getMetadata();
const json = {};
// Get all column metadata
for (const [key] of metadata.columns) {
const value = this.getAttributeValue(key);
if (value !== undefined) {
// Apply naming strategy to convert property name to JSON key
const jsonKey = this.model.constructor.namingStrategy.serializedName(this.model.constructor, key);
// Serialize the value for JSON
const serializedValue = this.serializeValueForJSON(value, key);
json[jsonKey] = serializedValue;
}
}
// Handle _id field specially - convert to string for JSON
if (this.model._id !== undefined) {
json._id = this.model._id.toString();
}
return json;
}
/**
* Hydrate model from MongoDB document
*/
hydrateFromDocument(document) {
const metadata = this.model.constructor.getMetadata();
// Set the _id field
if (document._id) {
this.model._id = document._id;
}
// Process each column
for (const [key, columnOptions] of metadata.columns) {
// Skip reference fields (virtual properties) and computed properties during hydration
if (columnOptions.isReference || columnOptions.isComputed) {
continue;
}
// Apply naming strategy to get database column name
const dbColumnName = this.model.constructor.namingStrategy.columnName(this.model.constructor, key);
const value = document[dbColumnName];
if (value !== undefined) {
let processedValue = value;
// Apply deserialize function if available
if (columnOptions.deserialize) {
processedValue = columnOptions.deserialize(value);
}
// Set the value using the private key for columns with metadata
const privateKey = `_${key}`;
this.model[privateKey] = processedValue;
}
}
// Mark as persisted and not dirty
this.model.$isPersisted = true;
this.model.$isLocal = false;
this.model.$dirty = {};
// Sync original values
this.syncOriginal();
}
/**
* Get attribute value (similar to AttributeManager but for internal use)
*/
getAttributeValue(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
if (columnOptions && !columnOptions.isReference) {
return this.model[key];
}
// For properties without metadata or reference fields, access directly
return this.model[key];
}
/**
* Serialize value for MongoDB storage
*/
serializeValue(value, key) {
const metadata = this.model.constructor.getMetadata();
const columnOptions = metadata.columns.get(key);
// Apply serialize function if available
if (columnOptions?.serialize) {
return columnOptions.serialize(value);
}
// Handle DateTime objects
if (value instanceof DateTime) {
return value.toJSDate();
}
// Handle Date objects
if (value instanceof Date) {
return value;
}
// Handle arrays
if (Array.isArray(value)) {
return value.map((item) => this.serializeValue(item, key));
}
// Handle objects (but not null)
if (value && typeof value === 'object') {
// If it's a model instance, convert to document
if (value.toDocument && typeof value.toDocument === 'function') {
return value.toDocument();
}
// For plain objects, serialize recursively
const serialized = {};
for (const [objKey, objValue] of Object.entries(value)) {
serialized[objKey] = this.serializeValue(objValue, key);
}
return serialized;
}
return value;
}
/**
* Serialize value for JSON output
*/
serializeValueForJSON(value, key) {
const metadata = this.model.constructor.getMetadata();
const columnOptions = metadata.columns.get(key);
// Apply serialize function if available
if (columnOptions?.serialize) {
const serialized = columnOptions.serialize(value);
// Further process the serialized value for JSON
return this.processValueForJSON(serialized);
}
return this.processValueForJSON(value);
}
/**
* Process value for JSON output
*/
processValueForJSON(value) {
// Handle DateTime objects
if (value instanceof DateTime) {
return value.toISO();
}
// Handle Date objects
if (value instanceof Date) {
return value.toISOString();
}
// Handle MongoDB Decimal128 objects
if (value instanceof Decimal128) {
return Number.parseFloat(value.toString());
}
// Handle MongoDB decimal objects (BSON format)
if (value && typeof value === 'object' && value.$numberDecimal) {
return Number.parseFloat(value.$numberDecimal);
}
// Handle arrays
if (Array.isArray(value)) {
return value.map((item) => this.processValueForJSON(item));
}
// Handle objects (but not null)
if (value && typeof value === 'object') {
// If it's a model instance, convert to JSON
if (value.toJSON && typeof value.toJSON === 'function') {
return value.toJSON();
}
// For plain objects, process recursively
const processed = {};
for (const [objKey, objValue] of Object.entries(value)) {
processed[objKey] = this.processValueForJSON(objValue);
}
return processed;
}
return value;
}
/**
* Sync original values with current values
*/
syncOriginal() {
this.model.$original = {};
const metadata = this.model.constructor.getMetadata();
for (const [key] of metadata.columns) {
const value = this.getAttributeValue(key);
if (value !== undefined) {
this.model.$original[key] = value;
}
}
}
}