RESTForge

Query Builder

Helper untuk membangun query SQL secara programatik

Query Builder (db.table()) adalah antarmuka query database di RESTForge yang memungkinkan developer mengeksekusi operasi database tanpa menulis SQL secara langsung. Query Builder tersedia di dalam Processor melalui services.db dan menghasilkan SQL yang sesuai secara otomatis berdasarkan database yang aktif (PostgreSQL, MySQL, atau Oracle).

Prinsip Dasar (Basic Principles)

  • Setiap query diawali dengan db.table('nama_tabel')
  • Method-method berikutnya dirangkai secara berantai (method chaining)
  • Query tidak dieksekusi sampai terminal method dipanggil: .get(), .first(), .insert(), .update(), .delete(), .count(), atau .exists()
  • Semua nilai parameter aman dari SQL injection secara otomatis (parameterized query)

Anatomi Query (Query Anatomy)

db.table('nama_tabel')
   .select(...)          ← opsional: pilih kolom
   .where(...)           ← opsional: filter baris
   .orderBy(...)         ← opsional: urutan hasil
   .limit(...)           ← opsional: batasi jumlah baris
   .offset(...)          ← opsional: lewati sejumlah baris
   .with(...)            ← opsional: relasi has-many
   .withOne(...)         ← opsional: relasi has-one
   .get()                ← terminal: eksekusi query
   .flatten()            ← post-processor (opsional)

Pemilihan Tabel dan Kolom (Table and Column Selection)

db.table()

Menentukan tabel target operasi. Mendukung prefix schema dengan notasi titik.

db.table('contact')
db.table('auth.user')
db.table('inventory.stock_item')

select()

Menentukan kolom yang dikembalikan. Jika tidak dipanggil, seluruh kolom dikembalikan (*).

db.table('contact')
  .select('contact_id', 'contact_name', 'email')
  .get()

Kondisi Filter (WHERE Conditions)

Semua kondisi filter digabungkan dengan operator AND.

where() — Kesetaraan dan Perbandingan (Equality and Comparison)

Object style untuk kondisi kesetaraan:

db.table('contact')
  .where({ city: 'Surabaya', is_active: true })
  .get()

Operator style untuk perbandingan (=, !=, >, >=, <, <=):

db.table('contact')
  .where('created_at', '>=', '2026-01-01')
  .get()

Beberapa kondisi dapat dirangkai:

db.table('contact')
  .where({ is_active: true })
  .where('created_at', '>=', '2026-01-01')
  .where('created_at', '<=', '2026-12-31')
  .get()

whereIn() dan whereNotIn()

db.table('contact')
  .whereIn('city', ['Surabaya', 'Jakarta', 'Bandung'])
  .get()

db.table('contact')
  .whereNotIn('contact_id', ['id-1', 'id-2'])
  .get()

whereNull() dan whereNotNull()

db.table('contact')
  .whereNull('email')
  .get()

db.table('contact')
  .whereNotNull('email')
  .get()

Pencarian bersifat case-insensitive di semua database. Karakter wildcard: % (nol atau lebih karakter), _ (tepat satu karakter).

db.table('contact')
  .whereLike('contact_name', '%budi%')
  .get()
DatabaseImplementasi Internal
PostgreSQLILIKE
MySQLLIKE (case-insensitive via collation)
OracleUPPER(kolom) LIKE UPPER(pola)

whereBetween()

db.table('orders')
  .whereBetween('order_date', '2026-01-01', '2026-12-31')
  .get()

Pengurutan dan Pembatasan (Ordering and Limiting)

orderBy()

// Ascending (default)
db.table('contact')
  .orderBy('contact_name')
  .get()

// Descending
db.table('orders')
  .orderBy('order_date', 'desc')
  .get()

// Multi-kolom
db.table('contact')
  .orderBy('city', 'asc')
  .orderBy('contact_name', 'asc')
  .get()

limit() dan offset()

// 10 baris pertama
db.table('contact')
  .orderBy('contact_name')
  .limit(10)
  .get()

// Halaman 2 (baris 11-20)
db.table('contact')
  .orderBy('contact_name')
  .limit(10)
  .offset(10)
  .get()

Rumus pagination: offset = (nomor_halaman - 1) x ukuran_halaman. Pada Oracle, LIMIT n otomatis dikonversi menjadi FETCH FIRST n ROWS ONLY.

Terminal Method: Pengambilan Data (Data Retrieval)

get() — Ambil Semua Baris (Fetch All Rows)

Mengembalikan array of objects. Jika tidak ada data, mengembalikan [].

const contacts = await db.table('contact')
  .where({ is_active: true })
  .orderBy('contact_name')
  .get()

first() — Ambil Satu Baris (Fetch Single Row)

