RESTForge

Component Engine

Event lifecycle hooks: onBeforeInsert, onAfterUpdate, dan handler lainnya

Component Engine adalah fitur event lifecycle yang memungkinkan eksekusi custom logic (handler) sebelum dan sesudah operasi CRUD. Handler dikonfigurasi melalui property components di payload JSON dan dieksekusi secara otomatis di dalam transaction scope, sehingga konsistensi (consistency) data antara operasi utama dan custom logic terjamin.

Referensi Cepat (Quick Reference)

PropertiNilai
KonfigurasiProperty components di payload JSON
Event single tableonBeforeInsert, onAfterInsert, onBeforeUpdate, onAfterUpdate, onBeforeDelete, onAfterDelete
Event compositeonBeforeCompositeInsert, onAfterCompositeInsert, onBeforeCompositeUpdate, onAfterCompositeUpdate
Handler signatureasync function(table_name, request_data, old_data, new_data, services)
Services injection{ db, logger, redis, kafka, cache } otomatis sebagai parameter terakhir
Return value{ success: true/false, message: '...' }
Sifat eventSemua blocking (seperti database trigger)
RollbackOtomatis jika handler return {success: false} atau throw exception
Lokasi file handlersrc/components/handlers/

Cara Kerja (How It Works)

Setiap operasi CRUD memiliki dua event hook yang dieksekusi di dalam transaction scope:

OperasiEvent SebelumEvent Sesudah
INSERTonBeforeInsertonAfterInsert
UPDATEonBeforeUpdateonAfterUpdate
DELETEonBeforeDeleteonAfterDelete

Seluruh event bersifat blocking dan berperilaku seperti database trigger. Jika handler mengembalikan {success: false} atau throw exception, transaction di-rollback secara keseluruhan.

Alur Eksekusi (Execution Flow)

Request masuk (POST /create, /update, /delete)


┌─────────────────────────────┐
│  BEGIN Transaction           │
└─────────────┬───────────────┘


┌─────────────────────────────┐
│  Fetch oldData (UPDATE/DEL) │
└─────────────┬───────────────┘


┌─────────────────────────────┐
│  onBefore handler           │ ← Jika {success: false} → ROLLBACK
└─────────────┬───────────────┘


┌─────────────────────────────┐
│  Operasi SQL utama          │ ← INSERT / UPDATE / DELETE
└─────────────┬───────────────┘


┌─────────────────────────────┐
│  Fetch newData              │ ← SELECT setelah operasi
└─────────────┬───────────────┘


┌─────────────────────────────┐
│  onAfter handler            │ ← Jika {success: false} → ROLLBACK
└─────────────┬───────────────┘


┌─────────────────────────────┐
│  COMMIT Transaction         │
└─────────────────────────────┘

Perbedaan onBefore dan onAfter

AspekonBeforeonAfter
EksekusiSebelum operasi SQLSetelah operasi SQL
newDataSelalu nullData hasil operasi (null untuk DELETE)
oldDataData existing untuk UPDATE/DELETE, null untuk INSERTSama dengan onBefore
Kasus penggunaanValidasi, pengecekan dependensiSinkronisasi tabel terkait, computed fields
Jika gagalTransaction di-rollbackTransaction di-rollback

Konfigurasi Payload (Payload Configuration)

Handler didaftarkan melalui property components di file payload JSON:

payload/stock_inbound.json
{
    "tableName": "stock_inbound",
    "primaryKey": "stock_inbound_id",
    "fieldName": [
        "stock_inbound_id",
        "inbound_number",
        "warehouse_id",
        "inbound_date",
        "status",
        "total_qty"
    ],
    "action": {
        "create": true,
        "update": true,
        "delete": true
    },
    "components": [
        {
            "properties": {
                "filename": "components/handlers/stock-inbound-handler.js",
                "methods": [
                    {
                        "name": "validateBeforeInsert",
                        "events": "onBeforeInsert",
                        "params": [
                            {"value": "{tableName}"},
                            {"value": "{requestData}"},
                            {"value": "{oldData}"},
                            {"value": "{newData}"}
                        ]
                    },
                    {
                        "name": "syncAfterUpdate",
                        "events": "onAfterUpdate",
                        "params": [
                            {"value": "{tableName}"},
                            {"value": "{requestData}"},
                            {"value": "{oldData}"},
                            {"value": "{newData}"}
                        ]
                    }
                ]
            }
        }
    ]
}

