Files
rspade_system/app/RSpade/man/model_fetch.txt
2025-12-26 02:17:31 +00:00

1141 lines
44 KiB
Plaintext
Executable File

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 '<a href="mailto:' . e($this->email) . '">' . e($this->email) . '</a>';
}
#[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)