mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 12:10:41 -05:00
Merge remote-tracking branch 'upstream/develop' into user-profile-info
This commit is contained in:
commit
0f3c7e03d0
@ -89,3 +89,6 @@ DB_HUB_COLLATION=utf8mb4_0900_ai_ci
|
||||
|
||||
GITEA_DOMAIN=
|
||||
GITEA_TOKEN=
|
||||
|
||||
# API key for Scribe documentation.
|
||||
SCRIBE_AUTH_KEY=
|
||||
|
@ -42,3 +42,6 @@ SCOUT_DRIVER=collection
|
||||
MAIL_MAILER=log
|
||||
MAIL_FROM_ADDRESS="no-reply@sp-tarkov.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# API key for Scribe documentation.
|
||||
SCRIBE_AUTH_KEY=
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,3 +24,4 @@ Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.scribe
|
||||
|
@ -9,41 +9,77 @@ use App\Traits\ApiResponses;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Knuckles\Scribe\Attributes\BodyParam;
|
||||
use Knuckles\Scribe\Attributes\Response;
|
||||
use Knuckles\Scribe\Attributes\ResponseField;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
use ApiResponses;
|
||||
|
||||
/**
|
||||
* Login
|
||||
*
|
||||
* Authenticates the user and returns a read-only API token. This API token can then be saved and used for future
|
||||
* requests that require authentication. <aside class="warning">This method is made available for mod authors to
|
||||
* incorporate into their mods so that users can easily authenticate using their own API token. For typical API use,
|
||||
* you should log into the website, create an API token, and use that token for your API requests.</aside>
|
||||
*
|
||||
* @unauthenticated
|
||||
*
|
||||
* @group Authentication
|
||||
*/
|
||||
#[BodyParam('token_name', 'string', 'The name of the API token.', required: false, example: 'Dynamic API Token')]
|
||||
#[Response(['message' => 'authenticated', 'data' => ['token' => 'YOUR_API_KEY'], 'status' => 200], status: 200, description: 'Authenticated successfully')]
|
||||
#[Response(['message' => 'invalid credentials', 'status' => 401], status: 401, description: 'Invalid credentials')]
|
||||
#[ResponseField('token', description: 'The newly created read-only API token to use for future authenticated requests.')]
|
||||
public function login(LoginUserRequest $request): JsonResponse
|
||||
{
|
||||
$request->validated($request->all());
|
||||
|
||||
if (! Auth::attempt($request->only('email', 'password'))) {
|
||||
return $this->error(__('Invalid credentials'), 401);
|
||||
return $this->error(__('invalid credentials'), 401);
|
||||
}
|
||||
|
||||
$user = User::firstWhere('email', $request->email);
|
||||
$tokenName = $request->token_name ?? __('Dynamic API Token');
|
||||
|
||||
return $this->success(__('Authenticated'), [
|
||||
return $this->success(__('authenticated'), [
|
||||
// Only allowing the 'read' scope to be dynamically created. Can revisit later when writes are possible.
|
||||
'token' => $user->createToken($tokenName, ['read'])->plainTextToken,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*
|
||||
* Destroys the user's current API token, effectively logging them out.
|
||||
*
|
||||
* @group Authentication
|
||||
*/
|
||||
#[Response(['message' => 'success', 'status' => 200], status: 200, description: 'Token destroyed successfully')]
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
/** @var \Laravel\Sanctum\PersonalAccessToken $token */
|
||||
/** @var PersonalAccessToken $token */
|
||||
$token = $request->user()->currentAccessToken();
|
||||
$token->delete();
|
||||
|
||||
return $this->success(__('Revoked API token'));
|
||||
return $this->success(__('success'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout All
|
||||
*
|
||||
* Destroys all the user's API tokens, effectively logging everyone out of the account.
|
||||
*
|
||||
* @group Authentication
|
||||
*/
|
||||
#[Response(['message' => 'success', 'status' => 200], status: 200, description: 'Tokens destroyed successfully')]
|
||||
public function logoutAll(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->tokens()->delete();
|
||||
|
||||
return $this->success(__('Revoked all API tokens'));
|
||||
return $this->success(__('success'));
|
||||
}
|
||||
}
|
||||
|
@ -3,43 +3,58 @@
|
||||
namespace App\Http\Controllers\Api\V0;
|
||||
|
||||
use App\Http\Filters\V1\ModFilter;
|
||||
use App\Http\Requests\Api\V0\StoreModRequest;
|
||||
use App\Http\Requests\Api\V0\UpdateModRequest;
|
||||
use App\Http\Resources\Api\V0\ModResource;
|
||||
use App\Models\Mod;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Knuckles\Scribe\Attributes\QueryParam;
|
||||
use Knuckles\Scribe\Attributes\UrlParam;
|
||||
|
||||
class ModController extends ApiController
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
* Get Mods
|
||||
*
|
||||
* List, filter, and sort basic information about mods.
|
||||
*
|
||||
* @group Mods
|
||||
*/
|
||||
#[QueryParam('include', 'string', 'The relationships to include within the `includes` key. By default no relationships are automatically included.', required: false, example: 'users,versions,license')]
|
||||
#[QueryParam('filter[id]', 'string', 'Filter by the `id`. Select multiple by separating the IDs with a comma.', required: false, example: '5,10,15')]
|
||||
#[QueryParam('filter[hub_id]', 'string', 'Filter by the `hub_id` attribute. Select multiple by separating the IDs with a comma.', required: false, example: '20')]
|
||||
#[QueryParam('filter[name]', 'string', 'Filter by the `name` attribute. Use `*` as the wildcard character.', required: false, example: '*SAIN*')]
|
||||
#[QueryParam('filter[slug]', 'string', 'Filter by the `slug` attribute. Use `*` as the wildcard character.', required: false, example: '*raid-times')]
|
||||
#[QueryParam('filter[teaser]', 'string', 'Filter by the `teaser` attribute. Use `*` as the wildcard character.', required: false, example: '*weighted*random*times*')]
|
||||
#[QueryParam('filter[source_code_link]', 'string', 'Filter by the `source_code_link` attribute. Use `*` as the wildcard character.', required: false, example: '*https*.net*')]
|
||||
#[QueryParam('filter[featured]', 'boolean', 'Filter by the `featured` attribute. All "truthy" or "falsy" values are supported.', required: false, example: 'true')]
|
||||
#[QueryParam('filter[contains_ads]', 'boolean', 'Filter by the `contains_ads` attribute. All "truthy" or "falsy" values are supported.', required: false, example: 'true')]
|
||||
#[QueryParam('filter[contains_ai_content]', 'boolean', 'Filter by the `contains_ai_content` attribute. All "truthy" or "falsy" values are supported.', required: false, example: 'true')]
|
||||
#[QueryParam('filter[created_at]', 'string', 'Filter by the `created_at` attribute. Ranges are possible by separating the dates with a comma.', required: false, example: '2023-12-31,2024-12-31')]
|
||||
#[QueryParam('filter[updated_at]', 'string', 'Filter by the `updated_at` attribute. Ranges are possible by separating the dates with a comma.', required: false, example: '2023-12-31,2024-12-31')]
|
||||
#[QueryParam('filter[published_at]', 'string', 'Filter by the `published_at` attribute. Ranges are possible by seperating the dates with a comma.', required: false, example: '2023-12-31,2024-12-31')]
|
||||
#[QueryParam('sort', 'string', 'Sort the results by a comma seperated list of attributes. The default sort direction is ASC, append the attribute name with a minus to sort DESC.', required: false, example: '-featured,name')]
|
||||
public function index(ModFilter $filters): AnonymousResourceCollection
|
||||
{
|
||||
return ModResource::collection(Mod::filter($filters)->paginate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(StoreModRequest $request): void {}
|
||||
//public function store(StoreModRequest $request): void {}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
* Get Mod
|
||||
*
|
||||
* Display more detailed information about a specific mod.
|
||||
*
|
||||
* @group Mods
|
||||
*/
|
||||
#[UrlParam('id', 'integer', 'The ID of the mod.', required: true, example: 558)]
|
||||
#[QueryParam('include', 'string', 'The relationships to include within the `includes` key. By default no relationships are automatically included.', required: false, example: 'users,versions,license')]
|
||||
public function show(Mod $mod): JsonResource
|
||||
{
|
||||
return new ModResource($mod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(UpdateModRequest $request, Mod $mod): void {}
|
||||
//public function update(UpdateModRequest $request, Mod $mod): void {}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Mod $mod): void {}
|
||||
//public function destroy(Mod $mod): void {}
|
||||
}
|
||||
|
@ -3,43 +3,48 @@
|
||||
namespace App\Http\Controllers\Api\V0;
|
||||
|
||||
use App\Http\Filters\V1\UserFilter;
|
||||
use App\Http\Requests\Api\V0\StoreUserRequest;
|
||||
use App\Http\Requests\Api\V0\UpdateUserRequest;
|
||||
use App\Http\Resources\Api\V0\UserResource;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Knuckles\Scribe\Attributes\QueryParam;
|
||||
|
||||
class UsersController extends ApiController
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
* Get Users
|
||||
*
|
||||
* List, filter, and sort basic information about users.
|
||||
*
|
||||
* @group Users
|
||||
*/
|
||||
#[QueryParam('include', 'string', 'The relationships to include within the `includes` key. By default no relationships are automatically included.', required: false, example: 'user_role')]
|
||||
#[QueryParam('filter[id]', 'string', 'Filter by the `id`. Select multiple by separating the IDs with a comma.', required: false, example: '5,10,15')]
|
||||
#[QueryParam('filter[name]', 'string', 'Filter by the `name` attribute. Use `*` as the wildcard character.', required: false, example: '*fringe')]
|
||||
#[QueryParam('filter[created_at]', 'string', 'Filter by the `created_at` attribute. Ranges are possible by separating the dates with a comma.', required: false, example: '2023-12-31,2024-12-31')]
|
||||
#[QueryParam('filter[updated_at]', 'string', 'Filter by the `updated_at` attribute. Ranges are possible by separating the dates with a comma.', required: false, example: '2023-12-31,2024-12-31')]
|
||||
#[QueryParam('sort', 'string', 'Sort the results by a comma seperated list of attributes. The default sort direction is ASC, append the attribute name with a minus to sort DESC.', required: false, example: 'created_at,-name')]
|
||||
public function index(UserFilter $filters): AnonymousResourceCollection
|
||||
{
|
||||
return UserResource::collection(User::filter($filters)->paginate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(StoreUserRequest $request): void {}
|
||||
//public function store(StoreUserRequest $request): void {}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
* Get User
|
||||
*
|
||||
* Display more detailed information about a specific user.
|
||||
*
|
||||
* @group Users
|
||||
*/
|
||||
#[QueryParam('include', 'string', 'The relationships to include within the `includes` key. By default no relationships are automatically included.', required: false, example: 'user_role')]
|
||||
public function show(User $user): JsonResource
|
||||
{
|
||||
return new UserResource($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(UpdateUserRequest $request, User $user): void {}
|
||||
//public function update(UpdateUserRequest $request, User $user): void {}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(User $user): void {}
|
||||
//public function destroy(User $user): void {}
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ class ModFilter
|
||||
private function order(string $type): Builder
|
||||
{
|
||||
return match ($type) {
|
||||
'updated' => $this->builder->orderByDesc('mods.updated_at'), // TODO: This needs to be updated when a version is updated.
|
||||
'updated' => $this->builder->orderByDesc('mods.updated_at'),
|
||||
'downloaded' => $this->builder->orderByDesc('mods.downloads'),
|
||||
default => $this->builder->orderByDesc('mods.created_at'),
|
||||
};
|
||||
|
@ -4,13 +4,17 @@ namespace App\Http\Filters\V1;
|
||||
|
||||
use App\Models\Mod;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends QueryFilter<Mod>
|
||||
*/
|
||||
class ModFilter extends QueryFilter
|
||||
{
|
||||
/**
|
||||
* The sortable fields.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $sortable = [
|
||||
'name',
|
||||
'slug',
|
||||
@ -24,9 +28,6 @@ class ModFilter extends QueryFilter
|
||||
'published_at',
|
||||
];
|
||||
|
||||
// TODO: Many of these are repeated across UserFilter and ModFilter. Consider refactoring into a shared trait.
|
||||
// Also, consider using common filter types and making the field names dynamic.
|
||||
|
||||
/**
|
||||
* Filter by ID.
|
||||
*
|
||||
@ -34,9 +35,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function id(string $value): Builder
|
||||
{
|
||||
$ids = array_map('trim', explode(',', $value));
|
||||
|
||||
return $this->builder->whereIn('id', $ids);
|
||||
return $this->filterWhereIn('id', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,9 +45,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function hub_id(string $value): Builder
|
||||
{
|
||||
$ids = array_map('trim', explode(',', $value));
|
||||
|
||||
return $this->builder->whereIn('hub_id', $ids);
|
||||
return $this->filterWhereIn('hub_id', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,10 +55,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function name(string $value): Builder
|
||||
{
|
||||
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where('name', 'like', $like);
|
||||
return $this->filterByWildcardLike('name', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,10 +65,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function slug(string $value): Builder
|
||||
{
|
||||
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where('slug', 'like', $like);
|
||||
return $this->filterByWildcardLike('slug', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,10 +75,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function teaser(string $value): Builder
|
||||
{
|
||||
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where('teaser', 'like', $like);
|
||||
return $this->filterByWildcardLike('teaser', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,10 +85,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function source_code_link(string $value): Builder
|
||||
{
|
||||
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where('source_code_link', 'like', $like);
|
||||
return $this->filterByWildcardLike('source_code_link', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,13 +95,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function created_at(string $value): Builder
|
||||
{
|
||||
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween('created_at', $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate('created_at', $value);
|
||||
return $this->filterByDate('created_at', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -126,13 +105,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function updated_at(string $value): Builder
|
||||
{
|
||||
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween('updated_at', $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate('updated_at', $value);
|
||||
return $this->filterByDate('updated_at', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -142,13 +115,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function published_at(string $value): Builder
|
||||
{
|
||||
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween('published_at', $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate('published_at', $value);
|
||||
return $this->filterByDate('published_at', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -158,15 +125,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function featured(string $value): Builder
|
||||
{
|
||||
// We need to convert the string user input to a boolean, or null if it's not a valid "truthy/falsy" value.
|
||||
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
|
||||
// This column is not nullable.
|
||||
if ($value === null) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
return $this->builder->where('featured', $value);
|
||||
return $this->filterByBoolean('featured', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -176,15 +135,7 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function contains_ads(string $value): Builder
|
||||
{
|
||||
// We need to convert the string user input to a boolean, or null if it's not a valid "truthy/falsy" value.
|
||||
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
|
||||
// This column is not nullable.
|
||||
if ($value === null) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
return $this->builder->where('contains_ads', $value);
|
||||
return $this->filterByBoolean('contains_ads', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -194,14 +145,6 @@ class ModFilter extends QueryFilter
|
||||
*/
|
||||
public function contains_ai_content(string $value): Builder
|
||||
{
|
||||
// We need to convert the string user input to a boolean, or null if it's not a valid "truthy/falsy" value.
|
||||
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
|
||||
// This column is not nullable.
|
||||
if ($value === null) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
return $this->builder->where('contains_ai_content', $value);
|
||||
return $this->filterByBoolean('contains_ai_content', $value);
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,23 @@
|
||||
|
||||
namespace App\Http\Filters\V1;
|
||||
|
||||
use App\Traits\V1\FilterMethods;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @template TModelClass of Model
|
||||
*/
|
||||
abstract class QueryFilter
|
||||
{
|
||||
/**
|
||||
* Include general filter methods.
|
||||
*
|
||||
* @use FilterMethods<TModelClass>
|
||||
*/
|
||||
use FilterMethods;
|
||||
|
||||
/**
|
||||
* The query builder instance.
|
||||
*
|
||||
@ -19,18 +26,45 @@ abstract class QueryFilter
|
||||
*/
|
||||
protected Builder $builder;
|
||||
|
||||
/**
|
||||
* The request instance.
|
||||
*/
|
||||
protected Request $request;
|
||||
|
||||
/** @var array<string> */
|
||||
/**
|
||||
* The sortable fields.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $sortable = [];
|
||||
|
||||
/**
|
||||
* Create a new QueryFilter instance.
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the filter to the query builder.
|
||||
* Iterate over each of the filter options and call the appropriate method if it exists.
|
||||
*
|
||||
* @param array<string, string> $filters
|
||||
* @return Builder<TModelClass>
|
||||
*/
|
||||
public function filter(array $filters): Builder
|
||||
{
|
||||
foreach ($filters as $attribute => $value) {
|
||||
if (method_exists($this, $attribute)) {
|
||||
$this->$attribute($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over all request data and call the appropriate method if it exists.
|
||||
*
|
||||
* @param Builder<TModelClass> $builder
|
||||
* @return Builder<TModelClass>
|
||||
@ -47,25 +81,4 @@ abstract class QueryFilter
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the sort type to the query.
|
||||
*
|
||||
* @return Builder<TModelClass>
|
||||
*/
|
||||
protected function sort(string $values): Builder
|
||||
{
|
||||
$sortables = array_map('trim', explode(',', $values));
|
||||
|
||||
foreach ($sortables as $sortable) {
|
||||
$direction = Str::startsWith($sortable, '-') ? 'desc' : 'asc';
|
||||
$column = Str::of($sortable)->remove('-')->value();
|
||||
|
||||
if (in_array($column, $this->sortable)) {
|
||||
$this->builder->orderBy($column, $direction);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ namespace App\Http\Filters\V1;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends QueryFilter<User>
|
||||
@ -22,9 +21,6 @@ class UserFilter extends QueryFilter
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
// TODO: Many of these are repeated across UserFilter and ModFilter. Consider refactoring into a shared trait.
|
||||
// Also, consider using common filter types and making the field names dynamic.
|
||||
|
||||
/**
|
||||
* Filter by ID.
|
||||
*
|
||||
@ -32,9 +28,7 @@ class UserFilter extends QueryFilter
|
||||
*/
|
||||
public function id(string $value): Builder
|
||||
{
|
||||
$ids = array_map('trim', explode(',', $value));
|
||||
|
||||
return $this->builder->whereIn('id', $ids);
|
||||
return $this->filterWhereIn('id', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,10 +38,7 @@ class UserFilter extends QueryFilter
|
||||
*/
|
||||
public function name(string $value): Builder
|
||||
{
|
||||
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
return $this->builder->where('name', 'like', $like);
|
||||
return $this->filterByWildcardLike('name', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,13 +48,7 @@ class UserFilter extends QueryFilter
|
||||
*/
|
||||
public function created_at(string $value): Builder
|
||||
{
|
||||
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween('created_at', $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate('created_at', $value);
|
||||
return $this->filterByDate('created_at', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,12 +58,6 @@ class UserFilter extends QueryFilter
|
||||
*/
|
||||
public function updated_at(string $value): Builder
|
||||
{
|
||||
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
if (count($dates) > 1) {
|
||||
return $this->builder->whereBetween('updated_at', $dates);
|
||||
}
|
||||
|
||||
return $this->builder->whereDate('updated_at', $value);
|
||||
return $this->filterByDate('updated_at', $value);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,13 @@ class ModVersion extends Model
|
||||
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* Update the parent mod's updated_at timestamp when the mod version is updated.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $touches = ['mod'];
|
||||
|
||||
/**
|
||||
* Post boot method to configure the model.
|
||||
*/
|
||||
|
@ -8,8 +8,6 @@ trait ApiResponses
|
||||
{
|
||||
/**
|
||||
* Return a success JSON response.
|
||||
*
|
||||
* @param array<mixed> $data
|
||||
*/
|
||||
protected function success(string $message, ?array $data = []): JsonResponse
|
||||
{
|
||||
@ -18,15 +16,17 @@ trait ApiResponses
|
||||
|
||||
/**
|
||||
* The base response.
|
||||
*
|
||||
* @param array<mixed> $data
|
||||
*/
|
||||
private function baseResponse(?string $message = '', ?array $data = [], ?int $code = 200): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
], $code);
|
||||
$response = [];
|
||||
$response['message'] = $message;
|
||||
if ($data) {
|
||||
$response['data'] = $data;
|
||||
}
|
||||
$response['status'] = $code;
|
||||
|
||||
return response()->json($response, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
|
106
app/Traits/V1/FilterMethods.php
Normal file
106
app/Traits/V1/FilterMethods.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\V1;
|
||||
|
||||
use App\Http\Filters\V1\QueryFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @template TModelClass of Model
|
||||
*
|
||||
* @mixin QueryFilter<TModelClass>
|
||||
*/
|
||||
trait FilterMethods
|
||||
{
|
||||
/**
|
||||
* Filter using a whereIn clause.
|
||||
*
|
||||
* @return Builder<TModelClass>
|
||||
*/
|
||||
public function filterWhereIn(string $column, string $value): Builder
|
||||
{
|
||||
$ids = array_map('trim', explode(',', $value));
|
||||
|
||||
$result = $this->builder->whereIn($column, $ids);
|
||||
|
||||
/** @var Builder<TModelClass> $result */
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter using a LIKE clause with a wildcard characters.
|
||||
*
|
||||
* @return Builder<TModelClass>
|
||||
*/
|
||||
public function filterByWildcardLike(string $column, string $value): Builder
|
||||
{
|
||||
$like = Str::replace('*', '%', $value);
|
||||
|
||||
$result = $this->builder->where($column, 'like', $like);
|
||||
|
||||
/** @var Builder<TModelClass> $result */
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by date range or specific date.
|
||||
*
|
||||
* @return Builder<TModelClass>
|
||||
*/
|
||||
public function filterByDate(string $column, string $value): Builder
|
||||
{
|
||||
$dates = array_map('trim', explode(',', $value));
|
||||
|
||||
if (count($dates) > 1) {
|
||||
$result = $this->builder->whereBetween($column, $dates);
|
||||
} else {
|
||||
$result = $this->builder->whereDate($column, $dates[0]);
|
||||
}
|
||||
|
||||
/** @var Builder<TModelClass> $result */
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by boolean value.
|
||||
*
|
||||
* @return Builder<TModelClass>
|
||||
*/
|
||||
public function filterByBoolean(string $column, string $value): Builder
|
||||
{
|
||||
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
if ($value === null) {
|
||||
$result = $this->builder; // The unmodified builder
|
||||
} else {
|
||||
$result = $this->builder->where($column, $value);
|
||||
}
|
||||
|
||||
/** @var Builder<TModelClass> $result */
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the sort type to the query.
|
||||
*
|
||||
* @return Builder<TModelClass>
|
||||
*/
|
||||
protected function sort(string $values): Builder
|
||||
{
|
||||
$result = $this->builder;
|
||||
$sortables = array_map('trim', explode(',', $values));
|
||||
|
||||
foreach ($sortables as $sortable) {
|
||||
$direction = Str::startsWith($sortable, '-') ? 'desc' : 'asc';
|
||||
$column = Str::of($sortable)->remove('-')->value();
|
||||
|
||||
if (in_array($column, $this->sortable)) {
|
||||
$result = $this->builder->orderBy($column, $direction);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var Builder<TModelClass> $result */
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.13",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"knuckleswtf/scribe": "^4.37",
|
||||
"larastan/larastan": "^2.9",
|
||||
"laravel/pint": "^1.16",
|
||||
"laravel/sail": "^1.29",
|
||||
|
445
composer.lock
generated
445
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "aef706ee9aa7b671ca81c5ced4a7bfb7",
|
||||
"content-hash": "a687bfe8867c5e74bf6a974aaa969f62",
|
||||
"packages": [
|
||||
{
|
||||
"name": "anourvalar/eloquent-serialize",
|
||||
@ -9510,6 +9510,56 @@
|
||||
],
|
||||
"time": "2024-02-20T07:24:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "erusev/parsedown",
|
||||
"version": "1.7.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/erusev/parsedown.git",
|
||||
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
|
||||
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Parsedown": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Emanuil Rusev",
|
||||
"email": "hello@erusev.com",
|
||||
"homepage": "http://erusev.com"
|
||||
}
|
||||
],
|
||||
"description": "Parser for Markdown.",
|
||||
"homepage": "http://parsedown.org",
|
||||
"keywords": [
|
||||
"markdown",
|
||||
"parser"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/erusev/parsedown/issues",
|
||||
"source": "https://github.com/erusev/parsedown/tree/1.7.x"
|
||||
},
|
||||
"time": "2019-12-30T22:54:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"version": "v1.23.1",
|
||||
@ -9815,6 +9865,101 @@
|
||||
},
|
||||
"time": "2024-03-08T09:58:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "knuckleswtf/scribe",
|
||||
"version": "4.37.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/knuckleswtf/scribe.git",
|
||||
"reference": "6318f3f68cbf09328e5cb6843ce1739e529ef1ac"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/knuckleswtf/scribe/zipball/6318f3f68cbf09328e5cb6843ce1739e529ef1ac",
|
||||
"reference": "6318f3f68cbf09328e5cb6843ce1739e529ef1ac",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"erusev/parsedown": "1.7.4",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"illuminate/console": "^8.0|^9.0|^10.0|^11.0",
|
||||
"illuminate/routing": "^8.0|^9.0|^10.0|^11.0",
|
||||
"illuminate/support": "^8.0|^9.0|^10.0|^11.0",
|
||||
"league/flysystem": "^1.1.4|^2.1.1|^3.0",
|
||||
"mpociot/reflection-docblock": "^1.0.1",
|
||||
"nikic/php-parser": "^5.0",
|
||||
"nunomaduro/collision": "^5.10|^6.0|^7.0|^8.0",
|
||||
"php": ">=8.0",
|
||||
"ramsey/uuid": "^4.2.2",
|
||||
"shalvah/clara": "^3.1.0",
|
||||
"shalvah/upgrader": ">=0.6.0",
|
||||
"spatie/data-transfer-object": "^2.6|^3.0",
|
||||
"symfony/var-exporter": "^5.4|^6.0|^7.0",
|
||||
"symfony/yaml": "^5.4|^6.0|^7.0"
|
||||
},
|
||||
"replace": {
|
||||
"mpociot/laravel-apidoc-generator": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^6.0",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.4",
|
||||
"laravel/legacy-factories": "^1.3.0",
|
||||
"laravel/lumen-framework": "^8.0|^9.0|^10.0",
|
||||
"league/fractal": "^0.20",
|
||||
"nikic/fast-route": "^1.3",
|
||||
"orchestra/testbench": "^6.0|^7.0|^8.0",
|
||||
"pestphp/pest": "^1.21",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpunit/phpunit": "^9.0|^10.0",
|
||||
"symfony/css-selector": "^5.4|^6.0",
|
||||
"symfony/dom-crawler": "^5.4|^6.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Knuckles\\Scribe\\ScribeServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Knuckles\\Camel\\": "camel/",
|
||||
"Knuckles\\Scribe\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Shalvah"
|
||||
}
|
||||
],
|
||||
"description": "Generate API documentation for humans from your Laravel codebase.✍",
|
||||
"homepage": "http://github.com/knuckleswtf/scribe",
|
||||
"keywords": [
|
||||
"api",
|
||||
"dingo",
|
||||
"documentation",
|
||||
"laravel"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/knuckleswtf/scribe/issues",
|
||||
"source": "https://github.com/knuckleswtf/scribe/tree/4.37.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://patreon.com/shalvah",
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-30T12:15:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "larastan/larastan",
|
||||
"version": "v2.9.8",
|
||||
@ -10197,6 +10342,59 @@
|
||||
},
|
||||
"time": "2024-05-16T03:13:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mpociot/reflection-docblock",
|
||||
"version": "1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mpociot/reflection-docblock.git",
|
||||
"reference": "c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mpociot/reflection-docblock/zipball/c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587",
|
||||
"reference": "c8b2e2b1f5cebbb06e2b5ccbf2958f2198867587",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.0"
|
||||
},
|
||||
"suggest": {
|
||||
"dflydev/markdown": "~1.0",
|
||||
"erusev/parsedown": "~1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Mpociot": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mike van Riel",
|
||||
"email": "mike.vanriel@naenius.com"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/mpociot/reflection-docblock/issues",
|
||||
"source": "https://github.com/mpociot/reflection-docblock/tree/master"
|
||||
},
|
||||
"time": "2016-06-20T20:53:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "myclabs/deep-copy",
|
||||
"version": "1.12.0",
|
||||
@ -12426,6 +12624,111 @@
|
||||
],
|
||||
"time": "2023-02-07T11:34:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "shalvah/clara",
|
||||
"version": "3.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/shalvah/clara.git",
|
||||
"reference": "cdbb5737cbdd101756d97dd2279a979a1af7710b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/shalvah/clara/zipball/cdbb5737cbdd101756d97dd2279a979a1af7710b",
|
||||
"reference": "cdbb5737cbdd101756d97dd2279a979a1af7710b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.4",
|
||||
"symfony/console": "^4.0|^5.0|^6.0|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"eloquent/phony-phpunit": "^7.0",
|
||||
"phpunit/phpunit": "^9.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Shalvah\\Clara\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "🔊 Simple, pretty, testable console output for CLI apps.",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"log",
|
||||
"logging"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/shalvah/clara/issues",
|
||||
"source": "https://github.com/shalvah/clara/tree/3.2.0"
|
||||
},
|
||||
"time": "2024-02-27T20:30:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "shalvah/upgrader",
|
||||
"version": "0.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/shalvah/upgrader.git",
|
||||
"reference": "d95ed17fe9f5e1ee7d47ad835595f1af080a867f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/shalvah/upgrader/zipball/d95ed17fe9f5e1ee7d47ad835595f1af080a867f",
|
||||
"reference": "d95ed17fe9f5e1ee7d47ad835595f1af080a867f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/support": ">=8.0",
|
||||
"nikic/php-parser": "^5.0",
|
||||
"php": ">=8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dms/phpunit-arraysubset-asserts": "^0.2.0",
|
||||
"pestphp/pest": "^1.21",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"spatie/ray": "^1.33"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Shalvah\\Upgrader\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Shalvah",
|
||||
"email": "hello@shalvah.me"
|
||||
}
|
||||
],
|
||||
"description": "Create automatic upgrades for your package.",
|
||||
"homepage": "http://github.com/shalvah/upgrader",
|
||||
"keywords": [
|
||||
"upgrade"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/shalvah/upgrader/issues",
|
||||
"source": "https://github.com/shalvah/upgrader/tree/0.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://patreon.com/shalvah",
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2024-02-20T11:51:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/backtrace",
|
||||
"version": "1.6.2",
|
||||
@ -12489,6 +12792,70 @@
|
||||
],
|
||||
"time": "2024-07-22T08:21:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/data-transfer-object",
|
||||
"version": "3.9.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/data-transfer-object.git",
|
||||
"reference": "1df0906c4e9e3aebd6c0506fd82c8b7d5548c1c8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/data-transfer-object/zipball/1df0906c4e9e3aebd6c0506fd82c8b7d5548c1c8",
|
||||
"reference": "1df0906c4e9e3aebd6c0506fd82c8b7d5548c1c8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"illuminate/collections": "^8.36",
|
||||
"jetbrains/phpstorm-attributes": "^1.0",
|
||||
"larapack/dd": "^1.1",
|
||||
"phpunit/phpunit": "^9.5.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\DataTransferObject\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Brent Roose",
|
||||
"email": "brent@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Data transfer objects with batteries included",
|
||||
"homepage": "https://github.com/spatie/data-transfer-object",
|
||||
"keywords": [
|
||||
"data-transfer-object",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/data-transfer-object/issues",
|
||||
"source": "https://github.com/spatie/data-transfer-object/tree/3.9.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://spatie.be/open-source/support-us",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"abandoned": "spatie/laravel-data",
|
||||
"time": "2022-09-16T13:34:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/error-solutions",
|
||||
"version": "1.1.1",
|
||||
@ -12806,6 +13173,82 @@
|
||||
],
|
||||
"time": "2024-06-12T15:01:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-exporter",
|
||||
"version": "v7.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-exporter.git",
|
||||
"reference": "b80a669a2264609f07f1667f891dbfca25eba44c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/b80a669a2264609f07f1667f891dbfca25eba44c",
|
||||
"reference": "b80a669a2264609f07f1667f891dbfca25eba44c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/property-access": "^6.4|^7.0",
|
||||
"symfony/serializer": "^6.4|^7.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\VarExporter\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Allows exporting any serializable PHP data structure to plain PHP code",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"clone",
|
||||
"construct",
|
||||
"export",
|
||||
"hydrate",
|
||||
"instantiate",
|
||||
"lazy-loading",
|
||||
"proxy",
|
||||
"serialize"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-exporter/tree/v7.1.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-28T08:00:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v7.1.4",
|
||||
|
269
config/scribe.php
Normal file
269
config/scribe.php
Normal file
@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
use Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
return [
|
||||
// The HTML <title> for the generated documentation. If this is empty, Scribe will infer it from config('app.name').
|
||||
'title' => 'The Forge API Documentation',
|
||||
|
||||
// A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec.
|
||||
'description' => '',
|
||||
|
||||
// The base URL displayed in the docs. If this is empty, Scribe will use the value of config('app.url') at generation time.
|
||||
// If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL.
|
||||
'base_url' => null,
|
||||
|
||||
'routes' => [
|
||||
[
|
||||
// Routes that match these conditions will be included in the docs
|
||||
'match' => [
|
||||
// Match only routes whose paths match this pattern (use * as a wildcard to match any characters). Example: 'users/*'.
|
||||
'prefixes' => ['api/*'],
|
||||
|
||||
// Match only routes whose domains match this pattern (use * as a wildcard to match any characters). Example: 'api.*'.
|
||||
'domains' => ['*'],
|
||||
|
||||
// [Dingo router only] Match only routes registered under this version. Wildcards are NOT supported.
|
||||
'versions' => ['v0'],
|
||||
],
|
||||
|
||||
// Include these routes even if they did not match the rules above.
|
||||
'include' => [
|
||||
// 'users.index', 'POST /new', '/auth/*'
|
||||
],
|
||||
|
||||
// Exclude these routes even if they matched the rules above.
|
||||
'exclude' => [
|
||||
// 'GET /health', 'admin.*'
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// The type of documentation output to generate.
|
||||
// - "static" will generate a static HTMl page in the /public/docs folder,
|
||||
// - "laravel" will generate the documentation as a Blade view, so you can add routing and authentication.
|
||||
// - "external_static" and "external_laravel" do the same as above, but generate a basic template,
|
||||
// passing the OpenAPI spec as a URL, allowing you to easily use the docs with an external generator
|
||||
'type' => 'static',
|
||||
|
||||
// See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options
|
||||
'theme' => 'default',
|
||||
|
||||
'static' => [
|
||||
// HTML documentation, assets and Postman collection will be generated to this folder.
|
||||
// Source Markdown will still be in resources/docs.
|
||||
'output_path' => 'public/docs',
|
||||
],
|
||||
|
||||
'laravel' => [
|
||||
// Whether to automatically create a docs endpoint for you to view your generated docs.
|
||||
// If this is false, you can still set up routing manually.
|
||||
'add_routes' => true,
|
||||
|
||||
// URL path to use for the docs endpoint (if `add_routes` is true).
|
||||
// By default, `/docs` opens the HTML page, `/docs.postman` opens the Postman collection, and `/docs.openapi` the OpenAPI spec.
|
||||
'docs_url' => '/docs',
|
||||
|
||||
// Directory within `public` in which to store CSS and JS assets.
|
||||
// By default, assets are stored in `public/vendor/scribe`.
|
||||
// If set, assets will be stored in `public/{{assets_directory}}`
|
||||
'assets_directory' => null,
|
||||
|
||||
// Middleware to attach to the docs endpoint (if `add_routes` is true).
|
||||
'middleware' => [],
|
||||
],
|
||||
|
||||
'external' => [
|
||||
'html_attributes' => [],
|
||||
],
|
||||
|
||||
'try_it_out' => [
|
||||
// Add a Try It Out button to your endpoints so consumers can test endpoints right from their browser.
|
||||
// Don't forget to enable CORS headers for your endpoints.
|
||||
'enabled' => true,
|
||||
|
||||
// The base URL for the API tester to use (for example, you can set this to your staging URL).
|
||||
// Leave as null to use the current app URL when generating (config("app.url")).
|
||||
'base_url' => null,
|
||||
|
||||
// [Laravel Sanctum] Fetch a CSRF token before each request, and add it as an X-XSRF-TOKEN header.
|
||||
'use_csrf' => false,
|
||||
|
||||
// The URL to fetch the CSRF token from (if `use_csrf` is true).
|
||||
'csrf_url' => '/sanctum/csrf-cookie',
|
||||
],
|
||||
|
||||
// How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls.
|
||||
'auth' => [
|
||||
// Set this to true if ANY endpoints in your API use authentication.
|
||||
'enabled' => true,
|
||||
|
||||
// Set this to true if your API should be authenticated by default. If so, you must also set `enabled` (above) to true.
|
||||
// You can then use @unauthenticated or @authenticated on individual endpoints to change their status from the default.
|
||||
'default' => true,
|
||||
|
||||
// Where is the auth value meant to be sent in a request?
|
||||
// Options: query, body, basic, bearer, header (for custom header)
|
||||
'in' => 'bearer',
|
||||
|
||||
// The name of the auth parameter (eg token, key, apiKey) or header (eg Authorization, Api-Key).
|
||||
'name' => 'Authorization',
|
||||
|
||||
// The value of the parameter to be used by Scribe to authenticate response calls.
|
||||
// This will NOT be included in the generated documentation. If empty, Scribe will use a random value.
|
||||
'use_value' => env('SCRIBE_AUTH_KEY'),
|
||||
|
||||
// Placeholder your users will see for the auth parameter in the example requests.
|
||||
// Set this to null if you want Scribe to use a random value as placeholder instead.
|
||||
'placeholder' => 'YOUR_API_KEY',
|
||||
|
||||
// Any extra authentication-related info for your users. Markdown and HTML are supported.
|
||||
'extra_info' => 'You can generate your own API token by logging into The Forge, clicking your profile picture, and clicking <b>API Tokens</b>.',
|
||||
],
|
||||
|
||||
// Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported.
|
||||
'intro_text' => <<<'INTRO'
|
||||
This documentation aims to provide all the information you need to work with our API.
|
||||
|
||||
<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
|
||||
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>
|
||||
INTRO
|
||||
,
|
||||
|
||||
// Example requests for each endpoint will be shown in each of these languages.
|
||||
// Supported options are: bash, javascript, php, python
|
||||
// To add a language of your own, see https://scribe.knuckles.wtf/laravel/advanced/example-requests
|
||||
'example_languages' => [
|
||||
'javascript',
|
||||
'php',
|
||||
'python',
|
||||
],
|
||||
|
||||
// Generate a Postman collection (v2.1.0) in addition to HTML docs.
|
||||
// For 'static' docs, the collection will be generated to public/docs/collection.json.
|
||||
// For 'laravel' docs, it will be generated to storage/app/scribe/collection.json.
|
||||
// Setting `laravel.add_routes` to true (above) will also add a route for the collection.
|
||||
'postman' => [
|
||||
'enabled' => true,
|
||||
'overrides' => [
|
||||
// 'info.version' => '2.0.0',
|
||||
],
|
||||
],
|
||||
|
||||
// Generate an OpenAPI spec (v3.0.1) in addition to docs webpage.
|
||||
// For 'static' docs, the collection will be generated to public/docs/openapi.yaml.
|
||||
// For 'laravel' docs, it will be generated to storage/app/scribe/openapi.yaml.
|
||||
// Setting `laravel.add_routes` to true (above) will also add a route for the spec.
|
||||
'openapi' => [
|
||||
'enabled' => true,
|
||||
'overrides' => [
|
||||
// 'info.version' => '2.0.0',
|
||||
],
|
||||
],
|
||||
|
||||
'groups' => [
|
||||
// Endpoints which don't have a @group will be placed in this default group.
|
||||
'default' => 'Endpoints',
|
||||
|
||||
// By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined.
|
||||
// You can override this by listing the groups, subgroups and endpoints here in the order you want them.
|
||||
// See https://scribe.knuckles.wtf/blog/laravel-v4#easier-sorting and https://scribe.knuckles.wtf/laravel/reference/config#order for details
|
||||
'order' => [],
|
||||
],
|
||||
|
||||
// Custom logo path. This will be used as the value of the src attribute for the <img> tag,
|
||||
// so make sure it points to an accessible URL or path. Set to false to not use a logo.
|
||||
// For example, if your logo is in public/img:
|
||||
// - 'logo' => '../img/logo.png' // for `static` type (output folder is public/docs)
|
||||
// - 'logo' => 'img/logo.png' // for `laravel` type
|
||||
'logo' => false,
|
||||
|
||||
// Customize the "Last updated" value displayed in the docs by specifying tokens and formats.
|
||||
// Examples:
|
||||
// - {date:F j Y} => March 28, 2022
|
||||
// - {git:short} => Short hash of the last Git commit
|
||||
// Available tokens are `{date:<format>}` and `{git:<format>}`.
|
||||
// The format you pass to `date` will be passed to PHP's `date()` function.
|
||||
// The format you pass to `git` can be either "short" or "long".
|
||||
'last_updated' => 'Last updated: {date:F j, Y}',
|
||||
|
||||
'examples' => [
|
||||
// Set this to any number (eg. 1234) to generate the same example values for parameters on each run,
|
||||
'faker_seed' => null,
|
||||
|
||||
// With API resources and transformers, Scribe tries to generate example models to use in your API responses.
|
||||
// By default, Scribe will try the model's factory, and if that fails, try fetching the first from the database.
|
||||
// You can reorder or remove strategies here.
|
||||
'models_source' => ['factoryCreate', 'factoryMake', 'databaseFirst'],
|
||||
],
|
||||
|
||||
// The strategies Scribe will use to extract information about your routes at each stage.
|
||||
// If you create or install a custom strategy, add it here.
|
||||
'strategies' => [
|
||||
'metadata' => [
|
||||
Strategies\Metadata\GetFromDocBlocks::class,
|
||||
Strategies\Metadata\GetFromMetadataAttributes::class,
|
||||
],
|
||||
'urlParameters' => [
|
||||
Strategies\UrlParameters\GetFromLaravelAPI::class,
|
||||
Strategies\UrlParameters\GetFromUrlParamAttribute::class,
|
||||
Strategies\UrlParameters\GetFromUrlParamTag::class,
|
||||
],
|
||||
'queryParameters' => [
|
||||
Strategies\QueryParameters\GetFromFormRequest::class,
|
||||
Strategies\QueryParameters\GetFromInlineValidator::class,
|
||||
Strategies\QueryParameters\GetFromQueryParamAttribute::class,
|
||||
Strategies\QueryParameters\GetFromQueryParamTag::class,
|
||||
],
|
||||
'headers' => [
|
||||
Strategies\Headers\GetFromHeaderAttribute::class,
|
||||
Strategies\Headers\GetFromHeaderTag::class,
|
||||
[
|
||||
'override',
|
||||
[
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
],
|
||||
],
|
||||
'bodyParameters' => [
|
||||
Strategies\BodyParameters\GetFromFormRequest::class,
|
||||
Strategies\BodyParameters\GetFromInlineValidator::class,
|
||||
Strategies\BodyParameters\GetFromBodyParamAttribute::class,
|
||||
Strategies\BodyParameters\GetFromBodyParamTag::class,
|
||||
],
|
||||
'responses' => [
|
||||
Strategies\Responses\UseResponseAttributes::class,
|
||||
Strategies\Responses\UseTransformerTags::class,
|
||||
Strategies\Responses\UseApiResourceTags::class,
|
||||
Strategies\Responses\UseResponseTag::class,
|
||||
Strategies\Responses\UseResponseFileTag::class,
|
||||
[
|
||||
Strategies\Responses\ResponseCalls::class,
|
||||
[
|
||||
'only' => ['GET *'],
|
||||
// Disable debug mode when generating response calls to avoid error stack traces in responses
|
||||
'config' => [
|
||||
'app.debug' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'responseFields' => [
|
||||
Strategies\ResponseFields\GetFromResponseFieldAttribute::class,
|
||||
Strategies\ResponseFields\GetFromResponseFieldTag::class,
|
||||
],
|
||||
],
|
||||
|
||||
// For response calls, API resource responses and transformer responses,
|
||||
// Scribe will try to start database transactions, so no changes are persisted to your database.
|
||||
// Tell Scribe which connections should be transacted here. If you only use one db connection, you can leave this as is.
|
||||
'database_connections_to_transact' => [config('database.default')],
|
||||
|
||||
'fractal' => [
|
||||
// If you are using a custom serializer with league/fractal, you can specify it here.
|
||||
'serializer' => null,
|
||||
],
|
||||
|
||||
'routeMatcher' => \Knuckles\Scribe\Matching\RouteMatcher::class,
|
||||
];
|
@ -1,7 +1,7 @@
|
||||
includes:
|
||||
- ./vendor/larastan/larastan/extension.neon
|
||||
parameters:
|
||||
level: 6
|
||||
level: 5
|
||||
paths:
|
||||
- app
|
||||
- bootstrap
|
||||
|
486
public/docs/collection.json
Normal file
486
public/docs/collection.json
Normal file
File diff suppressed because one or more lines are too long
393
public/docs/css/theme-default.print.css
Normal file
393
public/docs/css/theme-default.print.css
Normal file
@ -0,0 +1,393 @@
|
||||
/* Copied from https://github.com/slatedocs/slate/blob/c4b4c0b8f83e891ca9fab6bbe9a1a88d5fe41292/stylesheets/print.css and unminified */
|
||||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block
|
||||
}
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block;
|
||||
vertical-align: baseline
|
||||
}
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0
|
||||
}
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent
|
||||
}
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0
|
||||
}
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0
|
||||
}
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 1em 40px
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
button {
|
||||
overflow: visible
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none
|
||||
}
|
||||
|
||||
button,
|
||||
html input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
input {
|
||||
line-height: normal
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield;
|
||||
box-sizing: content-box
|
||||
}
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em
|
||||
}
|
||||
|
||||
legend {
|
||||
border: 0;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto
|
||||
}
|
||||
|
||||
optgroup {
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.content h1,
|
||||
.content h2,
|
||||
.content h3,
|
||||
.content h4,
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
.content h1,
|
||||
.content h2,
|
||||
.content h3,
|
||||
.content h4 {
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
.content pre,
|
||||
.content code {
|
||||
font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, serif;
|
||||
font-size: 12px;
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.content pre,
|
||||
.content code {
|
||||
word-break: break-all;
|
||||
-webkit-hyphens: auto;
|
||||
-ms-hyphens: auto;
|
||||
hyphens: auto
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'slate';
|
||||
src: url(../fonts/slate.eot?-syv14m);
|
||||
src: url(../fonts/slate.eot?#iefix-syv14m) format("embedded-opentype"), url(../fonts/slate.woff2?-syv14m) format("woff2"), url(../fonts/slate.woff?-syv14m) format("woff"), url(../fonts/slate.ttf?-syv14m) format("truetype"), url(../fonts/slate.svg?-syv14m#slate) format("svg");
|
||||
font-weight: normal;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
.content aside.warning:before,
|
||||
.content aside.notice:before,
|
||||
.content aside.success:before {
|
||||
font-family: 'slate';
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1
|
||||
}
|
||||
|
||||
.content aside.warning:before {
|
||||
content: "\e600"
|
||||
}
|
||||
|
||||
.content aside.notice:before {
|
||||
content: "\e602"
|
||||
}
|
||||
|
||||
.content aside.success:before {
|
||||
content: "\e606"
|
||||
}
|
||||
|
||||
.tocify,
|
||||
.toc-footer,
|
||||
.lang-selector,
|
||||
.search,
|
||||
#nav-button {
|
||||
display: none
|
||||
}
|
||||
|
||||
.tocify-wrapper>img {
|
||||
margin: 0 auto;
|
||||
display: block
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
.content pre,
|
||||
.content code {
|
||||
border: 1px solid #999;
|
||||
border-radius: 5px;
|
||||
font-size: 0.8em
|
||||
}
|
||||
|
||||
.content pre code {
|
||||
border: 0
|
||||
}
|
||||
|
||||
.content pre {
|
||||
padding: 1.3em
|
||||
}
|
||||
|
||||
.content code {
|
||||
padding: 0.2em
|
||||
}
|
||||
|
||||
.content table {
|
||||
border: 1px solid #999
|
||||
}
|
||||
|
||||
.content table tr {
|
||||
border-bottom: 1px solid #999
|
||||
}
|
||||
|
||||
.content table td,
|
||||
.content table th {
|
||||
padding: 0.7em
|
||||
}
|
||||
|
||||
.content p {
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.content a {
|
||||
text-decoration: none;
|
||||
color: #000
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
font-size: 2.5em;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 21px;
|
||||
border: 2px solid #ccc;
|
||||
border-width: 2px 0;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.content h2 {
|
||||
font-size: 1.8em;
|
||||
margin-top: 2em;
|
||||
border-top: 2px solid #ccc;
|
||||
padding-top: 0.8em
|
||||
}
|
||||
|
||||
.content h1+h2,
|
||||
.content h1+div+h2 {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
.content h3,
|
||||
.content h4 {
|
||||
font-size: 0.8em;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.8em;
|
||||
text-transform: uppercase
|
||||
}
|
||||
|
||||
.content h5,
|
||||
.content h6 {
|
||||
text-transform: uppercase
|
||||
}
|
||||
|
||||
.content aside {
|
||||
padding: 1em;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
line-height: 1.6
|
||||
}
|
||||
|
||||
.content aside:before {
|
||||
vertical-align: middle;
|
||||
padding-right: 0.5em;
|
||||
font-size: 14px
|
||||
}
|
1085
public/docs/css/theme-default.style.css
Normal file
1085
public/docs/css/theme-default.style.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/docs/images/navbar.png
Normal file
BIN
public/docs/images/navbar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 B |
11426
public/docs/index.html
Normal file
11426
public/docs/index.html
Normal file
File diff suppressed because it is too large
Load Diff
149
public/docs/js/theme-default-4.37.2.js
Normal file
149
public/docs/js/theme-default-4.37.2.js
Normal file
@ -0,0 +1,149 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const updateHash = function (id) {
|
||||
window.location.hash = `#${id}`;
|
||||
};
|
||||
|
||||
const navButton = document.getElementById('nav-button');
|
||||
const menuWrapper = document.querySelector('.tocify-wrapper');
|
||||
function toggleSidebar(event) {
|
||||
event.preventDefault();
|
||||
if (menuWrapper) {
|
||||
menuWrapper.classList.toggle('open');
|
||||
navButton.classList.toggle('open');
|
||||
}
|
||||
}
|
||||
function closeSidebar() {
|
||||
if (menuWrapper) {
|
||||
menuWrapper.classList.remove('open');
|
||||
navButton.classList.remove('open');
|
||||
}
|
||||
}
|
||||
navButton.addEventListener('click', toggleSidebar);
|
||||
|
||||
window.hljs.highlightAll();
|
||||
|
||||
const wrapper = document.getElementById('toc');
|
||||
// https://jets.js.org/
|
||||
window.jets = new window.Jets({
|
||||
// *OR - Selects elements whose values contains at least one part of search substring
|
||||
searchSelector: '*OR',
|
||||
searchTag: '#input-search',
|
||||
contentTag: '#toc li',
|
||||
didSearch: function(term) {
|
||||
wrapper.classList.toggle('jets-searching', String(term).length > 0)
|
||||
},
|
||||
// map these accent keys to plain values
|
||||
diacriticsMap: {
|
||||
a: 'ÀÁÂÃÄÅàáâãäåĀāąĄ',
|
||||
c: 'ÇçćĆčČ',
|
||||
d: 'đĐďĎ',
|
||||
e: 'ÈÉÊËèéêëěĚĒēęĘ',
|
||||
i: 'ÌÍÎÏìíîïĪī',
|
||||
l: 'łŁ',
|
||||
n: 'ÑñňŇńŃ',
|
||||
o: 'ÒÓÔÕÕÖØòóôõöøŌō',
|
||||
r: 'řŘ',
|
||||
s: 'ŠšśŚ',
|
||||
t: 'ťŤ',
|
||||
u: 'ÙÚÛÜùúûüůŮŪū',
|
||||
y: 'ŸÿýÝ',
|
||||
z: 'ŽžżŻźŹ'
|
||||
}
|
||||
});
|
||||
|
||||
function hashChange() {
|
||||
const currentItems = document.querySelectorAll('.tocify-subheader.visible, .tocify-item.tocify-focus');
|
||||
Array.from(currentItems).forEach((elem) => {
|
||||
elem.classList.remove('visible', 'tocify-focus');
|
||||
});
|
||||
|
||||
const currentTag = document.querySelector(`a[href="${window.location.hash}"]`);
|
||||
if (currentTag) {
|
||||
const parent = currentTag.closest('.tocify-subheader');
|
||||
if (parent) {
|
||||
parent.classList.add('visible');
|
||||
}
|
||||
|
||||
const siblings = currentTag.closest('.tocify-header');
|
||||
if (siblings) {
|
||||
Array.from(siblings.querySelectorAll('.tocify-subheader')).forEach((elem) => {
|
||||
elem.classList.add('visible');
|
||||
});
|
||||
}
|
||||
|
||||
currentTag.parentElement.classList.add('tocify-focus');
|
||||
|
||||
// wait for dom changes to be done
|
||||
setTimeout(() => {
|
||||
currentTag.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
|
||||
// only close the sidebar on level-2 events
|
||||
if (currentTag.parentElement.classList.contains('level-2')) {
|
||||
closeSidebar();
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
let languages = JSON.parse(document.body.getAttribute('data-languages'));
|
||||
// Support a key => value object where the key is the name, or an array of strings where the value is the name
|
||||
if (!Array.isArray(languages)) {
|
||||
languages = Object.values(languages);
|
||||
}
|
||||
// if there is no language use the first one
|
||||
const currentLanguage = window.localStorage.getItem('language') || languages[0];
|
||||
const languageStyle = document.getElementById('language-style');
|
||||
const langSelector = document.querySelectorAll('.lang-selector button.lang-button');
|
||||
|
||||
function setActiveLanguage(newLanguage) {
|
||||
window.localStorage.setItem('language', newLanguage);
|
||||
if (!languageStyle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newStyle = languages.map((language) => {
|
||||
return language === newLanguage
|
||||
// the current one should be visible
|
||||
? `body .content .${language}-example pre { display: block; }`
|
||||
// the inactive one should be hidden
|
||||
: `body .content .${language}-example pre { display: none; }`;
|
||||
}).join(`\n`);
|
||||
|
||||
Array.from(langSelector).forEach((elem) => {
|
||||
elem.classList.toggle('active', elem.getAttribute('data-language-name') === newLanguage);
|
||||
});
|
||||
|
||||
const activeHash = window.location.hash.slice(1);
|
||||
|
||||
languageStyle.innerHTML = newStyle;
|
||||
|
||||
setTimeout(() => {
|
||||
updateHash(activeHash);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
setActiveLanguage(currentLanguage);
|
||||
|
||||
Array.from(langSelector).forEach((elem) => {
|
||||
elem.addEventListener('click', () => {
|
||||
const newLanguage = elem.getAttribute('data-language-name');
|
||||
setActiveLanguage(newLanguage);
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', hashChange, false);
|
||||
|
||||
const divs = document.querySelectorAll('.content h1[id], .content h2[id]');
|
||||
|
||||
document.addEventListener('scroll', () => {
|
||||
divs.forEach(item => {
|
||||
const rect = item.getBoundingClientRect();
|
||||
if (rect.top > 0 && rect.top < 150) {
|
||||
const location = window.location.toString().split('#')[0];
|
||||
history.replaceState(null, null, location + '#' + item.id);
|
||||
hashChange();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
hashChange();
|
||||
});
|
277
public/docs/js/tryitout-4.37.2.js
Normal file
277
public/docs/js/tryitout-4.37.2.js
Normal file
@ -0,0 +1,277 @@
|
||||
window.abortControllers = {};
|
||||
|
||||
function cacheAuthValue() {
|
||||
// Whenever the auth header is set for one endpoint, cache it for the others
|
||||
window.lastAuthValue = '';
|
||||
let authInputs = document.querySelectorAll(`.auth-value`)
|
||||
authInputs.forEach(el => {
|
||||
el.addEventListener('input', (event) => {
|
||||
window.lastAuthValue = event.target.value;
|
||||
authInputs.forEach(otherInput => {
|
||||
if (otherInput === el) return;
|
||||
// Don't block the main thread
|
||||
setTimeout(() => {
|
||||
otherInput.value = window.lastAuthValue;
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', cacheAuthValue);
|
||||
|
||||
function getCookie(name) {
|
||||
if (!document.cookie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cookies = document.cookie.split(';')
|
||||
.map(c => c.trim())
|
||||
.filter(c => c.startsWith(name + '='));
|
||||
|
||||
if (cookies.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decodeURIComponent(cookies[0].split('=')[1]);
|
||||
}
|
||||
|
||||
function tryItOut(endpointId) {
|
||||
document.querySelector(`#btn-tryout-${endpointId}`).hidden = true;
|
||||
document.querySelector(`#btn-canceltryout-${endpointId}`).hidden = false;
|
||||
const executeBtn = document.querySelector(`#btn-executetryout-${endpointId}`).hidden = false;
|
||||
executeBtn.disabled = false;
|
||||
|
||||
// Show all input fields
|
||||
document.querySelectorAll(`input[data-endpoint=${endpointId}],label[data-endpoint=${endpointId}]`)
|
||||
.forEach(el => el.style.display = 'block');
|
||||
|
||||
if (document.querySelector(`#form-${endpointId}`).dataset.authed === "1") {
|
||||
const authElement = document.querySelector(`#auth-${endpointId}`);
|
||||
authElement && (authElement.hidden = false);
|
||||
}
|
||||
// Expand all nested fields
|
||||
document.querySelectorAll(`#form-${endpointId} details`)
|
||||
.forEach(el => el.open = true);
|
||||
}
|
||||
|
||||
function cancelTryOut(endpointId) {
|
||||
if (window.abortControllers[endpointId]) {
|
||||
window.abortControllers[endpointId].abort();
|
||||
delete window.abortControllers[endpointId];
|
||||
}
|
||||
|
||||
document.querySelector(`#btn-tryout-${endpointId}`).hidden = false;
|
||||
const executeBtn = document.querySelector(`#btn-executetryout-${endpointId}`);
|
||||
executeBtn.hidden = true;
|
||||
executeBtn.textContent = executeBtn.dataset.initialText;
|
||||
document.querySelector(`#btn-canceltryout-${endpointId}`).hidden = true;
|
||||
// Hide inputs
|
||||
document.querySelectorAll(`input[data-endpoint=${endpointId}],label[data-endpoint=${endpointId}]`)
|
||||
.forEach(el => el.style.display = 'none');
|
||||
document.querySelectorAll(`#form-${endpointId} details`)
|
||||
.forEach(el => el.open = false);
|
||||
const authElement = document.querySelector(`#auth-${endpointId}`);
|
||||
authElement && (authElement.hidden = true);
|
||||
|
||||
document.querySelector('#execution-results-' + endpointId).hidden = true;
|
||||
document.querySelector('#execution-error-' + endpointId).hidden = true;
|
||||
|
||||
// Revert to sample code blocks
|
||||
document.querySelector('#example-requests-' + endpointId).hidden = false;
|
||||
document.querySelector('#example-responses-' + endpointId).hidden = false;
|
||||
}
|
||||
|
||||
function makeAPICall(method, path, body = {}, query = {}, headers = {}, endpointId = null) {
|
||||
console.log({endpointId, path, body, query, headers});
|
||||
|
||||
if (!(body instanceof FormData) && typeof body !== "string") {
|
||||
body = JSON.stringify(body)
|
||||
}
|
||||
|
||||
const url = new URL(window.tryItOutBaseUrl + '/' + path.replace(/^\//, ''));
|
||||
|
||||
// We need this function because if you try to set an array or object directly to a URLSearchParams object,
|
||||
// you'll get [object Object] or the array.toString()
|
||||
function addItemToSearchParamsObject(key, value, searchParams) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v, i) => {
|
||||
// Append {filters: [first, second]} as filters[0]=first&filters[1]second
|
||||
addItemToSearchParamsObject(key + '[' + i + ']', v, searchParams);
|
||||
})
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
Object.keys(value).forEach((i) => {
|
||||
// Append {filters: {name: first}} as filters[name]=first
|
||||
addItemToSearchParamsObject(key + '[' + i + ']', value[i], searchParams);
|
||||
});
|
||||
} else {
|
||||
searchParams.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(query)
|
||||
.forEach(key => addItemToSearchParamsObject(key, query[key], url.searchParams));
|
||||
|
||||
window.abortControllers[endpointId] = new AbortController();
|
||||
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: method === 'GET' ? undefined : body,
|
||||
signal: window.abortControllers[endpointId].signal,
|
||||
referrer: window.tryItOutBaseUrl,
|
||||
mode: 'cors',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => Promise.all([response.status, response.statusText, response.text(), response.headers]));
|
||||
}
|
||||
|
||||
function hideCodeSamples(endpointId) {
|
||||
document.querySelector('#example-requests-' + endpointId).hidden = true;
|
||||
document.querySelector('#example-responses-' + endpointId).hidden = true;
|
||||
}
|
||||
|
||||
function handleResponse(endpointId, response, status, headers) {
|
||||
hideCodeSamples(endpointId);
|
||||
|
||||
// Hide error views
|
||||
document.querySelector('#execution-error-' + endpointId).hidden = true;
|
||||
|
||||
const responseContentEl = document.querySelector('#execution-response-content-' + endpointId);
|
||||
|
||||
// Prettify it if it's JSON
|
||||
let isJson = false;
|
||||
try {
|
||||
const jsonParsed = JSON.parse(response);
|
||||
if (jsonParsed !== null) {
|
||||
isJson = true;
|
||||
response = JSON.stringify(jsonParsed, null, 4);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
responseContentEl.textContent = response === '' ? responseContentEl.dataset.emptyResponseText : response;
|
||||
isJson && window.hljs.highlightElement(responseContentEl);
|
||||
const statusEl = document.querySelector('#execution-response-status-' + endpointId);
|
||||
statusEl.textContent = ` (${status})`;
|
||||
document.querySelector('#execution-results-' + endpointId).hidden = false;
|
||||
statusEl.scrollIntoView({behavior: "smooth", block: "center"});
|
||||
}
|
||||
|
||||
function handleError(endpointId, err) {
|
||||
hideCodeSamples(endpointId);
|
||||
// Hide response views
|
||||
document.querySelector('#execution-results-' + endpointId).hidden = true;
|
||||
|
||||
// Show error views
|
||||
let errorMessage = err.message || err;
|
||||
const $errorMessageEl = document.querySelector('#execution-error-message-' + endpointId);
|
||||
$errorMessageEl.textContent = errorMessage + $errorMessageEl.textContent;
|
||||
const errorEl = document.querySelector('#execution-error-' + endpointId);
|
||||
errorEl.hidden = false;
|
||||
errorEl.scrollIntoView({behavior: "smooth", block: "center"});
|
||||
|
||||
}
|
||||
|
||||
async function executeTryOut(endpointId, form) {
|
||||
const executeBtn = document.querySelector(`#btn-executetryout-${endpointId}`);
|
||||
executeBtn.textContent = executeBtn.dataset.loadingText;
|
||||
executeBtn.disabled = true;
|
||||
executeBtn.scrollIntoView({behavior: "smooth", block: "center"});
|
||||
|
||||
let body;
|
||||
let setter;
|
||||
if (form.dataset.hasfiles === "1") {
|
||||
body = new FormData();
|
||||
setter = (name, value) => body.append(name, value);
|
||||
} else if (form.dataset.isarraybody === "1") {
|
||||
body = [];
|
||||
setter = (name, value) => _.set(body, name, value);
|
||||
} else {
|
||||
body = {};
|
||||
setter = (name, value) => _.set(body, name, value);
|
||||
}
|
||||
const bodyParameters = form.querySelectorAll('input[data-component=body]');
|
||||
bodyParameters.forEach(el => {
|
||||
let value = el.value;
|
||||
|
||||
if (el.type === 'number' && typeof value === 'string') {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
if (el.type === 'file' && el.files[0]) {
|
||||
setter(el.name, el.files[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.type !== 'radio') {
|
||||
if (value === "" && el.required === false) {
|
||||
// Don't include empty optional values in the request
|
||||
return;
|
||||
}
|
||||
setter(el.name, value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (el.checked) {
|
||||
value = (value === 'false') ? false : true;
|
||||
setter(el.name, value);
|
||||
}
|
||||
});
|
||||
|
||||
const query = {};
|
||||
const queryParameters = form.querySelectorAll('input[data-component=query]');
|
||||
queryParameters.forEach(el => {
|
||||
if (el.type !== 'radio' || (el.type === 'radio' && el.checked)) {
|
||||
if (el.value === '') {
|
||||
// Don't include empty values in the request
|
||||
return;
|
||||
}
|
||||
|
||||
_.set(query, el.name, el.value);
|
||||
}
|
||||
});
|
||||
|
||||
let path = form.dataset.path;
|
||||
const urlParameters = form.querySelectorAll('input[data-component=url]');
|
||||
urlParameters.forEach(el => (path = path.replace(new RegExp(`\\{${el.name}\\??}`), el.value)));
|
||||
|
||||
const headers = Object.fromEntries(Array.from(form.querySelectorAll('input[data-component=header]'))
|
||||
.map(el => [el.name, el.value]));
|
||||
|
||||
// When using FormData, the browser sets the correct content-type + boundary
|
||||
let method = form.dataset.method;
|
||||
if (body instanceof FormData) {
|
||||
delete headers['Content-Type'];
|
||||
|
||||
// When using FormData with PUT or PATCH, use method spoofing so PHP can access the post body
|
||||
if (['PUT', 'PATCH'].includes(form.dataset.method)) {
|
||||
method = 'POST';
|
||||
setter('_method', form.dataset.method);
|
||||
}
|
||||
}
|
||||
|
||||
let preflightPromise = Promise.resolve();
|
||||
if (window.useCsrf && window.csrfUrl) {
|
||||
preflightPromise = makeAPICall('GET', window.csrfUrl).then(() => {
|
||||
headers['X-XSRF-TOKEN'] = getCookie('XSRF-TOKEN');
|
||||
});
|
||||
}
|
||||
|
||||
return preflightPromise.then(() => makeAPICall(method, path, body, query, headers, endpointId))
|
||||
.then(([responseStatus, statusText, responseContent, responseHeaders]) => {
|
||||
handleResponse(endpointId, responseContent, responseStatus, responseHeaders)
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.name === "AbortError") {
|
||||
console.log("Request cancelled");
|
||||
return;
|
||||
}
|
||||
console.log("Error while making request: ", err);
|
||||
handleError(endpointId, err);
|
||||
})
|
||||
.finally(() => {
|
||||
executeBtn.disabled = false;
|
||||
executeBtn.textContent = executeBtn.dataset.initialText;
|
||||
});
|
||||
}
|
15082
public/docs/openapi.yaml
Normal file
15082
public/docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,10 +3,6 @@
|
||||
use App\Http\Controllers\Api\AuthController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/status', function () {
|
||||
return response()->json(['message' => 'okay']);
|
||||
});
|
||||
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
Route::group(['middleware' => 'auth:sanctum'], function () {
|
||||
Route::delete('/logout', [AuthController::class, 'logout']);
|
||||
|
54
tests/Feature/Api/V1/ModFilterTest.php
Normal file
54
tests/Feature/Api/V1/ModFilterTest.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Filters\V1\ModFilter;
|
||||
use App\Models\Mod;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Mod::factory()->create(['name' => 'Mod C', 'slug' => 'mod-c', 'featured' => true]);
|
||||
Mod::factory()->create(['name' => 'Mod B', 'slug' => 'mod-b', 'featured' => false]);
|
||||
Mod::factory()->create(['name' => 'Mod A', 'slug' => 'mod-a', 'featured' => true]);
|
||||
});
|
||||
|
||||
it('can filter mods by id', function () {
|
||||
$request = new Request(['id' => '1,2']);
|
||||
$filter = new ModFilter($request);
|
||||
$query = $filter->apply(Mod::query());
|
||||
|
||||
expect($query->get()->pluck('id')->toArray())->toBe([1, 2]);
|
||||
});
|
||||
|
||||
it('can filter mods by name with wildcard', function () {
|
||||
$request = new Request(['name' => 'Mod*']);
|
||||
$filter = new ModFilter($request);
|
||||
$query = $filter->apply(Mod::query());
|
||||
|
||||
expect($query->get()->pluck('name')->toArray())->toContain('Mod A', 'Mod B', 'Mod C');
|
||||
});
|
||||
|
||||
it('can filter mods by featured status', function () {
|
||||
$request = new Request(['featured' => 'true']);
|
||||
$filter = new ModFilter($request);
|
||||
$query = $filter->apply(Mod::query());
|
||||
|
||||
expect($query->get()->pluck('name')->toArray())->toContain('Mod A', 'Mod C');
|
||||
});
|
||||
|
||||
it('can sort mods by name in ascending order', function () {
|
||||
$request = new Request(['sort' => 'name']);
|
||||
$filter = new ModFilter($request);
|
||||
$query = $filter->apply(Mod::query());
|
||||
|
||||
expect($query->get()->pluck('name')->toArray())->toBe(['Mod A', 'Mod B', 'Mod C']);
|
||||
});
|
||||
|
||||
it('can sort mods by name in descending order', function () {
|
||||
$request = new Request(['sort' => '-name']);
|
||||
$filter = new ModFilter($request);
|
||||
$query = $filter->apply(Mod::query());
|
||||
|
||||
expect($query->get()->pluck('name')->toArray())->toBe(['Mod C', 'Mod B', 'Mod A']);
|
||||
});
|
@ -128,3 +128,16 @@ it('handles null published_at as not published', function () {
|
||||
|
||||
expect($mods->contains($modWithNoPublishDate))->toBeFalse();
|
||||
});
|
||||
|
||||
it('updates the parent mods updated_at column when updated', function () {
|
||||
$originalDate = now()->subDays(10);
|
||||
$version = ModVersion::factory()->create(['updated_at' => $originalDate]);
|
||||
|
||||
$version->downloads++;
|
||||
$version->save();
|
||||
|
||||
$version->refresh();
|
||||
|
||||
expect($version->mod->updated_at)->not->toEqual($originalDate)
|
||||
->and($version->mod->updated_at->format('Y-m-d'))->toEqual(now()->format('Y-m-d'));
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user