UNPKG

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
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); } } }