Add notification system with Rsx_Throttle utility

Add action log feature, fix Bootstrap variables and DataGrid styling

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-29 17:44:31 +00:00
parent b8a454bca0
commit f48cda006a
21 changed files with 444 additions and 121 deletions

View File

@@ -33,7 +33,7 @@ use App\RSpade\Core\Models\User_Model;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: _api_keys
*
* @property int $id
@@ -53,7 +53,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent
*/
class Api_Key_Model extends Rsx_System_Model_Abstract
{
{
protected $table = '_api_keys';
public static $enums = [];

View File

@@ -30,42 +30,50 @@ use App\RSpade\Core\Files\File_Storage_Model;
* provides the basic structure for categorizing uploaded files.
*/
/**
* _AUTO_GENERATED_
* @property integer $id
* @property string $key
* @property integer $file_storage_id
* @property string $file_name
* @property string $file_extension
* @property integer $file_type_id
* @property integer $width
* @property integer $height
* @property integer $duration
* @property boolean $is_animated
* @property integer $frame_count
* @property integer $fileable_type
* @property integer $fileable_id
* @property string $fileable_category
* @property string $fileable_type_meta
* @property integer $fileable_order
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2026-01-29 08:25:50
* Table: _file_attachments
*
* @property int $id
* @property mixed $key
* @property int $file_storage_id
* @property mixed $file_name
* @property mixed $file_extension
* @property int $file_type_id
* @property int $width
* @property int $height
* @property int $duration
* @property bool $is_animated
* @property int $frame_count
* @property int $fileable_type
* @property int $fileable_id
* @property mixed $fileable_category
* @property mixed $fileable_type_meta
* @property int $fileable_order
* @property string $fileable_meta
* @property integer $site_id
* @property string $session_id
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property integer $created_by
* @property integer $updated_by
* @method static mixed file_type_id_enum()
* @method static mixed file_type_id_enum_select()
* @method static mixed file_type_id_enum_ids()
* @property-read mixed $file_type_id_constant
* @property-read mixed $file_type_id_label
* @property int $site_id
* @property mixed $session_id
* @property string $created_at
* @property string $updated_at
* @property int $created_by
* @property int $updated_by
*
* @property-read string $file_type_id__label
* @property-read string $file_type_id__constant
*
* @method static array file_type_id__enum() Get all enum definitions with full metadata
* @method static array file_type_id__enum_select() Get selectable items for dropdowns
* @method static array file_type_id__enum_labels() Get simple id => label map
* @method static array file_type_id__enum_ids() Get array of all valid enum IDs
*
* @mixin \Eloquent
*/
class File_Attachment_Model extends Rsx_Site_Model_Abstract
{
/** __AUTO_GENERATED: */
{
/**
* _AUTO_GENERATED_ Enum constants
*/
const FILE_TYPE_IMAGE = 1;
const FILE_TYPE_ANIMATED_IMAGE = 2;
const FILE_TYPE_VIDEO = 3;
@@ -73,6 +81,9 @@ class File_Attachment_Model extends Rsx_Site_Model_Abstract
const FILE_TYPE_TEXT = 5;
const FILE_TYPE_DOCUMENT = 6;
const FILE_TYPE_OTHER = 7;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */
/**

View File

@@ -16,7 +16,7 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: _file_storage
*
* @property int $id
@@ -30,7 +30,7 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
* @mixin \Eloquent
*/
class File_Storage_Model extends Rsx_Model_Abstract
{
{
// Required static properties from parent abstract class
public static $enums = [];
public static $rel = [];

View File

@@ -14,7 +14,7 @@ use App\RSpade\Core\Models\Region_Model;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: countries
*
* @property int $id
@@ -32,7 +32,7 @@ use App\RSpade\Core\Models\Region_Model;
* @mixin \Eloquent
*/
class Country_Model extends Rsx_Model_Abstract
{
{
public static $enums = [];
protected $table = 'countries';

View File

@@ -12,7 +12,7 @@ use App\RSpade\Core\Database\Models\Rsx_System_Model_Abstract;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: ip_addresses
*
* @property int $id
@@ -30,7 +30,7 @@ use App\RSpade\Core\Database\Models\Rsx_System_Model_Abstract;
* @mixin \Eloquent
*/
class Ip_Address_Model extends Rsx_System_Model_Abstract
{
{
/**
* Enum field definitions
* @var array

View File

@@ -24,7 +24,7 @@ use App\RSpade\Core\Session\Session;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: login_users
*
* @property int $id
@@ -61,7 +61,7 @@ class Login_User_Model extends Rsx_Model_Abstract implements
\Illuminate\Contracts\Auth\Authenticatable,
\Illuminate\Contracts\Auth\Access\Authorizable,
\Illuminate\Contracts\Auth\CanResetPassword
{
{
/**
* _AUTO_GENERATED_ Enum constants
*/

View File

@@ -14,7 +14,7 @@ use App\RSpade\Core\Models\Country_Model;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: regions
*
* @property int $id
@@ -31,7 +31,7 @@ use App\RSpade\Core\Models\Country_Model;
* @mixin \Eloquent
*/
class Region_Model extends Rsx_Model_Abstract
{
{
public static $enums = [];
protected $table = 'regions';

View File

@@ -14,12 +14,13 @@ use App\RSpade\Core\Models\User_Model;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: sites
*
* @property int $id
* @property mixed $slug
* @property mixed $name
* @property mixed $timezone
* @property bool $is_enabled
* @property string $deleted_at
* @property string $created_at
@@ -31,7 +32,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent
*/
class Site_Model extends Rsx_Model_Abstract
{
{
use SoftDeletes;
/**

View File

@@ -12,7 +12,7 @@ use App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: user_invites
*
* @property int $id
@@ -28,7 +28,7 @@ use App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract;
* @mixin \Eloquent
*/
class User_Invite_Model extends Rsx_Site_Model_Abstract
{
{
/**
* Enum field definitions
* @var array

View File

@@ -23,41 +23,46 @@ use App\RSpade\Core\Models\User_Profile_Model;
* See: php artisan rsx:man acls
*/
/**
* _AUTO_GENERATED_
* @property integer $id
* @property integer $login_user_id
* @property integer $site_id
* @property string $first_name
* @property string $last_name
* @property string $phone
* @property integer $role_id
* @property boolean $is_enabled
* @property integer $user_role_id
* @property string $email
* @property \Carbon\Carbon $deleted_at
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property integer $created_by
* @property integer $updated_by
* @property integer $deleted_by
* @property string $invite_code
* @property \Carbon\Carbon $invite_accepted_at
* @property \Carbon\Carbon $invite_expires_at
* @method static mixed role_id_enum()
* @method static mixed role_id_enum_select()
* @method static mixed role_id_enum_ids()
* @property-read mixed $role_id_constant
* @property-read mixed $role_id_label
* @property-read mixed $role_id_permissions
* @property-read mixed $role_id_can_admin_roles
* @property-read mixed $role_id_selectable
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2026-01-29 08:25:50
* Table: users
*
* @property int $id
* @property int $login_user_id
* @property int $site_id
* @property mixed $first_name
* @property mixed $last_name
* @property mixed $phone
* @property int $role_id
* @property bool $is_enabled
* @property int $user_role_id
* @property mixed $email
* @property string $deleted_at
* @property string $created_at
* @property string $updated_at
* @property int $created_by
* @property int $updated_by
* @property int $deleted_by
* @property mixed $invite_code
* @property string $invite_accepted_at
* @property string $invite_expires_at
*
* @property-read string $role_id__label
* @property-read string $role_id__constant
*
* @method static array role_id__enum() Get all enum definitions with full metadata
* @method static array role_id__enum_select() Get selectable items for dropdowns
* @method static array role_id__enum_labels() Get simple id => label map
* @method static array role_id__enum_ids() Get array of all valid enum IDs
*
* @mixin \Eloquent
*/
class User_Model extends Rsx_Site_Model_Abstract
{
/** __AUTO_GENERATED: */
{
/**
* _AUTO_GENERATED_ Enum constants
*/
const ROLE_DEVELOPER = 100;
const ROLE_ROOT_ADMIN = 200;
const ROLE_SITE_OWNER = 300;
@@ -66,6 +71,9 @@ class User_Model extends Rsx_Site_Model_Abstract
const ROLE_USER = 600;
const ROLE_VIEWER = 700;
const ROLE_DISABLED = 800;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */
// =========================================================================

View File

@@ -7,7 +7,7 @@ use App\RSpade\Core\Models\User_Model;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: user_permissions
*
* @property int $id
@@ -22,7 +22,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent
*/
class User_Permission_Model extends Rsx_Model_Abstract
{
{
protected $table = 'user_permissions';
protected $fillable = []; // No mass assignment - always explicit

View File

@@ -35,7 +35,7 @@ use App\RSpade\Core\Models\User_Model;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: user_profiles
*
* @property int $id
@@ -51,7 +51,7 @@ use App\RSpade\Core\Models\User_Model;
* @mixin \Eloquent
*/
class User_Profile_Model extends Rsx_Model_Abstract
{
{
/**
* The table associated with the model
*

View File

@@ -11,33 +11,44 @@ use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
* and two-factor authentication via email or SMS.
*/
/**
* _AUTO_GENERATED_
* @property integer $id
* @property string $email
* @property string $verification_code
* @property integer $verification_type_id
* @property \Carbon\Carbon $verified_at
* @property \Carbon\Carbon $expires_at
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property integer $created_by
* @property integer $updated_by
* @method static mixed verification_type_id_enum()
* @method static mixed verification_type_id_enum_select()
* @method static mixed verification_type_id_enum_ids()
* @property-read mixed $verification_type_id_constant
* @property-read mixed $verification_type_id_label
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2026-01-29 08:25:50
* Table: user_verifications
*
* @property int $id
* @property mixed $email
* @property mixed $verification_code
* @property int $verification_type_id
* @property string $verified_at
* @property string $expires_at
* @property string $created_at
* @property string $updated_at
* @property int $created_by
* @property int $updated_by
*
* @property-read string $verification_type_id__label
* @property-read string $verification_type_id__constant
*
* @method static array verification_type_id__enum() Get all enum definitions with full metadata
* @method static array verification_type_id__enum_select() Get selectable items for dropdowns
* @method static array verification_type_id__enum_labels() Get simple id => label map
* @method static array verification_type_id__enum_ids() Get array of all valid enum IDs
*
* @mixin \Eloquent
*/
class User_Verification_Model extends Rsx_Model_Abstract
{
/** __AUTO_GENERATED: */
{
/**
* _AUTO_GENERATED_ Enum constants
*/
const VERIFICATION_TYPE_EMAIL = 1;
const VERIFICATION_TYPE_SMS = 2;
const VERIFICATION_TYPE_EMAIL_RECOVERY = 3;
const VERIFICATION_TYPE_SMS_RECOVERY = 4;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */
/**

View File

@@ -17,11 +17,11 @@ use App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract;
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: _search_indexes
*
* @property int $id
* @property mixed $indexable_type
* @property int $indexable_type
* @property int $indexable_id
* @property string $content
* @property array $metadata
@@ -37,7 +37,7 @@ use App\RSpade\Core\Database\Models\Rsx_Site_Model_Abstract;
* @mixin \Eloquent
*/
class Search_Index_Model extends Rsx_Site_Model_Abstract
{
{
// Required static properties from parent abstract class
public static $enums = [];
public static $rel = [];

View File

@@ -41,7 +41,7 @@ use App\RSpade\Core\Session\User_Agent;
*/
/**
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2025-12-26 02:43:29
* Generated on: 2026-01-29 08:25:50
* Table: _sessions
*
* @property int $id
@@ -63,7 +63,7 @@ use App\RSpade\Core\Session\User_Agent;
* @mixin \Eloquent
*/
class Session extends Rsx_System_Model_Abstract
{
{
// Enum definitions (required by abstract parent)
public static $enums = [];

View File

@@ -0,0 +1,105 @@
<?php
namespace App\RSpade\Core\Throttle;
use Illuminate\Support\Facades\DB;
use App\RSpade\Core\Session\Session;
/**
* Rsx_Throttle - Rate limiting utility for periodic per-user actions
*
* Provides atomic check-and-execute functionality for throttling operations.
* Database implementation is hidden - API exposes only check().
*
* Use cases:
* - Periodic maintenance tasks (notification expiry, cache refresh)
* - Rate-limited background calculations
* - Any operation that should run at most once per interval per user
*
* @example
* if (Rsx_Throttle::check('NOTIFICATION_EXPIRE_CHECK', $user_id, minutes: 30)) {
* Notification::expire_old();
* }
*/
class Rsx_Throttle
{
/**
* Check if action should run and atomically mark as executed
*
* First-time calls return true and create record.
* Subsequent calls return true only after interval has passed.
*
* @param string $action_key Unique action identifier (use SCREAMING_SNAKE_CASE)
* @param int $user_id Login user ID (authentication identity)
* @param int $minutes Throttle interval in minutes (default 30)
* @return bool True if action should execute now, false if throttled
*/
public static function check(string $action_key, int $user_id, int $minutes = 30): bool
{
$site_id = Session::get_site_id();
$threshold = now()->subMinutes($minutes);
// TODO: Future optimization - use row-level locking with SELECT ... FOR UPDATE
// instead of table lock for better concurrency per user/site
// Atomic check-and-update using table lock
DB::statement('LOCK TABLES _throttle WRITE');
try {
$record = DB::table('_throttle')
->where('site_id', $site_id)
->where('user_id', $user_id)
->where('action_key', $action_key)
->first();
if (!$record) {
// First time - create record and return true
DB::table('_throttle')->insert([
'site_id' => $site_id,
'user_id' => $user_id,
'action_key' => $action_key,
'last_executed_at' => now(),
'created_at' => now(),
'updated_at' => now(),
]);
return true;
}
if ($record->last_executed_at < $threshold) {
// Interval passed - update and return true
DB::table('_throttle')
->where('id', $record->id)
->update([
'last_executed_at' => now(),
'updated_at' => now(),
]);
return true;
}
// Still within throttle interval
return false;
} finally {
DB::statement('UNLOCK TABLES');
}
}
/**
* Force reset throttle for an action
*
* Useful for testing or forcing immediate re-execution.
*
* @param string $action_key Action identifier to reset
* @param int $user_id Login user ID
*/
public static function reset(string $action_key, int $user_id): void
{
$site_id = Session::get_site_id();
DB::table('_throttle')
->where('site_id', $site_id)
->where('user_id', $user_id)
->where('action_key', $action_key)
->delete();
}
}

View File

@@ -5,29 +5,41 @@ namespace App\RSpade\Lib\Flash;
use App\RSpade\Core\Database\Models\Rsx_Model_Abstract;
/**
* _AUTO_GENERATED_
* @property integer $id
* @property integer $session_id
* @property integer $type_id
* _AUTO_GENERATED_ Database type hints - do not edit manually
* Generated on: 2026-01-29 08:25:50
* Table: _flash_alerts
*
* @property int $id
* @property int $session_id
* @property int $type_id
* @property string $message
* @property \Carbon\Carbon $created_at
* @property integer $created_by
* @property integer $updated_by
* @property \Carbon\Carbon $updated_at
* @method static mixed type_id_enum()
* @method static mixed type_id_enum_select()
* @method static mixed type_id_enum_ids()
* @property-read mixed $type_id_constant
* @property-read mixed $type_id_label
* @property string $created_at
* @property int $created_by
* @property int $updated_by
* @property string $updated_at
*
* @property-read string $type_id__label
* @property-read string $type_id__constant
*
* @method static array type_id__enum() Get all enum definitions with full metadata
* @method static array type_id__enum_select() Get selectable items for dropdowns
* @method static array type_id__enum_labels() Get simple id => label map
* @method static array type_id__enum_ids() Get array of all valid enum IDs
*
* @mixin \Eloquent
*/
class Flash_Alert_Model extends Rsx_Model_Abstract
{
/** __AUTO_GENERATED: */
{
/**
* _AUTO_GENERATED_ Enum constants
*/
const TYPE_SUCCESS = 1;
const TYPE_ERROR = 2;
const TYPE_INFO = 3;
const TYPE_WARNING = 4;
/** __AUTO_GENERATED: */
/** __/AUTO_GENERATED */
// Enum constants (auto-generated by rsx:migrate:document_models)

View File

@@ -381,6 +381,21 @@
"created_at": "2026-01-12T07:36:24+00:00",
"created_by": "root",
"command": "php artisan make:migration:safe add_timezone_to_sites_table"
},
"2026_01_29_070242_create_action_logs_tables.php": {
"created_at": "2026-01-29T07:02:42+00:00",
"created_by": "root",
"command": "php artisan make:migration:safe create_action_logs_tables"
},
"2026_01_29_081808_create_throttle_table.php": {
"created_at": "2026-01-29T08:18:08+00:00",
"created_by": "root",
"command": "php artisan make:migration:safe create_throttle_table"
},
"2026_01_29_081902_create_notifications_table.php": {
"created_at": "2026-01-29T08:19:02+00:00",
"created_by": "root",
"command": "php artisan make:migration:safe create_notifications_table"
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*
* IMPORTANT: Use raw MySQL queries for clarity and auditability
* DB::statement() with raw SQL
* Schema::create() with Blueprint
*
* REQUIRED: ALL tables MUST have: id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
* No exceptions - every table needs this exact ID column (SIGNED for easier migrations)
*
* Integer types: Use BIGINT for all integers, TINYINT(1) for booleans only
* Never use unsigned - all integers should be signed
*
* Migrations must be self-contained - no Model/Service references
*
* @return void
*/
public function up()
{
// Main action log table
DB::statement("
CREATE TABLE action_logs (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
site_id BIGINT NOT NULL,
type_id BIGINT NOT NULL,
actor_type BIGINT NULL,
actor_id BIGINT NULL,
subject_type BIGINT NOT NULL,
subject_id BIGINT NOT NULL,
metadata JSON NULL,
created_by BIGINT NULL,
created_at TIMESTAMP(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at TIMESTAMP(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
updated_by BIGINT NULL,
INDEX idx_action_logs_site_id (site_id),
INDEX idx_action_logs_type_id (type_id),
INDEX idx_action_logs_actor (actor_type, actor_id),
INDEX idx_action_logs_subject (subject_type, subject_id),
INDEX idx_action_logs_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
// Related entities table (one-to-many from action_logs)
DB::statement("
CREATE TABLE action_log_related (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
action_log_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
related_type BIGINT NOT NULL,
related_id BIGINT NOT NULL,
INDEX idx_alr_action_log_id (action_log_id),
INDEX idx_alr_related (related_type, related_id),
FOREIGN KEY (action_log_id) REFERENCES action_logs(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
}
};

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*
* IMPORTANT: Use raw MySQL queries for clarity and auditability
* DB::statement() with raw SQL
* Schema::create() with Blueprint
*
* REQUIRED: ALL tables MUST have: id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
* No exceptions - every table needs this exact ID column (SIGNED for easier migrations)
*
* Integer types: Use BIGINT for all integers, TINYINT(1) for booleans only
* Never use unsigned - all integers should be signed
*
* Migrations must be self-contained - no Model/Service references
*
* @return void
*/
public function up()
{
DB::statement("
CREATE TABLE _throttle (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
site_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
action_key VARCHAR(255) NOT NULL,
last_executed_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_at TIMESTAMP(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at TIMESTAMP(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_throttle_site_user_action (site_id, user_id, action_key),
INDEX idx_throttle_last_executed (last_executed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
}
};

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*
* IMPORTANT: Use raw MySQL queries for clarity and auditability
* DB::statement() with raw SQL
* Schema::create() with Blueprint
*
* REQUIRED: ALL tables MUST have: id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
* No exceptions - every table needs this exact ID column (SIGNED for easier migrations)
*
* Integer types: Use BIGINT for all integers, TINYINT(1) for booleans only
* Never use unsigned - all integers should be signed
*
* Migrations must be self-contained - no Model/Service references
*
* @return void
*/
public function up()
{
DB::statement("
CREATE TABLE notifications (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
site_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
type_id BIGINT NOT NULL,
entity_type BIGINT NULL,
entity_id BIGINT NULL,
metadata JSON NULL,
read_at TIMESTAMP(3) NULL,
expires_at TIMESTAMP(3) NOT NULL,
created_by BIGINT NULL,
created_at TIMESTAMP(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at TIMESTAMP(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
updated_by BIGINT NULL,
INDEX idx_notifications_site_user (site_id, user_id),
INDEX idx_notifications_user_unread (site_id, user_id, read_at),
INDEX idx_notifications_entity (entity_type, entity_id),
INDEX idx_notifications_expires (expires_at),
INDEX idx_notifications_type (type_id),
INDEX idx_notifications_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
}
};