Files
rspade_system/docs/skills/model-fetch/SKILL.md
root 1b46c5270c Add skills documentation and misc updates
Add form value persistence across cache revalidation re-renders

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 04:38:06 +00:00

6.1 KiB
Executable File

name, description
name description
model-fetch 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

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

#[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

#[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)

#[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

// 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)

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

// In Project_Model
#[Ajax_Endpoint_Model_Fetch]
public function client()
{
    return $this->belongsTo(Client_Model::class);
}
const project = await Project_Model.fetch(123);
const client = await project.client();  // Returns Client_Model or null
console.log(client.name);

hasMany Relationship

// In Project_Model
#[Ajax_Endpoint_Model_Fetch]
public function tasks()
{
    return $this->hasMany(Task_Model::class);
}
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

// In Activity_Model
#[Ajax_Endpoint_Model_Fetch]
public function subject()
{
    return $this->morphTo();
}
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:

// 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()

async on_load() {
    const project = await Project_Model.fetch(this.args.id);
    this.data.project = project;
}

Loading with Relationships

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

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