Skip to content

Perbaikan error subquery pada fitur notifikasi desa di halaman desa#652

Open
habibie11 wants to merge 4 commits intorilis-devfrom
dev-651
Open

Perbaikan error subquery pada fitur notifikasi desa di halaman desa#652
habibie11 wants to merge 4 commits intorilis-devfrom
dev-651

Conversation

@habibie11
Copy link
Copy Markdown
Contributor

issue #651 (comment)

Perubahan ini memperbaiki error:
SQLSTATE[21000]: Cardinality violation: Subquery returns more than 1 row

hasil test
php artisan test --filter=LaporanDesaTest ``
image

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

🔄 AI PR Review sedang antri di server...

Proses review akan segera dimulai di background — hasil akan muncul sebagai komentar setelah selesai.
Powered by CrewAI · PR #652

@devopsopendesa
Copy link
Copy Markdown

🔒 Security Review

Total Temuan: 0 isu

Tidak ada kerentanan security ditemukan pada kode yang ditambahkan/diubah

Analisis Perubahan

Aspek Status Keterangan
SQL Injection ✅ FIXED Raw query dengan string interpolation diganti dengan Eloquent parameter binding
Mass Assignment ✅ N/A Tidak ada operasi create/update di scope ini
IDOR ✅ N/A Authorization harus ditangani di controller layer (tidak berubah)
XSS ✅ N/A Ini adalah query scope, tidak ada output ke view
Type Safety ✅ SAFE Laravel Eloquent otomatis handle type casting dan parameter binding

🎉 Security Improvement

Refactoring ini MENINGKATKAN keamanan dengan menghilangkan SQL injection vulnerability dari query lama:

Sebelum (VULNERABLE):

"... nd.id_desa = '{$desaId}' ..."  // ❌ String interpolation = SQL injection risk

Sesudah (SECURE):

$q->where('id_desa', $desaId)  // ✅ Parameter binding otomatis

📝 Catatan

Meskipun tidak ada isu security pada kode baru, perlu diperhatikan:

  1. Testing Required: Tidak ada test coverage untuk memverifikasi logic equivalence
  2. Authorization: Pastikan controller sudah validasi bahwa user memiliki akses ke $desaId
  3. Performance: Monitor query performance dengan whereHas di production

Refactoring ini adalah security win 🎯 - menghilangkan SQL injection tanpa menambah kerentanan baru.

@devopsopendesa
Copy link
Copy Markdown

⚡ Performance Review

Total Temuan: 2 isu (1 Critical, 1 High)

Severity File Baris Isu Estimasi Dampak
🚨 CRITICAL app/Models/Notifikasi.php ~15-25 whereHas + orWhereDoesntHave tanpa index +500ms-2s per query pada 10k+ rows
⚠️ HIGH app/Models/Notifikasi.php ~15-25 Missing eager loading untuk relasi N+1 queries jika relasi diakses di view

Detail lengkap tersedia sebagai inline comment pada setiap baris.

@@ -22,6 +22,15 @@ class Notifikasi extends Model
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] ⚡ Performance: whereHas + orWhereDoesntHave Subquery Tanpa Index

Masalah: Query menggunakan whereHas dan orWhereDoesntHave yang menghasilkan 2 subquery EXISTS/NOT EXISTS. Pada tabel notifikasi_desa dengan ribuan rows, ini akan sangat lambat tanpa composite index pada (id_desa, id_notifikasi, status).

Kode:

$q->whereHas('notifikasiDesa', function ($q) use ($desaId) {
    $q->where('id_desa', $desaId)
        ->where('status', '!=', 0);
})
->orWhereDoesntHave('notifikasiDesa', function ($q) use ($desaId) {
    $q->where('id_desa', $desaId);
});

Dampak: Pada production dengan 10,000+ notifikasi dan 50,000+ notifikasi_desa records:

  • Query time: 500ms - 2 detik per request
  • Database CPU spike saat traffic tinggi
  • Potential timeout pada shared hosting

Fix:

// 1. Tambahkan composite index di migration:
Schema::table('notifikasi_desa', function (Blueprint $table) {
    $table->index(['id_desa', 'id_notifikasi', 'status'], 'idx_desa_notif_status');
});

