CRUD Composite
Operasi master-detail untuk create, read, dan update data berhierarki
CRUD Composite menyediakan endpoint untuk mengelola data master-detail (header dan detail item) dalam satu request. Seluruh operasi composite dijalankan dalam satu database transaction sehingga jika salah satu bagian gagal, seluruh perubahan di-rollback. Fitur ini cocok untuk transaksi yang memiliki struktur induk-anak, seperti Stock Inbound + Item, Purchase Order + Line Item, atau Invoice + Detail.
Kapan Menggunakan Composite (When to Use Composite)
| Skenario | Endpoint | Alasan |
|---|---|---|
| Membuat data header + detail sekaligus | /create-composite | Satu transaksi atomik, foreign key detail otomatis terisi |
| Membaca header beserta seluruh detail item | /read-composite | Mengembalikan data gabungan header + detail dalam satu response |
| Memperbarui header + insert/update/delete detail | /update-composite | Seluruh perubahan dalam satu transaksi, header totals di-recalculate |
| Mengubah satu field di tabel tunggal (tanpa detail) | /update | Gunakan CRUD Dasar untuk entitas tunggal |
Konfigurasi Payload (Payload Configuration)
Fitur composite diaktifkan melalui action createComposite, updateComposite, dan readComposite di file payload, disertai konfigurasi masterDetail untuk mendefinisikan relasi header-detail:
{
"tableName": "stock_inbound",
"primaryKey": "stock_inbound_id",
"fieldName": [
"stock_inbound_id", "inbound_number", "inbound_date",
"warehouse_id", "supplier_id", "reference_number",
"notes", "total_items", "total_qty", "total_amount", "status"
],
"action": {
"datatables": true,
"create": true,
"update": true,
"delete": true,
"first": true,
"lookup": true,
"read": true,
"createComposite": true,
"updateComposite": true,
"readComposite": true
},
"masterDetail": {
"enabled": true,
"detailTable": "stock_inbound_item",
"foreignKey": "stock_inbound_id",
"detailConfig": {
"tableName": "stock_inbound_item",
"primaryKey": "stock_inbound_item_id",
"fieldName": [
"stock_inbound_item_id", "stock_inbound_id", "line_number",
"item_product_id", "qty_received", "uom",
"unit_price", "total_amount", "notes"
],
"requiredFields": [
"line_number", "item_product_id", "qty_received", "uom", "unit_price"
],
"autoCalculateFields": {
"total_amount": {
"type": "generated",
"formula": "qty_received * unit_price"
}
}
},
"headerCalculations": {
"total_items": { "type": "count", "source": "items.length" },
"total_qty": { "type": "sum", "source": "items.qty_received" },
"total_amount": { "type": "sum", "source": "items.total_amount" }
},
"cascadeDelete": true,
"transactionMode": "required"
}
}Penjelasan Konfigurasi (Configuration Reference)
| Property | Tipe | Keterangan |
|---|---|---|
masterDetail.enabled | boolean | Mengaktifkan fitur master-detail |
masterDetail.detailTable | string | Nama tabel detail |
masterDetail.foreignKey | string | Kolom foreign key yang menghubungkan detail ke header |
detailConfig.primaryKey | string | Primary key tabel detail |
detailConfig.fieldName | array | Daftar kolom yang dikelola di tabel detail |
detailConfig.requiredFields | array | Kolom wajib saat insert detail item |
detailConfig.autoCalculateFields | object | Kolom detail yang dihitung otomatis berdasarkan formula |
headerCalculations | object | Kolom header yang dihitung dari data detail (count, sum) |
cascadeDelete | boolean | Jika true, hapus header otomatis menghapus seluruh detail |
transactionMode | string | "required" = seluruh operasi dalam satu transaction |
Membuat Data Composite (Creating Composite Data)
Endpoint /create-composite menyisipkan data header beserta seluruh detail item dalam satu transaksi.
Contoh: membuat stock inbound dengan 2 item
{
"stock_inbound": {
"inbound_number": "INB/2026/001",
"inbound_date": "2026-04-16",
"warehouse_id": "d1000000-0000-0000-0000-000000000000",
"supplier_id": "b1000000-0000-0000-0000-000000000000",
"notes": "Pengadaan peralatan kantor",
"status": "draft",
"stock_inbound_item": [
{
"line_number": 1,
"item_product_id": "04d71c62-0000-0000-0000-000000000000",
"qty_received": 25,
"uom": "pcs",
"unit_price": 500000
},
{
"line_number": 2,
"item_product_id": "15e82d73-0000-0000-0000-000000000000",
"qty_received": 10,
"uom": "pcs",
"unit_price": 750000
}
]
}
}Request body menggunakan nama tabel header (stock_inbound) sebagai root key, dengan detail item berupa array menggunakan nama tabel detail (stock_inbound_item) sebagai key di dalamnya.
Response (201 Created):
{
"success": true,
"message": "stock_inbound data successfully added",
"data": {
"stock_inbound_id": "a1b2c3d4-0000-0000-0000-000000000000",
"inbound_number": "INB/2026/001",
"inbound_date": "2026-04-16T00:00:00.000Z",
"warehouse_id": "d1000000-0000-0000-0000-000000000000",
"supplier_id": "b1000000-0000-0000-0000-000000000000",
"notes": "Pengadaan peralatan kantor",
"total_items": 2,
"total_qty": 35,
"total_amount": 20000000,
"status": "draft",
"created_at": "2026-04-16T10:30:00.000Z",
"created_by": "Input from API",
"stock_inbound_item": [
{
"stock_inbound_item_id": "e5f6a7b8-0000-0000-0000-000000000001",
"stock_inbound_id": "a1b2c3d4-0000-0000-0000-000000000000",
"line_number": 1,
"item_product_id": "04d71c62-0000-0000-0000-000000000000",
"qty_received": 25,
"uom": "pcs",
"unit_price": 500000,
"total_amount": 12500000
},
{
"stock_inbound_item_id": "e5f6a7b8-0000-0000-0000-000000000002",
"stock_inbound_id": "a1b2c3d4-0000-0000-0000-000000000000",
"line_number": 2,
"item_product_id": "15e82d73-0000-0000-0000-000000000000",
"qty_received": 10,
"uom": "pcs",
"unit_price": 750000,
"total_amount": 7500000
}
]
},
"timestamp": "2026-04-16T10:30:00.000Z"
}Field Otomatis saat Create (Auto-Populated Fields)
| Field | Keterangan |
|---|---|
| Primary key header | UUID v4 di-generate otomatis |
| Primary key detail | UUID v4 di-generate otomatis untuk setiap item |
| Foreign key detail | Diisi otomatis dari primary key header yang baru dibuat |
total_amount (detail) | Dihitung otomatis dari formula qty_received * unit_price berdasarkan autoCalculateFields |
total_items (header) | COUNT dari detail item berdasarkan headerCalculations |
total_qty (header) | SUM field qty_received dari seluruh detail item |
total_amount (header) | SUM field total_amount dari seluruh detail item |
created_at | Timestamp saat ini, di-set pada header dan setiap detail |
created_by | Di-set ke "Input from API" (dapat di-override) |
Detail item wajib minimal 1 item dalam array. Request dengan array stock_inbound_item kosong atau tanpa array detail akan ditolak dengan error 400.
Membaca Data Composite (Reading Composite Data)
Endpoint /read-composite mengambil data header beserta seluruh detail item dalam satu response. Data detail diambil menggunakan detailQuery dari payload yang dapat menyertakan JOIN ke tabel referensi.
Contoh: mengambil data berdasarkan primary key
{
"where": [
{ "key": "stock_inbound_id", "value": "a1b2c3d4-0000-0000-0000-000000000000" }
]
}Response (200 OK):
{
"success": true,
"data": {
"stock_inbound_id": "a1b2c3d4-0000-0000-0000-000000000000",
"inbound_number": "INB/2026/001",
"inbound_date": "2026-04-16T00:00:00.000Z",
"warehouse_id": "d1000000-0000-0000-0000-000000000000",
"supplier_id": "b1000000-0000-0000-0000-000000000000",
"notes": "Pengadaan peralatan kantor",
"total_items": 2,
"total_qty": 35,
"total_amount": 20000000,
"status": "draft",
"stock_inbound_item": [
{
"stock_inbound_item_id": "e5f6a7b8-0000-0000-0000-000000000001",
"stock_inbound_id": "a1b2c3d4-0000-0000-0000-000000000000",
"line_number": 1,
"item_product_id": "04d71c62-0000-0000-0000-000000000000",
"product_code": "PRD-001",
"product_name": "Keyboard Wireless",
"qty_received": 25,
"uom": "pcs",
"unit_price": 500000,
"total_amount": 12500000
},
{
"stock_inbound_item_id": "e5f6a7b8-0000-0000-0000-000000000002",
"stock_inbound_id": "a1b2c3d4-0000-0000-0000-000000000000",
"line_number": 2,
"item_product_id": "15e82d73-0000-0000-0000-000000000000",
"product_code": "PRD-002",
"product_name": "Mouse Optical",
"qty_received": 10,
"uom": "pcs",
"unit_price": 750000,
"total_amount": 7500000
}
]
},
"timestamp": "2026-04-16T10:30:00.000Z"
}Detail item pada response /read-composite menyertakan kolom tambahan dari tabel referensi (seperti product_code dan product_name dari tabel item_product) karena detailQuery di payload menggunakan JOIN. Kolom tambahan ini tidak tersedia di endpoint /first yang hanya mengembalikan data header.
Endpoint /read-composite tidak menggunakan cache. Setiap request selalu dijalankan langsung ke database untuk memastikan data yang dikembalikan merupakan versi terbaru.
Memperbarui Data Composite (Updating Composite Data)
Endpoint /update-composite memperbarui data header dan mengelola detail item (insert, update, delete) dalam satu transaksi. Operasi header bersifat partial update, artinya hanya field yang dikirim yang berubah.
Format detail item pada /update-composite berbeda dengan /create-composite. Pada create, detail berupa array. Pada update, detail berupa object dengan tiga properti: insert, update, dan delete.
Update Header Saja (Header Only)
Jika hanya perlu memperbarui field header tanpa menyentuh detail, cukup kirim data header saja:
{
"stock_inbound": {
"stock_inbound_id": "a1b2c3d4-0000-0000-0000-000000000000",
"status": "confirmed",
"notes": "Sudah diterima dan dicek"
}
}Update dengan Operasi Detail (With Detail Operations)
Contoh berikut menunjukkan kombinasi lengkap: menghapus satu item, memperbarui satu item, dan menambahkan satu item baru sekaligus:
{
"stock_inbound": {
"stock_inbound_id": "a1b2c3d4-0000-0000-0000-000000000000",
"notes": "Revisi lengkap detail item",
"stock_inbound_item": {
"delete": [
{
"stock_inbound_item_id": "e5f6a7b8-0000-0000-0000-000000000002"
}
],
"update": [
{
"stock_inbound_item_id": "e5f6a7b8-0000-0000-0000-000000000001",
"qty_received": 30,
"unit_price": 480000
}
],
"insert": [
{
"line_number": 3,
"item_product_id": "26f93e84-0000-0000-0000-000000000000",
"qty_received": 15,
"uom": "pcs",
"unit_price": 600000
}
]
}
}
}Response (200 OK):
{
"success": true,
"message": "stock_inbound data successfully updated",
"data": {
"stock_inbound_id": "a1b2c3d4-0000-0000-0000-000000000000",
"inbound_number": "INB/2026/001",
"inbound_date": "2026-04-16T00:00:00.000Z",
"notes": "Revisi lengkap detail item",
"total_items": 2,
"total_qty": 45,
"total_amount": 23400000,
"status": "draft",
"updated_at": "2026-04-16T10:30:00.000Z",
"updated_by": "Update from API",
"_operations": {
"deleted": 1,
"updated": 1,
"inserted": 1
}
},
"timestamp": "2026-04-16T10:30:00.000Z"
}Urutan Eksekusi Detail (Detail Execution Order)
Operasi detail dijalankan dalam urutan tetap berikut, terlepas dari urutan properti dalam request body:
| Urutan | Operasi | Field Wajib | Keterangan |
|---|---|---|---|
| 1 | delete | Primary key detail | Menghapus item berdasarkan primary key |
| 2 | update | Primary key detail + field yang diubah | Partial update pada item yang sudah ada |
| 3 | insert | Sesuai requiredFields di payload | Menambah item baru, PK dan FK di-generate otomatis |
Urutan DELETE → UPDATE → INSERT dipilih untuk menghindari konflik constraint. Menghapus item terlebih dahulu membebaskan unique constraint yang mungkin diperlukan oleh item baru yang akan di-insert.
Kalkulasi Ulang Otomatis (Auto Recalculation)
Setelah seluruh operasi detail selesai, field header yang dikonfigurasi di headerCalculations dihitung ulang secara otomatis berdasarkan state akhir detail item. Properti _operations pada response merangkum jumlah operasi yang berhasil dijalankan.
Perbandingan Format Request (Request Format Comparison)
| Aspek | /create-composite | /update-composite |
|---|---|---|
| Primary key header | Tidak dikirim (auto UUID) | Wajib dikirim |
| Format detail item | Array [ {...}, {...} ] | Object { insert: [], update: [], delete: [] } |
| Detail item wajib | Ya (minimal 1 item) | Tidak (bisa update header saja) |
| Foreign key detail | Otomatis | Otomatis (untuk insert baru) |
| Header totals | Auto-calculated | Auto-recalculated setelah operasi |
Error Umum (Common Errors)
| Situasi | HTTP Code | Penyebab |
|---|---|---|
| Root key tidak sesuai nama tabel | 400 | Root key harus sesuai dengan tableName di payload |
| Array detail kosong saat create | 400 | /create-composite mewajibkan minimal 1 detail item |
| Format detail salah saat update | 400 | /update-composite memerlukan object {insert, update, delete}, bukan array |
| PK header tidak dikirim saat update | 400 | Primary key header wajib disertakan untuk identifikasi record |
| PK detail tidak ada pada update/delete | 400 | Setiap item di array update dan delete wajib menyertakan primary key detail |
| Nilai duplicate | 409 | Unique constraint violation (misalnya inbound_number sudah ada) |
| Foreign key tidak ditemukan | 400 | Nilai FK merujuk ke record yang tidak ada di tabel referensi |
| Record header tidak ditemukan | 404 | Primary key header yang dikirim tidak ada di database |
Langkah Selanjutnya (Next Steps)
- CRUD Dasar untuk operasi create, update, dan delete entitas tunggal
- Adjust untuk operasi increment/decrement atomik pada field numerik
- POST /create-composite untuk spesifikasi teknis lengkap endpoint create-composite
- POST /read-composite untuk spesifikasi teknis lengkap endpoint read-composite
- POST /update-composite untuk spesifikasi teknis lengkap endpoint update-composite