mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 12:10:41 -05:00
Listing Query & Relationship Clean-up
Rebuilt the filtering queries and many of the mod-to-modVersion-to-sptVersion relationships. Resolves #38
This commit is contained in:
parent
58e8adb730
commit
45a211b66d
@ -30,14 +30,12 @@ class ModController extends Controller
|
||||
{
|
||||
$mod = Mod::with([
|
||||
'versions',
|
||||
'versions.latestSptVersion:id,version,color_class',
|
||||
'versions.latestSptVersion',
|
||||
'versions.latestResolvedDependencies',
|
||||
'versions.latestResolvedDependencies.mod:id,name,slug',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->whereHas('latestVersion')
|
||||
->findOrFail($modId);
|
||||
'versions.latestResolvedDependencies.mod',
|
||||
'license',
|
||||
'users',
|
||||
])->findOrFail($modId);
|
||||
|
||||
if ($mod->slug !== $slug) {
|
||||
abort(404);
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Http\Filters;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@ -17,19 +16,21 @@ class ModFilter
|
||||
protected Builder $builder;
|
||||
|
||||
/**
|
||||
* The filter that should be applied to the query.
|
||||
* The filters to apply.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $filters;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array<string, mixed> $filters
|
||||
*/
|
||||
public function __construct(array $filters)
|
||||
{
|
||||
$this->builder = $this->baseQuery();
|
||||
$this->filters = $filters;
|
||||
$this->builder = $this->baseQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,25 +40,31 @@ class ModFilter
|
||||
*/
|
||||
private function baseQuery(): Builder
|
||||
{
|
||||
// TODO: The latestVersion relationship is sometimes null here and it causes issues. We need to ensure that
|
||||
// empty relationships are filtered out. Normally we would use the whereHas method, but it causes *major*
|
||||
// performance issues in this case. We need to find a better solution.
|
||||
return Mod::query()
|
||||
->select('mods.*')
|
||||
->whereExists(function ($query) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('mod_versions')
|
||||
->join('mod_version_spt_version', 'mod_versions.id', '=', 'mod_version_spt_version.mod_version_id')
|
||||
->join('spt_versions', 'mod_version_spt_version.spt_version_id', '=', 'spt_versions.id')
|
||||
->whereColumn('mod_versions.mod_id', 'mods.id')
|
||||
->where('spt_versions.version', '!=', '0.0.0');
|
||||
})
|
||||
->with([
|
||||
'users:id,name',
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion',
|
||||
]);
|
||||
}
|
||||
|
||||
return Mod::select([
|
||||
'mods.id',
|
||||
'mods.name',
|
||||
'mods.slug',
|
||||
'mods.teaser',
|
||||
'mods.thumbnail',
|
||||
'mods.featured',
|
||||
'mods.downloads',
|
||||
'mods.created_at',
|
||||
])->with([
|
||||
'users:id,name',
|
||||
'latestVersion' => function ($query) {
|
||||
$query->with('latestSptVersion:id,version,color_class');
|
||||
},
|
||||
]);
|
||||
/**
|
||||
* Filter the results by the given search term.
|
||||
*
|
||||
* @return Builder<Mod>
|
||||
*/
|
||||
private function query(string $term): Builder
|
||||
{
|
||||
return $this->builder->whereLike('mods.name', "%{$term}%");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,36 +90,11 @@ class ModFilter
|
||||
*/
|
||||
private function order(string $type): Builder
|
||||
{
|
||||
// We order the "recently updated" mods by the ModVersion's updated_at value.
|
||||
if ($type === 'updated') {
|
||||
return $this->builder
|
||||
->joinSub(
|
||||
ModVersion::select('mod_id', DB::raw('MAX(updated_at) as latest_updated_at'))->groupBy('mod_id'),
|
||||
'latest_versions',
|
||||
'mods.id',
|
||||
'=',
|
||||
'latest_versions.mod_id'
|
||||
)
|
||||
->orderByDesc('latest_versions.latest_updated_at');
|
||||
}
|
||||
|
||||
// By default, we simply order by the column on the mods table/query.
|
||||
$column = match ($type) {
|
||||
'downloaded' => 'downloads',
|
||||
default => 'created_at',
|
||||
return match ($type) {
|
||||
'updated' => $this->builder->orderByDesc('mods.updated_at'), // TODO: This needs to be updated when a version is updated.
|
||||
'downloaded' => $this->builder->orderByDesc('mods.downloads'),
|
||||
default => $this->builder->orderByDesc('mods.created_at'),
|
||||
};
|
||||
|
||||
return $this->builder->orderByDesc($column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the results by the given search term.
|
||||
*
|
||||
* @return Builder<Mod>
|
||||
*/
|
||||
private function query(string $term): Builder
|
||||
{
|
||||
return $this->builder->whereLike('name', "%$term%");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,8 +105,8 @@ class ModFilter
|
||||
private function featured(string $option): Builder
|
||||
{
|
||||
return match ($option) {
|
||||
'exclude' => $this->builder->where('featured', false),
|
||||
'only' => $this->builder->where('featured', true),
|
||||
'exclude' => $this->builder->where('mods.featured', false),
|
||||
'only' => $this->builder->where('mods.featured', true),
|
||||
default => $this->builder,
|
||||
};
|
||||
}
|
||||
@ -137,28 +119,14 @@ class ModFilter
|
||||
*/
|
||||
private function sptVersions(array $versions): Builder
|
||||
{
|
||||
// Parse the versions into major, minor, and patch arrays
|
||||
$parsedVersions = array_map(fn ($version) => [
|
||||
'major' => (int) explode('.', $version)[0],
|
||||
'minor' => (int) (explode('.', $version)[1] ?? 0),
|
||||
'patch' => (int) (explode('.', $version)[2] ?? 0),
|
||||
], $versions);
|
||||
|
||||
[$majorVersions, $minorVersions, $patchVersions] = array_map('array_unique', [
|
||||
array_column($parsedVersions, 'major'),
|
||||
array_column($parsedVersions, 'minor'),
|
||||
array_column($parsedVersions, 'patch'),
|
||||
]);
|
||||
|
||||
return $this->builder
|
||||
->join('mod_versions as mv', 'mods.id', '=', 'mv.mod_id')
|
||||
->join('mod_version_spt_version as mvsv', 'mv.id', '=', 'mvsv.mod_version_id')
|
||||
->join('spt_versions as sv', 'mvsv.spt_version_id', '=', 'sv.id')
|
||||
->whereIn('sv.version_major', $majorVersions)
|
||||
->whereIn('sv.version_minor', $minorVersions)
|
||||
->whereIn('sv.version_patch', $patchVersions)
|
||||
->where('sv.version', '!=', '0.0.0')
|
||||
->groupBy('mods.id')
|
||||
->distinct();
|
||||
return $this->builder->whereExists(function ($query) use ($versions) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('mod_versions')
|
||||
->join('mod_version_spt_version', 'mod_versions.id', '=', 'mod_version_spt_version.mod_version_id')
|
||||
->join('spt_versions', 'mod_version_spt_version.spt_version_id', '=', 'spt_versions.id')
|
||||
->whereColumn('mod_versions.mod_id', 'mods.id')
|
||||
->whereIn('spt_versions.version', $versions)
|
||||
->where('spt_versions.version', '!=', '0.0.0');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Jobs\Import;
|
||||
|
||||
use App\Exceptions\InvalidVersionNumberException;
|
||||
use App\Jobs\Import\DataTransferObjects\HubUser;
|
||||
use App\Models\License;
|
||||
use App\Models\Mod;
|
||||
@ -9,6 +10,7 @@ use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRole;
|
||||
use App\Support\Version;
|
||||
use Carbon\Carbon;
|
||||
use CurlHandle;
|
||||
use Exception;
|
||||
@ -969,10 +971,20 @@ class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
$sptVersionTemp = DB::table('temp_spt_version_tags')->where('hub_id', $versionLabel->labelID)->value('version');
|
||||
$sptVersionConstraint = $this->extractSemanticVersion($sptVersionTemp, appendPatch: true) ?? '0.0.0';
|
||||
|
||||
try {
|
||||
$modVersion = new Version($version->versionNumber);
|
||||
} catch (InvalidVersionNumberException $e) {
|
||||
$modVersion = new Version('0.0.0');
|
||||
}
|
||||
|
||||
$insertData[] = [
|
||||
'hub_id' => (int) $version->versionID,
|
||||
'mod_id' => $modId,
|
||||
'version' => $this->extractSemanticVersion($version->versionNumber) ?? '0.0.0',
|
||||
'version' => $modVersion,
|
||||
'version_major' => $modVersion->getMajor(),
|
||||
'version_minor' => $modVersion->getMinor(),
|
||||
'version_patch' => $modVersion->getPatch(),
|
||||
'version_pre_release' => $modVersion->getPreRelease(),
|
||||
'description' => $this->cleanHubContent($versionContent->description ?? ''),
|
||||
'link' => $version->downloadURL,
|
||||
'spt_version_constraint' => $sptVersionConstraint,
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Mod;
|
||||
|
||||
use App\Http\Filters\ModFilter;
|
||||
use App\Models\Mod;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Contracts\View\View;
|
||||
@ -95,6 +96,8 @@ class Listing extends Component
|
||||
|
||||
/**
|
||||
* Check if the current page is greater than the last page. Redirect if it is.
|
||||
*
|
||||
* @param LengthAwarePaginator<Mod> $mods
|
||||
*/
|
||||
private function redirectOutOfBoundsPage(LengthAwarePaginator $mods): void
|
||||
{
|
||||
|
@ -66,6 +66,19 @@ class Mod extends Model
|
||||
return $this->belongsTo(License::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its last updated version.
|
||||
*
|
||||
* @return HasOne<ModVersion>
|
||||
*/
|
||||
public function latestUpdatedVersion(): HasOne
|
||||
{
|
||||
return $this->versions()
|
||||
->one()
|
||||
->ofMany('updated_at', 'max')
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its versions.
|
||||
*
|
||||
@ -74,21 +87,10 @@ class Mod extends Model
|
||||
public function versions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ModVersion::class)
|
||||
->whereHas('latestSptVersion')
|
||||
->orderByDesc('version')
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its last updated version.
|
||||
*
|
||||
* @return HasOne<ModVersion>
|
||||
*/
|
||||
public function lastUpdatedVersion(): HasOne
|
||||
{
|
||||
return $this->hasOne(ModVersion::class)
|
||||
->whereHas('latestSptVersion')
|
||||
->orderByDesc('updated_at')
|
||||
->orderByDesc('version_major')
|
||||
->orderByDesc('version_minor')
|
||||
->orderByDesc('version_patch')
|
||||
->orderByDesc('version_pre_release')
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
@ -109,26 +111,11 @@ class Mod extends Model
|
||||
'created_at' => strtotime($this->created_at),
|
||||
'updated_at' => strtotime($this->updated_at),
|
||||
'published_at' => strtotime($this->published_at),
|
||||
'latestVersion' => $this->latestVersion()->first()->latestSptVersion()->first()->version_formatted,
|
||||
'latestVersionColorClass' => $this->latestVersion()->first()->latestSptVersion()->first()->color_class,
|
||||
'latestVersion' => $this->latestVersion->latestSptVersion->version_formatted,
|
||||
'latestVersionColorClass' => $this->latestVersion->latestSptVersion->color_class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship to the latest mod version, dictated by the mod version number.
|
||||
*
|
||||
* @return HasOne<ModVersion>
|
||||
*/
|
||||
public function latestVersion(): HasOne
|
||||
{
|
||||
return $this->hasOne(ModVersion::class)
|
||||
->whereHas('sptVersions')
|
||||
->orderByDesc('version')
|
||||
->orderByDesc('updated_at')
|
||||
->take(1)
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model instance should be searchable.
|
||||
*/
|
||||
@ -144,16 +131,13 @@ class Mod extends Model
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch the latest version instance.
|
||||
$latestVersion = $this->latestVersion()->first();
|
||||
|
||||
// Ensure the mod has a latest version.
|
||||
if (is_null($latestVersion)) {
|
||||
if ($this->latestVersion()->doesntExist()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the latest version has a latest SPT version.
|
||||
if ($latestVersion->latestSptVersion()->doesntExist()) {
|
||||
if ($this->latestVersion->latestSptVersion()->doesntExist()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -161,7 +145,7 @@ class Mod extends Model
|
||||
$activeSptVersions = Cache::remember('active-spt-versions', 60 * 60, function () {
|
||||
return SptVersion::getVersionsForLastThreeMinors();
|
||||
});
|
||||
if (! in_array($latestVersion->latestSptVersion()->first()->version, $activeSptVersions->pluck('version')->toArray())) {
|
||||
if (! in_array($this->latestVersion->latestSptVersion->version, $activeSptVersions->pluck('version')->toArray())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -169,6 +153,24 @@ class Mod extends Model
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its latest version.
|
||||
*
|
||||
* @return HasOne<ModVersion>
|
||||
*/
|
||||
public function latestVersion(string $sort = 'version'): HasOne
|
||||
{
|
||||
return $this->versions()
|
||||
->one()
|
||||
->ofMany([
|
||||
'version_major' => 'max',
|
||||
'version_minor' => 'max',
|
||||
'version_patch' => 'max',
|
||||
'version_pre_release' => 'max',
|
||||
])
|
||||
->chaperone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the URL to the mod's thumbnail.
|
||||
*
|
||||
|
@ -2,14 +2,17 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\InvalidVersionNumberException;
|
||||
use App\Models\Scopes\DisabledScope;
|
||||
use App\Models\Scopes\PublishedScope;
|
||||
use App\Support\Version;
|
||||
use Database\Factories\ModFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ModVersion extends Model
|
||||
@ -25,7 +28,25 @@ class ModVersion extends Model
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::addGlobalScope(new DisabledScope);
|
||||
|
||||
static::addGlobalScope(new PublishedScope);
|
||||
|
||||
static::saving(function (ModVersion $model) {
|
||||
// Extract the version sections from the version string.
|
||||
try {
|
||||
$version = new Version($model->version);
|
||||
|
||||
$model->version_major = $version->getMajor();
|
||||
$model->version_minor = $version->getMinor();
|
||||
$model->version_patch = $version->getPatch();
|
||||
$model->version_pre_release = $version->getPreRelease();
|
||||
} catch (InvalidVersionNumberException $e) {
|
||||
$model->version_major = 0;
|
||||
$model->version_minor = 0;
|
||||
$model->version_patch = 0;
|
||||
$model->version_pre_release = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,15 +100,17 @@ class ModVersion extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and each of its SPT versions' latest version.
|
||||
* Hint: Be sure to call `->first()` on this to get the actual instance.
|
||||
* The relationship between a mod version and its latest SPT version.
|
||||
*
|
||||
* @return BelongsToMany<SptVersion>
|
||||
* @return HasOneThrough<SptVersion>
|
||||
*/
|
||||
public function latestSptVersion(): BelongsToMany
|
||||
public function latestSptVersion(): HasOneThrough
|
||||
{
|
||||
return $this->belongsToMany(SptVersion::class, 'mod_version_spt_version')
|
||||
->orderBy('version', 'desc')
|
||||
return $this->hasOneThrough(SptVersion::class, ModVersionSptVersion::class, 'mod_version_id', 'id', 'id', 'spt_version_id')
|
||||
->orderByDesc('spt_versions.version_major')
|
||||
->orderByDesc('spt_versions.version_minor')
|
||||
->orderByDesc('spt_versions.version_patch')
|
||||
->orderByDesc('spt_versions.version_pre_release')
|
||||
->limit(1);
|
||||
}
|
||||
|
||||
@ -98,7 +121,11 @@ class ModVersion extends Model
|
||||
*/
|
||||
public function sptVersions(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(SptVersion::class, 'mod_version_spt_version')
|
||||
->orderByDesc('version');
|
||||
return $this->belongsToMany(SptVersion::class)
|
||||
->using(ModVersionSptVersion::class)
|
||||
->orderByDesc('version_major')
|
||||
->orderByDesc('version_minor')
|
||||
->orderByDesc('version_patch')
|
||||
->orderByDesc('version_pre_release');
|
||||
}
|
||||
}
|
||||
|
10
app/Models/ModVersionSptVersion.php
Normal file
10
app/Models/ModVersionSptVersion.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
|
||||
class ModVersionSptVersion extends Pivot
|
||||
{
|
||||
public $incrementing = true;
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\InvalidVersionNumberException;
|
||||
use App\Support\Version;
|
||||
use Database\Factories\SptVersionFactory;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@ -71,35 +72,6 @@ class SptVersion extends Model
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the model is booted.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
// Callback that runs before saving the model.
|
||||
static::saving(function (SptVersion $model) {
|
||||
// Extract the version sections from the version string.
|
||||
if (! empty($model->version)) {
|
||||
// Default values in case there's an exception.
|
||||
$model->version_major = 0;
|
||||
$model->version_minor = 0;
|
||||
$model->version_patch = 0;
|
||||
$model->version_pre_release = '';
|
||||
|
||||
try {
|
||||
$versionSections = self::extractVersionSections($model->version);
|
||||
} catch (InvalidVersionNumberException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$model->version_major = $versionSections['major'];
|
||||
$model->version_minor = $versionSections['minor'];
|
||||
$model->version_patch = $versionSections['patch'];
|
||||
$model->version_pre_release = $versionSections['pre_release'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the version sections from the version string.
|
||||
*
|
||||
@ -126,6 +98,29 @@ class SptVersion extends Model
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the model is booted.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (SptVersion $model) {
|
||||
// Extract the version sections from the version string.
|
||||
try {
|
||||
$version = new Version($model->version);
|
||||
|
||||
$model->version_major = $version->getMajor();
|
||||
$model->version_minor = $version->getMinor();
|
||||
$model->version_patch = $version->getPatch();
|
||||
$model->version_pre_release = $version->getPreRelease();
|
||||
} catch (InvalidVersionNumberException $e) {
|
||||
$model->version_major = 0;
|
||||
$model->version_minor = 0;
|
||||
$model->version_patch = 0;
|
||||
$model->version_pre_release = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the mod count for this SptVersion.
|
||||
*/
|
||||
@ -146,7 +141,8 @@ class SptVersion extends Model
|
||||
*/
|
||||
public function modVersions(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(ModVersion::class, 'mod_version_spt_version');
|
||||
return $this->belongsToMany(ModVersion::class)
|
||||
->using(ModVersionSptVersion::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
74
app/Support/Version.php
Normal file
74
app/Support/Version.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Exceptions\InvalidVersionNumberException;
|
||||
|
||||
class Version
|
||||
{
|
||||
protected int $major = 0;
|
||||
|
||||
protected int $minor = 0;
|
||||
|
||||
protected int $patch = 0;
|
||||
|
||||
protected string $preRelease = '';
|
||||
|
||||
protected string $version;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @throws InvalidVersionNumberException
|
||||
*/
|
||||
public function __construct(string $version)
|
||||
{
|
||||
$this->version = $version;
|
||||
$this->parseVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the version string into its components.
|
||||
*
|
||||
* @throws InvalidVersionNumberException
|
||||
*/
|
||||
protected function parseVersion(): void
|
||||
{
|
||||
$matches = [];
|
||||
|
||||
// Regex to match semantic versioning, including pre-release identifiers
|
||||
if (preg_match('/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([\w.-]+))?$/', $this->version, $matches)) {
|
||||
$this->major = (int) ($matches[1] ?? 0);
|
||||
$this->minor = (int) ($matches[2] ?? 0);
|
||||
$this->patch = (int) ($matches[3] ?? 0);
|
||||
$this->preRelease = $matches[4] ?? '';
|
||||
} else {
|
||||
throw new InvalidVersionNumberException('Invalid version number: '.$this->version);
|
||||
}
|
||||
}
|
||||
|
||||
public function getMajor(): int
|
||||
{
|
||||
return $this->major;
|
||||
}
|
||||
|
||||
public function getMinor(): int
|
||||
{
|
||||
return $this->minor;
|
||||
}
|
||||
|
||||
public function getPatch(): int
|
||||
{
|
||||
return $this->patch;
|
||||
}
|
||||
|
||||
public function getPreRelease(): string
|
||||
{
|
||||
return $this->preRelease;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
}
|
@ -3,10 +3,8 @@
|
||||
namespace App\View\Components;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class HomepageMods extends Component
|
||||
@ -39,13 +37,13 @@ class HomepageMods extends Component
|
||||
*/
|
||||
private function fetchFeaturedMods(): Collection
|
||||
{
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'downloads', 'updated_at'])
|
||||
return Mod::whereFeatured(true)
|
||||
->with([
|
||||
'latestVersion.latestSptVersion:id,version,color_class',
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->whereFeatured(true)
|
||||
->inRandomOrder()
|
||||
->limit(6)
|
||||
->get();
|
||||
@ -58,13 +56,13 @@ class HomepageMods extends Component
|
||||
*/
|
||||
private function fetchLatestMods(): Collection
|
||||
{
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at', 'downloads', 'updated_at'])
|
||||
return Mod::orderByDesc('created_at')
|
||||
->with([
|
||||
'latestVersion.latestSptVersion:id,version,color_class',
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->latest()
|
||||
->limit(6)
|
||||
->get();
|
||||
}
|
||||
@ -76,20 +74,13 @@ class HomepageMods extends Component
|
||||
*/
|
||||
private function fetchUpdatedMods(): Collection
|
||||
{
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'downloads', 'updated_at'])
|
||||
return Mod::orderByDesc('updated_at')
|
||||
->with([
|
||||
'lastUpdatedVersion.latestSptVersion:id,version,color_class',
|
||||
'latestUpdatedVersion',
|
||||
'latestUpdatedVersion.latestSptVersion',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->joinSub(
|
||||
ModVersion::select('mod_id', DB::raw('MAX(updated_at) as latest_updated_at'))->groupBy('mod_id'),
|
||||
'latest_versions',
|
||||
'mods.id',
|
||||
'=',
|
||||
'latest_versions.mod_id'
|
||||
)
|
||||
->orderByDesc('latest_versions.latest_updated_at')
|
||||
->limit(6)
|
||||
->get();
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ namespace Database\Factories;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use App\Support\Version;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
@ -17,9 +18,20 @@ class ModVersionFactory extends Factory
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$versionString = $this->faker->numerify('#.#.#');
|
||||
try {
|
||||
$version = new Version($versionString);
|
||||
} catch (\Exception $e) {
|
||||
$version = new Version('0.0.0');
|
||||
}
|
||||
|
||||
return [
|
||||
'mod_id' => Mod::factory(),
|
||||
'version' => fake()->numerify('#.#.#'),
|
||||
'version' => $versionString,
|
||||
'version_major' => $version->getMajor(),
|
||||
'version_minor' => $version->getMinor(),
|
||||
'version_patch' => $version->getPatch(),
|
||||
'version_pre_release' => $version->getPreRelease(),
|
||||
'description' => fake()->text(),
|
||||
'link' => fake()->url(),
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\SptVersion;
|
||||
use App\Support\Version;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
@ -15,8 +16,19 @@ class SptVersionFactory extends Factory
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$versionString = $this->faker->numerify('#.#.#');
|
||||
try {
|
||||
$version = new Version($versionString);
|
||||
} catch (\Exception $e) {
|
||||
$version = new Version('0.0.0');
|
||||
}
|
||||
|
||||
return [
|
||||
'version' => $this->faker->numerify('#.#.#'),
|
||||
'version' => $versionString,
|
||||
'version_major' => $version->getMajor(),
|
||||
'version_minor' => $version->getMinor(),
|
||||
'version_patch' => $version->getPatch(),
|
||||
'version_pre_release' => $version->getPreRelease(),
|
||||
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
||||
'link' => $this->faker->url,
|
||||
'created_at' => Carbon::now(),
|
||||
|
@ -15,18 +15,17 @@ return new class extends Migration
|
||||
->default(null)
|
||||
->unique();
|
||||
$table->string('version');
|
||||
$table->unsignedInteger('version_major');
|
||||
$table->unsignedInteger('version_minor');
|
||||
$table->unsignedInteger('version_patch');
|
||||
$table->string('version_pre_release');
|
||||
$table->unsignedInteger('version_major')->default(0);
|
||||
$table->unsignedInteger('version_minor')->default(0);
|
||||
$table->unsignedInteger('version_patch')->default(0);
|
||||
$table->string('version_pre_release')->default('');
|
||||
$table->unsignedInteger('mod_count')->default(0);
|
||||
$table->string('link');
|
||||
$table->string('color_class');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['version', 'deleted_at', 'id'], 'spt_versions_filtering_index');
|
||||
$table->index(['version_major', 'version_minor', 'version_patch', 'version_pre_release', 'deleted_at'], 'spt_versions_lookup_index');
|
||||
$table->index(['version', 'version_major', 'version_minor', 'version_patch', 'version_pre_release'], 'spt_versions_lookup_index');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,10 @@ return new class extends Migration
|
||||
->cascadeOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->string('version');
|
||||
$table->unsignedInteger('version_major')->default(0);
|
||||
$table->unsignedInteger('version_minor')->default(0);
|
||||
$table->unsignedInteger('version_patch')->default(0);
|
||||
$table->string('version_pre_release')->default('');
|
||||
$table->longText('description');
|
||||
$table->string('link');
|
||||
$table->string('spt_version_constraint');
|
||||
@ -32,6 +36,7 @@ return new class extends Migration
|
||||
|
||||
$table->index(['version']);
|
||||
$table->index(['mod_id', 'deleted_at', 'disabled', 'published_at'], 'mod_versions_filtering_index');
|
||||
$table->index(['version_major', 'version_minor', 'version_patch', 'version_pre_release'], 'mod_versions_version_components_index');
|
||||
$table->index(['id', 'deleted_at'], 'mod_versions_id_deleted_at_index');
|
||||
});
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ return new class extends Migration
|
||||
$table->id();
|
||||
$table->foreignId('mod_version_id')->constrained('mod_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('spt_version_id')->constrained('spt_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['mod_version_id', 'spt_version_id'], 'mod_version_spt_version_index');
|
||||
$table->index(['spt_version_id', 'mod_version_id'], 'spt_version_mod_version_index');
|
||||
|
@ -16,7 +16,7 @@
|
||||
<x-page-content-title :title="$updated['title']" :button-text="__('View All')" :button-link="$updated['link']" />
|
||||
<div class="my-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
@foreach ($updated['mods'] as $mod)
|
||||
<x-mod-card :mod="$mod" :version="$mod->lastUpdatedVersion" />
|
||||
<x-mod-card :mod="$mod" :version="$mod->latestUpdatedVersion" />
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,8 +18,8 @@
|
||||
<div class="pb-3">
|
||||
<div class="flex justify-between items-center space-x-3">
|
||||
<h3 class="block mt-1 text-lg leading-tight font-medium text-black dark:text-white group-hover:underline">{{ $mod->name }}</h3>
|
||||
<span class="badge-version {{ $version->latestSptVersion->first()->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $version->latestSptVersion->first()->version_formatted }}
|
||||
<span class="badge-version {{ $version->latestSptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $version->latestSptVersion->version_formatted }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm italic text-slate-600 dark:text-gray-200">
|
||||
|
@ -40,8 +40,8 @@
|
||||
</p>
|
||||
<p title="{{ __('Exactly') }} {{ $mod->downloads }}">{{ Number::downloads($mod->downloads) }} {{ __('Downloads') }}</p>
|
||||
<p class="mt-2">
|
||||
<span class="badge-version {{ $mod->latestVersion->latestSptVersion->first()->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $mod->latestVersion->latestSptVersion->first()->version_formatted }} {{ __('Compatible') }}
|
||||
<span class="badge-version {{ $mod->latestVersion->latestSptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $mod->latestVersion->latestSptVersion->version_formatted }} {{ __('Compatible') }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -108,8 +108,8 @@
|
||||
<p class="text-gray-700 dark:text-gray-300" title="{{ __('Exactly') }} {{ $version->downloads }}">{{ Number::downloads($version->downloads) }} {{ __('Downloads') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="badge-version {{ $version->latestSptVersion->first()->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $version->latestSptVersion->first()->version_formatted }}
|
||||
<span class="badge-version {{ $version->latestSptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $version->latestSptVersion->version_formatted }}
|
||||
</span>
|
||||
<a href="{{ $version->virus_total_link }}">{{__('Virus Total Results')}}</a>
|
||||
</div>
|
||||
|
@ -99,6 +99,36 @@ it('returns no mods when no SPT versions match', function () {
|
||||
expect($filteredMods)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('filters mods based on a exact search term', function () {
|
||||
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
|
||||
$mod = Mod::factory()->create(['name' => 'BigBrain']);
|
||||
ModVersion::factory()->recycle($mod)->create(['spt_version_constraint' => '^1.0.0']);
|
||||
|
||||
Mod::factory()->create(['name' => 'SmallFeet']);
|
||||
ModVersion::factory()->recycle($mod)->create(['spt_version_constraint' => '^1.0.0']);
|
||||
|
||||
$filters = ['query' => 'BigBrain'];
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
expect($filteredMods)->toHaveCount(1)->and($filteredMods->first()->id)->toBe($mod->id);
|
||||
});
|
||||
|
||||
it('filters mods based featured status', function () {
|
||||
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
|
||||
$mod = Mod::factory()->create(['name' => 'BigBrain', 'featured' => true]);
|
||||
ModVersion::factory()->recycle($mod)->create(['spt_version_constraint' => '^1.0.0']);
|
||||
|
||||
Mod::factory()->create(['name' => 'SmallFeet']);
|
||||
ModVersion::factory()->recycle($mod)->create(['spt_version_constraint' => '^1.0.0']);
|
||||
|
||||
$filters = ['featured' => true];
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
expect($filteredMods)->toHaveCount(1)->and($filteredMods->first()->id)->toBe($mod->id);
|
||||
});
|
||||
|
||||
it('filters mods correctly with combined filters', function () {
|
||||
// Create the SPT versions
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
@ -128,8 +158,7 @@ it('filters mods correctly with combined filters', function () {
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that only the correct mod is returned
|
||||
expect($filteredMods)->toHaveCount(1)
|
||||
->and($filteredMods->first()->id)->toBe($mod1->id);
|
||||
expect($filteredMods)->toHaveCount(1)->and($filteredMods->first()->id)->toBe($mod1->id);
|
||||
});
|
||||
|
||||
it('handles an empty SPT versions array correctly', function () {
|
||||
|
Loading…
x
Reference in New Issue
Block a user