Parcourir la source

Asset add, edit, list, delete and other APIs

moell il y a 1 an
Parent
commit
e5f04edac6

+ 1 - 0
.env.testing

@@ -3,6 +3,7 @@ APP_ENV=local
 APP_KEY=base64:hIJv76Q0WaJOgjaaoBY22yHbn/ayrqQPOOzakcNQzEk=
 APP_DEBUG=true
 APP_URL=http://localhost
+LOG_QUERY=true
 
 LOG_CHANNEL=stack
 LOG_DEPRECATIONS_CHANNEL=null

+ 73 - 0
app/Http/Controllers/API/AssetController.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Http\Controllers\Controller;
+use App\Http\Requests\API\Asset\CreateOrUpdateRequest;
+use App\Http\Resources\API\AssetResource;
+use App\Models\Asset;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class AssetController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     */
+    public function index(Request $request)
+    {
+        $assets = Asset::allowed()->filter($request->all())->get();
+
+        return AssetResource::collection($assets);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     */
+    public function store(CreateOrUpdateRequest $request)
+    {
+        Asset::create([
+            ...$request->all(),
+            'company_id' => Auth::user()->company_id,
+            'whitelist' => $request->whitelist ? sprintf(",%s", implode(',', $request->whitelist)) : null,
+            'created_by' => Auth::id(),
+        ]);
+
+        return $this->noContent();
+    }
+
+    /**
+     * Display the specified resource.
+     */
+    public function show(string $id)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     */
+    public function update(Request $request, string $id)
+    {
+        $asset = Asset::allowed()->findOrFail($id);
+
+        $asset->update([
+            ...$request->all(),
+            'whitelist' => $request->whitelist ? sprintf(",%s", implode(',', $request->whitelist)) : null,
+        ]);
+
+        return $this->noContent();
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     */
+    public function destroy(string $id)
+    {
+        $asset = Asset::allowed()->findOrFail($id);
+
+        $asset->delete();
+
+        return $this->noContent();
+    }
+}

+ 72 - 0
app/Http/Requests/API/Asset/CreateOrUpdateRequest.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace App\Http\Requests\API\Asset;
+
+use App\Http\Requests\RuleHelper;
+use App\Models\Enums\AssetStatus;
+use App\Models\User;
+use Illuminate\Database\Query\Builder;
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\Rules\Enum;
+
+class CreateOrUpdateRequest extends FormRequest
+{
+    use RuleHelper;
+
+    /**
+     * Determine if the user is authorized to make this request.
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
+     */
+    public function rules(): array
+    {
+        return [
+            'name' => 'required|max:100',
+            'code' => [
+                'required',
+                'max:45',
+                Rule::unique('assets')
+                    ->where($this->userCompanyWhere())
+                    ->ignore($this->route()->parameter('asset')),
+            ],
+            'status' => [
+                'required',
+                new Enum(AssetStatus::class),
+            ],
+            'owner' => [
+                'required',
+                Rule::exists('users', 'id')
+                    ->where($this->userCompanyWhere()),
+            ],
+            'address' => 'max:255',
+            'group_id' => [
+                'required',
+                Rule::exists('asset_groups', 'id')
+                    ->where($this->userCompanyWhere()),
+            ],
+            'geo_address_code' => 'max:255',
+            'acl' => 'required',
+            'whitelist' => [
+                'array',
+                function ($attribute, $value, $fail) {
+                    $userCount = User::where("company_id", Auth::user()->company_id)->whereIn('id', $value)->count();
+                    if ($userCount != count($value)) {
+                        $fail('The selected user is invalid.');
+                    }
+                }
+            ],
+            'latitude' => 'numeric',
+            'longitude' => 'numeric',
+        ];
+    }
+}

+ 14 - 0
app/Http/Requests/RuleHelper.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Database\Query\Builder;
+use Illuminate\Support\Facades\Auth;
+
+trait RuleHelper
+{
+    protected function userCompanyWhere(): \Closure
+    {
+        return fn (Builder $query) => $query->where('company_id', Auth::user()->company_id);
+    }
+}

+ 34 - 0
app/Http/Resources/API/AssetResource.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Http\Resources\API;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class AssetResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id' => $this->id,
+            'name' => $this->name,
+            'code' => $this->code,
+            'description' => $this->description,
+            'status' => 'doing',
+            'created_by' => $this->created_by,
+            'owner' => $this->owner,
+            'address' => $this->address,
+            'group_id' => $this->group_id,
+            'geo_address_code' => $this->geo_address_code,
+            'acl' => $this->acl,
+            'whitelist' => $this->whitelist,
+            'latitude' => $this->latitude,
+            'longitude' => $this->longitude,
+        ];
+    }
+}