Properti Method

PropertiTipeKeterangan
namestringNama function yang di-export dari file handler
eventsstringNama event (onBeforeInsert, onAfterInsert, dll.)
paramsarrayArray template variable yang di-pass ke function

Template Variable

VariableTipeKeterangan
{tableName}stringNama tabel yang sedang dioperasikan
{requestData}objectData asli dari request body
{oldData}object/nullData existing sebelum operasi (null untuk INSERT)
{newData}object/nullData setelah operasi (null untuk onBefore dan DELETE)
{operation}stringTipe operasi: INSERT, UPDATE, DELETE
{user_id}stringUser ID dari request header
{timestamp}stringISO timestamp saat event dieksekusi
{record_id}string/nullID record yang dioperasikan

Signature Function

Setiap handler menerima 5 parameter. Parameter kelima (services) di-inject otomatis oleh component engine:

async function namaHandler(table_name, request_data, old_data, new_data, services) {
    const { db, logger } = services;

    // Custom logic

    return {
        success: true,
        message: 'Handler executed successfully'
    };
}

module.exports = { namaHandler };

Parameter

UrutanNamaTipeKeterangan
1table_namestringNama tabel (contoh: "stock_inbound")
2request_dataobjectData dari request body
3old_dataobject/nullData sebelum operasi (null untuk INSERT)
4new_dataobject/nullData setelah operasi (null untuk onBefore dan DELETE)
5servicesobjectInjected services: { db, logger, redis, kafka, cache }

Return Value

Handler wajib mengembalikan object dengan property success:

// Sukses — operasi dilanjutkan
return { success: true, message: 'Validation passed' };

// Gagal — transaction di-rollback
return { success: false, message: 'Inbound number sudah ada di sistem' };

Aturan Penulisan (Handler Rules)

  1. Function harus bersifat async (atau return Promise)
  2. Function harus di-export melalui module.exports
  3. Return value harus memiliki property success (boolean)
  4. Return {success: false} men-trigger rollback untuk semua event (onBefore maupun onAfter)
  5. Exception/throw di dalam handler juga men-trigger rollback

Data Flow per Operasi (Data Flow per Operation)

INSERT

ParameteronBeforeInsertonAfterInsert
table_name"stock_inbound""stock_inbound"
request_data{inbound_number: "INB-001", ...}{inbound_number: "INB-001", ...}
old_datanullnull
new_datanull{stock_inbound_id: "uuid...", inbound_number: "INB-001", ...}

UPDATE

ParameteronBeforeUpdateonAfterUpdate
table_name"stock_inbound""stock_inbound"
request_data{stock_inbound_id: "uuid...", status: "confirmed"}{stock_inbound_id: "uuid...", status: "confirmed"}
old_data{stock_inbound_id: "uuid...", status: "draft", ...}{stock_inbound_id: "uuid...", status: "draft", ...}
new_datanull{stock_inbound_id: "uuid...", status: "confirmed", ...}

DELETE

ParameteronBeforeDeleteonAfterDelete
table_name"stock_inbound""stock_inbound"
request_data{where: [{key: "stock_inbound_id", value: "uuid..."}]}{where: [{key: "stock_inbound_id", value: "uuid..."}]}
old_data{stock_inbound_id: "uuid...", inbound_number: "INB-001", ...}{stock_inbound_id: "uuid...", inbound_number: "INB-001", ...}
new_datanullnull

Contoh Penggunaan (Usage Examples)

Validasi sebelum INSERT

Handler berikut memvalidasi bahwa inbound_number tidak kosong dan belum ada di database sebelum INSERT dilakukan:

src/components/handlers/stock-inbound-handler.js
async function validateBeforeInsert(table_name, request_data, old_data, new_data, services) {
    const { db, logger } = services;
    logger.info({ table: table_name }, '[VALIDATE] Checking inbound number');

    if (!request_data.inbound_number || request_data.inbound_number.trim() === '') {
        return {
            success: false,
            message: 'Inbound number wajib diisi'
        };
    }

    const existing = await db.executeQuery(
        'SELECT stock_inbound_id FROM stock_inbound WHERE inbound_number = $1',
        [request_data.inbound_number]
    );

    if (existing.rows.length > 0) {
        return {
            success: false,
            message: `Inbound number ${request_data.inbound_number} sudah ada di sistem`
        };
    }

    return { success: true, message: 'Validation passed' };
}

