mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 12:10:41 -05:00
SPT Semvar & Automatic Resolution
This update gives mod versions a supported SPT version field that accepts a semantic version. The latest supported SPT version will be automatically resolved based on the semvar. Next up: I need to update the ModVersion to SptVersion relationship to be a many-to-many and expand the resolution to resolve multiple versions.
This commit is contained in:
parent
d1bfdf5424
commit
db578071e4
27
.env.full
27
.env.full
@ -31,18 +31,6 @@ DB_PASSWORD=password
|
|||||||
DB_CHARSET=utf8mb4
|
DB_CHARSET=utf8mb4
|
||||||
DB_COLLATION=utf8mb4_0900_ai_ci
|
DB_COLLATION=utf8mb4_0900_ai_ci
|
||||||
|
|
||||||
# This is only needed if you are running the `artisan app:import-hub` command.
|
|
||||||
# For normal development you should just seed the database with fake data by
|
|
||||||
# running the command: `php artisan migrate:fresh --seed`
|
|
||||||
DB_HUB_CONNECTION=mysql
|
|
||||||
DB_HUB_HOST=
|
|
||||||
DB_HUB_PORT=
|
|
||||||
DB_HUB_DATABASE=
|
|
||||||
DB_HUB_USERNAME=
|
|
||||||
DB_HUB_PASSWORD=
|
|
||||||
DB_HUB_CHARSET=utf8mb4
|
|
||||||
DB_HUB_COLLATION=utf8mb4_0900_ai_ci
|
|
||||||
|
|
||||||
SESSION_DRIVER=redis
|
SESSION_DRIVER=redis
|
||||||
SESSION_CONNECTION=default
|
SESSION_CONNECTION=default
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
@ -86,3 +74,18 @@ OCTANE_SERVER=swoole
|
|||||||
OCTANE_HTTPS=false
|
OCTANE_HTTPS=false
|
||||||
|
|
||||||
SAIL_XDEBUG_MODE=develop,debug,coverage
|
SAIL_XDEBUG_MODE=develop,debug,coverage
|
||||||
|
|
||||||
|
# Everything below is only needed if you are running the `artisan app:import-hub` command.
|
||||||
|
# For normal development you should just seed the database with fake data by
|
||||||
|
# running the command: `php artisan migrate:fresh --seed`
|
||||||
|
DB_HUB_CONNECTION=mysql
|
||||||
|
DB_HUB_HOST=
|
||||||
|
DB_HUB_PORT=
|
||||||
|
DB_HUB_DATABASE=
|
||||||
|
DB_HUB_USERNAME=
|
||||||
|
DB_HUB_PASSWORD=
|
||||||
|
DB_HUB_CHARSET=utf8mb4
|
||||||
|
DB_HUB_COLLATION=utf8mb4_0900_ai_ci
|
||||||
|
|
||||||
|
GITEA_DOMAIN=
|
||||||
|
GITEA_TOKEN=
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Jobs\ImportHubData;
|
use App\Jobs\ImportHubDataJob;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class ImportHub extends Command
|
class ImportHubCommand extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'app:import-hub';
|
protected $signature = 'app:import-hub';
|
||||||
|
|
||||||
@ -13,8 +13,7 @@ class ImportHub extends Command
|
|||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
// Add the ImportHubData job to the queue.
|
ImportHubDataJob::dispatch()->onQueue('long');
|
||||||
ImportHubData::dispatch()->onQueue('long');
|
|
||||||
|
|
||||||
$this->info('The import job has been added to the queue.');
|
$this->info('The import job has been added to the queue.');
|
||||||
}
|
}
|
22
app/Console/Commands/ResolveVersionsCommand.php
Normal file
22
app/Console/Commands/ResolveVersionsCommand.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Jobs\ResolveDependenciesJob;
|
||||||
|
use App\Jobs\ResolveSptVersionsJob;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class ResolveVersionsCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'app:resolve-versions';
|
||||||
|
|
||||||
|
protected $description = 'Resolve SPT and dependency versions for all mods.';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
ResolveSptVersionsJob::dispatch()->onQueue('long');
|
||||||
|
ResolveDependenciesJob::dispatch()->onQueue('long');
|
||||||
|
|
||||||
|
$this->info('The import job has been added to the queue.');
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ namespace App\Console\Commands;
|
|||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
|
||||||
class SearchSync extends Command
|
class SearchSyncCommand extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'app:search-sync';
|
protected $signature = 'app:search-sync';
|
||||||
|
|
@ -6,7 +6,7 @@ use Illuminate\Console\Command;
|
|||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class UploadAssets extends Command
|
class UploadAssetsCommand extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'app:upload-assets';
|
protected $signature = 'app:upload-assets';
|
||||||
|
|
@ -4,7 +4,4 @@ namespace App\Exceptions;
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class CircularDependencyException extends Exception
|
class CircularDependencyException extends Exception {}
|
||||||
{
|
|
||||||
protected $message = 'Circular dependency detected.';
|
|
||||||
}
|
|
||||||
|
@ -4,7 +4,4 @@ namespace App\Exceptions;
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class InvalidVersionNumberException extends Exception
|
class InvalidVersionNumberException extends Exception {}
|
||||||
{
|
|
||||||
protected $message = 'The version number is an invalid semantic version.';
|
|
||||||
}
|
|
||||||
|
@ -45,7 +45,7 @@ class ModController extends Controller
|
|||||||
|
|
||||||
$this->authorize('view', $mod);
|
$this->authorize('view', $mod);
|
||||||
|
|
||||||
$latestVersion = $mod->versions->sortByDesc('version')->first();
|
$latestVersion = $mod->versions->first();
|
||||||
|
|
||||||
return view('mod.show', compact(['mod', 'latestVersion']));
|
return view('mod.show', compact(['mod', 'latestVersion']));
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,7 @@ class ModFilter
|
|||||||
{
|
{
|
||||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
|
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
|
||||||
->withTotalDownloads()
|
->withTotalDownloads()
|
||||||
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name']);
|
||||||
->whereHas('latestVersion');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,9 +97,10 @@ class ModFilter
|
|||||||
*/
|
*/
|
||||||
private function sptVersion(array $versions): Builder
|
private function sptVersion(array $versions): Builder
|
||||||
{
|
{
|
||||||
return $this->builder->withWhereHas('latestVersion.sptVersion', function ($query) use ($versions) {
|
return $this->builder->whereHas('latestVersion', function ($query) use ($versions) {
|
||||||
$query->whereIn('version', $versions);
|
$query->whereHas('sptVersion', function ($query) use ($versions) {
|
||||||
$query->orderByDesc('version');
|
$query->whereIn('version', $versions);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ use Illuminate\Support\Str;
|
|||||||
use League\HTMLToMarkdown\HtmlConverter;
|
use League\HTMLToMarkdown\HtmlConverter;
|
||||||
use Stevebauman\Purify\Facades\Purify;
|
use Stevebauman\Purify\Facades\Purify;
|
||||||
|
|
||||||
class ImportHubData implements ShouldBeUnique, ShouldQueue
|
class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
$this->bringFileContentLocal();
|
$this->bringFileContentLocal();
|
||||||
$this->bringFileVersionLabelsLocal();
|
$this->bringFileVersionLabelsLocal();
|
||||||
$this->bringFileVersionContentLocal();
|
$this->bringFileVersionContentLocal();
|
||||||
|
$this->bringSptVersionTagsLocal();
|
||||||
|
|
||||||
// Begin to import the data into the permanent local database tables.
|
// Begin to import the data into the permanent local database tables.
|
||||||
$this->importUsers();
|
$this->importUsers();
|
||||||
@ -53,9 +54,8 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
// Ensure that we've disconnected from the Hub database, clearing temporary tables.
|
// Ensure that we've disconnected from the Hub database, clearing temporary tables.
|
||||||
DB::connection('mysql_hub')->disconnect();
|
DB::connection('mysql_hub')->disconnect();
|
||||||
|
|
||||||
// Re-sync search.
|
|
||||||
Artisan::call('app:search-sync');
|
Artisan::call('app:search-sync');
|
||||||
|
Artisan::call('app:resolve-versions');
|
||||||
Artisan::call('cache:clear');
|
Artisan::call('cache:clear');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,19 +70,24 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
avatarExtension VARCHAR(255),
|
avatarExtension VARCHAR(255),
|
||||||
userID INT,
|
userID INT,
|
||||||
fileHash VARCHAR(255)
|
fileHash VARCHAR(255)
|
||||||
)');
|
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('wcf1_user_avatar')
|
->table('wcf1_user_avatar')
|
||||||
->orderBy('avatarID')
|
->orderBy('avatarID')
|
||||||
->chunk(200, function ($avatars) {
|
->chunk(200, function ($avatars) {
|
||||||
|
$insertData = [];
|
||||||
foreach ($avatars as $avatar) {
|
foreach ($avatars as $avatar) {
|
||||||
DB::table('temp_user_avatar')->insert([
|
$insertData[] = [
|
||||||
'avatarID' => (int) $avatar->avatarID,
|
'avatarID' => (int) $avatar->avatarID,
|
||||||
'avatarExtension' => $avatar->avatarExtension,
|
'avatarExtension' => $avatar->avatarExtension,
|
||||||
'userID' => (int) $avatar->userID,
|
'userID' => (int) $avatar->userID,
|
||||||
'fileHash' => $avatar->fileHash,
|
'fileHash' => $avatar->fileHash,
|
||||||
]);
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($insertData) {
|
||||||
|
DB::table('temp_user_avatar')->insert($insertData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -93,17 +98,25 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
protected function bringFileAuthorsLocal(): void
|
protected function bringFileAuthorsLocal(): void
|
||||||
{
|
{
|
||||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_author (fileID INT, userID INT) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
DB::statement('CREATE TEMPORARY TABLE temp_file_author (
|
||||||
|
fileID INT,
|
||||||
|
userID INT
|
||||||
|
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('filebase1_file_author')
|
->table('filebase1_file_author')
|
||||||
->orderBy('fileID')
|
->orderBy('fileID')
|
||||||
->chunk(200, function ($relationships) {
|
->chunk(200, function ($relationships) {
|
||||||
|
$insertData = [];
|
||||||
foreach ($relationships as $relationship) {
|
foreach ($relationships as $relationship) {
|
||||||
DB::table('temp_file_author')->insert([
|
$insertData[] = [
|
||||||
'fileID' => (int) $relationship->fileID,
|
'fileID' => (int) $relationship->fileID,
|
||||||
'userID' => (int) $relationship->userID,
|
'userID' => (int) $relationship->userID,
|
||||||
]);
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($insertData) {
|
||||||
|
DB::table('temp_file_author')->insert($insertData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -114,18 +127,27 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
protected function bringFileOptionsLocal(): void
|
protected function bringFileOptionsLocal(): void
|
||||||
{
|
{
|
||||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_option_values (fileID INT, optionID INT, optionValue VARCHAR(255)) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
DB::statement('CREATE TEMPORARY TABLE temp_file_option_values (
|
||||||
|
fileID INT,
|
||||||
|
optionID INT,
|
||||||
|
optionValue VARCHAR(255)
|
||||||
|
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('filebase1_file_option_value')
|
->table('filebase1_file_option_value')
|
||||||
->orderBy('fileID')
|
->orderBy('fileID')
|
||||||
->chunk(200, function ($options) {
|
->chunk(200, function ($options) {
|
||||||
|
$insertData = [];
|
||||||
foreach ($options as $option) {
|
foreach ($options as $option) {
|
||||||
DB::table('temp_file_option_values')->insert([
|
$insertData[] = [
|
||||||
'fileID' => (int) $option->fileID,
|
'fileID' => (int) $option->fileID,
|
||||||
'optionID' => (int) $option->optionID,
|
'optionID' => (int) $option->optionID,
|
||||||
'optionValue' => $option->optionValue,
|
'optionValue' => $option->optionValue,
|
||||||
]);
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($insertData) {
|
||||||
|
DB::table('temp_file_option_values')->insert($insertData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -136,19 +158,29 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
protected function bringFileContentLocal(): void
|
protected function bringFileContentLocal(): void
|
||||||
{
|
{
|
||||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_content (fileID INT, subject VARCHAR(255), teaser VARCHAR(255), message LONGTEXT) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
DB::statement('CREATE TEMPORARY TABLE temp_file_content (
|
||||||
|
fileID INT,
|
||||||
|
subject VARCHAR(255),
|
||||||
|
teaser VARCHAR(255),
|
||||||
|
message LONGTEXT
|
||||||
|
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('filebase1_file_content')
|
->table('filebase1_file_content')
|
||||||
->orderBy('fileID')
|
->orderBy('fileID')
|
||||||
->chunk(200, function ($contents) {
|
->chunk(200, function ($contents) {
|
||||||
|
$insertData = [];
|
||||||
foreach ($contents as $content) {
|
foreach ($contents as $content) {
|
||||||
DB::table('temp_file_content')->insert([
|
$insertData[] = [
|
||||||
'fileID' => (int) $content->fileID,
|
'fileID' => (int) $content->fileID,
|
||||||
'subject' => $content->subject,
|
'subject' => $content->subject,
|
||||||
'teaser' => $content->teaser,
|
'teaser' => $content->teaser,
|
||||||
'message' => $content->message,
|
'message' => $content->message,
|
||||||
]);
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($insertData) {
|
||||||
|
DB::table('temp_file_content')->insert($insertData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -159,18 +191,26 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
protected function bringFileVersionLabelsLocal(): void
|
protected function bringFileVersionLabelsLocal(): void
|
||||||
{
|
{
|
||||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels (labelID INT, objectID INT) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels (
|
||||||
|
labelID INT,
|
||||||
|
objectID INT
|
||||||
|
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('wcf1_label_object')
|
->table('wcf1_label_object')
|
||||||
->where('objectTypeID', 387)
|
->where('objectTypeID', 387)
|
||||||
->orderBy('labelID')
|
->orderBy('labelID')
|
||||||
->chunk(200, function ($options) {
|
->chunk(200, function ($options) {
|
||||||
|
$insertData = [];
|
||||||
foreach ($options as $option) {
|
foreach ($options as $option) {
|
||||||
DB::table('temp_file_version_labels')->insert([
|
$insertData[] = [
|
||||||
'labelID' => (int) $option->labelID,
|
'labelID' => (int) $option->labelID,
|
||||||
'objectID' => (int) $option->objectID,
|
'objectID' => (int) $option->objectID,
|
||||||
]);
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($insertData) {
|
||||||
|
DB::table('temp_file_version_labels')->insert($insertData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -181,17 +221,54 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
protected function bringFileVersionContentLocal(): void
|
protected function bringFileVersionContentLocal(): void
|
||||||
{
|
{
|
||||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_version_content (versionID INT, description TEXT) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
DB::statement('CREATE TEMPORARY TABLE temp_file_version_content (
|
||||||
|
versionID INT,
|
||||||
|
description TEXT
|
||||||
|
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('filebase1_file_version_content')
|
->table('filebase1_file_version_content')
|
||||||
->orderBy('versionID')
|
->orderBy('versionID')
|
||||||
->chunk(200, function ($options) {
|
->chunk(200, function ($options) {
|
||||||
|
$insertData = [];
|
||||||
foreach ($options as $option) {
|
foreach ($options as $option) {
|
||||||
DB::table('temp_file_version_content')->insert([
|
$insertData[] = [
|
||||||
'versionID' => (int) $option->versionID,
|
'versionID' => (int) $option->versionID,
|
||||||
'description' => $option->description,
|
'description' => $option->description,
|
||||||
]);
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($insertData) {
|
||||||
|
DB::table('temp_file_version_content')->insert($insertData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bringSptVersionTagsLocal(): void
|
||||||
|
{
|
||||||
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_spt_version_tags');
|
||||||
|
DB::statement('CREATE TEMPORARY TABLE temp_spt_version_tags (
|
||||||
|
hub_id INT,
|
||||||
|
version VARCHAR(255),
|
||||||
|
color_class VARCHAR(255)
|
||||||
|
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
|
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('wcf1_label')
|
||||||
|
->where('groupID', 1)
|
||||||
|
->orderBy('labelID')
|
||||||
|
->chunk(100, function (Collection $versions) {
|
||||||
|
$insertData = [];
|
||||||
|
foreach ($versions as $version) {
|
||||||
|
$insertData[] = [
|
||||||
|
'hub_id' => (int) $version->labelID,
|
||||||
|
'version' => $version->label,
|
||||||
|
'color_class' => $version->cssClassName,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($insertData) {
|
||||||
|
DB::table('temp_spt_version_tags')->insert($insertData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -531,40 +608,142 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Import the SPT versions from the Hub database to the local database.
|
* Import the SPT versions from the Hub database to the local database.
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
protected function importSptVersions(): void
|
protected function importSptVersions(): void
|
||||||
{
|
{
|
||||||
DB::connection('mysql_hub')
|
$domain = config('services.gitea.domain');
|
||||||
->table('wcf1_label')
|
$token = config('services.gitea.token');
|
||||||
->where('groupID', 1)
|
|
||||||
->chunkById(100, function (Collection $versions) {
|
|
||||||
$insertData = [];
|
|
||||||
foreach ($versions as $version) {
|
|
||||||
$insertData[] = [
|
|
||||||
'hub_id' => (int) $version->labelID,
|
|
||||||
'version' => $version->label,
|
|
||||||
'color_class' => $this->translateColour($version->cssClassName),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($insertData)) {
|
if (empty($domain) || empty($token)) {
|
||||||
DB::table('spt_versions')->upsert($insertData, ['hub_id'], ['version', 'color_class']);
|
return;
|
||||||
}
|
}
|
||||||
}, 'labelID');
|
|
||||||
|
$url = "{$domain}/api/v1/repos/SPT/Stable-releases/releases?draft=false&pre-release=false&token={$token}";
|
||||||
|
|
||||||
|
$response = json_decode(file_get_contents($url), true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
throw new Exception('JSON Decode Error: '.json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($response)) {
|
||||||
|
throw new Exception('No version data found in the API response.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$latestVersion = $this->getLatestVersion($response);
|
||||||
|
|
||||||
|
$insertData = [];
|
||||||
|
foreach ($response as $version) {
|
||||||
|
$insertData[] = [
|
||||||
|
'version' => $version['tag_name'],
|
||||||
|
'link' => $version['html_url'],
|
||||||
|
'color_class' => $this->detectVersionColor($version['tag_name'], $latestVersion),
|
||||||
|
'created_at' => Carbon::parse($version['created_at'], 'UTC'),
|
||||||
|
'updated_at' => Carbon::parse($version['created_at'], 'UTC'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a fake 0.0.0 version for outdated mods.
|
||||||
|
$insertData[] = [
|
||||||
|
'version' => '0.0.0',
|
||||||
|
'link' => '',
|
||||||
|
'color_class' => 'black',
|
||||||
|
'created_at' => Carbon::now('UTC'),
|
||||||
|
'updated_at' => Carbon::now('UTC'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Upsert won't work here. Do it manually. :(
|
||||||
|
foreach ($insertData as $data) {
|
||||||
|
$existingVersion = SptVersion::where('version', $data['version'])->first();
|
||||||
|
if ($existingVersion) {
|
||||||
|
$existingVersion->update([
|
||||||
|
'link' => $data['link'],
|
||||||
|
'color_class' => $data['color_class'],
|
||||||
|
'created_at' => $data['created_at'],
|
||||||
|
'updated_at' => $data['updated_at'],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
SptVersion::create($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate the colour class from the Hub database to the local database.
|
* Get the latest current version from the response data.
|
||||||
*/
|
*/
|
||||||
protected function translateColour(string $colour = ''): string
|
protected function getLatestVersion(array $versions): string
|
||||||
{
|
{
|
||||||
return match ($colour) {
|
$semanticVersions = array_map(
|
||||||
'green' => 'green',
|
fn ($version) => $this->extractSemanticVersion($version['tag_name']),
|
||||||
'slightly-outdated' => 'lime',
|
$versions
|
||||||
'yellow' => 'yellow',
|
);
|
||||||
'red' => 'red',
|
|
||||||
default => 'gray',
|
usort($semanticVersions, 'version_compare');
|
||||||
};
|
|
||||||
|
return end($semanticVersions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the last semantic version from a string.
|
||||||
|
* If the version has no patch number, return it as `~<major>.<minor>.0`.
|
||||||
|
*/
|
||||||
|
protected function extractSemanticVersion(string $versionString, bool $appendPatch = false): ?string
|
||||||
|
{
|
||||||
|
// Match both two-part and three-part semantic versions
|
||||||
|
preg_match_all('/\b\d+\.\d+(?:\.\d+)?\b/', $versionString, $matches);
|
||||||
|
|
||||||
|
// Get the last version found, if any
|
||||||
|
$version = end($matches[0]) ?: null;
|
||||||
|
|
||||||
|
if (! $appendPatch) {
|
||||||
|
return $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If version is two-part (e.g., "3.9"), add ".0" and prefix with "~"
|
||||||
|
if ($version && preg_match('/^\d+\.\d+$/', $version)) {
|
||||||
|
$version = '~'.$version.'.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate the version string into a color class.
|
||||||
|
*/
|
||||||
|
protected function detectVersionColor(string $versionString, string $currentVersion): string
|
||||||
|
{
|
||||||
|
$version = $this->extractSemanticVersion($versionString);
|
||||||
|
if (! $version) {
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($version === '0.0.0') {
|
||||||
|
return 'black';
|
||||||
|
}
|
||||||
|
|
||||||
|
[$currentMajor, $currentMinor] = explode('.', $currentVersion);
|
||||||
|
[$major, $minor] = explode('.', $version);
|
||||||
|
|
||||||
|
$currentMajor = (int) $currentMajor;
|
||||||
|
$currentMinor = (int) $currentMinor;
|
||||||
|
$major = (int) $major;
|
||||||
|
$minor = (int) $minor;
|
||||||
|
|
||||||
|
if ($major == $currentMajor) {
|
||||||
|
$difference = $currentMinor - $minor;
|
||||||
|
|
||||||
|
return match ($difference) {
|
||||||
|
0 => 'green',
|
||||||
|
1 => 'lime',
|
||||||
|
2 => 'yellow',
|
||||||
|
3 => 'red',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'gray';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -748,17 +927,22 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch the version string using the labelID from the hub.
|
||||||
|
$sptVersionTemp = DB::table('temp_spt_version_tags')->where('hub_id', $versionLabel->labelID)->value('version');
|
||||||
|
$sptVersionConstraint = $this->extractSemanticVersion($sptVersionTemp, appendPatch: true) ?? '0.0.0';
|
||||||
|
|
||||||
$insertData[] = [
|
$insertData[] = [
|
||||||
'hub_id' => (int) $version->versionID,
|
'hub_id' => (int) $version->versionID,
|
||||||
'mod_id' => $modId,
|
'mod_id' => $modId,
|
||||||
'version' => $version->versionNumber,
|
'version' => $this->extractSemanticVersion($version->versionNumber) ?? '0.0.0',
|
||||||
'description' => $this->cleanHubContent($versionContent->description ?? ''),
|
'description' => $this->cleanHubContent($versionContent->description ?? ''),
|
||||||
'link' => $version->downloadURL,
|
'link' => $version->downloadURL,
|
||||||
'spt_version_id' => SptVersion::whereHubId($versionLabel->labelID)->value('id'),
|
'spt_version_constraint' => $sptVersionConstraint,
|
||||||
|
'resolved_spt_version_id' => null,
|
||||||
'virus_total_link' => $optionVirusTotal?->virus_total_link ?? '',
|
'virus_total_link' => $optionVirusTotal?->virus_total_link ?? '',
|
||||||
'downloads' => max((int) $version->downloads, 0), // At least 0.
|
'downloads' => max((int) $version->downloads, 0), // At least 0.
|
||||||
'disabled' => (bool) $version->isDisabled,
|
'disabled' => (bool) $version->isDisabled,
|
||||||
'published_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
'published_at' => $sptVersionConstraint === '0.0.0' ? null : Carbon::parse($version->uploadTime, 'UTC'),
|
||||||
'created_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
'created_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||||
'updated_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
'updated_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||||
];
|
];
|
||||||
@ -770,7 +954,8 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
'version',
|
'version',
|
||||||
'description',
|
'description',
|
||||||
'link',
|
'link',
|
||||||
'spt_version_id',
|
'spt_version_constraint',
|
||||||
|
'resolved_spt_version_id',
|
||||||
'virus_total_link',
|
'virus_total_link',
|
||||||
'downloads',
|
'downloads',
|
||||||
'published_at',
|
'published_at',
|
||||||
@ -793,6 +978,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
||||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
|
||||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
|
||||||
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_spt_version_tags');
|
||||||
|
|
||||||
// Close the connections. This should drop the temporary tables as well, but I like to be explicit.
|
// Close the connections. This should drop the temporary tables as well, but I like to be explicit.
|
||||||
DB::connection('mysql_hub')->disconnect();
|
DB::connection('mysql_hub')->disconnect();
|
29
app/Jobs/ResolveDependenciesJob.php
Normal file
29
app/Jobs/ResolveDependenciesJob.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use App\Services\DependencyVersionService;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ResolveDependenciesJob implements ShouldBeUnique, ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the SPT versions for each of the mod versions.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$dependencyVersionService = new DependencyVersionService;
|
||||||
|
|
||||||
|
foreach (ModVersion::all() as $modVersion) {
|
||||||
|
$dependencyVersionService->resolve($modVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
app/Jobs/ResolveSptVersionsJob.php
Normal file
29
app/Jobs/ResolveSptVersionsJob.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use App\Services\SptVersionService;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ResolveSptVersionsJob implements ShouldBeUnique, ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the SPT versions for each of the mod versions.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$sptVersionService = new SptVersionService;
|
||||||
|
|
||||||
|
foreach (ModVersion::all() as $modVersion) {
|
||||||
|
$sptVersionService->resolve($modVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -49,7 +49,10 @@ class Index extends Component
|
|||||||
*/
|
*/
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
|
// TODO: This should be updated to only pull versions that have mods associated with them.
|
||||||
|
// To do this, the ModVersion to SptVersion relationship needs to be converted to a many-to-many relationship. Ugh.
|
||||||
$this->availableSptVersions = SptVersion::select(['id', 'version', 'color_class'])->orderByDesc('version')->get();
|
$this->availableSptVersions = SptVersion::select(['id', 'version', 'color_class'])->orderByDesc('version')->get();
|
||||||
|
|
||||||
$this->sptVersion = $this->getLatestMinorVersions()->pluck('version')->toArray();
|
$this->sptVersion = $this->getLatestMinorVersions()->pluck('version')->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +78,7 @@ class Index extends Component
|
|||||||
'order' => $this->order,
|
'order' => $this->order,
|
||||||
'sptVersion' => $this->sptVersion,
|
'sptVersion' => $this->sptVersion,
|
||||||
];
|
];
|
||||||
$mods = (new ModFilter($filters))->apply()->paginate(24);
|
$mods = (new ModFilter($filters))->apply()->paginate(16);
|
||||||
|
|
||||||
return view('livewire.mod.index', compact('mods'));
|
return view('livewire.mod.index', compact('mods'));
|
||||||
}
|
}
|
||||||
|
@ -58,9 +58,16 @@ class Mod extends Model
|
|||||||
/**
|
/**
|
||||||
* The relationship between a mod and its versions.
|
* The relationship between a mod and its versions.
|
||||||
*/
|
*/
|
||||||
public function versions(): HasMany
|
public function versions(bool $resolvedOnly = true): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(ModVersion::class)->orderByDesc('version');
|
$relation = $this->hasMany(ModVersion::class)
|
||||||
|
->orderByDesc('version');
|
||||||
|
|
||||||
|
if ($resolvedOnly) {
|
||||||
|
$relation->whereNotNull('resolved_spt_version_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $relation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,10 +84,16 @@ class Mod extends Model
|
|||||||
/**
|
/**
|
||||||
* The relationship between a mod and its last updated version.
|
* The relationship between a mod and its last updated version.
|
||||||
*/
|
*/
|
||||||
public function lastUpdatedVersion(): HasOne
|
public function lastUpdatedVersion(bool $resolvedOnly = true): HasOne
|
||||||
{
|
{
|
||||||
return $this->hasOne(ModVersion::class)
|
$relation = $this->hasOne(ModVersion::class)
|
||||||
->orderByDesc('updated_at');
|
->orderByDesc('updated_at');
|
||||||
|
|
||||||
|
if ($resolvedOnly) {
|
||||||
|
$relation->whereNotNull('resolved_spt_version_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $relation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,12 +121,18 @@ class Mod extends Model
|
|||||||
/**
|
/**
|
||||||
* The relationship to the latest mod version, dictated by the mod version number.
|
* The relationship to the latest mod version, dictated by the mod version number.
|
||||||
*/
|
*/
|
||||||
public function latestVersion(): HasOne
|
public function latestVersion(bool $resolvedOnly = true): HasOne
|
||||||
{
|
{
|
||||||
return $this->hasOne(ModVersion::class)
|
$relation = $this->hasOne(ModVersion::class)
|
||||||
->orderByDesc('version')
|
->orderByDesc('version')
|
||||||
->orderByDesc('updated_at')
|
->orderByDesc('updated_at')
|
||||||
->take(1);
|
->take(1);
|
||||||
|
|
||||||
|
if ($resolvedOnly) {
|
||||||
|
$relation->whereNotNull('resolved_spt_version_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $relation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,7 +26,7 @@ class ModDependency extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The relationship between a mod dependency and mod.
|
* The relationship between the mod dependency and the mod that is depended on.
|
||||||
*/
|
*/
|
||||||
public function dependencyMod(): BelongsTo
|
public function dependencyMod(): BelongsTo
|
||||||
{
|
{
|
||||||
|
@ -39,9 +39,15 @@ class ModVersion extends Model
|
|||||||
/**
|
/**
|
||||||
* The relationship between a mod version and its dependencies.
|
* The relationship between a mod version and its dependencies.
|
||||||
*/
|
*/
|
||||||
public function dependencies(): HasMany
|
public function dependencies(bool $resolvedOnly = true): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(ModDependency::class);
|
$relation = $this->hasMany(ModDependency::class);
|
||||||
|
|
||||||
|
if ($resolvedOnly) {
|
||||||
|
$relation->whereNotNull('resolved_version_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $relation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,6 +55,6 @@ class ModVersion extends Model
|
|||||||
*/
|
*/
|
||||||
public function sptVersion(): BelongsTo
|
public function sptVersion(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(SptVersion::class);
|
return $this->belongsTo(SptVersion::class, 'resolved_spt_version_id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,22 +36,21 @@ class SptVersion extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$currentMinorVersion = $this->extractMinorVersion($this->version);
|
[$currentMajor, $currentMinor, $currentPatch] = $this->extractVersionParts($this->version);
|
||||||
$latestMinorVersion = $this->extractMinorVersion($latestVersion->version);
|
[$latestMajor, $latestMinor, $latestPatch] = $this->extractVersionParts($latestVersion->version);
|
||||||
} catch (InvalidVersionNumberException $e) {
|
} catch (InvalidVersionNumberException $e) {
|
||||||
// Could not parse a semver version number.
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $currentMinorVersion === $latestMinorVersion;
|
return $currentMajor == $latestMajor && $currentMinor === $latestMinor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the minor version from a full version string.
|
* Extract the version components from a full version string.
|
||||||
*
|
*
|
||||||
* @throws InvalidVersionNumberException
|
* @throws InvalidVersionNumberException
|
||||||
*/
|
*/
|
||||||
private function extractMinorVersion(string $version): int
|
private function extractVersionParts(string $version): array
|
||||||
{
|
{
|
||||||
// Remove everything from the version string except the numbers and dots.
|
// Remove everything from the version string except the numbers and dots.
|
||||||
$version = preg_replace('/[^0-9.]/', '', $version);
|
$version = preg_replace('/[^0-9.]/', '', $version);
|
||||||
@ -63,7 +62,10 @@ class SptVersion extends Model
|
|||||||
|
|
||||||
$parts = explode('.', $version);
|
$parts = explode('.', $version);
|
||||||
|
|
||||||
// Return the minor version part.
|
return [
|
||||||
return (int) $parts[1];
|
(int) $parts[0],
|
||||||
|
(int) $parts[1],
|
||||||
|
(int) $parts[2],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,32 +2,50 @@
|
|||||||
|
|
||||||
namespace App\Observers;
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Exceptions\CircularDependencyException;
|
||||||
use App\Models\ModDependency;
|
use App\Models\ModDependency;
|
||||||
use App\Models\ModVersion;
|
use App\Models\ModVersion;
|
||||||
use App\Services\ModVersionService;
|
use App\Services\DependencyVersionService;
|
||||||
|
|
||||||
class ModDependencyObserver
|
class ModDependencyObserver
|
||||||
{
|
{
|
||||||
protected ModVersionService $modVersionService;
|
protected DependencyVersionService $dependencyVersionService;
|
||||||
|
|
||||||
public function __construct(ModVersionService $modVersionService)
|
public function __construct(DependencyVersionService $dependencyVersionService)
|
||||||
{
|
{
|
||||||
$this->modVersionService = $modVersionService;
|
$this->dependencyVersionService = $dependencyVersionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the ModDependency "saved" event.
|
||||||
|
*
|
||||||
|
* @throws CircularDependencyException
|
||||||
|
*/
|
||||||
public function saved(ModDependency $modDependency): void
|
public function saved(ModDependency $modDependency): void
|
||||||
{
|
{
|
||||||
$modVersion = ModVersion::find($modDependency->mod_version_id);
|
$this->resolveDependencyVersion($modDependency);
|
||||||
if ($modVersion) {
|
|
||||||
$this->modVersionService->resolveDependencies($modVersion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleted(ModDependency $modDependency): void
|
/**
|
||||||
|
* Resolve the ModDependency's dependencies.
|
||||||
|
*
|
||||||
|
* @throws CircularDependencyException
|
||||||
|
*/
|
||||||
|
public function resolveDependencyVersion(ModDependency $modDependency): void
|
||||||
{
|
{
|
||||||
$modVersion = ModVersion::find($modDependency->mod_version_id);
|
$modVersion = ModVersion::find($modDependency->mod_version_id);
|
||||||
if ($modVersion) {
|
if ($modVersion) {
|
||||||
$this->modVersionService->resolveDependencies($modVersion);
|
$this->dependencyVersionService->resolve($modVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the ModDependency "deleted" event.
|
||||||
|
*
|
||||||
|
* @throws CircularDependencyException
|
||||||
|
*/
|
||||||
|
public function deleted(ModDependency $modDependency): void
|
||||||
|
{
|
||||||
|
$this->resolveDependencyVersion($modDependency);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,32 +2,57 @@
|
|||||||
|
|
||||||
namespace App\Observers;
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Exceptions\CircularDependencyException;
|
||||||
use App\Models\ModDependency;
|
use App\Models\ModDependency;
|
||||||
use App\Models\ModVersion;
|
use App\Models\ModVersion;
|
||||||
use App\Services\ModVersionService;
|
use App\Services\DependencyVersionService;
|
||||||
|
use App\Services\SptVersionService;
|
||||||
|
|
||||||
class ModVersionObserver
|
class ModVersionObserver
|
||||||
{
|
{
|
||||||
protected ModVersionService $modVersionService;
|
protected DependencyVersionService $dependencyVersionService;
|
||||||
|
|
||||||
public function __construct(ModVersionService $modVersionService)
|
protected SptVersionService $sptVersionService;
|
||||||
{
|
|
||||||
$this->modVersionService = $modVersionService;
|
public function __construct(
|
||||||
|
DependencyVersionService $dependencyVersionService,
|
||||||
|
SptVersionService $sptVersionService,
|
||||||
|
) {
|
||||||
|
$this->dependencyVersionService = $dependencyVersionService;
|
||||||
|
$this->sptVersionService = $sptVersionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the ModVersion "saved" event.
|
||||||
|
*
|
||||||
|
* @throws CircularDependencyException
|
||||||
|
*/
|
||||||
public function saved(ModVersion $modVersion): void
|
public function saved(ModVersion $modVersion): void
|
||||||
{
|
{
|
||||||
$dependencies = ModDependency::where('resolved_version_id', $modVersion->id)->get();
|
$this->resolveDependencyVersion($modVersion);
|
||||||
foreach ($dependencies as $dependency) {
|
$this->sptVersionService->resolve($modVersion);
|
||||||
$this->modVersionService->resolveDependencies($dependency->modVersion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleted(ModVersion $modVersion): void
|
/**
|
||||||
|
* Resolve the ModVersion's dependencies.
|
||||||
|
*
|
||||||
|
* @throws CircularDependencyException
|
||||||
|
*/
|
||||||
|
private function resolveDependencyVersion(ModVersion $modVersion): void
|
||||||
{
|
{
|
||||||
$dependencies = ModDependency::where('resolved_version_id', $modVersion->id)->get();
|
$dependencies = ModDependency::where('resolved_version_id', $modVersion->id)->get();
|
||||||
foreach ($dependencies as $dependency) {
|
foreach ($dependencies as $dependency) {
|
||||||
$this->modVersionService->resolveDependencies($dependency->modVersion);
|
$this->dependencyVersionService->resolve($dependency->modVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the ModVersion "deleted" event.
|
||||||
|
*
|
||||||
|
* @throws CircularDependencyException
|
||||||
|
*/
|
||||||
|
public function deleted(ModVersion $modVersion): void
|
||||||
|
{
|
||||||
|
$this->resolveDependencyVersion($modVersion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
44
app/Observers/SptVersionObserver.php
Normal file
44
app/Observers/SptVersionObserver.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use App\Services\SptVersionService;
|
||||||
|
|
||||||
|
class SptVersionObserver
|
||||||
|
{
|
||||||
|
protected SptVersionService $sptVersionService;
|
||||||
|
|
||||||
|
public function __construct(SptVersionService $sptVersionService)
|
||||||
|
{
|
||||||
|
$this->sptVersionService = $sptVersionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the SptVersion "saved" event.
|
||||||
|
*/
|
||||||
|
public function saved(): void
|
||||||
|
{
|
||||||
|
$this->resolveSptVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the SptVersion's dependencies.
|
||||||
|
*/
|
||||||
|
private function resolveSptVersion(): void
|
||||||
|
{
|
||||||
|
$modVersions = ModVersion::all();
|
||||||
|
|
||||||
|
foreach ($modVersions as $modVersion) {
|
||||||
|
$this->sptVersionService->resolve($modVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the SptVersion "deleted" event.
|
||||||
|
*/
|
||||||
|
public function deleted(): void
|
||||||
|
{
|
||||||
|
$this->resolveSptVersion();
|
||||||
|
}
|
||||||
|
}
|
@ -4,9 +4,11 @@ namespace App\Providers;
|
|||||||
|
|
||||||
use App\Models\ModDependency;
|
use App\Models\ModDependency;
|
||||||
use App\Models\ModVersion;
|
use App\Models\ModVersion;
|
||||||
|
use App\Models\SptVersion;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Observers\ModDependencyObserver;
|
use App\Observers\ModDependencyObserver;
|
||||||
use App\Observers\ModVersionObserver;
|
use App\Observers\ModVersionObserver;
|
||||||
|
use App\Observers\SptVersionObserver;
|
||||||
use App\Services\LatestSptVersionService;
|
use App\Services\LatestSptVersionService;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
@ -36,6 +38,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
// Register observers.
|
// Register observers.
|
||||||
ModVersion::observe(ModVersionObserver::class);
|
ModVersion::observe(ModVersionObserver::class);
|
||||||
ModDependency::observe(ModDependencyObserver::class);
|
ModDependency::observe(ModDependencyObserver::class);
|
||||||
|
SptVersion::observe(SptVersionObserver::class);
|
||||||
|
|
||||||
// This gate determines who can access the Pulse dashboard.
|
// This gate determines who can access the Pulse dashboard.
|
||||||
Gate::define('viewPulse', function (User $user) {
|
Gate::define('viewPulse', function (User $user) {
|
||||||
|
119
app/Services/DependencyVersionService.php
Normal file
119
app/Services/DependencyVersionService.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Exceptions\CircularDependencyException;
|
||||||
|
use App\Models\ModDependency;
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use Composer\Semver\Semver;
|
||||||
|
|
||||||
|
class DependencyVersionService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Keep track of visited versions to avoid resolving them again.
|
||||||
|
*/
|
||||||
|
protected array $visited = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep track of the current path in the depth-first search.
|
||||||
|
*/
|
||||||
|
protected array $stack = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve dependencies for the given mod version.
|
||||||
|
*
|
||||||
|
* @throws CircularDependencyException
|
||||||
|
*/
|
||||||
|
public function resolve(ModVersion $modVersion): array
|
||||||
|
{
|
||||||
|
$this->visited = [];
|
||||||
|
$this->stack = [];
|
||||||
|
|
||||||
|
// Store the resolved versions for each dependency.
|
||||||
|
$resolvedVersions = [];
|
||||||
|
|
||||||
|
// Start the recursive depth-first search to resolve dependencies.
|
||||||
|
$this->processDependencies($modVersion, $resolvedVersions);
|
||||||
|
|
||||||
|
return $resolvedVersions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a depth-first search to resolve dependencies for the given mod version.
|
||||||
|
*
|
||||||
|
* @throws CircularDependencyException
|
||||||
|
*/
|
||||||
|
protected function processDependencies(ModVersion $modVersion, array &$resolvedVersions): void
|
||||||
|
{
|
||||||
|
// Detect circular dependencies
|
||||||
|
if (in_array($modVersion->id, $this->stack)) {
|
||||||
|
throw new CircularDependencyException("Circular dependency detected in ModVersion ID: {$modVersion->id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip already processed versions
|
||||||
|
if (in_array($modVersion->id, $this->visited)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the current version
|
||||||
|
$this->visited[] = $modVersion->id;
|
||||||
|
$this->stack[] = $modVersion->id;
|
||||||
|
|
||||||
|
// Get the dependencies for the current mod version.
|
||||||
|
$dependencies = $modVersion->dependencies(resolvedOnly: false)->get();
|
||||||
|
|
||||||
|
foreach ($dependencies as $dependency) {
|
||||||
|
// Resolve the latest mod version ID that satisfies the version constraint on the mod version dependency.
|
||||||
|
$resolvedId = $this->resolveDependency($dependency);
|
||||||
|
|
||||||
|
// Update the resolved version ID for the dependency if it has changed.
|
||||||
|
// Do it "quietly" to avoid triggering the observer again.
|
||||||
|
if ($dependency->resolved_version_id !== $resolvedId) {
|
||||||
|
$dependency->updateQuietly(['resolved_version_id' => $resolvedId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, the dependency has been resolved (or not) and we can add it to the resolved versions to
|
||||||
|
// avoid resolving it again in the future and to help with circular dependency detection.
|
||||||
|
$resolvedVersions[$dependency->id] = $resolvedId ? ModVersion::find($resolvedId) : null;
|
||||||
|
|
||||||
|
// Recursively process the resolved dependency.
|
||||||
|
if ($resolvedId) {
|
||||||
|
$nextModVersion = ModVersion::find($resolvedId);
|
||||||
|
if ($nextModVersion) {
|
||||||
|
$this->processDependencies($nextModVersion, $resolvedVersions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the current version from the stack now that we have processed all its dependencies.
|
||||||
|
array_pop($this->stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the latest mod version ID that satisfies the version constraint on the mod version dependency.
|
||||||
|
*/
|
||||||
|
protected function resolveDependency(ModDependency $dependency): ?int
|
||||||
|
{
|
||||||
|
$dependencyModVersions = $dependency->dependencyMod->versions(resolvedOnly: false);
|
||||||
|
|
||||||
|
// There are no mod versions for the dependency mod.
|
||||||
|
if ($dependencyModVersions->doesntExist()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$availableVersions = $dependencyModVersions->pluck('id', 'version')->toArray();
|
||||||
|
$satisfyingVersions = Semver::satisfiedBy(array_keys($availableVersions), $dependency->version_constraint);
|
||||||
|
|
||||||
|
// There are no mod versions that satisfy the version constraint.
|
||||||
|
if (empty($satisfyingVersions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the satisfying versions in descending order using the version_compare function.
|
||||||
|
usort($satisfyingVersions, 'version_compare');
|
||||||
|
$satisfyingVersions = array_reverse($satisfyingVersions);
|
||||||
|
|
||||||
|
// Return the latest (highest version number) satisfying version.
|
||||||
|
return $availableVersions[$satisfyingVersions[0]];
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,10 @@ namespace App\Services;
|
|||||||
|
|
||||||
use App\Models\SptVersion;
|
use App\Models\SptVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for fetching the latest SPT version. It's registered as a singleton in the service
|
||||||
|
* container so that the latest version is only fetched once per request.
|
||||||
|
*/
|
||||||
class LatestSptVersionService
|
class LatestSptVersionService
|
||||||
{
|
{
|
||||||
protected ?SptVersion $version = null;
|
protected ?SptVersion $version = null;
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use App\Exceptions\CircularDependencyException;
|
|
||||||
use App\Models\ModDependency;
|
|
||||||
use App\Models\ModVersion;
|
|
||||||
use Composer\Semver\Semver;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
|
|
||||||
class ModVersionService
|
|
||||||
{
|
|
||||||
protected array $visited = [];
|
|
||||||
|
|
||||||
protected array $stack = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve dependencies for the given mod version.
|
|
||||||
*
|
|
||||||
* @throws CircularDependencyException
|
|
||||||
*/
|
|
||||||
public function resolveDependencies(ModVersion $modVersion): array
|
|
||||||
{
|
|
||||||
$resolvedVersions = [];
|
|
||||||
$this->visited = [];
|
|
||||||
$this->stack = [];
|
|
||||||
|
|
||||||
$this->processDependencies($modVersion, $resolvedVersions);
|
|
||||||
|
|
||||||
return $resolvedVersions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform a depth-first search to resolve dependencies for the given mod version.
|
|
||||||
*
|
|
||||||
* @throws CircularDependencyException
|
|
||||||
*/
|
|
||||||
protected function processDependencies(ModVersion $modVersion, array &$resolvedVersions): void
|
|
||||||
{
|
|
||||||
if (in_array($modVersion->id, $this->stack)) {
|
|
||||||
throw new CircularDependencyException("Circular dependency detected in ModVersion ID: {$modVersion->id}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($modVersion->id, $this->visited)) {
|
|
||||||
return; // Skip already processed versions
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->visited[] = $modVersion->id;
|
|
||||||
$this->stack[] = $modVersion->id;
|
|
||||||
|
|
||||||
/** @var Collection|ModDependency[] $dependencies */
|
|
||||||
$dependencies = $this->getDependencies($modVersion);
|
|
||||||
|
|
||||||
foreach ($dependencies as $dependency) {
|
|
||||||
$resolvedVersionId = $this->resolveVersionIdForDependency($dependency);
|
|
||||||
|
|
||||||
if ($dependency->resolved_version_id !== $resolvedVersionId) {
|
|
||||||
$dependency->updateQuietly(['resolved_version_id' => $resolvedVersionId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$resolvedVersions[$dependency->id] = $resolvedVersionId ? ModVersion::find($resolvedVersionId) : null;
|
|
||||||
|
|
||||||
if ($resolvedVersionId) {
|
|
||||||
$nextModVersion = ModVersion::find($resolvedVersionId);
|
|
||||||
if ($nextModVersion) {
|
|
||||||
$this->processDependencies($nextModVersion, $resolvedVersions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
array_pop($this->stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the dependencies for the given mod version.
|
|
||||||
*/
|
|
||||||
protected function getDependencies(ModVersion $modVersion): Collection
|
|
||||||
{
|
|
||||||
return $modVersion->dependencies()->with(['dependencyMod.versions'])->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the latest version ID that satisfies the version constraint on given dependency.
|
|
||||||
*/
|
|
||||||
protected function resolveVersionIdForDependency(ModDependency $dependency): ?int
|
|
||||||
{
|
|
||||||
$mod = $dependency->dependencyMod;
|
|
||||||
|
|
||||||
if (! $mod || $mod->versions->isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$availableVersions = $mod->versions->pluck('id', 'version')->toArray();
|
|
||||||
$satisfyingVersions = Semver::satisfiedBy(array_keys($availableVersions), $dependency->version_constraint);
|
|
||||||
|
|
||||||
// Versions are sorted in descending order by default. Take the first key (the latest version) using `reset()`.
|
|
||||||
return $satisfyingVersions ? $availableVersions[reset($satisfyingVersions)] : null;
|
|
||||||
}
|
|
||||||
}
|
|
42
app/Services/SptVersionService.php
Normal file
42
app/Services/SptVersionService.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use App\Models\SptVersion;
|
||||||
|
use Composer\Semver\Semver;
|
||||||
|
|
||||||
|
class SptVersionService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Resolve dependencies for the given mod version.
|
||||||
|
*/
|
||||||
|
public function resolve(ModVersion $modVersion): void
|
||||||
|
{
|
||||||
|
$modVersion->resolved_spt_version_id = $this->satisfyconstraint($modVersion);
|
||||||
|
$modVersion->saveQuietly();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Satisfies the version constraint of a given ModVersion. Returns the ID of the satisfying SptVersion.
|
||||||
|
*/
|
||||||
|
private function satisfyConstraint(ModVersion $modVersion): ?int
|
||||||
|
{
|
||||||
|
$availableVersions = SptVersion::query()
|
||||||
|
->orderBy('version', 'desc')
|
||||||
|
->pluck('id', 'version')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$satisfyingVersions = Semver::satisfiedBy(array_keys($availableVersions), $modVersion->spt_version_constraint);
|
||||||
|
if (empty($satisfyingVersions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the satisfying versions are sorted in descending order to get the latest version
|
||||||
|
usort($satisfyingVersions, 'version_compare');
|
||||||
|
$satisfyingVersions = array_reverse($satisfyingVersions);
|
||||||
|
|
||||||
|
// Return the ID of the latest satisfying version
|
||||||
|
return $availableVersions[$satisfyingVersions[0]];
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,6 @@ class ModListSection extends Component
|
|||||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
||||||
->withTotalDownloads()
|
->withTotalDownloads()
|
||||||
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
||||||
->whereHas('latestVersion')
|
|
||||||
->where('featured', true)
|
->where('featured', true)
|
||||||
->latest()
|
->latest()
|
||||||
->limit(6)
|
->limit(6)
|
||||||
@ -44,7 +43,6 @@ class ModListSection extends Component
|
|||||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
|
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
|
||||||
->withTotalDownloads()
|
->withTotalDownloads()
|
||||||
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
||||||
->whereHas('latestVersion')
|
|
||||||
->latest()
|
->latest()
|
||||||
->limit(6)
|
->limit(6)
|
||||||
->get();
|
->get();
|
||||||
@ -57,7 +55,6 @@ class ModListSection extends Component
|
|||||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
||||||
->withTotalDownloads()
|
->withTotalDownloads()
|
||||||
->with(['lastUpdatedVersion', 'lastUpdatedVersion.sptVersion', 'users:id,name'])
|
->with(['lastUpdatedVersion', 'lastUpdatedVersion.sptVersion', 'users:id,name'])
|
||||||
->whereHas('lastUpdatedVersion')
|
|
||||||
->orderByDesc(
|
->orderByDesc(
|
||||||
ModVersion::select('updated_at')
|
ModVersion::select('updated_at')
|
||||||
->whereColumn('mod_id', 'mods.id')
|
->whereColumn('mod_id', 'mods.id')
|
||||||
|
@ -35,4 +35,9 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'gitea' => [
|
||||||
|
'domain' => env('GITEA_DOMAIN', ''),
|
||||||
|
'token' => env('GITEA_TOKEN', ''),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -17,9 +17,19 @@ class ModDependencyFactory extends Factory
|
|||||||
return [
|
return [
|
||||||
'mod_version_id' => ModVersion::factory(),
|
'mod_version_id' => ModVersion::factory(),
|
||||||
'dependency_mod_id' => Mod::factory(),
|
'dependency_mod_id' => Mod::factory(),
|
||||||
'version_constraint' => '^'.$this->faker->numerify('#.#.#'),
|
'version_constraint' => fake()->numerify($this->generateVersionConstraint()),
|
||||||
'created_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
'created_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
'updated_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
'updated_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method generates a random version constraint from a predefined set of options.
|
||||||
|
*/
|
||||||
|
private function generateVersionConstraint(): string
|
||||||
|
{
|
||||||
|
$versionConstraints = ['*', '^1.#.#', '>=2.#.#', '~1.#.#', '>=1.2.# <2.#.#'];
|
||||||
|
|
||||||
|
return $versionConstraints[array_rand($versionConstraints)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,15 @@ class ModVersionFactory extends Factory
|
|||||||
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
|
$constraint = fake()->numerify($this->generateVersionConstraint());
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'mod_id' => Mod::factory(),
|
'mod_id' => Mod::factory(),
|
||||||
'version' => fake()->numerify('#.#.#'),
|
'version' => fake()->numerify('#.#.#'),
|
||||||
'description' => fake()->text(),
|
'description' => fake()->text(),
|
||||||
'link' => fake()->url(),
|
'link' => fake()->url(),
|
||||||
'spt_version_id' => SptVersion::factory(),
|
'spt_version_constraint' => $constraint,
|
||||||
|
'resolved_spt_version_id' => null,
|
||||||
'virus_total_link' => fake()->url(),
|
'virus_total_link' => fake()->url(),
|
||||||
'downloads' => fake()->randomNumber(),
|
'downloads' => fake()->randomNumber(),
|
||||||
'published_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
'published_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
@ -28,6 +31,31 @@ class ModVersionFactory extends Factory
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method generates a random version constraint from a predefined set of options.
|
||||||
|
*/
|
||||||
|
private function generateVersionConstraint(): string
|
||||||
|
{
|
||||||
|
$versionConstraints = ['*', '^1.#.#', '>=2.#.#', '~1.#.#'];
|
||||||
|
|
||||||
|
return $versionConstraints[array_rand($versionConstraints)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the mod version should have a resolved SPT version.
|
||||||
|
*/
|
||||||
|
public function sptVersionResolved(): static
|
||||||
|
{
|
||||||
|
$constraint = fake()->numerify('#.#.#');
|
||||||
|
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'spt_version_constraint' => $constraint,
|
||||||
|
'resolved_spt_version_id' => SptVersion::factory()->create([
|
||||||
|
'version' => $constraint,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate that the mod version should be disabled.
|
* Indicate that the mod version should be disabled.
|
||||||
*/
|
*/
|
||||||
|
@ -13,8 +13,9 @@ class SptVersionFactory extends Factory
|
|||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'version' => $this->faker->numerify('SPT 1.#.#'),
|
'version' => $this->faker->numerify('#.#.#'),
|
||||||
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
||||||
|
'link' => $this->faker->url,
|
||||||
'created_at' => Carbon::now(),
|
'created_at' => Carbon::now(),
|
||||||
'updated_at' => Carbon::now(),
|
'updated_at' => Carbon::now(),
|
||||||
];
|
];
|
||||||
|
@ -15,6 +15,7 @@ return new class extends Migration
|
|||||||
->default(null)
|
->default(null)
|
||||||
->unique();
|
->unique();
|
||||||
$table->string('version');
|
$table->string('version');
|
||||||
|
$table->string('link');
|
||||||
$table->string('color_class');
|
$table->string('color_class');
|
||||||
$table->softDeletes();
|
$table->softDeletes();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
use App\Models\SptVersion;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@ -23,9 +22,9 @@ return new class extends Migration
|
|||||||
$table->string('version');
|
$table->string('version');
|
||||||
$table->longText('description');
|
$table->longText('description');
|
||||||
$table->string('link');
|
$table->string('link');
|
||||||
$table->foreignIdFor(SptVersion::class)
|
$table->string('spt_version_constraint');
|
||||||
|
$table->foreignId('resolved_spt_version_id')
|
||||||
->nullable()
|
->nullable()
|
||||||
->default(null)
|
|
||||||
->constrained('spt_versions')
|
->constrained('spt_versions')
|
||||||
->nullOnDelete()
|
->nullOnDelete()
|
||||||
->cascadeOnUpdate();
|
->cascadeOnUpdate();
|
||||||
|
@ -18,7 +18,7 @@ return new class extends Migration
|
|||||||
->constrained('mods')
|
->constrained('mods')
|
||||||
->cascadeOnDelete()
|
->cascadeOnDelete()
|
||||||
->cascadeOnUpdate();
|
->cascadeOnUpdate();
|
||||||
$table->string('version_constraint'); // e.g., ^1.0.1
|
$table->string('version_constraint');
|
||||||
$table->foreignId('resolved_version_id')
|
$table->foreignId('resolved_version_id')
|
||||||
->nullable()
|
->nullable()
|
||||||
->constrained('mod_versions')
|
->constrained('mod_versions')
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('uuid')->unique();
|
||||||
|
$table->text('connection');
|
||||||
|
$table->text('queue');
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->longText('exception');
|
||||||
|
$table->timestamp('failed_at')->useCurrent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('failed_jobs');
|
||||||
|
}
|
||||||
|
};
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Exceptions\CircularDependencyException;
|
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
use App\Models\ModDependency;
|
use App\Models\ModDependency;
|
||||||
@ -20,7 +19,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// Create a few SPT versions.
|
// Create a few SPT versions.
|
||||||
$spt_versions = SptVersion::factory(10)->create();
|
$spt_versions = SptVersion::factory(30)->create();
|
||||||
|
|
||||||
// Create some code licenses.
|
// Create some code licenses.
|
||||||
$licenses = License::factory(10)->create();
|
$licenses = License::factory(10)->create();
|
||||||
@ -39,40 +38,26 @@ class DatabaseSeeder extends Seeder
|
|||||||
// Add 100 users.
|
// Add 100 users.
|
||||||
$users = User::factory(100)->create();
|
$users = User::factory(100)->create();
|
||||||
|
|
||||||
// Add 200 mods, assigning them to the users we just created.
|
// Add 300 mods, assigning them to the users we just created.
|
||||||
$allUsers = $users->merge([$administrator, $moderator]);
|
$allUsers = $users->merge([$administrator, $moderator]);
|
||||||
$mods = Mod::factory(200)->recycle([$licenses])->create();
|
$mods = Mod::factory(300)->recycle([$licenses])->create();
|
||||||
foreach ($mods as $mod) {
|
foreach ($mods as $mod) {
|
||||||
$userIds = $allUsers->random(rand(1, 3))->pluck('id')->toArray();
|
$userIds = $allUsers->random(rand(1, 3))->pluck('id')->toArray();
|
||||||
$mod->users()->attach($userIds);
|
$mod->users()->attach($userIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add 1000 mod versions, assigning them to the mods we just created.
|
// Add 3000 mod versions, assigning them to the mods we just created.
|
||||||
$modVersions = ModVersion::factory(1000)->recycle([$mods, $spt_versions])->create();
|
$modVersions = ModVersion::factory(3000)->recycle([$mods, $spt_versions])->create();
|
||||||
|
|
||||||
// Add ModDependencies to a subset of ModVersions.
|
// Add ModDependencies to a subset of ModVersions.
|
||||||
foreach ($modVersions as $modVersion) {
|
foreach ($modVersions as $modVersion) {
|
||||||
$hasDependencies = rand(0, 100) < 30; // 30% chance to have dependencies
|
$hasDependencies = rand(0, 100) < 30; // 30% chance to have dependencies
|
||||||
if ($hasDependencies) {
|
if ($hasDependencies) {
|
||||||
$numDependencies = rand(1, 3); // 1 to 3 dependencies
|
$dependencyMods = $mods->random(rand(1, 3)); // 1 to 3 dependencies
|
||||||
$dependencyMods = $mods->random($numDependencies);
|
|
||||||
foreach ($dependencyMods as $dependencyMod) {
|
foreach ($dependencyMods as $dependencyMod) {
|
||||||
try {
|
ModDependency::factory()->recycle([$modVersion, $dependencyMod])->create();
|
||||||
ModDependency::factory()->recycle([$modVersion, $dependencyMod])->create([
|
|
||||||
'version_constraint' => $this->generateVersionConstraint(),
|
|
||||||
]);
|
|
||||||
} catch (CircularDependencyException $e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateVersionConstraint(): string
|
|
||||||
{
|
|
||||||
$versionConstraints = ['*', '^1.0.0', '>=2.0.0', '~1.1.0', '>=1.2.0 <2.0.0'];
|
|
||||||
|
|
||||||
return $versionConstraints[array_rand($versionConstraints)];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "html",
|
"name": "forge",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
12
phpstan.neon
12
phpstan.neon
@ -1,9 +1,13 @@
|
|||||||
includes:
|
includes:
|
||||||
- ./vendor/larastan/larastan/extension.neon
|
- ./vendor/larastan/larastan/extension.neon
|
||||||
parameters:
|
parameters:
|
||||||
|
level: 6
|
||||||
paths:
|
paths:
|
||||||
- app/
|
- app
|
||||||
|
- bootstrap
|
||||||
|
- config
|
||||||
|
- database
|
||||||
|
- lang
|
||||||
|
- routes
|
||||||
excludePaths:
|
excludePaths:
|
||||||
analyseAndScan:
|
- tests/**/*
|
||||||
- tests/
|
|
||||||
level: 4
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@props(['mods, versionScope, title'])
|
@props(['mods', 'versionScope', 'title'])
|
||||||
|
|
||||||
<div class="mx-auto max-w-7xl px-4 pt-16 sm:px-6 lg:px-8">
|
<div class="mx-auto max-w-7xl px-4 pt-16 sm:px-6 lg:px-8">
|
||||||
{{--
|
{{--
|
||||||
|
@ -156,7 +156,7 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="text-center">
|
<div class="text-center text-gray-700 dark:text-gray-300">
|
||||||
<p>{{ __('There were no mods found with those filters applied. ') }}</p>
|
<p>{{ __('There were no mods found with those filters applied. ') }}</p>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mx-auto">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mx-auto">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.182 16.318A4.486 4.486 0 0 0 12.016 15a4.486 4.486 0 0 0-3.198 1.318M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.182 16.318A4.486 4.486 0 0 0 12.016 15a4.486 4.486 0 0 0-3.198 1.318M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" />
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Console\Commands\ImportHub;
|
use App\Console\Commands\ImportHubCommand;
|
||||||
|
use App\Console\Commands\ResolveVersionsCommand;
|
||||||
use Illuminate\Support\Facades\Schedule;
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
Schedule::command(ImportHub::class)->hourly();
|
Schedule::command(ImportHubCommand::class)->hourly();
|
||||||
|
Schedule::command(ResolveVersionsCommand::class)->hourlyAt(30);
|
||||||
|
|
||||||
Schedule::command('horizon:snapshot')->everyFiveMinutes();
|
Schedule::command('horizon:snapshot')->everyFiveMinutes();
|
||||||
|
@ -13,13 +13,13 @@ it('resolves mod version dependency when mod version is created', function () {
|
|||||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
|
||||||
// Create versions for Mod B
|
// Create versions for Mod B
|
||||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
ModVersion::factory()->recycle($modB)->create(['version' => '1.0.0']);
|
||||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
ModVersion::factory()->recycle($modB)->create(['version' => '1.1.0']);
|
||||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
ModVersion::factory()->recycle($modB)->create(['version' => '1.1.1']);
|
||||||
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
ModVersion::factory()->recycle($modB)->create(['version' => '2.0.0']);
|
||||||
|
|
||||||
// Create versions for Mod A that depends on Mod B
|
// Create versions for Mod A that depends on Mod B
|
||||||
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
$modAv1 = ModVersion::factory()->recycle($modA)->create(['version' => '1.0.0']);
|
||||||
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
'version_constraint' => '^1.0.0',
|
'version_constraint' => '^1.0.0',
|
||||||
]);
|
]);
|
||||||
@ -74,7 +74,7 @@ it('resolves mod version dependency when mod version is deleted', function () {
|
|||||||
$modDependency->refresh();
|
$modDependency->refresh();
|
||||||
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||||
|
|
||||||
// Update the mod B version
|
// Delete the mod B version
|
||||||
$modBv3->delete();
|
$modBv3->delete();
|
||||||
|
|
||||||
$modDependency->refresh();
|
$modDependency->refresh();
|
||||||
@ -155,6 +155,34 @@ it('resolves mod version dependency with complex semantic version constraint', f
|
|||||||
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves previously unresolved mod version dependency after semantic version constraint is updated', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
|
||||||
|
// Create versions for Mod B
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.1']);
|
||||||
|
|
||||||
|
// Create version for Mod A that depends on Mod B, but no version satisfies the constraint.
|
||||||
|
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||||
|
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '^3.0.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolved_version_id)->toBeNull();
|
||||||
|
|
||||||
|
// Update the dependency version constraint
|
||||||
|
$modDependency->update(['version_constraint' => '^2.0.0']);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolved_version_id)->not->toBeNull();
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('2.0.1');
|
||||||
|
});
|
||||||
|
|
||||||
it('resolves null when no mod versions are available', function () {
|
it('resolves null when no mod versions are available', function () {
|
||||||
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
@ -6,11 +6,28 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
|||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
it('shows the latest version on the mod detail page', function () {
|
it('can retrieve all unresolved versions', function () {
|
||||||
// Create a mod instance
|
// Create a mod instance
|
||||||
$mod = Mod::factory()->create();
|
$mod = Mod::factory()->create();
|
||||||
|
ModVersion::factory(5)->recycle($mod)->create();
|
||||||
|
|
||||||
// Create 5 mod versions with specified versions
|
ModVersion::all()->each(function (ModVersion $modVersion) {
|
||||||
|
$modVersion->resolved_spt_version_id = null;
|
||||||
|
$modVersion->saveQuietly();
|
||||||
|
});
|
||||||
|
|
||||||
|
$unresolvedMix = $mod->versions(resolvedOnly: false);
|
||||||
|
|
||||||
|
$unresolvedMix->each(function (ModVersion $modVersion) {
|
||||||
|
expect($modVersion)->toBeInstanceOf(ModVersion::class)
|
||||||
|
->and($modVersion->resolved_spt_version_id)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($unresolvedMix->count())->toBe(5)
|
||||||
|
->and($mod->versions->count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the latest version on the mod detail page', function () {
|
||||||
$versions = [
|
$versions = [
|
||||||
'1.0.0',
|
'1.0.0',
|
||||||
'1.1.0',
|
'1.1.0',
|
||||||
@ -18,21 +35,16 @@ it('shows the latest version on the mod detail page', function () {
|
|||||||
'2.0.0',
|
'2.0.0',
|
||||||
'2.1.0',
|
'2.1.0',
|
||||||
];
|
];
|
||||||
|
|
||||||
// get the highest version in the array
|
|
||||||
$latestVersion = max($versions);
|
$latestVersion = max($versions);
|
||||||
|
|
||||||
|
$mod = Mod::factory()->create();
|
||||||
foreach ($versions as $version) {
|
foreach ($versions as $version) {
|
||||||
ModVersion::factory()->create([
|
ModVersion::factory()->sptVersionResolved()->recycle($mod)->create(['version' => $version]);
|
||||||
'mod_id' => $mod->id,
|
|
||||||
'version' => $version,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a request to the mod's detail URL
|
|
||||||
$response = $this->get($mod->detailUrl());
|
$response = $this->get($mod->detailUrl());
|
||||||
|
|
||||||
$this->assertEquals('2.1.0', $latestVersion);
|
expect($latestVersion)->toBe('2.1.0');
|
||||||
|
|
||||||
// Assert the latest version is next to the mod's name
|
// Assert the latest version is next to the mod's name
|
||||||
$response->assertSeeInOrder(explode(' ', "$mod->name $latestVersion"));
|
$response->assertSeeInOrder(explode(' ', "$mod->name $latestVersion"));
|
||||||
|
@ -1,11 +1,81 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\ModVersion;
|
use App\Models\ModVersion;
|
||||||
|
use App\Models\SptVersion;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('resolves spt version when mod version is created', function () {
|
||||||
|
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.2.0']);
|
||||||
|
|
||||||
|
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||||
|
|
||||||
|
$modVersion->refresh();
|
||||||
|
|
||||||
|
expect($modVersion->resolved_spt_version_id)->not->toBeNull();
|
||||||
|
expect($modVersion->sptVersion->version)->toBe('1.1.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves spt version when constraint is updated', function () {
|
||||||
|
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.2.0']);
|
||||||
|
|
||||||
|
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||||
|
|
||||||
|
expect($modVersion->resolved_spt_version_id)->not->toBeNull();
|
||||||
|
expect($modVersion->sptVersion->version)->toBe('1.1.1');
|
||||||
|
|
||||||
|
$modVersion->spt_version_constraint = '~1.2.0';
|
||||||
|
$modVersion->save();
|
||||||
|
|
||||||
|
$modVersion->refresh();
|
||||||
|
|
||||||
|
expect($modVersion->resolved_spt_version_id)->not->toBeNull();
|
||||||
|
expect($modVersion->sptVersion->version)->toBe('1.2.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves spt version when spt version is created', function () {
|
||||||
|
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.2.0']);
|
||||||
|
|
||||||
|
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||||
|
|
||||||
|
expect($modVersion->resolved_spt_version_id)->not->toBeNull();
|
||||||
|
expect($modVersion->sptVersion->version)->toBe('1.1.1');
|
||||||
|
|
||||||
|
SptVersion::factory()->create(['version' => '1.1.2']);
|
||||||
|
|
||||||
|
$modVersion->refresh();
|
||||||
|
|
||||||
|
expect($modVersion->sptVersion->version)->toBe('1.1.2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves spt version when spt version is deleted', function () {
|
||||||
|
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||||
|
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||||
|
$sptVersion = SptVersion::factory()->create(['version' => '1.1.2']);
|
||||||
|
|
||||||
|
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||||
|
|
||||||
|
expect($modVersion->resolved_spt_version_id)->not->toBeNull();
|
||||||
|
expect($modVersion->sptVersion->version)->toBe('1.1.2');
|
||||||
|
|
||||||
|
$sptVersion->delete();
|
||||||
|
$modVersion->refresh();
|
||||||
|
|
||||||
|
expect($modVersion->sptVersion->version)->toBe('1.1.1');
|
||||||
|
});
|
||||||
|
|
||||||
it('includes only published mod versions', function () {
|
it('includes only published mod versions', function () {
|
||||||
$publishedMod = ModVersion::factory()->create([
|
$publishedMod = ModVersion::factory()->create([
|
||||||
'published_at' => Carbon::now()->subDay(),
|
'published_at' => Carbon::now()->subDay(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user