// 2. Alternatif: Gunakan JOIN untuk performa lebih baik
public function scopeSemuaNotifDesa($query, $desaId)
{
    return $query->where('notifikasi.aktif', 1)
        ->leftJoin('notifikasi_desa as nd', function($join) use ($desaId) {
            $join->on('nd.id_notifikasi', '=', 'notifikasi.id')
                 ->where('nd.id_desa', '=', $desaId);
        })
        ->where(function($q) {
            $q->whereNull('nd.id')  // tidak ada entry untuk desa ini
              ->orWhere('nd.status', '!=', 0);  // atau ada entry dengan status aktif
        })
        ->select('notifikasi.*')
        ->distinct();
}

@@ -22,6 +22,15 @@ class Notifikasi extends Model
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance: Missing Eager Loading untuk Relasi

Masalah: Scope ini tidak melakukan eager loading untuk relasi notifikasiDesa. Jika di controller atau view ada akses ke $notifikasi->notifikasiDesa, akan terjadi N+1 query problem.

Kode:

return $query->where('aktif', 1)
    ->where(function ($q) use ($desaId) {
        // ... whereHas logic
    });
// Missing: ->with('notifikasiDesa')

Dampak: Jika controller memanggil Notifikasi::semuaNotifDesa($desaId)->get() dan menghasilkan 50 notifikasi, lalu di view ada loop yang akses relasi:

  • 1 query utama + 50 query relasi = 51 total queries
  • Response time bertambah 200-500ms
  • Database connection pool exhaustion pada concurrent requests

Fix:

public function scopeSemuaNotifDesa($query, $desaId)
{
    return $query->where('aktif', 1)
        ->where(function ($q) use ($desaId) {
            $q->whereHas('notifikasiDesa', function ($q) use ($desaId) {
                $q->where('id_desa', $desaId)
                    ->where('status', '!=', 0);
            })
            ->orWhereDoesntHave('notifikasiDesa', function ($q) use ($desaId) {
                $q->where('id_desa', $desaId);
            });
        })
        ->with(['notifikasiDesa' => function($q) use ($desaId) {
            $q->where('id_desa', $desaId);
        }]);
}

// Atau di controller:
Notifikasi::semuaNotifDesa($desaId)->with('notifikasiDesa')->get();

@devopsopendesa
Copy link
Copy Markdown

📝 Code Quality Review

Total Temuan: 2 isu (0 Critical, 2 High)

Severity Kategori File Baris Isu
⚠️ HIGH PHP Quality app/Models/Notifikasi.php 26 Missing type hints pada method parameter dan return type
⚠️ HIGH Testing app/Models/Notifikasi.php 26 Missing tests untuk logic change yang critical

Detail lengkap tersedia sebagai inline comment pada setiap baris.

