Fix bin/publish: use correct .env path for rspade_system Fix bin/publish script: prevent grep exit code 1 from terminating script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
345 lines
11 KiB
Plaintext
Executable File
345 lines
11 KiB
Plaintext
Executable File
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
|
|
|
|
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.
|
|
|
|
No Mass Fetching:
|
|
Framework splits array requests into individual fetch calls.
|
|
Prevents bulk data extraction without individual authorization.
|
|
|
|
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 or false
|
|
|
|
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 (!RsxAuth::check()) {
|
|
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 = RsxAuth::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 (!RsxAuth::check()) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
JAVASCRIPT USAGE
|
|
Single Record Fetching:
|
|
// Fetch single record
|
|
const product = await Product_Model.fetch(1);
|
|
if (product) {
|
|
console.log(product.name);
|
|
console.log(product.price);
|
|
}
|
|
|
|
// Handle fetch failure
|
|
const order = await Order_Model.fetch(999);
|
|
if (!order) {
|
|
console.log('Order not found or access denied');
|
|
}
|
|
|
|
Multiple Record Fetching:
|
|
// Framework automatically splits array into individual calls
|
|
const products = await Product_Model.fetch([1, 2, 3]);
|
|
products.forEach(product => {
|
|
if (product) {
|
|
console.log(product.name);
|
|
}
|
|
});
|
|
|
|
// Mixed results (some succeed, some fail authorization)
|
|
const orders = await Order_Model.fetch([101, 102, 103]);
|
|
const validOrders = orders.filter(order => order !== false);
|
|
|
|
Error Handling:
|
|
try {
|
|
const user = await User_Model.fetch(userId);
|
|
if (user) {
|
|
updateUserInterface(user);
|
|
} else {
|
|
showAccessDeniedMessage();
|
|
}
|
|
} catch (error) {
|
|
console.error('Fetch failed:', error);
|
|
showErrorMessage();
|
|
}
|
|
|
|
ARRAY HANDLING
|
|
Framework Behavior:
|
|
When JavaScript passes an array to fetch(), the framework:
|
|
1. Splits array into individual IDs
|
|
2. Calls fetch() once for each ID
|
|
3. Collects results maintaining array order
|
|
4. Returns array with same length (false for failed fetches)
|
|
|
|
Implementation Rules:
|
|
- NEVER use is_array($id) checks in fetch() method
|
|
- Always handle exactly one ID parameter
|
|
- Framework handles array splitting automatically
|
|
- Results maintain original array order
|
|
|
|
Example Results:
|
|
// JavaScript call
|
|
const results = await Product_Model.fetch([1, 2, 999]);
|
|
|
|
// Results array (999 not found or unauthorized)
|
|
[
|
|
{id: 1, name: "Product A"}, // Successful fetch
|
|
{id: 2, name: "Product B"}, // Successful fetch
|
|
false // Failed fetch
|
|
]
|
|
|
|
STUB GENERATION
|
|
Automatic JavaScript Stubs:
|
|
The framework automatically generates JavaScript stub classes
|
|
for models with #[Ajax_Endpoint_Model_Fetch] attributes.
|
|
|
|
Stub Class Generation:
|
|
// Generated stub for Product_Model
|
|
class Product_Model {
|
|
static async fetch(id) {
|
|
// Generated implementation calls PHP fetch() method
|
|
return await Rsx._internal_api_call('Product_Model', 'fetch', {id});
|
|
}
|
|
}
|
|
|
|
Bundle Integration:
|
|
Stubs are automatically included in JavaScript bundles when
|
|
models are discovered in the bundle's include paths.
|
|
|
|
AUTHORIZATION PATTERNS
|
|
User-Specific Access:
|
|
class User_Profile_Model extends Rsx_Model
|
|
{
|
|
#[Ajax_Endpoint_Model_Fetch]
|
|
public static function fetch($id)
|
|
{
|
|
$current_user = RsxAuth::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
|
|
{
|
|
#[Ajax_Endpoint_Model_Fetch]
|
|
public static function fetch($id)
|
|
{
|
|
$user = RsxAuth::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
|
|
{
|
|
#[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
|
|
|
|
TESTING FETCH METHODS
|
|
PHP Testing:
|
|
// Test authorization
|
|
$user = User_Model::factory()->create();
|
|
RsxAuth::login($user);
|
|
|
|
$product = Product_Model::fetch(1);
|
|
$this->assertNotFalse($product);
|
|
|
|
RsxAuth::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 = RsxAuth::user();
|
|
if (!$user || !$user->is_admin) {
|
|
// Remove admin-only fields for non-admin users
|
|
unset($model->internal_notes);
|
|
unset($model->cost_price);
|
|
}
|
|
|
|
return $model;
|
|
}
|
|
|
|
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 RsxAuth::check() and RsxAuth::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
|
|
|
|
Array Handling Issues:
|
|
- Never use is_array($id) in fetch() method
|
|
- Framework handles array splitting automatically
|
|
- Check for typos in model class names
|
|
- Verify all IDs in array are valid integers
|
|
|
|
SEE ALSO
|
|
controller - Internal API attribute patterns
|
|
manifest_api - Model discovery and stub generation
|
|
coding_standards - Security patterns and authorization |