adonis-odm
Version:
A comprehensive MongoDB ODM for AdonisJS with Lucid-style API, type-safe relationships, embedded documents, and transaction support
227 lines (226 loc) • 8.78 kB
JavaScript
import { DatabaseOperationException, HookExecutionException } from '../exceptions/index.js';
/**
* Enhanced embedded model instance that behaves like a full model
* Provides CRUD operations that automatically sync with the parent document
*/
export class EmbeddedModelInstance {
_modelClass;
_parent;
_fieldName;
_isArrayItem;
_arrayIndex;
_instance;
constructor(modelClass, parent, fieldName, data, isArrayItem = false, arrayIndex) {
this._modelClass = modelClass;
this._parent = parent;
this._fieldName = fieldName;
this._isArrayItem = isArrayItem;
this._arrayIndex = arrayIndex;
// Create the actual model instance and hydrate it properly with naming strategy
this._instance = new modelClass();
if (data) {
this._instance.hydrateFromDocument(data);
}
// Override the instance's save method to prevent it from trying to save itself
// This is crucial for embedded documents that don't have their own _id
this._instance.save = async () => {
return this.save();
};
// Create a proxy that forwards all operations to the instance
// but intercepts save/delete operations
return new Proxy(this._instance, {
get: (target, prop, receiver) => {
// Intercept CRUD operations
if (prop === 'save') {
return this.save.bind(this);
}
if (prop === 'delete') {
return this.delete.bind(this);
}
if (prop === 'refresh') {
return this.refresh.bind(this);
}
if (prop === 'fill') {
return this.fill.bind(this);
}
// Forward all other operations to the actual instance
const value = Reflect.get(target, prop, receiver);
// If it's a function, bind it to the target
if (typeof value === 'function') {
return value.bind(target);
}
return value;
},
set: (target, prop, value, receiver) => {
// Set the value on the actual instance
const result = Reflect.set(target, prop, value, receiver);
// Mark the parent as dirty when any property changes
this.markParentDirty();
return result;
},
});
}
/**
* Save the embedded document (updates the parent document)
*/
async save() {
try {
// Execute beforeSave hooks on the embedded instance
const { executeHooks } = await import('../base_model/hooks_executor.js');
try {
if (!(await executeHooks(this._instance, 'beforeSave', this._modelClass))) {
return this._instance;
}
}
catch (error) {
throw new HookExecutionException('beforeSave', this._modelClass.name, error);
}
// Mark parent as dirty and save the parent document
this.markParentDirty();
try {
await this._parent.save();
}
catch (error) {
throw new DatabaseOperationException(`save embedded ${this._modelClass.name} via parent`, error);
}
// Execute afterSave hooks on the embedded instance
try {
await executeHooks(this._instance, 'afterSave', this._modelClass);
}
catch (error) {
throw new HookExecutionException('afterSave', this._modelClass.name, error);
}
return this._instance;
}
catch (error) {
// Re-throw our custom exceptions as-is, wrap others
if (error instanceof DatabaseOperationException || error instanceof HookExecutionException) {
throw error;
}
throw new DatabaseOperationException(`save embedded ${this._modelClass.name}`, error);
}
}
/**
* Delete the embedded document (removes it from parent and saves)
*/
async delete() {
try {
// Execute beforeDelete hooks on the embedded instance
const { executeHooks } = await import('../base_model/hooks_executor.js');
try {
if (!(await executeHooks(this._instance, 'beforeDelete', this._modelClass))) {
return false;
}
}
catch (error) {
throw new HookExecutionException('beforeDelete', this._modelClass.name, error);
}
if (this._isArrayItem) {
// Remove from array
const parentArray = this._parent[this._fieldName];
if (Array.isArray(parentArray) && this._arrayIndex !== undefined) {
parentArray.splice(this._arrayIndex, 1);
// Update array indices for remaining items
for (let i = this._arrayIndex; i < parentArray.length; i++) {
if (parentArray[i] &&
typeof parentArray[i] === 'object' &&
'_arrayIndex' in parentArray[i]) {
;
parentArray[i]._arrayIndex = i;
}
}
}
}
else {
// Set single embedded to null
;
this._parent[this._fieldName] = null;
}
// Save the parent document
try {
await this._parent.save();
}
catch (error) {
throw new DatabaseOperationException(`delete embedded ${this._modelClass.name} via parent`, error);
}
// Execute afterDelete hooks on the embedded instance
try {
await executeHooks(this._instance, 'afterDelete', this._modelClass);
}
catch (error) {
throw new HookExecutionException('afterDelete', this._modelClass.name, error);
}
return true;
}
catch (error) {
// Re-throw our custom exceptions as-is, wrap others
if (error instanceof DatabaseOperationException || error instanceof HookExecutionException) {
throw error;
}
throw new DatabaseOperationException(`delete embedded ${this._modelClass.name}`, error);
}
}
/**
* Refresh the embedded document (reloads parent and gets fresh data)
*/
async refresh() {
// Refresh the parent document by reloading it from database
const ParentClass = this._parent.constructor;
const parentId = this._parent._id;
if (parentId) {
const freshParent = await ParentClass.find(parentId);
if (freshParent) {
// Update parent with fresh data
Object.assign(this._parent, freshParent);
this._parent.syncOriginal();
}
}
// Get the fresh embedded data
let freshData;
if (this._isArrayItem) {
const parentArray = this._parent[this._fieldName];
freshData =
Array.isArray(parentArray) && this._arrayIndex !== undefined
? parentArray[this._arrayIndex]
: null;
}
else {
freshData = this._parent[this._fieldName];
}
if (freshData) {
// Update the instance with fresh data
Object.assign(this._instance, freshData);
}
return this._instance;
}
/**
* Fill the embedded document with new data
*/
fill(data) {
// Use the model's fill method if available
if (typeof this._instance.fill === 'function') {
;
this._instance.fill(data);
}
else {
// Fallback: manually assign properties
Object.assign(this._instance, data);
}
this.markParentDirty();
return this._instance;
}
/**
* Mark the parent document as dirty
*/
markParentDirty() {
if (this._isArrayItem) {
// For array items, mark the entire array as dirty
const parentArray = this._parent[this._fieldName];
this._parent.setAttribute(this._fieldName, parentArray);
}
else {
// For single embedded, mark the field as dirty
this._parent.setAttribute(this._fieldName, this._instance);
}
}
}