{
return DB::select("select n.* from notifikasi as n where n.aktif = 1 and ((select nd.id from notifikasi_desa as nd where nd.id_notifikasi = n.id and nd.id_desa = '{$desaId}' and nd.status <> 0) is not null or (select nd.id from notifikasi_desa as nd where nd.id_notifikasi = n.id and nd.id_desa = '{$desaId}') is null)");
return $query->where('aktif', 1)
->where(function ($q) use ($desaId) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Type Hints

Kategori: PHP Quality
Masalah: Method scopeSemuaNotifDesa tidak memiliki type hints untuk parameter dan return type. Ini mengurangi type safety dan IDE autocomplete support, terutama penting karena ini adalah refactoring dari raw SQL ke Eloquent.

Kode:

public function scopeSemuaNotifDesa($query, $desaId)

Fix:

public function scopeSemuaNotifDesa(\Illuminate\Database\Eloquent\Builder $query, int|string $desaId): \Illuminate\Database\Eloquent\Builder

Penjelasan:

  • Parameter $query harus di-type hint sebagai Builder
  • Parameter $desaId harus di-type hint (int atau string tergantung tipe kolom id_desa)
  • Return type harus Builder untuk mendukung method chaining

{
return DB::select("select n.* from notifikasi as n where n.aktif = 1 and ((select nd.id from notifikasi_desa as nd where nd.id_notifikasi = n.id and nd.id_desa = '{$desaId}' and nd.status <> 0) is not null or (select nd.id from notifikasi_desa as nd where nd.id_notifikasi = n.id and nd.id_desa = '{$desaId}') is null)");
return $query->where('aktif', 1)
->where(function ($q) use ($desaId) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing Tests

Kategori: Testing
Masalah: Perubahan dari raw SQL ke Eloquent query builder adalah logic change yang critical dan digunakan di production controller (NotifikasiController::index()), namun tidak ada test coverage untuk memverifikasi behavior tetap identik.

Kode:

public function scopeSemuaNotifDesa($query, $desaId)
{
    return $query->where('aktif', 1)
        ->where(function ($q) use ($desaId) {
            $q->whereHas('notifikasiDesa', function ($q) use ($desaId) {
                $q->where('id_desa', $desaId)
                    ->where('status', '!=', 0);
            })
            ->orWhereDoesntHave('notifikasiDesa', function ($q) use ($desaId) {
                $q->where('id_desa', $desaId);
            });
        });
}

Fix:
Buat test file tests/Unit/Models/NotifikasiTest.php:

<?php

namespace Tests\Unit\Models;

use App\Models\Notifikasi;
use App\Models\NotifikasiDesa;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class NotifikasiTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function scope_semua_notif_desa_returns_active_notifications_with_desa_relation()
    {
        $desaId = 1;
        
        // Notifikasi aktif dengan relasi desa (status != 0)
        $notif1 = Notifikasi::factory()->create(['aktif' => 1]);
        NotifikasiDesa::factory()->create([
            'id_notifikasi' => $notif1->id,
            'id_desa' => $desaId,
            'status' => 1
        ]);
        
        // Notifikasi aktif tanpa relasi desa (global)
        $notif2 = Notifikasi::factory()->create(['aktif' => 1]);
        
        // Notifikasi aktif dengan relasi desa tapi status = 0 (tidak muncul)
        $notif3 = Notifikasi::factory()->create(['aktif' => 1]);
        NotifikasiDesa::factory()->create([
            'id_notifikasi' => $notif3->id,
            'id_desa' => $desaId,
            'status' => 0
        ]);
        
        // Notifikasi tidak aktif (tidak muncul)
        $notif4 = Notifikasi::factory()->create(['aktif' => 0]);
        
        $result = Notifikasi::semuaNotifDesa($desaId)->get();
        
        $this->assertCount(2, $result);
        $this->assertTrue($result->contains($notif1));
        $this->assertTrue($result->contains($notif2));
        $this->assertFalse($result->contains($notif3));
        $this->assertFalse($result->contains($notif4));
    }
}

Penjelasan: Test ini memverifikasi bahwa logic baru menghasilkan hasil yang sama dengan query SQL lama untuk berbagai skenario.

@devopsopendesa
Copy link
Copy Markdown

🐛 Bug Detection Review

Total Temuan: 0 isu (0 Critical, 0 High)

Tidak ada bug HIGH atau CRITICAL yang ditemukan pada baris yang ditambah/diubah.

Analisis Kode Baru (Baris dengan prefix "+")

File: app/Models/Notifikasi.php

Baris yang Dianalisis:

  • Line 28-37: Refactoring dari raw SQL ke Eloquent Query Builder

Hasil Pemeriksaan:

  • ✅ Tidak ada null dereference
  • ✅ Tidak ada loose comparison PHP (operator != di where() adalah SQL-level, bukan PHP-level)
  • ✅ Tidak ada division by zero
  • ✅ Tidak ada unhandled exceptions
  • ✅ Tidak ada missing early returns
  • ✅ Closure dengan use ($desaId) sudah benar
  • ✅ Relationship whereHas dan orWhereDoesntHave sudah proper

Catatan Penting:
Operator where('status', '!=', 0) menggunakan SQL comparison di database level, bukan PHP loose comparison. Eloquent akan translate ini ke SQL WHERE status != 0 atau WHERE status <> 0, sehingga type coercion ditangani oleh database engine, bukan PHP.

Kesimpulan:
Refactoring ini merupakan security improvement yang signifikan (menghilangkan SQL injection vulnerability dari raw query) tanpa menambahkan bug logika HIGH/CRITICAL.

@devopsopendesa
Copy link
Copy Markdown

🤖 AI Code Review — Selesai

📋 Ringkasan Semua Review

Agent Temuan Inline Comments
📊 Full-Stack Security Specialist (PHP + JavaScript) 0 ✅ Clean
📊 Full-Stack Performance Analyst 2 ✅ 2 posted
📊 Full-Stack Code Quality & Architecture Reviewer 2 ✅ 2 posted
📊 Full-Stack Logic Bug Hunter (PHP + JavaScript) 0 ✅ Clean

Total inline comments: 4
Setiap agent sudah mem-posting summary dan inline comment masing-masing di atas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants