diff --git a/composer.json b/composer.json index 18302f4..6507935 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/core-api", - "version": "1.5.24", + "version": "1.5.25", "description": "Core Framework and Resources for Fleetbase API", "keywords": [ "fleetbase", diff --git a/src/Support/DataPurger.php b/src/Support/DataPurger.php index bed9de2..99be31f 100644 --- a/src/Support/DataPurger.php +++ b/src/Support/DataPurger.php @@ -4,11 +4,12 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Str; class DataPurger { /** - * Delete all data related to a company. + * Delete all data related to a company, including foreign key relationships. * * @param \Fleetbase\Models\Company $company * @param bool $deletePermanently whether to permanently delete the company or soft delete @@ -27,43 +28,57 @@ public static function deleteCompanyData($company, bool $deletePermanently = tru DB::setDefaultConnection($connectionName); - // Disable foreign key checks + // Disable foreign key checks for safe deletion DB::statement('SET FOREIGN_KEY_CHECKS=0;'); try { // Fetch all table names $tables = DB::select('SHOW TABLES'); + // Track related records for deletion + $relatedRecords = []; + foreach ($tables as $table) { $tableName = array_values((array) $table)[0]; // Get table name $columns = Schema::getColumnListing($tableName); - // Check if table has a `company_uuid` column - if (!in_array('company_uuid', $columns)) { + // Skip system tables + if (Str::startsWith($tableName, ['registry_', 'billing_'])) { continue; } - // Count rows to be deleted - $rowCount = DB::table($tableName)->where('company_uuid', $companyUuid)->count(); - - if ($rowCount > 0) { - // Delete rows associated with the company UUID - DB::table($tableName)->where('company_uuid', $companyUuid)->delete(); - - // Output verbose logs - if ($verbose) { - echo "Deleted {$rowCount} rows from {$tableName} for company_uuid {$companyUuid}.\n"; + // Check if table has a `company_uuid` column (direct deletion) + if (in_array('company_uuid', $columns)) { + $rowCount = DB::table($tableName)->where('company_uuid', $companyUuid)->count(); + + if ($rowCount > 0) { + // Store related record primary keys for cascade deletion + $primaryKey = self::getPrimaryKey($columns); + if ($primaryKey) { + $relatedRecords[$tableName] = DB::table($tableName) + ->where('company_uuid', $companyUuid) + ->pluck($primaryKey) + ->toArray(); + } + + // Delete the records + DB::table($tableName)->where('company_uuid', $companyUuid)->delete(); + + if ($verbose) { + echo "Deleted {$rowCount} rows from {$tableName} for company_uuid {$companyUuid}.\n"; + } + } elseif ($verbose) { + echo "No rows found in {$tableName} for company_uuid {$companyUuid}.\n"; } - } elseif ($verbose) { - echo "No rows found in {$tableName} for company_uuid {$companyUuid}.\n"; } } + + // Handle dependent records by foreign keys + self::deleteRelatedRecords($relatedRecords, $verbose); } catch (\Exception $e) { - // Re-enable foreign key checks in case of an error DB::statement('SET FOREIGN_KEY_CHECKS=1;'); throw $e; } finally { - // Re-enable foreign key checks after processing DB::statement('SET FOREIGN_KEY_CHECKS=1;'); } } @@ -90,4 +105,74 @@ public static function deleteCompanyData($company, bool $deletePermanently = tru } } } + + /** + * Deletes records from tables that reference previously deleted records. + * + * @param array $relatedRecords an associative array of table names and their primary keys to delete + * @param bool $verbose whether to output logs + */ + protected static function deleteRelatedRecords(array $relatedRecords, bool $verbose = false) + { + $processedTables = []; + + foreach ($relatedRecords as $table => $primaryKeys) { + $columns = Schema::getColumnListing($table); + + foreach ($columns as $column) { + foreach (Schema::getAllTables() as $relatedTable) { + $relatedTableName = array_values((array) $relatedTable)[0]; + + // Skip system tables + if (Str::startsWith($relatedTableName, ['registry_', 'billing_'])) { + continue; + } + + if (in_array($relatedTableName, $processedTables)) { + continue; // Skip already processed tables + } + + $relatedColumns = Schema::getColumnListing($relatedTableName); + foreach ($relatedColumns as $relatedColumn) { + if (self::isForeignKey($relatedColumn, $table)) { + // Find dependent records + $dependentRecords = DB::table($relatedTableName) + ->whereIn($relatedColumn, $primaryKeys) + ->pluck(self::getPrimaryKey($relatedColumns)) + ->toArray(); + + if (!empty($dependentRecords)) { + DB::table($relatedTableName)->whereIn($relatedColumn, $primaryKeys)->delete(); + $processedTables[] = $relatedTableName; + + if ($verbose) { + echo 'Deleted ' . count($dependentRecords) . " dependent records from {$relatedTableName} where {$relatedColumn} matched deleted primary keys.\n"; + } + } + } + } + } + } + } + } + + /** + * Determines the primary key of a table (prioritizing `uuid`, falling back to `id`). + * + * @return string|null + */ + protected static function getPrimaryKey(array $columns) + { + return in_array('uuid', $columns) ? 'uuid' : (in_array('id', $columns) ? 'id' : null); + } + + /** + * Checks if a column is likely a foreign key referencing another table. + * + * @return bool + */ + protected static function isForeignKey(string $column, string $relatedTable) + { + return str_ends_with($column, '_id') || str_ends_with($column, '_uuid') || strpos($column, $relatedTable) !== false; + } } diff --git a/src/Support/SqlDumper.php b/src/Support/SqlDumper.php index 5e42ca6..698bd30 100644 --- a/src/Support/SqlDumper.php +++ b/src/Support/SqlDumper.php @@ -37,7 +37,7 @@ public static function createCompanyDump($company, ?string $fileName = null) } /** - * Generates the SQL dump string for the company data. + * Generates the SQL dump string for the company data, including foreign key relationships. * * @param \Fleetbase\Models\Company $company * @@ -58,34 +58,110 @@ public static function getCompanyDumpSql($company) $tables = Schema::getAllTables(); $dbPrefix = DB::getTablePrefix(); + // Store related records by their primary keys + $relatedRecords = []; + foreach ($tables as $table) { $tableName = $table->{"{$dbPrefix}Tables_in_" . $config['database']}; - $columns = Schema::getColumnListing($tableName); - - if (!in_array('company_uuid', $columns)) { + if (in_array($tableName, ['activity', 'api_events', 'api_request_logs', 'webhook_request_logs', 'carts'])) { continue; } - $records = DB::table($tableName)->where('company_uuid', $companyUuid)->get(); - - if ($records->isEmpty()) { - continue; + $columns = Schema::getColumnListing($tableName); + if (in_array('company_uuid', $columns)) { + // Get records related to the company + $records = DB::table($tableName)->where('company_uuid', $companyUuid)->get(); + + if ($records->isEmpty()) { + continue; + } + + $dump .= "-- Dumping data for table `{$tableName}`\n"; + foreach ($records as $record) { + $values = self::formatRecordValues((array) $record); + $columnsList = implode(', ', array_keys((array) $record)); + $valuesList = implode(', ', $values); + $dump .= "INSERT INTO `{$tableName}` ({$columnsList}) VALUES ({$valuesList});\n"; + + // ✅ Store primary keys to track dependencies + $primaryKey = self::getPrimaryKey($columns); + if ($primaryKey && isset($record->{$primaryKey})) { + $relatedRecords[$tableName][] = $record->{$primaryKey}; + } + } + $dump .= "\n"; } + } - $dump .= "-- Dumping data for table `{$tableName}`\n"; - foreach ($records as $record) { - $values = array_map(function ($value) { - return is_null($value) ? 'NULL' : "'" . addslashes($value) . "'"; - }, (array) $record); + // Handle dependent records based on foreign keys + foreach ($tables as $table) { + $tableName = $table->{"{$dbPrefix}Tables_in_" . $config['database']}; + $columns = Schema::getColumnListing($tableName); - $columnsList = implode(', ', array_keys((array) $record)); - $valuesList = implode(', ', $values); - $dump .= "INSERT INTO `{$tableName}` ({$columnsList}) VALUES ({$valuesList});\n"; + foreach ($columns as $column) { + foreach ($relatedRecords as $relatedTable => $primaryKeys) { + if (self::isForeignKey($column, $relatedTable)) { + // Fetch dependent records where the column value matches primary keys + $records = DB::table($tableName) + ->whereIn($column, $primaryKeys) + ->get(); + + if ($records->isEmpty()) { + continue; + } + + $dump .= "-- Dumping dependent data for table `{$tableName}` (linked to `{$relatedTable}` via `{$column}`)\n"; + foreach ($records as $record) { + $values = self::formatRecordValues((array) $record); + $columnsList = implode(', ', array_keys((array) $record)); + $valuesList = implode(', ', $values); + $dump .= "INSERT INTO `{$tableName}` ({$columnsList}) VALUES ({$valuesList});\n"; + + // ✅ Track additional primary keys for deeper dependencies + $primaryKey = self::getPrimaryKey($columns); + if ($primaryKey && isset($record->{$primaryKey})) { + $relatedRecords[$tableName][] = $record->{$primaryKey}; + } + } + $dump .= "\n"; + } + } } - $dump .= "\n"; } } return $dump; } + + /** + * Determines the primary key of a table (prioritizing `uuid`, falling back to `id`). + * + * @return string|null + */ + protected static function getPrimaryKey(array $columns) + { + return in_array('uuid', $columns) ? 'uuid' : (in_array('id', $columns) ? 'id' : null); + } + + /** + * Checks if a column is likely a foreign key referencing another table. + * + * @return bool + */ + protected static function isForeignKey(string $column, string $relatedTable) + { + return str_ends_with($column, '_id') || str_ends_with($column, '_uuid') || strpos($column, $relatedTable) !== false; + } + + /** + * Formats record values for SQL insertion. + * + * @return array + */ + protected static function formatRecordValues(array $record) + { + return array_map(function ($value) { + return is_null($value) ? 'NULL' : "'" . addslashes($value) . "'"; + }, $record); + } }