UNPKG

adonis-odm

Version:

A comprehensive MongoDB ODM for AdonisJS with Lucid-style API, type-safe relationships, embedded documents, and transaction support

216 lines (215 loc) 8.18 kB
import { DateTime } from 'luxon'; import { DatabaseOperationException, HookExecutionException } from '../exceptions/index.js'; /** * PersistenceManager - Handles database persistence operations * * This class encapsulates all the logic for saving, deleting, inserting, * and updating models in the database. */ export class PersistenceManager { model; constructor(model) { this.model = model; } /** * Save the model to the database */ async save() { try { const { executeHooks } = await import('./hooks_executor.js'); const modelClass = this.model.constructor; // Execute beforeSave hook try { if (!(await executeHooks(this.model, 'beforeSave', modelClass))) { return this.model; // Operation aborted by hook } } catch (error) { throw new HookExecutionException('beforeSave', modelClass.name, error); } const isNew = !this.model.$isPersisted; if (isNew) { // Execute beforeCreate hook for new models try { if (!(await executeHooks(this.model, 'beforeCreate', modelClass))) { return this.model; // Operation aborted by hook } } catch (error) { throw new HookExecutionException('beforeCreate', modelClass.name, error); } } else { // Execute beforeUpdate hook for existing models try { if (!(await executeHooks(this.model, 'beforeUpdate', modelClass))) { return this.model; // Operation aborted by hook } } catch (error) { throw new HookExecutionException('beforeUpdate', modelClass.name, error); } } this.applyTimestamps(); let dbOperationSuccessful = false; try { if (isNew) { await this.performInsert(); } else { await this.performUpdate(); } dbOperationSuccessful = true; } catch (error) { // If DB operation failed, don't run after hooks const operation = isNew ? 'insert' : 'update'; throw new DatabaseOperationException(`${operation} on ${modelClass.name}`, error); } if (dbOperationSuccessful) { this.syncOriginal(); this.model.$dirty = {}; this.model.$isPersisted = true; this.model.$isLocal = false; // Execute afterSave hook try { await executeHooks(this.model, 'afterSave', modelClass); } catch (error) { throw new HookExecutionException('afterSave', modelClass.name, error); } if (isNew) { // Execute afterCreate hook for new models try { await executeHooks(this.model, 'afterCreate', modelClass); } catch (error) { throw new HookExecutionException('afterCreate', modelClass.name, error); } } else { // Execute afterUpdate hook for existing models try { await executeHooks(this.model, 'afterUpdate', modelClass); } catch (error) { throw new HookExecutionException('afterUpdate', modelClass.name, error); } } } return this.model; } catch (error) { // Re-throw our custom exceptions as-is, wrap others if (error instanceof DatabaseOperationException || error instanceof HookExecutionException) { throw error; } const modelClass = this.model.constructor; throw new DatabaseOperationException(`save on ${modelClass.name}`, error); } } /** * Delete the model from the database */ async delete() { try { if (!this.model.$isPersisted || !this.model._id) { return false; } const { executeHooks } = await import('./hooks_executor.js'); const modelClass = this.model.constructor; // Execute beforeDelete hook try { if (!(await executeHooks(this.model, 'beforeDelete', modelClass))) { return false; // Operation aborted by hook } } catch (error) { throw new HookExecutionException('beforeDelete', modelClass.name, error); } let result; try { const queryOptions = this.model.$trx ? { client: this.model.$trx } : undefined; result = await this.model.constructor .query(queryOptions) .where('_id', this.model._id) .delete(); } catch (error) { throw new DatabaseOperationException(`delete on ${modelClass.name}`, error); } if (result > 0) { this.model.$isPersisted = false; this.model.$isLocal = true; // Execute afterDelete hook try { await executeHooks(this.model, 'afterDelete', modelClass); } catch (error) { throw new HookExecutionException('afterDelete', modelClass.name, error); } return true; } return false; } catch (error) { // Re-throw our custom exceptions as-is, wrap others if (error instanceof DatabaseOperationException || error instanceof HookExecutionException) { throw error; } const modelClass = this.model.constructor; throw new DatabaseOperationException(`delete on ${modelClass.name}`, error); } } /** * Apply automatic timestamps */ applyTimestamps() { const metadata = this.model.constructor.getMetadata(); const now = DateTime.now(); for (const [key, options] of metadata.columns) { const dateOptions = options; if (dateOptions.autoCreate && !this.model.$isPersisted) { this.model.setAttribute(key, now); } if (dateOptions.autoUpdate) { this.model.setAttribute(key, now); } } } /** * Perform database insert */ async performInsert() { const document = this.model.toDocument(); // Remove _id if it's undefined to let MongoDB generate it if (!document._id) { delete document._id; } // This would use the actual database connection // For now, we'll simulate the operation throw new Error('Database insert not implemented. Please register the MongoDB ODM provider.'); } /** * Perform database update */ async performUpdate() { if (!this.model._id) { throw new Error('Cannot update model without an ID'); } const updates = this.model.getDirtyAttributes(); if (Object.keys(updates).length === 0) { return; // Nothing to update } // This would use the actual database connection // For now, we'll simulate the operation throw new Error('Database update not implemented. Please register the MongoDB ODM provider.'); } /** * Sync the original values with current values */ syncOriginal() { this.model.$original = { ...this.model.toDocument() }; this.model.$dirty = {}; } }