+ 31 - 0
app/ModelFilters/AssetFilter.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\ModelFilters;
+
+use EloquentFilter\ModelFilter;
+
+class AssetFilter extends ModelFilter
+{
+    /**
+    * Related Models that have ModelFilters as well as the method on the ModelFilter
+    * As [relationMethod => [input_key1, input_key2]].
+    *
+    * @var array
+    */
+    public $relations = [];
+
+    public function status($status): ModelFilter
+    {
+        return $this->where('status', $status);
+    }
+
+    public function code($code): ModelFilter
+    {
+        return $this->where('code', 'like', "%$code%");
+    }
+
+    public function name($name): ModelFilter
+    {
+        return $this->where('name', 'like', "%$name%");
+    }
+}

+ 39 - 0
app/Models/Asset.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Models;
+
+use App\Models\Enums\AssetACL;
+use App\Models\Scopes\CompanyScope;
+use EloquentFilter\Filterable;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Auth;
+
+/**
+ * @method static \Illuminate\Database\Eloquent\Builder allowed()
+ */
+class Asset extends Model
+{
+    use HasFactory, Filterable;
+
+    protected $fillable = [
+        "name", "code", "description", "company_id", "status", "created_by",
+        "owner", "address", "group_id", "geo_address_code", "acl",
+        "whitelist", "latitude", "longitude"
+    ];
+
+    protected static function booted(): void
+    {
+        static::addGlobalScope(new CompanyScope);
+    }
+
+    public function scopeAllowed(Builder $query): void
+    {
+        $query->where(function (Builder $query) {
+            return $query->where('acl', AssetACL::PRIVATE->value)->where('owner', Auth::id());
+        })->orWhere(function (Builder $query) {
+            return $query->where('acl', AssetACL::CUSTOM->value)->where('whitelist', 'like', '%' . Auth::id() . '%');
+        });
+    }
+}

+ 10 - 0
app/Models/Enums/AssetACL.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models\Enums;
+
+enum AssetACL: string
+{
+    case PRIVATE = 'private';
+
+    case CUSTOM = 'custom';
+}

+ 12 - 0
app/Models/Enums/AssetStatus.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Models\Enums;
+
+enum AssetStatus: string
+{
+    case DOING = 'doing';
+
+    case DONE = 'done';
+
+    case CLOSED = 'closed';
+}

+ 1 - 0
composer.json

@@ -10,6 +10,7 @@
         "laravel/framework": "^10.10",
         "laravel/sanctum": "^3.3",
         "laravel/tinker": "^2.8",
+        "overtrue/laravel-query-logger": "^3.1",
         "tucker-eric/eloquentfilter": "^3.2"
     },
     "require-dev": {

+ 50 - 1
composer.lock

@@ -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": "71b37f0b21fd331d85701a14def36ca9",
+    "content-hash": "24c364909d2e0699464b848056f1a583",
     "packages": [
         {
             "name": "brick/math",
@@ -1862,6 +1862,55 @@
             ],
             "time": "2023-02-08T01:06:31+00:00"
         },
+        {
+            "name": "overtrue/laravel-query-logger",
+            "version": "3.1.0",
+            "dist": {
+                "type": "zip",
+                "url": "https://mirrors.cloud.tencent.com/repository/composer/overtrue/laravel-query-logger/3.1.0/overtrue-laravel-query-logger-3.1.0.zip",
+                "reference": "f9cf0b687be3fd0e976b1a86becc7cb10820c655",
+                "shasum": ""
+            },
+            "require": {
+                "laravel/framework": "^9.0|^10.0"
+            },
+            "require-dev": {
+                "brainmaestro/composer-git-hooks": "dev-master",
+                "laravel/pint": "^1.5"
+            },
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "providers": [
+                        "Overtrue\\LaravelQueryLogger\\ServiceProvider"
+                    ]
+                },
+                "hooks": {
+                    "pre-commit": [
+                        "composer check-style"
+                    ],
+                    "pre-push": [
+                        "composer check-style"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Overtrue\\LaravelQueryLogger\\": "src"
+                }
+            },
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "overtrue",
+                    "email": "anzhengchao@gmail.com"
+                }
+            ],
+            "description": "A dev tool to log all queries for laravel application.",
+            "time": "2023-02-15T08:34:54+00:00"
+        },
         {
             "name": "phpoption/phpoption",
             "version": "1.9.2",

+ 13 - 0
config/logging.php

@@ -127,5 +127,18 @@ return [
             'path' => storage_path('logs/laravel.log'),
         ],
     ],