module.exports = { validateBeforeInsert };

Konfigurasi payload untuk handler di atas:

payload/stock_inbound.json (bagian components)
{
    "components": [
        {
            "properties": {
                "filename": "components/handlers/stock-inbound-handler.js",
                "methods": [
                    {
                        "name": "validateBeforeInsert",
                        "events": "onBeforeInsert",
                        "params": [
                            {"value": "{tableName}"},
                            {"value": "{requestData}"},
                            {"value": "{oldData}"},
                            {"value": "{newData}"}
                        ]
                    }
                ]
            }
        }
    ]
}

Sinkronisasi Data setelah UPDATE

Handler berikut menghitung ulang total_qty di tabel stock_inbound berdasarkan data di stock_inbound_item setelah operasi UPDATE:

src/components/handlers/stock-sync-handler.js
async function syncTotalAfterUpdate(table_name, request_data, old_data, new_data, services) {
    const { db, logger } = services;

    if (!new_data || !new_data.stock_inbound_id) {
        return { success: true, message: 'No sync needed' };
    }

    try {
        const result = await db.executeQuery(
            'SELECT COALESCE(SUM(qty), 0) as total_qty FROM stock_inbound_item WHERE stock_inbound_id = $1',
            [new_data.stock_inbound_id]
        );

        await db.executeQuery(
            'UPDATE stock_inbound SET total_qty = $1, updated_at = NOW() WHERE stock_inbound_id = $2',
            [result.rows[0].total_qty, new_data.stock_inbound_id]
        );

        logger.info({ stockInboundId: new_data.stock_inbound_id }, '[SYNC] Total qty updated');
        return { success: true, message: 'Total quantity synced' };
    } catch (error) {
        logger.error({ error: error.message }, '[SYNC] Failed to sync total');
        return { success: false, message: `Sync failed: ${error.message}` };
    }
}

module.exports = { syncTotalAfterUpdate };

Validasi Dependensi sebelum DELETE

Handler berikut mencegah penghapusan stock_inbound yang masih memiliki item detail:

src/components/handlers/stock-inbound-handler.js
async function checkItemsBeforeDelete(table_name, request_data, old_data, new_data, services) {
    const { db, logger } = services;

    if (!old_data || !old_data.stock_inbound_id) {
        return { success: true };
    }

    const result = await db.executeQuery(
        'SELECT COUNT(*) as total FROM stock_inbound_item WHERE stock_inbound_id = $1',
        [old_data.stock_inbound_id]
    );

    const itemCount = parseInt(result.rows[0].total, 10);

    if (itemCount > 0) {
        return {
            success: false,
            message: `Stock inbound tidak dapat dihapus karena masih memiliki ${itemCount} item`
        };
    }

    logger.info({ table: table_name }, '[DEP-CHECK] No items found, delete allowed');
    return { success: true, message: 'Dependency check passed' };
}

module.exports = { checkItemsBeforeDelete };

Lokasi File Handler (Handler File Location)

File handler ditempatkan di folder src/components/handlers/ pada project deployment:

deployment/mini-inventory/
├── src/
│   ├── components/
│   │   └── handlers/
│   │       ├── stock-inbound-handler.js
│   │       ├── stock-sync-handler.js
│   │       └── audit-logger.js
│   └── modules/
│       └── ...
├── payload/
│   └── stock_inbound.json
└── config/
    └── *.env

Services yang Tersedia (Available Services)

Object services di-inject otomatis sebagai parameter terakhir handler:

ServiceKeyKeterangan
Databasedb{ executeQuery, executeTransaction, getPool }, auto-route berdasarkan DB_TYPE
Loggerloggerpino logger instance (fallback ke console)
RedisredisRedis client (null jika tidak dikonfigurasi)
KafkakafkaKafka service (null jika KAFKA_ENABLED bukan 'true')
CachecacheCache manager (null jika cache tidak dikonfigurasi)
async function myHandler(table_name, request_data, old_data, new_data, services) {
    const { db, logger, redis } = services;

    // Database query
    const result = await db.executeQuery(
        'SELECT * FROM product WHERE product_id = $1',
        [new_data.product_id]
    );

    // Structured logging
    logger.info({ table: table_name, event: 'handler_executed' }, 'Handler completed');

    // Redis (null-check diperlukan karena bersifat opsional)
    if (redis) {
        await redis.del(`stock:${new_data.stock_inbound_id}`);
    }

    return { success: true };
}

