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>
288 lines
6.1 KiB
Markdown
Executable File
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`
|