UNPKG

adonis-odm

Version:

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

348 lines (347 loc) 10.6 kB
import { CamelCaseNamingStrategy, } from '../naming_strategy/naming_strategy.js'; import { ModelRegistry } from './model_registry.js'; import { AttributeManager } from './attribute_manager.js'; import { SerializationManager } from './serialization_manager.js'; import { PersistenceManager } from './persistence_manager.js'; import { StaticQueryMethods } from './static_query_methods.js'; /** * Symbol to store model metadata */ export const MODEL_METADATA = Symbol('model_metadata'); /** * Base Model class for MongoDB ODM */ export class BaseModel { /** * Naming strategy for the model * Defaults to CamelCaseNamingStrategy which converts camelCase to snake_case for serialization */ static namingStrategy = new CamelCaseNamingStrategy(); /** * Indicates if the model instance exists in the database */ $isPersisted = false; /** * Indicates if the model instance is local and not persisted */ $isLocal = true; /** * Tracks modified properties */ $dirty = {}; /** * Original values before modifications */ $original = {}; /** * MongoDB document ID */ _id; /** * Transaction client for this model instance */ $trx; /** * Attribute manager instance */ $attributeManager; /** * Serialization manager instance */ $serializationManager; /** * Persistence manager instance */ $persistenceManager; constructor(attributes = {}) { // Initialize managers this.$attributeManager = new AttributeManager(this); this.$serializationManager = new SerializationManager(this); this.$persistenceManager = new PersistenceManager(this); this.fill(attributes); // For new models, mark all filled attributes as dirty so hooks can detect them if (!this.$isPersisted) { for (const [key, value] of Object.entries(attributes)) { const metadata = this.constructor.getMetadata(); const columnOptions = metadata.columns.get(key); // Skip reference fields and computed properties if (!columnOptions?.isReference && !columnOptions?.isComputed) { this.$dirty[key] = value; } } } this.initializeRelationshipProxies(); // Auto-register the model class in the registry ModelRegistry.register(this.constructor); } /** * Get model metadata */ static getMetadata() { if (!this[MODEL_METADATA]) { ; this[MODEL_METADATA] = { columns: new Map(), primaryKey: '_id', tableName: undefined, }; } return this[MODEL_METADATA]; } /** * Register a model in the global registry */ static register() { ModelRegistry.register(this); } /** * Get a model class from the registry */ static getModelClass(modelName) { return ModelRegistry.get(modelName); } /** * Check if a model is registered */ static hasModelClass(modelName) { return ModelRegistry.has(modelName); } /** * Get all registered models */ static getAllModels() { return ModelRegistry.getAll(); } /** * Clear the model registry (useful for testing) */ static clearRegistry() { ModelRegistry.clear(); } /** * Associate this model instance with a transaction */ useTransaction(trx) { this.$trx = trx; return this; } /** * Static collection name property (Lucid pattern) * Override this in your model to specify a custom collection name */ static collection; /** * Get collection name for the model * * Priority order: * 1. Static collection property (recommended Lucid pattern) * 2. Metadata tableName (backward compatibility) * 3. Auto-generated from class name */ static getCollectionName() { // First priority: static collection property (Lucid pattern) if (this.collection) { return this.collection; } // Second priority: metadata tableName (for backward compatibility) const metadata = this.getMetadata(); if (metadata.tableName) { return metadata.tableName; } // Third priority: auto-generate from class name // Convert class name to snake_case and pluralize const className = this.name; const snakeCase = className .replace(/([A-Z])/g, '_$1') .toLowerCase() .replace(/^_/, ''); // Better pluralization logic // Handle common patterns like "User" -> "users", "AdminUser" -> "admin_users" const words = snakeCase.split('_'); // Helper function to pluralize a word const pluralize = (word) => { // Handle words ending in 'y' preceded by a consonant if (word.endsWith('y') && word.length > 1) { const beforeY = word[word.length - 2]; // If the letter before 'y' is a vowel, just add 's' (e.g., "key" -> "keys") // If it's a consonant, replace 'y' with 'ies' (e.g., "city" -> "cities") if ('aeiou'.includes(beforeY)) { return word + 's'; } else { return word.slice(0, -1) + 'ies'; } } else if (word.endsWith('s') || word.endsWith('sh') || word.endsWith('ch') || word.endsWith('x') || word.endsWith('z')) { return word + 'es'; } else { return word + 's'; } }; // Pluralize only the last word (the main entity) // Examples: "AdminUser" -> "admin_users", "UserProfile" -> "user_profiles" if (words.length > 0) { words[words.length - 1] = pluralize(words[words.length - 1]); } return words.join('_'); } /** * Get connection name for the model */ static getConnection() { return 'mongodb'; // Default connection name } /** * Create a new query builder instance with type-safe relationship loading */ static query(options) { return StaticQueryMethods.query(this, options); } /** * Find a document by its ID */ static async find(id, options) { return StaticQueryMethods.find(this, id, options); } /** * Find a document by its ID or throw an exception */ static async findOrFail(id, options) { return StaticQueryMethods.findOrFail(this, id, options); } /** * Find a document by a specific field */ static async findBy(field, value) { return StaticQueryMethods.findBy(this, field, value); } /** * Find a document by a specific field or throw an exception */ static async findByOrFail(field, value) { return StaticQueryMethods.findByOrFail(this, field, value); } /** * Get the first document */ static async first() { return StaticQueryMethods.first(this); } /** * Get the first document or throw an exception */ static async firstOrFail() { return StaticQueryMethods.firstOrFail(this); } /** * Get all documents */ static async all() { return StaticQueryMethods.all(this); } /** * Create a new document */ static async create(attributes, options) { return StaticQueryMethods.create(this, attributes, options); } /** * Create multiple documents */ static async createMany(attributesArray) { return StaticQueryMethods.createMany(this, attributesArray); } /** * Update or create a document */ static async updateOrCreate(searchPayload, persistencePayload) { return StaticQueryMethods.updateOrCreate(this, searchPayload, persistencePayload); } /** * Fill the model with attributes */ fill(attributes) { this.$attributeManager.fill(attributes); return this; } /** * Merge new attributes into the model */ merge(attributes) { this.$attributeManager.merge(attributes); return this; } /** * Save the model to the database */ async save() { await this.$persistenceManager.save(); return this; } /** * Delete the model from the database */ async delete() { return this.$persistenceManager.delete(); } /** * Get an attribute value */ getAttribute(key) { return this.$attributeManager.getAttribute(key); } /** * Set an attribute value */ setAttribute(key, value) { this.$attributeManager.setAttribute(key, value); } /** * Hydrate the model from a MongoDB document * Handles conversion from database column names (snake_case) back to model properties (camelCase) */ hydrateFromDocument(document) { this.$serializationManager.hydrateFromDocument(document); } /** * Convert the model to a plain object for database storage * Following AdonisJS Lucid naming strategy (camelCase -> snake_case by default) */ toDocument() { return this.$serializationManager.toDocument(); } /** * Convert the model to a plain object for JSON serialization (API responses) * This includes computed properties and properly serializes relationships * Following AdonisJS Lucid naming strategy (camelCase -> snake_case by default) */ toJSON() { return this.$serializationManager.toJSON(); } /** * Get dirty attributes for the model * Returns attributes with database column names (snake_case) for database operations */ getDirtyAttributes() { return this.$attributeManager.getDirtyAttributes(); } /** * Sync the original values with current values */ syncOriginal() { this.$attributeManager.syncOriginal(); } /** * Initialize relationship proxies for Lucid-style property access * Note: Relationship proxies are now created directly by the decorators */ initializeRelationshipProxies() { // Relationship proxies are automatically created by the @hasOne, @hasMany, @belongsTo decorators // No manual initialization needed } }