UNPKG

adonis-odm

Version:

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

541 lines (540 loc) 19.4 kB
/** * SEAMLESS RELATIONSHIP PROXIES - LIKE ADONISJS LUCID! * * This module implements advanced JavaScript Proxy patterns to achieve seamless * property access for relationships, exactly like AdonisJS Lucid. * * Key Features: * - Direct property access: user.profile.firstName * - Array access for HasMany: user.posts[0].title * - Transparent method forwarding * - Zero developer overhead * - Full TypeScript IntelliSense support */ /** * SEAMLESS HASONE PROXY * * Creates a proxy that transparently forwards property access to the related model * when loaded, providing seamless access like: user.profile.firstName */ export function createHasOneProxy(relatedModel, _options = {}) { let related = null; let isLoaded = false; const proxy = new Proxy({}, { get(target, prop, receiver) { // Handle relationship-specific methods first if (prop === 'related') { return related; } if (prop === 'load') { return async () => { // TODO: Implement actual loading logic isLoaded = true; return related; }; } if (prop === 'create') { return async (attributes) => { // TODO: Implement create logic const RelatedModelClass = relatedModel(); const instance = new RelatedModelClass(); Object.assign(instance, attributes); related = instance; return instance; }; } if (prop === 'save') { return async (model) => { // TODO: Implement save logic related = model; return model; }; } if (prop === 'isLoaded') { return isLoaded; } if (prop === 'query') { return () => { // TODO: Return a query builder for the related model const RelatedModelClass = relatedModel(); return RelatedModelClass.query(); }; } // Handle special symbols and methods if (typeof prop === 'symbol') { return Reflect.get(target, prop, receiver); } // Handle inspection methods if (prop === 'toString' || prop === 'valueOf' || prop === 'inspect') { return () => `[HasOne Relationship: ${related ? 'loaded' : 'not loaded'}]`; } // SEAMLESS PROPERTY FORWARDING - THE MAGIC! // If the relationship is loaded, forward property access to the related model if (related && prop in related) { const value = related[prop]; // If it's a function, bind it to the related model if (typeof value === 'function') { return value.bind(related); } return value; } // If not loaded, return undefined (like AdonisJS Lucid) return undefined; }, set(_target, prop, value) { // Allow setting the related model if (prop === 'related') { related = value; isLoaded = value !== null && value !== undefined; // Mark as loaded when related data is set return true; } // Forward property setting to the related model if loaded if (related && typeof prop === 'string') { ; related[prop] = value; return true; } return false; }, has(_target, prop) { // Check relationship methods first if (prop === 'related' || prop === 'load' || prop === 'create' || prop === 'save' || prop === 'isLoaded' || prop === 'query') { return true; } // Check if property exists on related model if (related) { return prop in related; } return false; }, ownKeys(_target) { const keys = ['related', 'load', 'create', 'save', 'isLoaded', 'query']; if (related) { keys.push(...Object.keys(related)); } return keys; }, getOwnPropertyDescriptor(_target, prop) { if (prop === 'related' || prop === 'load' || prop === 'create' || prop === 'save' || prop === 'isLoaded' || prop === 'query') { return { enumerable: true, configurable: true }; } if (related && prop in related) { return Object.getOwnPropertyDescriptor(related, prop); } return undefined; }, }); return proxy; } /** * SEAMLESS HASMANY PROXY * * Creates a proxy that provides array-like access and seamless property forwarding * for HasMany relationships, like: user.posts[0].title */ export function createHasManyProxy(relatedModel, _options = {}) { let relatedArray = []; let isLoaded = false; const proxy = new Proxy([], { get(_target, prop, _receiver) { // Handle relationship-specific methods first if (prop === 'related') { return relatedArray; } if (prop === 'load') { return async () => { // TODO: Implement actual loading logic isLoaded = true; return relatedArray; }; } if (prop === 'create') { return async (attributes) => { // TODO: Implement create logic const RelatedModelClass = relatedModel(); const instance = new RelatedModelClass(); Object.assign(instance, attributes); relatedArray.push(instance); return instance; }; } if (prop === 'createMany') { return async (attributesArray) => { // TODO: Implement createMany logic const RelatedModelClass = relatedModel(); const instances = attributesArray.map((attributes) => { const instance = new RelatedModelClass(); Object.assign(instance, attributes); return instance; }); relatedArray.push(...instances); return instances; }; } if (prop === 'saveMany') { return async (models) => { // TODO: Implement saveMany logic relatedArray.push(...models); return models; }; } if (prop === 'isLoaded') { return isLoaded; } if (prop === 'query') { return () => { // TODO: Return a query builder for the related model const RelatedModelClass = relatedModel(); return RelatedModelClass.query(); }; } if (prop === 'save') { return async (model) => { // TODO: Implement save logic for single model relatedArray.push(model); return model; }; } // Handle array properties and methods if (prop === 'length') { return relatedArray.length; } if (prop === 'forEach') { return (callback) => { relatedArray.forEach(callback); }; } if (prop === 'map') { return (callback) => { return relatedArray.map(callback); }; } if (prop === 'filter') { return (callback) => { return relatedArray.filter(callback); }; } if (prop === 'find') { return (callback) => { return relatedArray.find(callback); }; } if (prop === 'push') { return (...items) => { return relatedArray.push(...items); }; } if (prop === 'pop') { return () => { return relatedArray.pop(); }; } if (prop === 'shift') { return () => { return relatedArray.shift(); }; } if (prop === 'unshift') { return (...items) => { return relatedArray.unshift(...items); }; } if (prop === 'slice') { return (start, end) => { return relatedArray.slice(start, end); }; } if (prop === 'splice') { return (start, deleteCount, ...items) => { return relatedArray.splice(start, deleteCount || 0, ...items); }; } // Handle numeric indices if (typeof prop === 'string' && /^\d+$/.test(prop)) { const index = Number.parseInt(prop, 10); return relatedArray[index]; } // Handle Symbol.iterator for for...of loops if (prop === Symbol.iterator) { return function* () { for (const item of relatedArray) { yield item; } }; } // Handle special symbols and methods if (typeof prop === 'symbol') { return Reflect.get(relatedArray, prop); } // Handle inspection methods if (prop === 'toString' || prop === 'valueOf' || prop === 'inspect') { return () => `[HasMany Relationship: ${relatedArray.length} items]`; } return undefined; }, set(_target, prop, value) { // Allow setting the related array if (prop === 'related') { relatedArray = Array.isArray(value) ? value : []; isLoaded = true; return true; } // Handle numeric indices if (typeof prop === 'string' && /^\d+$/.test(prop)) { const index = Number.parseInt(prop, 10); relatedArray[index] = value; return true; } // Handle length property if (prop === 'length') { relatedArray.length = value; return true; } return false; }, has(_target, prop) { // Check relationship methods first if (prop === 'related' || prop === 'load' || prop === 'create' || prop === 'createMany' || prop === 'save' || prop === 'saveMany' || prop === 'isLoaded' || prop === 'query' || prop === 'length' || prop === 'forEach' || prop === 'map' || prop === 'filter' || prop === 'find' || prop === 'push' || prop === 'pop' || prop === 'shift' || prop === 'unshift' || prop === 'slice' || prop === 'splice') { return true; } // Check numeric indices if (typeof prop === 'string' && /^\d+$/.test(prop)) { const index = Number.parseInt(prop, 10); return index >= 0 && index < relatedArray.length; } return false; }, ownKeys(_target) { const keys = [ 'related', 'load', 'create', 'createMany', 'save', 'saveMany', 'isLoaded', 'query', 'length', 'forEach', 'map', 'filter', 'find', 'push', 'pop', 'shift', 'unshift', 'slice', 'splice', ]; // Add numeric indices for (let i = 0; i < relatedArray.length; i++) { keys.push(i.toString()); } return keys; }, getOwnPropertyDescriptor(_target, prop) { if (prop === 'related' || prop === 'load' || prop === 'create' || prop === 'createMany' || prop === 'save' || prop === 'saveMany' || prop === 'isLoaded' || prop === 'query' || prop === 'length' || prop === 'forEach' || prop === 'map' || prop === 'filter' || prop === 'find' || prop === 'push' || prop === 'pop' || prop === 'shift' || prop === 'unshift' || prop === 'slice' || prop === 'splice') { return { enumerable: true, configurable: true }; } // Handle numeric indices if (typeof prop === 'string' && /^\d+$/.test(prop)) { const index = Number.parseInt(prop, 10); if (index >= 0 && index < relatedArray.length) { return { enumerable: true, configurable: true, writable: true }; } } return undefined; }, }); return proxy; } /** * SEAMLESS BELONGSTO PROXY * * Creates a proxy that transparently forwards property access to the related model * when loaded, providing seamless access like: post.author.name */ export function createBelongsToProxy(relatedModel, _options = {}) { let related = null; let isLoaded = false; const proxy = new Proxy({}, { get(target, prop, receiver) { // Handle relationship-specific methods first if (prop === 'related') { return related; } if (prop === 'load') { return async () => { // TODO: Implement actual loading logic isLoaded = true; return related; }; } if (prop === 'associate') { return async (model) => { // TODO: Implement associate logic related = model; return model; }; } if (prop === 'dissociate') { return async () => { // TODO: Implement dissociate logic related = null; }; } if (prop === 'isLoaded') { return isLoaded; } if (prop === 'query') { return () => { // TODO: Return a query builder for the related model const RelatedModelClass = relatedModel(); return RelatedModelClass.query(); }; } // Handle special symbols and methods if (typeof prop === 'symbol') { return Reflect.get(target, prop, receiver); } // Handle inspection methods if (prop === 'toString' || prop === 'valueOf' || prop === 'inspect') { return () => `[BelongsTo Relationship: ${related ? 'loaded' : 'not loaded'}]`; } // SEAMLESS PROPERTY FORWARDING - THE MAGIC! // If the relationship is loaded, forward property access to the related model if (related && prop in related) { const value = related[prop]; // If it's a function, bind it to the related model if (typeof value === 'function') { return value.bind(related); } return value; } // If not loaded, return undefined (like AdonisJS Lucid) return undefined; }, set(_target, prop, value) { // Allow setting the related model if (prop === 'related') { related = value; isLoaded = value !== null && value !== undefined; // Mark as loaded when related data is set return true; } // Forward property setting to the related model if loaded if (related && typeof prop === 'string') { ; related[prop] = value; return true; } return false; }, has(_target, prop) { // Check relationship methods first if (prop === 'related' || prop === 'load' || prop === 'associate' || prop === 'dissociate' || prop === 'isLoaded' || prop === 'query') { return true; } // Check if property exists on related model if (related) { return prop in related; } return false; }, ownKeys(_target) { const keys = ['related', 'load', 'associate', 'dissociate', 'isLoaded', 'query']; if (related) { keys.push(...Object.keys(related)); } return keys; }, getOwnPropertyDescriptor(_target, prop) { if (prop === 'related' || prop === 'load' || prop === 'associate' || prop === 'dissociate' || prop === 'isLoaded' || prop === 'query') { return { enumerable: true, configurable: true }; } if (related && prop in related) { return Object.getOwnPropertyDescriptor(related, prop); } return undefined; }, }); return proxy; } /** * PROXY FACTORY FUNCTIONS * * These functions create the appropriate proxy based on relationship type */ /** * Create a relationship proxy based on metadata */ export function createRelationshipProxy(type, relatedModel, options = {}) { switch (type) { case 'hasOne': return createHasOneProxy(relatedModel, options); case 'hasMany': return createHasManyProxy(relatedModel, options); case 'belongsTo': return createBelongsToProxy(relatedModel, options); default: throw new Error(`Unknown relationship type: ${type}`); } } /** * SEAMLESS TYPE SAFETY EXPORTS */