--- name: model-fetch description: Loading model data from JavaScript using Model.fetch() with secure opt-in, authorization, and lazy relationships. Use when implementing fetch() methods on models, loading records from JavaScript, accessing relationships via await, or understanding the Ajax_Endpoint_Model_Fetch attribute. --- # RSX Model Fetch System ## Overview RSX allows JavaScript to securely access ORM models through explicit opt-in. Unlike Laravel's API routes which can expose all fields, RSX requires each model to implement its own `fetch()` method with authorization and data filtering. --- ## Security Model - **Explicit Opt-In**: Models must implement `fetch()` with `#[Ajax_Endpoint_Model_Fetch]` - **No Default Access**: No models are fetchable by default - **Individual Authorization**: Each model controls who can fetch its records - **Data Filtering**: Complete control over what data JavaScript receives --- ## Implementing fetch() ### Basic Implementation ```php use Ajax_Endpoint_Model_Fetch; class Product_Model extends Rsx_Model_Abstract { #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { // Authorization check if (!Session::is_logged_in()) { return false; } // Fetch single record $model = static::find($id); return $model ?: false; } } ``` ### With Data Filtering ```php #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { if (!Session::is_logged_in()) { return false; } $user = static::find($id); if (!$user) { return false; } // Remove sensitive fields unset($user->password_hash); unset($user->remember_token); return $user; } ``` ### With Authorization Check ```php #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { $current_user = Session::get_user(); if (!$current_user) { return false; } $order = static::find($id); if (!$order) { return false; } // Only allow access to own orders or admin users if ($order->user_id !== $current_user->id && !$current_user->is_admin) { return false; } return $order; } ``` ### Augmented Array Return (Computed Fields) ```php #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { if (!Session::is_logged_in()) { return false; } $contact = static::find($id); if (!$contact) { return false; } // Start with toArray() to preserve __MODEL for hydration $data = $contact->toArray(); // Add computed fields $data['full_name'] = $contact->full_name(); $data['avatar_url'] = $contact->get_avatar_url(); return $data; } ``` **Important**: Always use `toArray()` as the base - it preserves `__MODEL` for JavaScript hydration. --- ## JavaScript Usage ### Fetch Single Record ```javascript // Throws if not found const project = await Project_Model.fetch(123); console.log(project.name); // Returns null if not found const maybe = await Project_Model.fetch_or_null(999); if (maybe) { console.log(maybe.name); } ``` ### Enum Properties (BEM-Style) ```javascript const project = await Project_Model.fetch(1); // Instance properties (from fetched data) console.log(project.status_id__label); // "Active" console.log(project.status_id__badge); // "bg-success" // Static constants if (project.status_id === Project_Model.STATUS_ACTIVE) { // ... } ``` --- ## Lazy Relationships Relationships can be loaded on-demand from JavaScript. The related model must also implement `fetch()` with `#[Ajax_Endpoint_Model_Fetch]`. ### belongsTo Relationship ```php // In Project_Model #[Ajax_Endpoint_Model_Fetch] public function client() { return $this->belongsTo(Client_Model::class); } ``` ```javascript const project = await Project_Model.fetch(123); const client = await project.client(); // Returns Client_Model or null console.log(client.name); ``` ### hasMany Relationship ```php // In Project_Model #[Ajax_Endpoint_Model_Fetch] public function tasks() { return $this->hasMany(Task_Model::class); } ``` ```javascript const project = await Project_Model.fetch(123); const tasks = await project.tasks(); // Returns Task_Model[] for (const task of tasks) { console.log(task.title); } ``` ### morphTo Relationship ```php // In Activity_Model #[Ajax_Endpoint_Model_Fetch] public function subject() { return $this->morphTo(); } ``` ```javascript const activity = await Activity_Model.fetch(1); const subject = await activity.subject(); // Returns polymorphic model ``` --- ## Return Value Rules | Return | Meaning | |--------|---------| | Model object | Serialized via `toArray()`, includes `__MODEL` for hydration | | Array (from `toArray()`) | Preserves `__MODEL`, can add computed fields | | `false` | Record not found or unauthorized | **MUST return `false`** (not `null`) when record is not found or unauthorized. --- ## Anti-Aliasing Policy **NEVER alias enum properties in fetch()** - BEM-style naming exists for grepability: ```php // WRONG - Aliasing obscures data source $data['type_label'] = $record->type_id__label; // RIGHT - Use full BEM-style names in JavaScript contact.type_id__label // Grepable, self-documenting ``` The `fetch()` method's purpose is **security** (removing private data), not aliasing. --- ## Common Patterns ### In SPA Action on_load() ```javascript async on_load() { const project = await Project_Model.fetch(this.args.id); this.data.project = project; } ``` ### Loading with Relationships ```javascript async on_load() { const project = await Project_Model.fetch(this.args.id); const [client, tasks] = await Promise.all([ project.client(), project.tasks() ]); this.data.project = project; this.data.client = client; this.data.tasks = tasks; } ``` ### Conditional Relationship Loading ```javascript async on_load() { const order = await Order_Model.fetch(this.args.id); this.data.order = order; // Only load customer if needed if (this.args.show_customer) { this.data.customer = await order.customer(); } } ``` ## More Information Details: `php artisan rsx:man model_fetch`