+    'query' => [
+        'enabled' => env('LOG_QUERY', env('APP_ENV') === 'local'),
 
+        // Only record queries that are slower than the following time
+        // Unit: milliseconds
+        'slower_than' => 0,
+
+        // Only record queries when the QUERY_LOG_TRIGGER is set in the environment,
+        // or when the trigger HEADER, GET, POST, or COOKIE variable is set.
+        'trigger' => env('QUERY_LOG_TRIGGER'),
+
+        // Log Channel
+        'channel' => 'stack',
+    ],
 ];

+ 38 - 0
database/factories/AssetFactory.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\AssetGroup;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Facades\Auth;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Asset>
+ */
+class AssetFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            'name' => fake()->name(),
+            'code' => fake()->randomNumber(5),
+            'description' => fake()->text(),
+            'company_id' => Auth::user()->company_id,
+            'status' => 'doing',
+            'created_by' => Auth::user()->id,
+            'owner' => Auth::id(),
+            'address' => fake()->address(),
+            'group_id' => AssetGroup::factory()->create()?->id,
+            'geo_address_code' => fake()->randomNumber(5),
+            'acl' => 'custom',
+            'whitelist' => ',1,',
+            'latitude' => fake()->latitude(),
+            'longitude' => fake()->longitude(),
+        ];
+    }
+}

+ 41 - 0
database/migrations/2024_01_17_134239_create_assets_table.php

@@ -0,0 +1,41 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('assets', function (Blueprint $table) {
+            $table->id();
+            $table->string('name', 100);
+            $table->string('code', 45);
+            $table->integer('company_id');
+            $table->enum('status', ['doing', 'done', 'closed'])->default('doing');
+            $table->integer('owner');
+            $table->integer('group_id')->unsigned();
+            $table->string("address", 255)->nullable();
+            $table->string("geo_address_code")->nullable();
+            $table->decimal("latitude", 10, 6)->nullable();
+            $table->decimal("longitude", 10, 6)->nullable();
+            $table->text("description")->nullable();
+            $table->enum("acl", ['private', 'custom'])->default('private');
+            $table->string('whitelist', 255)->nullable();
+            $table->integer("created_by")->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('assets');
+    }
+};

+ 1 - 0
routes/api.php

@@ -27,5 +27,6 @@ Route::middleware(['auth:sanctum'])->group(function () {
 
     Route::apiResources([
         'asset-group' => API\AssetGroupController::class,
+        'asset' => API\AssetController::class,
     ]);
 });

+ 76 - 0
tests/Feature/API/AssetTest.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace Tests\Feature\API;
+
+
+use App\Models\Asset;
+use Tests\Feature\TestCase;
+
+class AssetTest extends TestCase
+{
+    public function test_asset_group_list()
+    {
+        Asset::factory(30)->create();
+
+        $response = $this->get(route('asset.index'));
+
+        $response->assertStatus(200)
+            ->assertJsonStructure([
+                'data' => [
+                    '*' => [
+                        'id',
+                        'name',
+                        'code',
+                        'description',
+                        'status',
+                        'created_by',
+                        'owner',
+                        'address',
+                        'group_id',
+                        'geo_address_code',
+                        'acl',
+                        'whitelist',
+                        'latitude',
+                        'longitude',
+                    ]
+                ]
+            ]);
+    }
+
+    public function test_asset_group_create(): void
+    {
+        $form = Asset::factory()->make();
+        $form->whitelist = [1];
+
+        $response = $this->post(route('asset.store'), $form->toArray());
+
+        $response->assertStatus(204);
+    }
+
+    public function test_asset_group_update(): void
+    {
+        $asset = Asset::factory()->create();
+
+        $form = Asset::factory()->make();
+        $form->whitelist = [1];
+
+        $response = $this->put(route('asset.update', ['asset' => $asset->id]), $form->toArray());
+
+        $response->assertStatus(204);
+
+        $newAsset = Asset::find($asset->id);
+
+        $this->assertEquals($form->name, $newAsset->name);
+    }
+
+    public function test_asset_group_delete(): void
+    {
+        $asset = Asset::factory()->create();
+
+        $response = $this->delete(route('asset.destroy', ['asset' => $asset->id]));
+
+        $response->assertStatus(204);
+
+        $this->assertNull(Asset::find($asset->id));
+    }
+}