Mengembalikan satu object, atau null jika tidak ada data. Secara internal menambahkan LIMIT 1.

const contact = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .first()

count() — Hitung Jumlah Baris (Count Rows)

Mengembalikan integer.

const total = await db.table('contact')
  .where({ is_active: true })
  .count()

exists() — Cek Keberadaan (Check Existence)

Mengembalikan true atau false. Lebih efisien dari count() > 0 karena berhenti begitu satu baris ditemukan.

const emailTaken = await db.table('contact')
  .where({ email: 'budi@email.com' })
  .exists()

Terminal Method: Manipulasi Data (Data Manipulation)

insert() — Tambah Baris Baru (Insert New Row)

const newContact = await db.table('contact')
  .returning('*')
  .insert({
    contact_name: 'Budi Santoso',
    email: 'budi@email.com',
    city: 'Surabaya'
  })

.returning() wajib dipanggil sebelum .insert(), bukan sesudahnya. Tanpa .returning(), insert mengembalikan null.

DatabaseImplementasi returning()
PostgreSQLRETURNING * (native)
MySQLSELECT ulang setelah insert
OracleRETURNING ... INTO bind variable

update() — Ubah Data (Update Data)

const updated = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .returning('*')
  .update({
    contact_name: 'Budi Santoso',
    email: 'budi.baru@email.com'
  })

update() wajib didahului minimal satu .where(). Tanpa kondisi WHERE, query menghasilkan QueryBuilderError untuk mencegah pembaruan seluruh tabel secara tidak sengaja.

delete() — Hapus Baris (Delete Row)

Mengembalikan jumlah baris yang dihapus (integer).

const deletedCount = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .delete()

delete() wajib didahului minimal satu .where(). Tanpa kondisi WHERE, query menghasilkan QueryBuilderError.

Relation Helper

Relation Helper mengambil data dari tabel yang berelasi menggunakan strategi batch loading yaitu selalu dua query ke database, berapapun jumlah baris induk.

with() — Relasi Has-Many

.with(tabel_relasi, foreign_key [, local_key [, alias [, callback]]])

Data relasi dipasangkan sebagai array ([] jika tidak ada relasi).

// Satu relasi
const contact = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .with('orders', 'contact_id', 'contact_id')
  .first()

// Beberapa relasi sekaligus
const contact = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .with('orders', 'contact_id', 'contact_id')
  .with('contact_addresses', 'contact_id', 'contact_id')
  .first()

// Dengan alias
const contact = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .with('orders', 'contact_id', 'contact_id', 'riwayat_order')
  .first()

// Dengan filter callback
const contact = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .with(
    'orders', 'contact_id', 'contact_id', null,
    (q) => q.where({ status: 'confirmed' }).orderBy('order_date', 'desc').limit(5)
  )
  .first()

withOne() — Relasi Has-One

Data relasi dipasangkan sebagai object tunggal (null jika tidak ada relasi).

const contact = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .withOne('contact_profile', 'contact_id', 'contact_id', 'profile')
  .first()

Nested Relation — Relasi Bertingkat

Relasi bertingkat didukung maksimal 2 level menggunakan callback. Untuk relasi lebih dari 2 level, gunakan db.executeQuery() dengan JOIN.

const contact = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .with(
    'orders', 'contact_id', 'contact_id', null,
    (q) => q
      .where({ status: 'confirmed' })
      .orderBy('order_date', 'desc')
      .with('order_items', 'order_id', 'order_id')
  )
  .first()

Post-Processor: flatten()

flatten() mengubah output nested yang dihasilkan with() atau withOne() menjadi struktur flat, identik dengan hasil SQL JOIN. Method ini dipanggil setelah terminal method .get() atau .first().

// Output nested (tanpa flatten)
const nested = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .with('orders', 'contact_id', 'contact_id')
  .first()
// { contact_id: 'abc-123', contact_name: 'Budi',
//   orders: [{ order_id: '1', ... }, { order_id: '2', ... }] }

// Output flat (dengan flatten)
const flat = await db.table('contact')
  .where({ contact_id: 'abc-123' })
  .with('orders', 'contact_id', 'contact_id')
  .first()
  .flatten()
// [{ contact_id: 'abc-123', contact_name: 'Budi', order_id: '1', ... },
//  { contact_id: 'abc-123', contact_name: 'Budi', order_id: '2', ... }]

Perilaku flatten() (Flatten Behavior)

KondisiPerilaku
Induk punya relasi (has-many)Satu baris per kombinasi induk + baris relasi
Induk tidak punya relasiSatu baris induk, kolom relasi bernilai null (LEFT JOIN behavior)
Relasi has-oneSatu baris per induk, kolom relasi di-merge langsung
Konflik nama kolomKolom induk dipertahankan, kolom relasi mendapat prefix {nama_tabel}.{kolom}
Beberapa with() sekaligusCartesian product dari semua relasi
Tanpa with() sebelumnyaQueryBuilderError