Handler yang tidak mendeklarasikan parameter services tetap berfungsi normal. JavaScript mengabaikan argument tambahan yang tidak ada dalam function signature.

Event Composite (Composite Events)

Component engine juga mendukung event hook untuk operasi composite (/create-composite, /update-composite):

OperasiEvent SebelumEvent Sesudah
Composite INSERTonBeforeCompositeInsertonAfterCompositeInsert
Composite UPDATEonBeforeCompositeUpdateonAfterCompositeUpdate

Alur Eksekusi Composite (Composite Execution Flow)

create-composite:

BEGIN
  onBeforeCompositeInsert(headerData, detailItems)        ← Blocking
  INSERT header
  INSERT detail[0], detail[1], ...
  onAfterCompositeInsert(headerData, insertedHeader, insertedItems)  ← Blocking
COMMIT

update-composite:

BEGIN
  Prefetch oldData
  onBeforeCompositeUpdate(headerData, oldData, detailOperations)    ← Blocking
  UPDATE header
  DELETE/UPDATE/INSERT detail items
  onAfterCompositeUpdate(headerData, oldData, updatedHeader, detailResults)  ← Blocking
COMMIT

Context Object

Event composite menerima context yang berbeda dari event single table. Berikut context untuk onAfterCompositeInsert sebagai contoh:

FieldTipeKeterangan
requestDataobjectHeader data asli dari request
oldDatanullINSERT tidak memiliki data lama
newDataobjectHeader yang sudah di-insert
insertedItemsarrayDetail items yang sudah di-insert
detailTablestringNama tabel detail (contoh: "stock_inbound_item")
foreignKeystringForeign key header-detail
tableNamestringNama tabel header (contoh: "stock_inbound")
operationstring'COMPOSITE_INSERT'

Untuk context onBeforeCompositeUpdate, field detailOperations berisi object {insert: [], update: [], delete: []} yang merepresentasikan perubahan detail yang akan dilakukan.

Event composite hanya berlaku di level header. Tidak ada hook per-item detail seperti onBeforeDetailInsert. Composite DELETE juga tidak tersedia karena tidak ada endpoint /delete-composite.

Contoh: Validasi Status sebelum Composite Update

payload/stock_inbound.json (bagian components)
{
    "components": [
        {
            "properties": {
                "filename": "components/handlers/stock-inbound-handler.js",
                "methods": [
                    {
                        "name": "validateBeforeCompositeUpdate",
                        "events": "onBeforeCompositeUpdate",
                        "params": [
                            {"value": "{tableName}"},
                            {"value": "{oldData}"},
                            {"value": "{detailOperations}"}
                        ]
                    }
                ]
            }
        }
    ]
}
src/components/handlers/stock-inbound-handler.js
async function validateBeforeCompositeUpdate(table_name, old_data, detail_operations) {
    if (old_data && old_data.status !== 'draft') {
        return {
            success: false,
            message: `Stock inbound tidak dapat diubah karena status sudah '${old_data.status}'`
        };
    }
    return { success: true };
}

module.exports = { validateBeforeCompositeUpdate };

Catatan Penting (Important Notes)

Kompatibilitas Database (Database Compatibility)

Component engine berjalan identik pada PostgreSQL, MySQL, dan Oracle. Normalisasi key (Oracle mengembalikan UPPERCASE keys) ditangani secara internal, sehingga handler selalu menerima lowercase keys di semua platform.

Multiple Handler

Satu event dapat memiliki lebih dari satu handler. Handler dieksekusi secara sequential sesuai urutan definisi di payload. Jika satu handler mengembalikan {success: false}, handler berikutnya tidak dieksekusi dan transaction di-rollback.

Backward Compatibility

Jika payload tidak memiliki property components, seluruh operasi CRUD berjalan normal tanpa event lifecycle. Tidak ada perubahan perilaku untuk module yang sudah ada.

Import/Export

Component engine tidak diterapkan pada operasi import dan export. Operasi tersebut menggunakan mekanisme terpisah (importConfig dan exportQuery di payload).

Langkah Selanjutnya (Next Steps)

  • Processor untuk membuat custom endpoint dengan business logic manual
  • CRUD Dasar untuk operasi create, update, dan delete standar
  • CRUD Composite untuk operasi header-detail dalam satu transaksi

On this page