MODEL_FETCH(3) RSX Framework Manual MODEL_FETCH(3) NAME model_fetch - RSX Ajax ORM with secure model fetching from JavaScript SYNOPSIS Secure, controlled access to ORM models from JavaScript with explicit opt-in DESCRIPTION The RSX model fetch system allows JavaScript to securely access ORM models through an explicit opt-in mechanism. Unlike Laravel's default API routes which expose all model data, RSX requires each model to implement its own fetch() method with authorization and data filtering. Key differences from Laravel: - Laravel: API routes with resource controllers expose all fields - RSX: Each model implements custom fetch() with authorization Benefits: - Explicit security control per model - No mass exposure of sensitive data - Individual authorization checks for each record - Automatic JavaScript stub generation STATUS (as of 2025-11-23) The Model Fetch system is fully implemented for application development MVP. All core functionality documented in this manual is production-ready: Implemented: - fetch() and fetch_or_null() methods - Lazy relationship loading (belongsTo, hasMany, morphTo, etc.) - Enum properties on instances (BEM-style {column}__{field} pattern) - Static enum constants and accessor methods - Automatic model hydration from Ajax responses - JavaScript class hierarchy with Base_* stubs - Authorization patterns and security model Future enhancements (documented below under FUTURE DEVELOPMENT) will be additive and non-breaking to the existing API. SECURITY MODEL Explicit Opt-In: Models must deliberately implement fetch() with the attribute. No models are fetchable by default. Individual Authorization: Each model controls who can fetch its records. Authorization checked for every single record. Data Filtering: Models can filter sensitive fields before returning data. Complete control over what data JavaScript receives. Single Record Fetching: Each fetch() call retrieves exactly one record. See FUTURE DEVELOPMENT for planned batch fetching. IMPLEMENTING FETCHABLE MODELS Required Components: 1. Override static fetch() method 2. Add #[Ajax_Endpoint_Model_Fetch] attribute 3. Accept exactly one parameter: $id (single ID only) 4. Implement authorization checks 5. Return model object, custom array, or false Return Value Options: - Model object: Serialized via toArray(), includes __MODEL for hydration - Augmented array: Start with $model->toArray(), add computed fields (recommended) - false: Record not found or unauthorized (MUST be false, not null) Note: Always use toArray() as the base when returning arrays. This preserves the __MODEL property required for JavaScript hydration. See "Augmented Array Return" section below for the correct pattern. Basic Implementation: use Ajax_Endpoint_Model_Fetch; class Product_Model extends Rsx_Model { #[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; } } Advanced Authorization: class Order_Model extends Rsx_Model { #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { $user = Session::get_user(); if (!$user) { return false; } $order = static::find($id); if (!$order) { return false; } // Only allow access to own orders or admin users if ($order->user_id !== $user->id && !$user->is_admin) { return false; } return $order; } } Data Filtering: class User_Model extends Rsx_Model { #[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); unset($user->email_verification_token); return $user; } } Augmented Array Return (for model method outputs only): class Contact_Model extends Rsx_Model { // Model methods that produce NEW computed values public function full_name(): string { return trim($this->first_name . ' ' . $this->last_name); } public function mailto_link(): string { return '' . e($this->email) . ''; } #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { $contact = static::withTrashed()->find($id); if (!$contact) { return false; } // Start with model's toArray() to get __MODEL and base data $data = $contact->toArray(); // Augment ONLY with outputs from defined model methods $data['full_name'] = $contact->full_name(); $data['mailto_link'] = $contact->mailto_link(); return $data; } } IMPORTANT: Always start with $model->toArray() when augmenting data. This preserves the __MODEL property needed for JavaScript hydration. DO NOT manually construct the return array like this (outdated pattern): return [ 'id' => $client->id, 'name' => $client->name, // ... manually listing fields ]; This loses __MODEL and returns a plain object instead of a model instance. FETCH() IS FOR SECURITY, NOT ALIASING The fetch() method exists for ONE purpose: security filtering - removing private data that the current user shouldn't see. It is NOT for: - Renaming fields (aliasing) - Formatting dates (use Rsx_Date/Rsx_Time on client) - Adding computed properties that should be in enums - Reshaping data for frontend convenience ANTI-ALIASING POLICY RSpade considers aliasing in fetch() an anti-pattern. Enum magic properties use BEM-style double underscore naming specifically to be grepable and self-documenting. Why aliasing is harmful: 1. Makes grep searches unreliable (can't find all usages of type_id__label) 2. Adds no value (we're not paying by the byte in source code) 3. Creates maintenance burden (two names for the same thing) 4. Obscures the data source (is 'type_label' a DB column or computed?) VALID PATTERNS IN FETCH() 1. Security removal (the primary purpose): unset($data['password_hash']); unset($data['api_secret']); 2. Model method with MATCHING name: $data['full_name'] = $model->full_name(); $data['formatted_address'] = $model->formatted_address(); $data['unread_count'] = $model->unread_count(); The key MUST match the method name. This ensures: - Single source of truth (method defines the computation) - Same name in PHP and JavaScript - Grepable across codebase 3. Conditional with matching property or method: $data['foo'] = $condition ? $model->foo : null; $data['jazz'] = $model->jazz_allowed() ? $model->jazz : 'Not permitted'; $data['secret'] = $user->is_admin ? $model->secret : '[REDACTED]'; Both sides of the ternary must use matching property/method names or be literal values. This allows conditional defaults or permission- based field masking. INVALID PATTERNS (Violations) 1. Aliasing enum properties: $data['type_label'] = $model->type_id__label; // BAD - use full name $data['status_badge'] = $model->status_id__badge; // BAD - use full name 2. Date formatting: $data['created_formatted'] = $model->created_at->format('M d'); // BAD $data['updated_human'] = $model->updated_at->diffForHumans(); // BAD // Dates are YYYY-mm-dd, datetimes are ISO UTC - format on client! 3. Relationship plucking without method: $data['client_name'] = $model->client->name; // BAD - make a method 4. Inline computations: $data['is_owner'] = $model->user_id === Session::get_user_id(); // BAD // Make a method: $data['is_owner'] = $model->is_owner(); 5. Redundant explicit assignments: $data['id'] = $model->id; // UNNECESSARY - already in toArray() $data['name'] = $model->name; // UNNECESSARY - already in toArray() 6. Mismatched method names: $data['addr'] = $model->formatted_address(); // BAD - name must match // CORRECT: $data['formatted_address'] = $model->formatted_address(); CLIENT-SIDE PATTERNS Use full BEM-style names in JavaScript: contact.type_id__label contact.type_id__icon Format dates on client: Rsx_Date.format(contact.created_at) // "Dec 24, 2025" Rsx_Time.relative(contact.updated_at) // "2 hours ago" JAVASCRIPT USAGE Single Record Fetching: // fetch() throws if record not found - no need to check for null/false const project = await Project_Model.fetch(1); console.log(project.name); console.log(project.status_label); // Enum properties populated // fetch() errors are caught by Universal_Error_Page_Component automatically // in SPA actions, or can be caught with try/catch if needed Fetch With Null Fallback: // Use fetch_or_null() when you want graceful handling of missing records const order = await Order_Model.fetch_or_null(999); if (!order) { console.log('Order not found or access denied'); return; } // order is guaranteed to exist here When to Use Each: - fetch() - View/edit pages where record MUST exist (throws on not found) - fetch_or_null() - Optional lookups where missing is valid (returns null) Enum Properties on Instances (BEM-style double underscore): const project = await Project_Model.fetch(1); // All enum helper properties from PHP are available console.log(project.status_id); // 2 (raw value) console.log(project.status_id__label); // "Active" console.log(project.status_id__badge); // "bg-success" Static Enum Constants: // Constants available on the class if (project.status_id === Project_Model.STATUS_ACTIVE) { console.log('Project is active'); } // Get all enum values for dropdowns const statusOptions = Project_Model.status_id__enum_select(); // {1: "Planning", 2: "Active", 3: "On Hold", ...} // Get full enum config with all metadata const statusConfig = Project_Model.status_id__enum(); // {1: {label: "Planning", badge: "bg-info"}, ...} // Get single enum's metadata by ID const activeConfig = Project_Model.status_id__enum(Project_Model.STATUS_ACTIVE); // {label: "Active", badge: "bg-success", ...} Error Handling: // In SPA actions, errors bubble up to Universal_Error_Page_Component // No try/catch needed - just call fetch() and use the result async on_load() { this.data.user = await User_Model.fetch(this.args.id); // If we get here, user exists and we have access } // For explicit error handling outside SPA context: try { const user = await User_Model.fetch(userId); updateUserInterface(user); } catch (error) { if (error.code === Ajax.ERROR_NOT_FOUND) { showNotFoundMessage(); } else { showErrorMessage(error.message); } } JAVASCRIPT CLASS ARCHITECTURE Class Hierarchy: The framework generates a three-level class hierarchy for each model: Rsx_Js_Model // Framework base (fetch, refresh, toObject) └── Base_Project_Model // Generated stub (enums, constants, relationships) └── Project_Model // Concrete class (auto-generated or user-defined) Base Stub Classes (Auto-Generated): For each PHP model extending Rsx_Model_Abstract, the framework generates a Base_* stub class with: class Base_Project_Model extends Rsx_Js_Model { static __MODEL = 'Project_Model'; // PHP class name for API calls // Enum constants static STATUS_PLANNING = 1; static STATUS_ACTIVE = 2; // Enum accessor methods (BEM-style double underscore) static status__enum() { ... } // Full enum config (all values) static status__enum(id) { ... } // Single enum's metadata by ID static status__enum_select() { ... } // For dropdown population static status__enum_labels() { ... } // Simple id => label map static status__enum_ids() { ... } // Array of valid IDs // Relationship discovery static get_relationships() { ... } // Returns array of names // Relationship methods (async, lazy-loaded) async client() { ... } // belongsTo → Model or null async tasks() { ... } // hasMany → Model[] } Concrete Classes: Concrete classes (without Base_ prefix) are what you use in application code. They are either auto-generated or user-defined. Auto-Generated (default): If no custom JS file exists, the bundle compiler generates: class Project_Model extends Base_Project_Model {} User-Defined (optional): Create a JS file with matching class name to add custom methods: // rsx/models/Project_Model.js class Project_Model extends Base_Project_Model { get_display_title() { return `${this.name} (${this.status_label})`; } } Custom Base Class (Optional): Configure a custom base class in rsx/resource/config/rsx.php to add application-wide model functionality: 'js_model_base_class' => 'App_Model_Abstract', Hierarchy becomes: Rsx_Js_Model → App_Model_Abstract → Base_Project_Model → Project_Model Bundle Integration: - Base_* stubs auto-included when PHP model is in bundle - Concrete classes auto-generated unless user-defined JS exists - User-defined JS classes validated to extend Base_* directly - Error thrown if custom JS exists in manifest but not in bundle AUTHORIZATION PATTERNS User-Specific Access: class User_Profile_Model extends Rsx_Model_Abstract { #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { $current_user = Session::get_user(); // Users can only fetch their own profile if (!$current_user || $current_user->id != $id) { return false; } return static::find($id); } } Role-Based Access: class Admin_Report_Model extends Rsx_Model_Abstract { #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { $user = Session::get_user(); // Only admin users can fetch reports if (!$user || !$user->hasRole('admin')) { return false; } return static::find($id); } } Public Data Access: class Public_Article_Model extends Rsx_Model_Abstract { #[Ajax_Endpoint_Model_Fetch] public static function fetch($id) { // Public articles can be fetched by anyone $article = static::find($id); // But only return published articles if ($article && $article->status === 'published') { return $article; } return false; } } BASE MODEL PROTECTION Default Behavior: The base Rsx_Model::fetch() method throws an exception with clear instructions on how to implement fetch() in your model. Exception Message: "Model MyModel does not implement fetch() method. To enable Ajax fetching, add #[Ajax_Endpoint_Model_Fetch] attribute and implement static fetch($id) method with authorization checks." Purpose: - Prevents accidental exposure of model data - Forces developers to explicitly handle security - Provides clear implementation guidance - Ensures no models are fetchable by default CODE QUALITY VALIDATION The framework validates #[Ajax_Endpoint_Model_Fetch] attribute placement at manifest build time. The attribute can ONLY be applied to: 1. Methods marked with #[Relationship] - exposes relationship to JavaScript 2. The static fetch($id) method - enables Model.fetch() in JavaScript Invalid Placement: // This will fail code quality check (MODEL-AJAX-FETCH-01) #[Ajax_Endpoint_Model_Fetch] public function get_display_name() // Not a relationship or fetch() { return $this->name; } For Custom Server-Side Methods: If you need a custom method accessible from JavaScript: 1. Create a JS class extending Base_{ModelName} 2. Create an Ajax endpoint on an appropriate controller 3. Add the method to the JS class calling the Ajax endpoint Example: // PHP Controller #[Ajax_Endpoint] public static function get_project_stats(Request $request, array $params = []) { return Project_Model::calculate_stats($params['id']); } // JavaScript (rsx/models/Project_Model.js) class Project_Model extends Base_Project_Model { async get_stats() { return await Project_Controller.get_project_stats({id: this.id}); } } TESTING FETCH METHODS PHP Testing: // Test authorization $user = User_Model::factory()->create(); Session::login($user); $product = Product_Model::fetch(1); $this->assertNotFalse($product); Session::logout(); $product = Product_Model::fetch(1); $this->assertFalse($product); JavaScript Testing (via rsx:debug): php artisan rsx:debug /demo --eval="Product_Model.fetch(1).then(r => console.log(r))" COMMON PATTERNS Soft Delete Handling: public static function fetch($id) { // Only return non-deleted records $model = static::where('id', $id)->whereNull('deleted_at')->first(); return $model ?: false; } Relationship Preloading: public static function fetch($id) { $model = static::with(['category', 'tags'])->find($id); return $model ?: false; } Conditional Field Removal: public static function fetch($id) { $model = static::find($id); if (!$model) return false; $user = Session::get_user(); if (!$user || !$user->is_admin) { // Remove admin-only fields for non-admin users unset($model->internal_notes); unset($model->cost_price); } return $model; } AUTOMATIC HYDRATION All Ajax responses are automatically processed to convert objects with a __MODEL property into proper JavaScript class instances. This happens transparently in the Ajax layer. How It Works: 1. PHP model's toArray() includes __MODEL property with class name 2. Ajax layer receives response with __MODEL: "Project_Model" 3. Ajax calls Rsx_Js_Model._instantiate_models_recursive() on response 4. Hydrator looks up class via Manifest.get_class_by_name() 5. If class extends Rsx_Js_Model, creates instance: new Project_Model(data) 6. Instance constructor strips __MODEL, assigns remaining properties Result: const project = await Project_Model.fetch(1); console.log(project.constructor.name); // "Project_Model" console.log(project instanceof Rsx_Js_Model); // true Recursive Hydration: The hydrator processes nested objects and arrays recursively. Any object with __MODEL property at any depth will be instantiated as its class. // If PHP returns related models in response: { id: 1, name: "Project Alpha", client: { id: 5, name: "Acme Corp", __MODEL: "Client_Model" }, __MODEL: "Project_Model" } // Both objects are hydrated: project instanceof Project_Model; // true project.client instanceof Client_Model; // true Global Behavior: This hydration applies to ALL Ajax responses, not just fetch() calls. Any Ajax endpoint returning objects with __MODEL properties will have them automatically instantiated as proper JavaScript class instances. TROUBLESHOOTING Model Not Fetchable: - Verify #[Ajax_Endpoint_Model_Fetch] attribute present - Check fetch() method is static and public - Ensure method accepts single $id parameter - Verify model included in bundle manifest Authorization Failures: - Check Session::is_logged_in() and Session::get_user() values - Verify authorization logic in fetch() method - Test with different user roles and permissions - Use rsx_dump_die() to debug authorization flow JavaScript Stub Missing: - Verify model discovered during manifest build - Check bundle includes model's directory - Ensure bundle compiles without errors - Confirm JavaScript bundle loads in browser JAVASCRIPT ORM ARCHITECTURE The JavaScript ORM provides secure, read-only access to server-side models through explicit opt-in. This section describes the full architecture vision. Design Philosophy: - Read-only ORM: No save/create/delete operations from JavaScript - Explicit opt-in: Each model controls its own fetchability - Individual authorization: Per-record security checks - No query builder: Search/filter handled by dedicated Ajax endpoints - Instance methods: Records come as objects with helper methods What JavaScript ORM DOES: - Fetch individual records by ID - Provide enum helper methods on instances - Load related records via lazy relationship methods - Access attachments associated with records (see FUTURE DEVELOPMENT) - Receive real-time updates via websocket (see FUTURE DEVELOPMENT) What JavaScript ORM does NOT do: - Save or update records (use Ajax endpoints) - Create new records (use Ajax endpoints) - Delete records (use Ajax endpoints) - Search or query records (use DataGrid or Ajax) - Eager load relationships (lazy load only) LAZY RELATIONSHIPS Related records load through async methods that mirror PHP relationships. Each relationship method returns a Promise resolving to the related model(s). Supported Relationship Types: - belongsTo → Returns single model instance or null - hasOne → Returns single model instance or null - hasMany → Returns array of model instances (may be empty) - morphTo → Returns single model instance or null - morphOne → Returns single model instance or null - morphMany → Returns array of model instances (may be empty) JavaScript Usage: // Load belongsTo relationship const contact = await Contact_Model.fetch(123); const client = await contact.client(); // Returns Client_Model or null // Load hasMany relationship const client = await Client_Model.fetch(456); const contacts = await client.contacts(); // Returns Contact_Model[] // Load morphMany relationship const project = await Project_Model.fetch(1); const tasks = await project.tasks(); // Returns Task_Model[] // Chain relationships const contact = await Contact_Model.fetch(123); const client = await contact.client(); if (client) { const contacts = await client.contacts(); } Get Available Relationships: // Static method returns array of relationship names const rels = Project_Model.get_relationships(); // ["client", "contact", "tasks", "created_by", "owner"] Security Model: 1. Source model's fetch() called first to verify access to parent record 2. Relationship method called to get related IDs (efficient pluck query) 3. Each related record passed through its model's fetch() for authorization 4. Only records passing fetch() security check are returned Enabling Relationships for Ajax Fetch: Relationships require BOTH attributes to be fetchable from JavaScript: #[Relationship] #[Ajax_Endpoint_Model_Fetch] public function contacts() { return $this->hasMany(Contact_Model::class, 'client_id'); } Without #[Ajax_Endpoint_Model_Fetch], the relationship will not appear in get_relationships() and attempting to fetch it will return an error message instructing the developer to add the attribute. Implementation Notes: - #[Relationship] defines the method as a Laravel relationship - #[Ajax_Endpoint_Model_Fetch] exposes it to the JavaScript ORM - Only relationships with BOTH attributes are included in JS stubs - Related model must also have fetch() with #[Ajax_Endpoint_Model_Fetch] - Singular relationships return null if not found or unauthorized - Plural relationships return empty array if none found/authorized ENUM PROPERTIES Enum values are exposed as properties on fetched model instances, mirroring the PHP magic property behavior. Each custom field defined in the enum becomes a property using BEM-style naming: {column}__{field} (double underscore). PHP Enum Definition: public static $enums = [ 'status_id' => [ 1 => ['constant' => 'STATUS_PLANNING', 'label' => 'Planning', 'badge' => 'bg-info'], 2 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', 'badge' => 'bg-success'], 3 => ['constant' => 'STATUS_ON_HOLD', 'label' => 'On Hold', 'badge' => 'bg-warning'], ], ]; Resulting JavaScript Instance Properties (BEM-style double underscore): const project = await Project_Model.fetch(123); // Raw enum value project.status_id // 2 // Auto-generated properties from enum definition project.status_id__label // "Active" project.status_id__badge // "bg-success" // All custom fields become properties // If enum had 'button_class' => 'btn-success': project.status_id__button_class // "btn-success" Static Enum Constants: // Constants available on the class (from 'constant' field) if (project.status_id === Project_Model.STATUS_ACTIVE) { console.log('Project is active'); } Static Enum Methods (BEM-style double underscore): // Get full enum config (all values with metadata) const statusConfig = Project_Model.status_id__enum(); // {1: {label: "Planning", badge: "bg-info"}, 2: {...}, ...} // Get single enum's metadata by ID const activeConfig = Project_Model.status_id__enum(Project_Model.STATUS_ACTIVE); // {label: "Active", badge: "bg-success", ...} // Get enum values for dropdown population (id => label) const statusOptions = Project_Model.status_id__enum_select(); // {1: "Planning", 2: "Active", 3: "On Hold"} // Get simple id => label map (all items) const labels = Project_Model.status_id__enum_labels(); // {1: "Planning", 2: "Active", 3: "On Hold"} // Get array of valid IDs const ids = Project_Model.status_id__enum_ids(); // [1, 2, 3] MODEL CONSTANTS All public constants defined on a PHP model are automatically exported to the JavaScript stub as static properties. This includes both enum constants (generated from $enums) and manually defined constants. PHP Model Definition: class User_Model extends Rsx_Model_Abstract { // Enum constants (auto-generated by rsx:migrate:document_models) const ROLE_ADMIN = 1; const ROLE_USER = 2; // Permission constants (manually defined) const PERM_MANAGE_USERS = 1; const PERM_EDIT_DATA = 2; const PERM_VIEW_DATA = 3; // Invitation status constants const INVITATION_PENDING = 'pending'; const INVITATION_ACCEPTED = 'accepted'; const INVITATION_EXPIRED = 'expired'; } JavaScript Usage: // All constants available as static properties if (permission === User_Model.PERM_MANAGE_USERS) { showAdminPanel(); } if (invite.status === User_Model.INVITATION_PENDING) { showPendingBadge(); } SECURITY WARNING: All public constants are exported to JavaScript and visible in the browser. NEVER put sensitive values in model constants: // NEVER DO THIS - secrets visible in browser const API_SECRET = 'sk-abc123...'; const ENCRYPTION_KEY = 'my-secret-key'; Use private constants for sensitive values: // Safe - not exported to JavaScript private const API_SECRET = 'sk-abc123...'; Constant Types Exported: - Enum constants (from $enums 'constant' field) - Non-enum public constants (string, int, float, bool, array) - Constants with no visibility modifier (treated as public) Constants NOT Exported: - Private constants (private const FOO = 1) - Protected constants (protected const BAR = 2) - Constants inherited from parent classes - Framework constants from Rsx_Model_Abstract =============================================================================== FUTURE DEVELOPMENT =============================================================================== The features below are planned enhancements. They will be additive and will not break the existing API. Implementation priority and timeline are not committed. ATTACHMENTS INTEGRATION (planned) File attachments accessible through model instances. JavaScript Usage (API TBD): const project = await Project_Model.fetch(123); // Get all attachments const attachments = await project.attachments(); // Get specific attachment type const documents = await project.attachments('documents'); // Get attachment URLs attachments.forEach(att => { console.log(att.url); // View URL console.log(att.download_url); // Download URL console.log(att.thumbnail_url); // Thumbnail (images) }); DATE HANDLING (planned) Date fields converted to JavaScript Date objects or date library instances. Implementation details TBD - may use native Date or dayjs. JavaScript Usage (API TBD): const project = await Project_Model.fetch(123); // Access date fields project.created_at // Date object or dayjs instance project.due_date // Date object or dayjs instance // Format dates project.created_at.format('YYYY-MM-DD') // If dayjs project.created_at.toLocaleDateString() // If native Date REAL-TIME UPDATES (planned) Model instances can receive real-time updates via websocket. JavaScript Usage (API TBD): const project = await Project_Model.fetch(123); // Subscribe to updates project.subscribe((updatedProject) => { // Called when server broadcasts changes updateUI(updatedProject); }); // Or use event-based approach project.on('update', (changes) => { // Partial update with changed fields }); TAGS SYSTEM (future) Tags integration for taggable models (implementation TBD). Potential API: const project = await Project_Model.fetch(123); const tags = await project.tags(); // ['urgent', 'q4-2025'] FETCH BATCHING (planned, Phase 2) Automatic batching of multiple fetch requests made within same tick. Reduces HTTP requests and database queries when loading multiple records. Behavior: - Multiple Model.fetch() calls in same tick batched into single request - Server groups requests by model, uses IN clause for efficient queries - Only enabled when Ajax request batching is enabled (config) - Integrates with existing Ajax batching system Example - Without Batching (3 HTTP requests, 3 queries): const client = await Client_Model.fetch(1); const contact = await Contact_Model.fetch(5); const project = await Project_Model.fetch(10); Example - With Batching (1 HTTP request, 3 queries): // Same code, but requests batched automatically // Server receives: {Client_Model: [1], Contact_Model: [5], Project_Model: [10]} Example - Same Model Batching (1 HTTP request, 1 query with IN clause): const clients = await Promise.all([ Client_Model.fetch(1), Client_Model.fetch(2), Client_Model.fetch(3) ]); // Server receives: {Client_Model: [1, 2, 3]} // Executes: SELECT * FROM clients WHERE id IN (1, 2, 3) Server Implementation: - Orm_Controller receives batched request with model->ids map - Groups IDs by model class - Executes single query per model using WHERE id IN (...) - Returns results keyed by model and id - Client resolves individual promises from batched response Prerequisites: - Ajax request batching enabled in config - Works alongside existing Ajax.call() batching JQHTML CACHE INTEGRATION (planned, Phase 2, low priority) Integration with jqhtml's component caching for instant first renders. Visual polish feature - avoids brief loading indicators, not a performance gain. Problem: - jqhtml caches on_load results for instant duplicate component renders - First render still shows loading indicator while fetching - If ORM data already cached from prior fetch, loading indicator unnecessary Solution: - Provide jqhtml with mock data fetch functions during first render - Mock functions check ORM cache, return cached data if available - If cache hit: on_load completes synchronously, no loading indicator - If cache miss: falls back to normal async fetch with loading indicator Example Scenario: // User views Contact #5, data cached const contact = await Contact_Model.fetch(5); // User navigates away, then back to same contact // Without integration: loading indicator shown briefly // With integration: cached data used, renders instantly Implementation Notes: - ORM maintains client-side cache of fetched records - jqhtml receives cache-aware fetch stubs during render - Cache keyed by model name + id - Cache invalidation TBD (TTL, manual, websocket push) Priority: Low - purely visual improvement, no server/client perf benefit BATCH SECURITY OPTIMIZATION (todo) Current Limitation: The static fetch($id) method signature accepts a single ID, which means when fetching multiple related records (e.g., via relationship loading), each record requires a separate fetch($id) call and database query. This prevents optimization with IN (id1, id2, ...) queries when loading relationship result sets. Problem Scenario: // Loading a client's 50 contacts via relationship // Currently requires 50 individual fetch() calls: foreach ($contact_ids as $id) { $contact = Contact_Model::fetch($id); // Individual query each } // Cannot optimize to: // SELECT * FROM contacts WHERE id IN (1, 2, 3, ...) Proposed Solutions: Option A - Query Prefetch Cache: Before iterating through relationship results, prefetch all IDs with a single query and cache the results. Individual fetch() calls then hit the cache instead of the database. // Pseudocode $ids = $relationship->pluck('id'); static::prefetch_cache($ids); // Single IN query, cache results foreach ($ids as $id) { $record = static::fetch($id); // Hits cache, not database } Benefits: No API change, backwards compatible Drawback: Cache management complexity, memory usage Option B - Batch fetch() Variant: Add a new method like fetch_batch($ids) that accepts an array and returns filtered results using single query. // fetch_batch must apply same security logic as fetch() public static function fetch_batch(array $ids) { $records = static::whereIn('id', $ids)->get(); return $records->filter(fn($r) => static::can_fetch($r)); } Benefits: Clean API, explicit batch operation Drawback: Requires refactoring existing security logic Option C - Automatic Query Batching Layer: Database query layer automatically batches identical queries made within same request cycle using query deduplication. Benefits: Transparent, no code changes Drawback: Complex implementation, limited optimization scope Recommended Approach: Option A (Query Prefetch Cache) is likely the best balance of simplicity and effectiveness. It can be implemented without changing the fetch() API contract, and the cache can be request-scoped to avoid memory/staleness issues. Implementation Priority: Medium Required before relationship fetching is considered production-ready for models with large relationship sets. OPT-IN FETCH CACHING (todo) Current Limitation: When a model's fetch() method returns an array (for augmented data), relationship fetching must call the database twice: once via fetch() to verify access, and once via find() to get the actual Eloquent model for relationship method calls. Proposed Solution - Request-Scoped Cache: Implement opt-in caching of fetch() results scoped to the current page/action lifecycle. Cache automatically invalidates on navigation or form submission. Cache Scope: - Traditional pages: Single page instance (cache lives until navigation) - SPA applications: Single SPA action (cache resets on action navigation) Cache Invalidation Events: 1. Page navigation (traditional or SPA) 2. Rsx_Form submission (any form submit clears cache) 3. Developer-initiated (explicit cache clear call) Opt-In API (TBD): // Enable caching for a model class Project_Model extends Rsx_Model_Abstract { protected static $fetch_cache_enabled = true; } // Manual invalidation Project_Model.clear_fetch_cache(); // Clear single model cache Rsx_Js_Model.clear_all_fetch_caches(); // Clear all model caches Implementation Notes: - Cache keyed by model class + record ID - Cache stores the raw fetch() result (model or array) - For array results, also cache the underlying Eloquent model - Relationship fetching checks cache before database query - SPA integration via Spa.on('action:change') event - Form integration via Rsx_Form.on('submit') event Benefits: - Eliminates duplicate database queries for relationship fetching - Enables instant re-renders when returning to cached records - Predictable invalidation tied to user actions - Opt-in prevents unexpected caching behavior Implementation Priority: Medium Required before relationship fetching is considered production-ready for models where fetch() returns augmented arrays. PAGINATED RELATIONSHIP RESULTS (todo, low priority) Current Limitation: Relationship methods return all related records at once. For models with large relationship sets (e.g., a client with 500 contacts), this causes excessive data transfer and memory usage. Proposed Solution - Paginated Relationship API: Design a JavaScript API for paginated relationship fetching that feels natural and is easy to use for common UI patterns like infinite scroll, "load more" buttons, and traditional pagination. API Design Goals: - Simple default case: first page with sensible limit - Easy iteration for "load more" patterns - Support for offset/limit and cursor-based pagination - Chainable or async iterator patterns - Clear indication of "has more" and total count Potential API Patterns (TBD): Option A - Paginated Method Variant: const page1 = await client.contacts_paginated({limit: 20}); // { data: Contact_Model[], has_more: true, total: 150, next_cursor: '...' } const page2 = await client.contacts_paginated({cursor: page1.next_cursor}); Option B - Async Iterator: for await (const contact of client.contacts_iterator({batch: 20})) { // Yields one Contact_Model at a time, fetches in batches } Option C - Collection Object: const contacts = await client.contacts({paginate: true, limit: 20}); // Returns Paginated_Collection object contacts.data // Contact_Model[] (current page) contacts.has_more // boolean contacts.total // number (if available) await contacts.next() // Load next page, returns same collection await contacts.all() // Load all remaining (with warning for large sets) Option D - Parameter on Existing Method: const contacts = await client.contacts({limit: 20, offset: 0}); // Returns array but truncated to limit const more = await client.contacts({limit: 20, offset: 20}); Server-Side Considerations: - Orm_Controller::fetch_relationship() needs pagination params - Efficient COUNT query for total (optional, can skip for performance) - Cursor-based pagination for stable ordering during iteration - Security: pagination params must not bypass fetch() security checks UI Integration Patterns: - DataGrid: Already handles pagination, may not need ORM integration - Detail views: "Show more" buttons for related records - Infinite scroll: Async iterator or cursor-based pagination - Modal lists: Traditional page numbers Implementation Priority: Low Most large relationship sets are better served by DataGrid with server-side filtering. This feature is for edge cases where ORM-style access is preferred over DataGrid. SEE ALSO crud(3) - Standard CRUD implementation using Model.fetch() controller(3) - Internal API attribute patterns manifest_api(3) - Model discovery and stub generation coding_standards(3) - Security patterns and authorization enums(3) - Model enum definitions and magic properties RSX Framework 2025-11-23 MODEL_FETCH(3)