Untuk kasus multi-relasi yang menghasilkan cartesian product tidak diinginkan, gunakan db.executeQuery() dengan JOIN eksplisit.

Contoh Implementasi di Processor (Processor Examples)

Filter Dinamis dengan Pagination

const processor = {
  async process(input, services, req) {
    const { db, createResponse, createError } = services;

    try {
      let query = db.table('contact')
        .select('contact_id', 'contact_name', 'email', 'city')
        .where({ is_active: true });

      if (input.keyword) query = query.whereLike('contact_name', `%${input.keyword}%`);
      if (input.city)    query = query.where({ city: input.city });

      const page     = parseInt(input.page)  || 1;
      const pageSize = parseInt(input.limit) || 10;
      const total    = await query.count();

      const rows = await query
        .orderBy('contact_name', 'asc')
        .limit(pageSize)
        .offset((page - 1) * pageSize)
        .get();

      return createResponse(200, 'Data contact ditemukan.', {
        rows, total, page, page_size: pageSize,
        total_pages: Math.ceil(total / pageSize)
      });
    } catch (error) {
      return createError(500, `Gagal mengambil data: ${error.message}`);
    }
  }
};

module.exports = processor;

Data Relasi untuk Detail Page

const processor = {
  async process(input, services, req) {
    const { db, createResponse, createError } = services;

    try {
      const contact = await db.table('contact')
        .where({ contact_id: input.contact_id, is_active: true })
        .withOne('contact_profile', 'contact_id', 'contact_id', 'profile')
        .with('orders', 'contact_id', 'contact_id', null,
          (q) => q.where({ status: 'confirmed' }).orderBy('order_date', 'desc').limit(10)
            .with('order_items', 'order_id', 'order_id', 'items')
        )
        .first();

      if (!contact) return createError(404, 'Contact tidak ditemukan.');

      return createResponse(200, 'Data contact ditemukan.', contact);
    } catch (error) {
      return createError(500, `Gagal mengambil data: ${error.message}`);
    }
  }
};

module.exports = processor;

Data Flat untuk Tabel (DataTables / TanStack Table)

const processor = {
  async process(input, services, req) {
    const { db, createResponse, createError } = services;

    try {
      const rows = await db.table('contact')
        .select('contact_id', 'contact_name', 'city')
        .where({ is_active: true })
        .with('orders', 'contact_id', 'contact_id', null,
          (q) => q.where({ status: 'confirmed' })
        )
        .get()
        .flatten();

      return createResponse(200, 'Data siap untuk tabel.', rows);
    } catch (error) {
      return createError(500, `Gagal mengambil data: ${error.message}`);
    }
  }
};

module.exports = processor;

Kapan Menggunakan db.executeQuery() (When to Use Raw SQL)

KebutuhanGunakan
Query satu tabel dengan filterdb.table()
Data relasi nested (detail page)db.table().with().first()
Data relasi flat (tabel UI)db.table().with().get().flatten()
JOIN + GROUP BY + agregasidb.executeQuery()
Relasi lebih dari 2 leveldb.executeQuery()
Subquery, CTE, atau Window Functiondb.executeQuery()

Ringkasan Method (Method Summary)

MethodKategoriKeterangan
db.table(tabel)Entry pointMulai query pada tabel tertentu
.select(...kolom)KolomPilih kolom yang dikembalikan
.where(kondisi)FilterKondisi kesetaraan atau perbandingan
.whereIn(kolom, array)FilterNilai ada dalam daftar
.whereNotIn(kolom, array)FilterNilai tidak ada dalam daftar
.whereNull(kolom)FilterNilai adalah NULL
.whereNotNull(kolom)FilterNilai bukan NULL
.whereLike(kolom, pola)FilterPencarian teks case-insensitive
.whereBetween(kolom, min, max)FilterNilai dalam rentang
.orderBy(kolom, arah)UrutanTentukan urutan hasil
.limit(n)PembatasanBatasi jumlah baris
.offset(n)PembatasanLewati sejumlah baris
.with(...)RelasiRelasi has-many (array)
.withOne(...)RelasiRelasi has-one (object)
.returning(...)ModifierKembalikan data setelah insert/update
.get()TerminalAmbil semua baris → array
.first()TerminalAmbil satu baris → object atau null
.insert(data)TerminalTambah baris baru
.update(data)TerminalUbah baris (wajib .where())
.delete()TerminalHapus baris (wajib .where())
.count(kolom?)TerminalHitung jumlah baris → integer
.exists()TerminalCek keberadaan → true/false
.flatten()Post-processorUbah output nested ke flat array

Langkah Selanjutnya (Next Steps)

On this page