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

288 lines
6.1 KiB
Markdown
Executable File

---
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`