|
|
@@ -0,0 +1,1991 @@
|
|
|
+Saya akan melakukan audit lengkap dan mendalam terhadap folder REST_MB_RA. Mari saya mulai dengan mengeksplorasi struktur folder dan menganalisis semua file.
|
|
|
+
|
|
|
+Sekarang saya akan menganalisis shared library ACE_DLIB yang digunakan untuk memahami fungsi-fungsi yang dipanggil:
|
|
|
+
|
|
|
+Sekarang saya memiliki gambaran lengkap tentang project REST_MB_RA. Mari saya buat audit komprehensif:
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+# 🎯 AUDIT LENGKAP: IBM ACE PROJECT - REST_MB_RA
|
|
|
+
|
|
|
+## EXECUTIVE SUMMARY
|
|
|
+
|
|
|
+```
|
|
|
+🎯 OPTIMASI SCORE: 52/100
|
|
|
+📈 KESEHATAN KODE: FAIR (Perlu Improvement Signifikan)
|
|
|
+⚠️ CRITICAL ISSUES: 18
|
|
|
+🔧 QUICK WINS: 8 fixes mudah dengan impact besar
|
|
|
+```
|
|
|
+
|
|
|
+**Ringkasan Temuan:**
|
|
|
+- Project memiliki struktur dasar yang baik dengan shared library usage
|
|
|
+- **CRITICAL**: Multiple performance anti-patterns ditemukan (PASSTHRU, string operations berlebihan, code duplication)
|
|
|
+- **CRITICAL**: Security vulnerabilities (SQL injection, hardcoded credentials)
|
|
|
+- **GOOD**: Sudah menggunakan REFERENCE variables di beberapa tempat
|
|
|
+- **MISSING**: Error handling tidak konsisten, TryCatch nodes tidak ada
|
|
|
+- **MISSING**: Tidak ada input validation yang proper
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. ⚠️ ESQL CODE QUALITY
|
|
|
+
|
|
|
+**Status: ❌ CRITICAL - Multiple Anti-patterns Ditemukan**
|
|
|
+
|
|
|
+### Issues Found:
|
|
|
+
|
|
|
+#### 1. ❌ CRITICAL: MAPPING_REQUEST.esql - PASSTHRU dengan SQL Injection Vulnerability
|
|
|
+
|
|
|
+**File:** `ACE_DLIB/ACE_LIB/GEN_ERROR.esql` (dipanggil dari MAPPING_REQUEST.esql)
|
|
|
+**Lines:** 11, 36
|
|
|
+
|
|
|
+**Problem:** PASSTHRU statement dengan string concatenation membuka SQL injection vulnerability dan performance overhead.
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Line 11 & 36 di GEN_ERROR.esql
|
|
|
+SET res.data[] = PASSTHRU('SELECT E.GROUP, E.ORIGINAL, E.TARGET FROM MWCONFIG.ERROR_MAP AS E WHERE E.GROUP = ''' || group || '''');
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Security Risk**: SQL Injection vulnerability (HIGH SEVERITY)
|
|
|
+- **Performance**: PASSTHRU tidak di-prepare, setiap call re-parse SQL statement
|
|
|
+- **Memory**: Result set tidak dibatasi, bisa memory leak pada large dataset
|
|
|
+
|
|
|
+**Resiko jika tidak diubah:**
|
|
|
+- Attacker bisa inject SQL via `group` parameter
|
|
|
+- Pada 1000 TPS dengan 100 error codes, database CPU usage bisa 60%+ karena re-parsing
|
|
|
+- Potential memory exhaustion jika ERROR_MAP table besar
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk)
|
|
|
+SET res.data[] = PASSTHRU('SELECT E.GROUP, E.ORIGINAL, E.TARGET FROM MWCONFIG.ERROR_MAP AS E WHERE E.GROUP = ''' || group || '''');
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal)
|
|
|
+-- Gunakan parameterized query dengan SELECT statement
|
|
|
+SET res.data[] = (SELECT E."GROUP", E.ORIGINAL, E.TARGET
|
|
|
+ FROM Database.MWCONFIG.ERROR_MAP AS E
|
|
|
+ WHERE E."GROUP" = group);
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Security + Performance)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 2. ❌ CRITICAL: MAPPING_REQUEST.esql - Massive Code Duplication (DRY Violation)
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/MAPPING_REQUEST.esql`
|
|
|
+**Lines:** 16-67
|
|
|
+
|
|
|
+**Problem:** Validation logic untuk 4 fields (ref, cif, hp, nid) di-copy-paste dengan struktur identik. 52 lines of duplicated code!
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Lines 16-27: Validation ref
|
|
|
+IF (imb.ref IS NULL) OR (LENGTH(imb.ref) < 1) OR (LENGTH(imb.ref) > 12) THEN
|
|
|
+ SET OutputRoot = InputRoot;
|
|
|
+ DECLARE om REFERENCE TO OutputRoot.JSON.Data;
|
|
|
+ DECLARE omb REFERENCE TO OutputRoot.JSON.Data.body;
|
|
|
+ DECLARE omd REFERENCE TO OutputRoot.JSON.Data.dlog;
|
|
|
+ SET omb.rsp = errCodeInvMsg;
|
|
|
+ CALL ACE_LIB.generateError(Group, omb.rsp, omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ SET omb.rspdesc = 'Invalid Kode Referensi';
|
|
|
+ SET omd.dir = 'O';
|
|
|
+ SET omd.timestamp = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+ SET om.brokerlog = timenode || brokerLabel || '.' || node || '(Invalid Kode Referensi)' || '&&&';
|
|
|
+ PROPAGATE TO TERMINAL 'out1';
|
|
|
+ RETURN FALSE;
|
|
|
+END IF;
|
|
|
+
|
|
|
+-- Lines 28-39: EXACT SAME PATTERN untuk cif
|
|
|
+-- Lines 40-51: EXACT SAME PATTERN untuk hp
|
|
|
+-- Lines 52-63: EXACT SAME PATTERN untuk nid
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Maintainability**: Perubahan logic harus dilakukan di 4 tempat
|
|
|
+- **Code Size**: 52 lines bisa dikurangi jadi ~20 lines
|
|
|
+- **Bug Risk**: Inconsistency antar validations (typo di line 63: "Kode AKses")
|
|
|
+
|
|
|
+**Resiko jika tidak diubah:**
|
|
|
+- Developer lupa update salah satu validation block
|
|
|
+- Testing overhead (harus test 4 code paths yang identik)
|
|
|
+- Code review complexity meningkat
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk): 52 lines duplicated code
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal): Reusable validation procedure
|
|
|
+CREATE PROCEDURE validateField(
|
|
|
+ IN fieldValue CHARACTER,
|
|
|
+ IN minLen INTEGER,
|
|
|
+ IN maxLen INTEGER,
|
|
|
+ IN fieldName CHARACTER,
|
|
|
+ IN errorMsg CHARACTER,
|
|
|
+ INOUT OutputRoot REFERENCE,
|
|
|
+ IN InputRoot REFERENCE,
|
|
|
+ IN Group CHARACTER,
|
|
|
+ IN errCodeInvMsg CHARACTER,
|
|
|
+ IN cacheExpiry INTEGER,
|
|
|
+ INOUT OutputLocalEnvironment REFERENCE,
|
|
|
+ IN timenode CHARACTER,
|
|
|
+ IN brokerLabel CHARACTER,
|
|
|
+ IN node CHARACTER
|
|
|
+) RETURNS BOOLEAN
|
|
|
+BEGIN
|
|
|
+ IF (fieldValue IS NULL) OR (LENGTH(fieldValue) < minLen) OR (LENGTH(fieldValue) > maxLen) THEN
|
|
|
+ SET OutputRoot = InputRoot;
|
|
|
+ DECLARE om REFERENCE TO OutputRoot.JSON.Data;
|
|
|
+ DECLARE omb REFERENCE TO OutputRoot.JSON.Data.body;
|
|
|
+ DECLARE omd REFERENCE TO OutputRoot.JSON.Data.dlog;
|
|
|
+ SET omb.rsp = errCodeInvMsg;
|
|
|
+ CALL ACE_LIB.generateError(Group, omb.rsp, omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ SET omb.rspdesc = errorMsg;
|
|
|
+ SET omd.dir = 'O';
|
|
|
+ SET omd.timestamp = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+ SET om.brokerlog = timenode || brokerLabel || '.' || node || '(' || errorMsg || ')' || '&&&';
|
|
|
+ PROPAGATE TO TERMINAL 'out1';
|
|
|
+ RETURN FALSE;
|
|
|
+ END IF;
|
|
|
+ RETURN TRUE;
|
|
|
+END;
|
|
|
+
|
|
|
+-- Usage di Main():
|
|
|
+IF NOT validateField(imb.ref, 1, 12, 'ref', 'Invalid Kode Referensi',
|
|
|
+ OutputRoot, InputRoot, Group, errCodeInvMsg, cacheExpiry,
|
|
|
+ OutputLocalEnvironment, timenode, brokerLabel, node) THEN
|
|
|
+ RETURN FALSE;
|
|
|
+END IF;
|
|
|
+
|
|
|
+IF NOT validateField(imb.cif, 7, 19, 'cif', 'Invalid Nomor CIF',
|
|
|
+ OutputRoot, InputRoot, Group, errCodeInvMsg, cacheExpiry,
|
|
|
+ OutputLocalEnvironment, timenode, brokerLabel, node) THEN
|
|
|
+ RETURN FALSE;
|
|
|
+END IF;
|
|
|
+
|
|
|
+-- Repeat untuk hp dan nid
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Maintainability + Code Quality)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3. ❌ CRITICAL: REQUEST_IN.esql - Nested SUBSTRING Operations
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/REQUEST_IN.esql`
|
|
|
+**Lines:** 27, 31
|
|
|
+
|
|
|
+**Problem:** Multiple nested SUBSTRING operations yang expensive dan sulit dibaca.
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Line 27
|
|
|
+SET omd.serverIp = SUBSTRING(SUBSTRING(InputLocalEnvironment.REST.Input.URI AFTER '//') BEFORE '/');
|
|
|
+
|
|
|
+-- Line 31
|
|
|
+SET omd.correlId = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'MMddHHmmss') || '.' || InputRoot.JSON.Data.ref || ' ';
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Performance**: Nested SUBSTRING creates intermediate strings (memory allocation overhead)
|
|
|
+- **Readability**: Sulit di-debug dan maintain
|
|
|
+- **CPU**: String manipulation di hot path (setiap request)
|
|
|
+
|
|
|
+**Resiko jika tidak diubah:**
|
|
|
+- Pada 5000 TPS, string operations bisa consume 15-20% CPU
|
|
|
+- Memory churn dari intermediate string allocations
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk)
|
|
|
+SET omd.serverIp = SUBSTRING(SUBSTRING(InputLocalEnvironment.REST.Input.URI AFTER '//') BEFORE '/');
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal)
|
|
|
+-- Extract ke variable intermediate untuk clarity dan potential reuse
|
|
|
+DECLARE fullUri CHARACTER InputLocalEnvironment.REST.Input.URI;
|
|
|
+DECLARE uriWithoutProtocol CHARACTER SUBSTRING(fullUri AFTER '//');
|
|
|
+SET omd.serverIp = SUBSTRING(uriWithoutProtocol BEFORE '/');
|
|
|
+
|
|
|
+-- ATAU lebih baik lagi, extract ke shared function di ACE_LIB:
|
|
|
+CREATE FUNCTION extractServerIp(IN uri CHARACTER) RETURNS CHARACTER
|
|
|
+BEGIN
|
|
|
+ DECLARE uriWithoutProtocol CHARACTER SUBSTRING(uri AFTER '//');
|
|
|
+ RETURN SUBSTRING(uriWithoutProtocol BEFORE '/');
|
|
|
+END;
|
|
|
+
|
|
|
+-- Usage:
|
|
|
+SET omd.serverIp = ACE_LIB.extractServerIp(InputLocalEnvironment.REST.Input.URI);
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM (Performance optimization)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 4. ⚠️ WARNING: RESPONSE_IN.esql - Repetitive CAST Operations
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/RESPONSE_IN.esql`
|
|
|
+**Lines:** 9, 42, 62
|
|
|
+
|
|
|
+**Problem:** CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT ...) dipanggil multiple kali dengan format berbeda.
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Line 9
|
|
|
+SET InputLocalEnvironment.brokerlog = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY/MM/dd HH:mm:ss:SSS ') || ...
|
|
|
+
|
|
|
+-- Line 42
|
|
|
+SET omd.timestamp = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Inconsistency**: Dua format timestamp berbeda ('YYYY/MM/dd' vs 'YYYY-MM-dd')
|
|
|
+- **Performance**: CAST operation repeated (minor overhead)
|
|
|
+- **Maintainability**: Format changes harus dilakukan di multiple places
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk)
|
|
|
+SET InputLocalEnvironment.brokerlog = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY/MM/dd HH:mm:ss:SSS ') || ...
|
|
|
+SET omd.timestamp = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal)
|
|
|
+-- Declare once, reuse
|
|
|
+DECLARE currentTime TIMESTAMP CURRENT_TIMESTAMP;
|
|
|
+DECLARE logTimestamp CHARACTER CAST(currentTime AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+DECLARE brokerTimestamp CHARACTER CAST(currentTime AS CHARACTER FORMAT 'YYYY/MM/dd HH:mm:ss:SSS ');
|
|
|
+
|
|
|
+SET InputLocalEnvironment.brokerlog = brokerTimestamp || ...
|
|
|
+SET omd.timestamp = logTimestamp;
|
|
|
+
|
|
|
+-- BETTER: Standardize format di shared library
|
|
|
+-- ACE_LIB.esql:
|
|
|
+CREATE FUNCTION getStandardTimestamp() RETURNS CHARACTER
|
|
|
+BEGIN
|
|
|
+ RETURN CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+END;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟢 LOW (Standardization)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 5. ❌ CRITICAL: MAPPING_REQUEST.esql - KeyList Validation Loop Inefficiency
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/MAPPING_REQUEST.esql`
|
|
|
+**Lines:** 68-84
|
|
|
+
|
|
|
+**Problem:** Manual string parsing loop untuk API key validation. Inefficient dan error-prone.
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Lines 68-84
|
|
|
+DECLARE keyStatus CHARACTER 'no';
|
|
|
+DECLARE apiKey CHARACTER COALESCE(keyList);
|
|
|
+
|
|
|
+KeyListLoop:
|
|
|
+WHILE (apiKey <> '') DO
|
|
|
+ DECLARE token CHARACTER SUBSTRING(apiKey BEFORE ';');
|
|
|
+
|
|
|
+ IF token = InputRoot.HTTPInputHeader.Key THEN
|
|
|
+ SET keyStatus = 'yes';
|
|
|
+ LEAVE KeyListLoop;
|
|
|
+ ELSEIF apiKey <> '' AND (token = '' OR token = NULL) AND apiKey = InputRoot.HTTPInputHeader.Key THEN
|
|
|
+ SET keyStatus = 'yes';
|
|
|
+ LEAVE KeyListLoop;
|
|
|
+ END IF;
|
|
|
+
|
|
|
+ SET apiKey = SUBSTRING(apiKey AFTER ';');
|
|
|
+END WHILE;
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Performance**: O(n) string operations di hot path untuk setiap request
|
|
|
+- **Security**: Timing attack vulnerability (early exit reveals valid keys)
|
|
|
+- **Complexity**: Nested IF conditions sulit di-maintain
|
|
|
+
|
|
|
+**Resiko jika tidak diubah:**
|
|
|
+- Pada keyList dengan 100 keys, worst case 100 SUBSTRING operations per request
|
|
|
+- Timing attack bisa reveal valid API keys
|
|
|
+- Bug di edge cases (empty tokens, trailing semicolons)
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk): Manual loop dengan string operations
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal): Cache keys di shared memory dengan hash lookup
|
|
|
+-- Di ACE_LIB/COMMON.esql:
|
|
|
+DECLARE cacheApiKeys SHARED ROW;
|
|
|
+
|
|
|
+CREATE PROCEDURE loadApiKeys(IN keyList CHARACTER, IN cacheExpiry INTEGER)
|
|
|
+BEGIN
|
|
|
+ APIKEY_LOCK: BEGIN ATOMIC
|
|
|
+ DECLARE cacheRef REFERENCE TO cacheApiKeys.keys;
|
|
|
+
|
|
|
+ IF cacheRef.__EXPIRY IS NULL OR cacheRef.__EXPIRY < CURRENT_TIMESTAMP THEN
|
|
|
+ -- Parse once, store as hash map
|
|
|
+ DECLARE apiKey CHARACTER keyList;
|
|
|
+ WHILE (apiKey <> '') DO
|
|
|
+ DECLARE token CHARACTER SUBSTRING(apiKey BEFORE ';');
|
|
|
+ IF token <> '' THEN
|
|
|
+ SET cacheApiKeys.keys.{token} = TRUE;
|
|
|
+ END IF;
|
|
|
+ SET apiKey = SUBSTRING(apiKey AFTER ';');
|
|
|
+ END WHILE;
|
|
|
+ SET cacheRef.__EXPIRY = CURRENT_TIMESTAMP + CAST(cacheExpiry AS INTERVAL SECOND);
|
|
|
+ END IF;
|
|
|
+ END APIKEY_LOCK;
|
|
|
+END;
|
|
|
+
|
|
|
+CREATE FUNCTION isValidApiKey(IN key CHARACTER, IN keyList CHARACTER, IN cacheExpiry INTEGER) RETURNS BOOLEAN
|
|
|
+BEGIN
|
|
|
+ CALL loadApiKeys(keyList, cacheExpiry);
|
|
|
+ RETURN (cacheApiKeys.keys.{key} IS NOT NULL);
|
|
|
+END;
|
|
|
+
|
|
|
+-- Usage di MAPPING_REQUEST.esql:
|
|
|
+IF NOT ACE_LIB.isValidApiKey(InputRoot.HTTPInputHeader.Key, keyList, cacheExpiry) THEN
|
|
|
+ -- Handle invalid key
|
|
|
+ SET OutputRoot = InputRoot;
|
|
|
+ DECLARE om REFERENCE TO OutputRoot.JSON.Data;
|
|
|
+ DECLARE omb REFERENCE TO OutputRoot.JSON.Data.body;
|
|
|
+ DECLARE omd REFERENCE TO OutputRoot.JSON.Data.dlog;
|
|
|
+ SET omb.rsp = errCodeInvKey;
|
|
|
+ CALL ACE_LIB.generateError(Group, omb.rsp, omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ SET omb.rspdesc = 'Not Authorize';
|
|
|
+ SET omd.dir = 'O';
|
|
|
+ SET omd.timestamp = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+ SET om.brokerlog = timenode || brokerLabel || '.' || node || '(Invalid Key)' || '&&&';
|
|
|
+ PROPAGATE TO TERMINAL 'out1';
|
|
|
+ RETURN FALSE;
|
|
|
+END IF;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Security + Performance)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 6. ⚠️ WARNING: RESPONSE_IN.esql - Excessive REPLACE Calls
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/RESPONSE_IN.esql`
|
|
|
+**Lines:** 35, 37, 43, 45
|
|
|
+
|
|
|
+**Problem:** REPLACE function dipanggil 4 kali untuk same operation (removing spaces).
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Lines 35-46
|
|
|
+IF im.header.HDRRSP = '00' THEN
|
|
|
+ IF im.header.HDRERR = '' THEN
|
|
|
+ CALL ACE_LIB.generateError(Group, '00', omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ CALL ACE_LIB.generateErrorDesc(Group, '00', omb.rspdesc, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ ELSE
|
|
|
+ CALL ACE_LIB.generateError(Group, REPLACE(im.header.HDRERR, ' ', ''), omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ CALL ACE_LIB.generateErrorDesc(Group, REPLACE(im.header.HDRERR, ' ', ''), omb.rspdesc, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ END IF;
|
|
|
+ELSE
|
|
|
+ IF im.header.HDRERR = '' THEN
|
|
|
+ CALL ACE_LIB.generateError(Group, errCoreNoRC, omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ CALL ACE_LIB.generateErrorDesc(Group, errCoreNoRC, omb.rspdesc, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ ELSE
|
|
|
+ CALL ACE_LIB.generateError(Group, REPLACE(im.header.HDRERR, ' ', ''), omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ CALL ACE_LIB.generateErrorDesc(Group, REPLACE(im.header.HDRERR, ' ', ''), omb.rspdesc, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ END IF;
|
|
|
+END IF;
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Performance**: REPLACE called 4 times dengan same input
|
|
|
+- **Readability**: Nested IF-ELSE sulit dibaca
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk): 4x REPLACE calls
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal)
|
|
|
+-- Extract error code once
|
|
|
+DECLARE errorCode CHARACTER;
|
|
|
+IF im.header.HDRRSP = '00' THEN
|
|
|
+ IF im.header.HDRERR = '' THEN
|
|
|
+ SET errorCode = '00';
|
|
|
+ ELSE
|
|
|
+ SET errorCode = REPLACE(im.header.HDRERR, ' ', '');
|
|
|
+ END IF;
|
|
|
+ELSE
|
|
|
+ IF im.header.HDRERR = '' THEN
|
|
|
+ SET errorCode = errCoreNoRC;
|
|
|
+ ELSE
|
|
|
+ SET errorCode = REPLACE(im.header.HDRERR, ' ', '');
|
|
|
+ END IF;
|
|
|
+END IF;
|
|
|
+
|
|
|
+CALL ACE_LIB.generateError(Group, errorCode, omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+CALL ACE_LIB.generateErrorDesc(Group, errorCode, omb.rspdesc, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+
|
|
|
+-- Handle special case
|
|
|
+IF omb.rsp = '99' THEN
|
|
|
+ CALL ACE_LIB.generateError(Group, errCodeNotFound, omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ CALL ACE_LIB.generateErrorDesc(Group, errCodeNotFound, omb.rspdesc, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+END IF;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM (Performance)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 7. ✅ GOOD: Proper REFERENCE Variable Usage
|
|
|
+
|
|
|
+**Files:** REQUEST_IN.esql, RESPONSE_IN.esql, MAPPING_REQUEST.esql
|
|
|
+
|
|
|
+**Strengths:**
|
|
|
+```esql
|
|
|
+-- REQUEST_IN.esql lines 18-19
|
|
|
+DECLARE om REFERENCE TO OutputRoot.JSON.Data;
|
|
|
+DECLARE omd REFERENCE TO om.dlog;
|
|
|
+
|
|
|
+-- RESPONSE_IN.esql lines 20-23
|
|
|
+DECLARE im REFERENCE TO InputRoot.JSON.Data;
|
|
|
+DECLARE imb REFERENCE TO im.body;
|
|
|
+DECLARE om REFERENCE TO OutputRoot.JSON.Data;
|
|
|
+DECLARE omb REFERENCE TO om.body;
|
|
|
+```
|
|
|
+
|
|
|
+**Good Practice:** Project sudah menggunakan REFERENCE variables untuk tree navigation, menghindari expensive path traversal.
|
|
|
+
|
|
|
+**Recommendation:** Maintain this pattern consistently across all new code.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 🏗️ MESSAGE FLOW DESIGN PATTERNS
|
|
|
+
|
|
|
+**Status: ⚠️ NEEDS IMPROVEMENT - Separation of Concerns Issues**
|
|
|
+
|
|
|
+### Issues Found:
|
|
|
+
|
|
|
+#### 1. ⚠️ WARNING: postLogin.subflow - Complex Flow Without Proper Breakdown
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/postLogin.subflow`
|
|
|
+**Problem:** Subflow memiliki 16 nodes dengan mixed concerns (validation, transformation, logging, MQ operations).
|
|
|
+
|
|
|
+**Current Structure:**
|
|
|
+```
|
|
|
+Input → REQUEST_IN (validation + logging setup)
|
|
|
+ → LOG_DB_REQUEST (database logging)
|
|
|
+ → MAPPING_REQUEST (validation + transformation + error handling)
|
|
|
+ → REQUEST_OUT (MQ output)
|
|
|
+ → REPLY_IN (MQ input)
|
|
|
+ → RESPONSE_IN (transformation + error mapping)
|
|
|
+ → LOG_DB_RESPONSE (database logging)
|
|
|
+ → RESPONSE_OUT (final formatting)
|
|
|
+ → Output
|
|
|
+```
|
|
|
+
|
|
|
+**Issues:**
|
|
|
+- Validation logic mixed dengan transformation (MAPPING_REQUEST)
|
|
|
+- Error handling scattered across multiple nodes
|
|
|
+- No clear separation antara business logic dan infrastructure concerns
|
|
|
+
|
|
|
+**Resiko:**
|
|
|
+- Sulit untuk test individual components
|
|
|
+- Reusability rendah (tidak bisa reuse validation logic di flow lain)
|
|
|
+- Debugging complexity tinggi
|
|
|
+
|
|
|
+**Recommended Refactoring:**
|
|
|
+```
|
|
|
+Pecah menjadi logical subflows:
|
|
|
+
|
|
|
+1. validateRequest.subflow
|
|
|
+ - Input validation (ref, cif, hp, nid)
|
|
|
+ - API key validation
|
|
|
+ - Return: validated data atau error response
|
|
|
+
|
|
|
+2. transformRequest.subflow
|
|
|
+ - Map REST request ke Core MQ format
|
|
|
+ - Set headers dan metadata
|
|
|
+ - Pure transformation, no validation
|
|
|
+
|
|
|
+3. handleCoreResponse.subflow
|
|
|
+ - Parse Core response
|
|
|
+ - Error code mapping
|
|
|
+ - Transform ke REST response format
|
|
|
+
|
|
|
+4. logTransaction.subflow (reusable)
|
|
|
+ - Database logging
|
|
|
+ - Correlation ID tracking
|
|
|
+ - Reusable across multiple flows
|
|
|
+```
|
|
|
+
|
|
|
+**Benefits:**
|
|
|
+- Each subflow < 5 nodes (easy to understand)
|
|
|
+- Testable in isolation
|
|
|
+- Reusable across different APIs
|
|
|
+- Clear separation of concerns
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM (Architecture improvement)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 2. ⚠️ WARNING: Missing Reusable Error Handler Subflow
|
|
|
+
|
|
|
+**Problem:** Error handling logic duplicated di multiple Compute nodes:
|
|
|
+- REQUEST_IN_FAILURE
|
|
|
+- REQUEST_OUT_FAILURE
|
|
|
+- RESPONSE_OUT_FAILURE
|
|
|
+- REPLY_IN_FAILURE
|
|
|
+- REPLY_IN_CATCH
|
|
|
+- MAPPING_REQUEST_FAILURE
|
|
|
+
|
|
|
+**Current Pattern:**
|
|
|
+```xml
|
|
|
+<!-- Each failure terminal connected ke separate LOG_WARN node -->
|
|
|
+<nodes xmi:type="ComIbmCompute.msgnode:FCMComposite_1"
|
|
|
+ computeExpression="esql://routine/ACE_LIB#LOG_WARN.Main">
|
|
|
+ <translation xmi:type="utility:ConstantString" string="REQUEST_IN_FAILURE"/>
|
|
|
+</nodes>
|
|
|
+```
|
|
|
+
|
|
|
+**Recommendation:**
|
|
|
+```
|
|
|
+Create: ACE_DLIB/COMMON/errorHandler.subflow
|
|
|
+
|
|
|
+Input:
|
|
|
+- Error context (node name, error type)
|
|
|
+- Original message
|
|
|
+- Exception list
|
|
|
+
|
|
|
+Processing:
|
|
|
+1. Extract exception details
|
|
|
+2. Format error response
|
|
|
+3. Log to database
|
|
|
+4. Set HTTP error status
|
|
|
+5. Return standardized error response
|
|
|
+
|
|
|
+Output:
|
|
|
+- Formatted error response ready untuk HTTP Reply
|
|
|
+
|
|
|
+Usage:
|
|
|
+All failure terminals → errorHandler.subflow → HTTP Reply
|
|
|
+```
|
|
|
+
|
|
|
+**Benefits:**
|
|
|
+- Consistent error response format
|
|
|
+- Centralized error logging
|
|
|
+- Easier to add error monitoring/alerting
|
|
|
+- Reduced code duplication
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM (Standardization)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 🛡️ ERROR HANDLING & RESILIENCE
|
|
|
+
|
|
|
+**Status: ❌ CRITICAL - Missing Critical Error Handling Patterns**
|
|
|
+
|
|
|
+### Issues Found:
|
|
|
+
|
|
|
+#### 1. ❌ CRITICAL: No TryCatch Nodes Around External Calls
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/postLogin.subflow`
|
|
|
+**Problem:** MQ operations (REQUEST_OUT, REPLY_IN) tidak wrapped dengan TryCatch nodes.
|
|
|
+
|
|
|
+**Current Flow:**
|
|
|
+```
|
|
|
+MAPPING_REQUEST → REQUEST_OUT (MQOutput) → REQUEST_OUT_DONE
|
|
|
+ ↓ failure
|
|
|
+ REQUEST_OUT_FAILURE
|
|
|
+```
|
|
|
+
|
|
|
+**Issues:**
|
|
|
+- Unexpected exceptions (network timeout, MQ connection loss) tidak ter-handle
|
|
|
+- No retry logic untuk transient failures
|
|
|
+- Catch terminal di REPLY_IN tidak connected ke proper handler
|
|
|
+
|
|
|
+**Resiko jika tidak diubah:**
|
|
|
+- MQ connection timeout bisa crash flow
|
|
|
+- Transient network errors tidak di-retry
|
|
|
+- No graceful degradation
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```xml
|
|
|
+<!-- AFTER: Wrap dengan TryCatch -->
|
|
|
+<nodes xmi:type="ComIbmTryCatch.msgnode:FCMComposite_1" location="600,380">
|
|
|
+ <translation xmi:type="utility:ConstantString" string="TryCatch_MQ"/>
|
|
|
+</nodes>
|
|
|
+
|
|
|
+Flow:
|
|
|
+MAPPING_REQUEST → TryCatch → REQUEST_OUT → REQUEST_OUT_DONE
|
|
|
+ ↓ catch
|
|
|
+ MQ_ERROR_HANDLER (retry logic, circuit breaker)
|
|
|
+```
|
|
|
+
|
|
|
+**Retry Logic di MQ_ERROR_HANDLER:**
|
|
|
+```esql
|
|
|
+CREATE COMPUTE MODULE MQ_ERROR_HANDLER
|
|
|
+ DECLARE maxRetries EXTERNAL INTEGER 3;
|
|
|
+ DECLARE retryDelay EXTERNAL INTEGER 1000; -- milliseconds
|
|
|
+
|
|
|
+ CREATE FUNCTION Main() RETURNS BOOLEAN
|
|
|
+ BEGIN
|
|
|
+ DECLARE retryCount INTEGER COALESCE(Environment.RetryCount, 0);
|
|
|
+
|
|
|
+ IF retryCount < maxRetries THEN
|
|
|
+ -- Increment retry counter
|
|
|
+ SET Environment.RetryCount = retryCount + 1;
|
|
|
+
|
|
|
+ -- Log retry attempt
|
|
|
+ CALL ACE_LIB.logDebug('MQ_RETRY', InputLocalEnvironment, InputExceptionList, InputRoot);
|
|
|
+
|
|
|
+ -- Wait before retry (using SLEEP or timer)
|
|
|
+ -- SLEEP(retryDelay); -- If available in your ACE version
|
|
|
+
|
|
|
+ -- Retry: propagate back to MQ output
|
|
|
+ PROPAGATE TO TERMINAL 'out' MESSAGE InputRoot;
|
|
|
+ ELSE
|
|
|
+ -- Max retries exceeded, return error to client
|
|
|
+ CALL ACE_LIB.logError('MQ_MAX_RETRY', InputLocalEnvironment, InputExceptionList, InputRoot);
|
|
|
+
|
|
|
+ -- Format error response
|
|
|
+ CREATE LASTCHILD OF OutputRoot DOMAIN 'JSON' NAME 'JSON';
|
|
|
+ SET OutputRoot.JSON.Data.body.rsp = '96'; -- System error
|
|
|
+ SET OutputRoot.JSON.Data.body.rspdesc = 'Service temporarily unavailable';
|
|
|
+
|
|
|
+ PROPAGATE TO TERMINAL 'out1'; -- To error handler
|
|
|
+ END IF;
|
|
|
+
|
|
|
+ RETURN FALSE;
|
|
|
+ END;
|
|
|
+END MODULE;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Resilience)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 2. ❌ CRITICAL: Missing Timeout Handling
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/postLogin.subflow`
|
|
|
+**Problem:** No timeout configuration untuk MQ request-reply pattern.
|
|
|
+
|
|
|
+**Current:** MQOutput node tidak set timeout, bisa hang indefinitely.
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```xml
|
|
|
+<!-- Add timeout di MQOutput node properties -->
|
|
|
+<nodes xmi:type="ComIbmMQOutput.msgnode:FCMComposite_1"
|
|
|
+ queueName="BSN_MQ_OUT"
|
|
|
+ replyToQ="BSN_MQ_REPLY"
|
|
|
+ timeout="30000"> <!-- 30 seconds timeout -->
|
|
|
+</nodes>
|
|
|
+
|
|
|
+<!-- Add TimeoutControl node -->
|
|
|
+<nodes xmi:type="ComIbmTimeoutControl.msgnode:FCMComposite_1"
|
|
|
+ location="700,380"
|
|
|
+ timeoutInterval="30">
|
|
|
+ <translation xmi:type="utility:ConstantString" string="TimeoutControl"/>
|
|
|
+</nodes>
|
|
|
+
|
|
|
+Flow:
|
|
|
+REQUEST_OUT → TimeoutControl → REPLY_IN
|
|
|
+ ↓ timeout
|
|
|
+ TIMEOUT_HANDLER (return timeout error)
|
|
|
+```
|
|
|
+
|
|
|
+**TIMEOUT_HANDLER ESQL:**
|
|
|
+```esql
|
|
|
+CREATE COMPUTE MODULE TIMEOUT_HANDLER
|
|
|
+ CREATE FUNCTION Main() RETURNS BOOLEAN
|
|
|
+ BEGIN
|
|
|
+ CALL ACE_LIB.logError('MQ_TIMEOUT', InputLocalEnvironment, InputExceptionList, InputRoot);
|
|
|
+
|
|
|
+ -- Return timeout error to client
|
|
|
+ SET OutputRoot.HTTPResponseHeader."Content-Type" = 'application/json';
|
|
|
+ CREATE LASTCHILD OF OutputRoot DOMAIN 'JSON' NAME 'JSON';
|
|
|
+ SET OutputRoot.JSON.Data.body.rsp = '68'; -- Timeout error code
|
|
|
+ SET OutputRoot.JSON.Data.body.rspdesc = 'Request timeout';
|
|
|
+ SET OutputRoot.JSON.Data.body.ref = InputRoot.JSON.Data.OriginalMsg.JSON.Data.body.ref;
|
|
|
+
|
|
|
+ -- Set dlog for audit
|
|
|
+ SET OutputRoot.JSON.Data.dlog = InputRoot.JSON.Data.dlog;
|
|
|
+ SET OutputRoot.JSON.Data.dlog.dir = 'O';
|
|
|
+ SET OutputRoot.JSON.Data.dlog.timestamp = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+
|
|
|
+ RETURN TRUE;
|
|
|
+ END;
|
|
|
+END MODULE;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Resilience)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3. ⚠️ WARNING: Inconsistent Error Response Format
|
|
|
+
|
|
|
+**Problem:** Error responses tidak konsisten format-nya.
|
|
|
+
|
|
|
+**MAPPING_REQUEST.esql validation errors:**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "body": {
|
|
|
+ "rsp": "mdw_invaliddata",
|
|
|
+ "rspdesc": "Invalid Kode Referensi"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**RESPONSE_IN.esql errors:**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "body": {
|
|
|
+ "rsp": "00",
|
|
|
+ "rspdesc": "Success",
|
|
|
+ "ref": "...",
|
|
|
+ "hp": "...",
|
|
|
+ "cardnumber": "",
|
|
|
+ "cardstatus": "...",
|
|
|
+ ...
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Issue:** Validation errors return minimal fields, success returns full fields. Client harus handle different response structures.
|
|
|
+
|
|
|
+**Recommendation:**
|
|
|
+```json
|
|
|
+// Standardized error response format
|
|
|
+{
|
|
|
+ "body": {
|
|
|
+ "rsp": "error_code",
|
|
|
+ "rspdesc": "Error description",
|
|
|
+ "ref": "original_ref_if_available",
|
|
|
+ "timestamp": "2026-04-28T10:30:00.000Z"
|
|
|
+ },
|
|
|
+ "meta": {
|
|
|
+ "correlId": "...",
|
|
|
+ "source": "mb-7803"
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Success response always includes same base fields
|
|
|
+{
|
|
|
+ "body": {
|
|
|
+ "rsp": "00",
|
|
|
+ "rspdesc": "Success",
|
|
|
+ "ref": "...",
|
|
|
+ "hp": "...",
|
|
|
+ "cardnumber": "...",
|
|
|
+ "cardstatus": "...",
|
|
|
+ "carddesc": "...",
|
|
|
+ "cardexpired": "..."
|
|
|
+ },
|
|
|
+ "meta": {
|
|
|
+ "correlId": "...",
|
|
|
+ "source": "mb-7803",
|
|
|
+ "timestamp": "2026-04-28T10:30:00.000Z"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM (API consistency)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. ⚡ PERFORMANCE OPTIMIZATION (Lanjutan)
|
|
|
+
|
|
|
+**Status: ⚠️ NEEDS IMPROVEMENT**
|
|
|
+
|
|
|
+#### 1. ❌ CRITICAL: Database Calls Setiap Request di generateError
|
|
|
+
|
|
|
+**File:** `ACE_DLIB/ACE_LIB/GEN_ERROR.esql`
|
|
|
+**Lines:** 11, 36
|
|
|
+
|
|
|
+**Problem:** Meskipun ada caching, `generateError` dan `generateErrorDesc` melakukan **2 query terpisah** ke tabel yang sama (`MWCONFIG.ERROR_MAP`) dengan data yang sama, hanya berbeda kolom yang diambil.
|
|
|
+
|
|
|
+```esql
|
|
|
+-- generateError: SELECT TARGET
|
|
|
+SET res.data[] = PASSTHRU('SELECT E.GROUP, E.ORIGINAL, E.TARGET FROM MWCONFIG.ERROR_MAP ...');
|
|
|
+
|
|
|
+-- generateErrorDesc: SELECT DESCRIPTION (query SAMA, tabel SAMA)
|
|
|
+SET res.data[] = PASSTHRU('SELECT E.GROUP, E.ORIGINAL, E.DESCRIPTION FROM MWCONFIG.ERROR_MAP ...');
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Performance**: 2x database round-trip per cache miss, padahal bisa 1x
|
|
|
+- **Memory**: 2 cache structures (`cacheErrorMap` + `cacheErrorDesc`) untuk data dari tabel yang sama
|
|
|
+- **Consistency Risk**: Cache expiry bisa berbeda antara keduanya
|
|
|
+
|
|
|
+**Resiko jika tidak diubah:**
|
|
|
+- Pada cold start atau cache expiry, setiap request trigger 2 DB queries
|
|
|
+- Jika 100 concurrent requests hit cache miss bersamaan → 200 DB queries sekaligus
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk): 2 separate queries, 2 separate caches
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal): Single query, single cache, both TARGET + DESCRIPTION
|
|
|
+CREATE PROCEDURE loadErrorMap(IN group CHARACTER, IN cacheExpiry INTEGER, IN ref REFERENCE)
|
|
|
+BEGIN
|
|
|
+ ERROR_MAP_LOCK: BEGIN ATOMIC
|
|
|
+ DECLARE cacheErrRef REFERENCE TO cacheErrorMap.ERRORMAP.{group};
|
|
|
+
|
|
|
+ IF cacheErrRef.__EXPIRY IS NULL OR cacheErrRef.__EXPIRY < CURRENT_TIMESTAMP THEN
|
|
|
+ -- Single query untuk ambil SEMUA data sekaligus
|
|
|
+ DECLARE res ROW;
|
|
|
+ SET res.data[] = (SELECT E."GROUP", E.ORIGINAL, E.TARGET, E.DESCRIPTION
|
|
|
+ FROM Database.MWCONFIG.ERROR_MAP AS E
|
|
|
+ WHERE E."GROUP" = group);
|
|
|
+
|
|
|
+ DECLARE rowRef REFERENCE TO res.data[1];
|
|
|
+ WHILE LASTMOVE(rowRef) DO
|
|
|
+ -- Store both TARGET dan DESCRIPTION dalam satu cache entry
|
|
|
+ SET cacheErrorMap.ERRORMAP.{rowRef."GROUP"}.{rowRef.ORIGINAL}.target = rowRef.TARGET;
|
|
|
+ SET cacheErrorMap.ERRORMAP.{rowRef."GROUP"}.{rowRef.ORIGINAL}.desc = rowRef.DESCRIPTION;
|
|
|
+ MOVE rowRef NEXTSIBLING;
|
|
|
+ END WHILE;
|
|
|
+ SET cacheErrRef.__EXPIRY = CURRENT_TIMESTAMP + CAST(cacheExpiry AS INTERVAL SECOND);
|
|
|
+ END IF;
|
|
|
+ END ERROR_MAP_LOCK;
|
|
|
+END;
|
|
|
+
|
|
|
+CREATE PROCEDURE generateError(IN group CHARACTER, IN original CHARACTER,
|
|
|
+ IN OutputMessage REFERENCE, IN ref REFERENCE, IN cacheExpiry INTEGER)
|
|
|
+BEGIN
|
|
|
+ CALL loadErrorMap(group, cacheExpiry, ref);
|
|
|
+ DECLARE em CHARACTER cacheErrorMap.ERRORMAP.{group}.{original}.target;
|
|
|
+ SET OutputMessage = COALESCE(em, '99');
|
|
|
+END;
|
|
|
+
|
|
|
+CREATE PROCEDURE generateErrorDesc(IN group CHARACTER, IN original CHARACTER,
|
|
|
+ IN OutputMessage REFERENCE, IN ref REFERENCE, IN cacheExpiry INTEGER)
|
|
|
+BEGIN
|
|
|
+ CALL loadErrorMap(group, cacheExpiry, ref);
|
|
|
+ DECLARE em CHARACTER cacheErrorMap.ERRORMAP.{group}.{original}.desc;
|
|
|
+ SET OutputMessage = COALESCE(em, 'No Description');
|
|
|
+END;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Performance - 50% DB query reduction)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 2. ❌ CRITICAL: OriginalMsg Disimpan di Message Tree (Memory Overhead)
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/MAPPING_REQUEST.esql`
|
|
|
+**Lines:** 103-105
|
|
|
+
|
|
|
+**Problem:** Seluruh original request message di-copy ke output tree DAN ke `OriginalMsg` field.
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Line 103-105
|
|
|
+SET om.OriginalMsg = InputRoot;
|
|
|
+SET om.RequestIdentifier = BASE64ENCODE(InputLocalEnvironment.Destination.HTTP.RequestIdentifier);
|
|
|
+SET om.OriginalMsg.RequestIdentifier = BASE64ENCODE(InputLocalEnvironment.Destination.HTTP.RequestIdentifier);
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Memory**: Message tree di-copy 2x (om.OriginalMsg + om.OriginalMsg.RequestIdentifier redundant)
|
|
|
+- **Serialization**: Saat message dikirim ke MQ, seluruh OriginalMsg ikut ter-serialize
|
|
|
+- **MQ Overhead**: Message size membengkak, MQ throughput turun
|
|
|
+
|
|
|
+**Resiko jika tidak diubah:**
|
|
|
+- Pada payload 10KB, message ke MQ bisa jadi 20KB+ karena duplikasi
|
|
|
+- MQ queue depth meningkat lebih cepat
|
|
|
+- Memory usage per thread meningkat signifikan
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk)
|
|
|
+SET om.OriginalMsg = InputRoot;
|
|
|
+SET om.RequestIdentifier = BASE64ENCODE(InputLocalEnvironment.Destination.HTTP.RequestIdentifier);
|
|
|
+SET om.OriginalMsg.RequestIdentifier = BASE64ENCODE(InputLocalEnvironment.Destination.HTTP.RequestIdentifier);
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal): Simpan hanya data yang BENAR-BENAR dibutuhkan di response path
|
|
|
+-- Identifikasi apa yang dipakai di RESPONSE_IN.esql:
|
|
|
+-- omd.clientIp = InputRoot.JSON.Data.OriginalMsg.JSON.Data.dlog.clientIp
|
|
|
+-- omd.serverIp = InputRoot.JSON.Data.OriginalMsg.JSON.Data.dlog.serverIp
|
|
|
+-- omd.code = InputRoot.JSON.Data.OriginalMsg.JSON.Data.dlog.code
|
|
|
+-- omd.correlId = InputRoot.JSON.Data.OriginalMsg.JSON.Data.dlog.correlId
|
|
|
+-- omd.reqtimestamp = InputRoot.JSON.Data.OriginalMsg.JSON.Data.dlog.reqtimestamp
|
|
|
+
|
|
|
+-- Jadi hanya simpan dlog, bukan seluruh InputRoot!
|
|
|
+SET om.OriginalDlog = InputRoot.JSON.Data.dlog; -- Hanya dlog, bukan full message
|
|
|
+SET om.RequestIdentifier = BASE64ENCODE(InputLocalEnvironment.Destination.HTTP.RequestIdentifier);
|
|
|
+
|
|
|
+-- Update RESPONSE_IN.esql untuk pakai path baru:
|
|
|
+-- SET omd.clientIp = InputRoot.JSON.Data.OriginalDlog.clientIp;
|
|
|
+-- SET omd.correlId = InputRoot.JSON.Data.OriginalDlog.correlId;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Memory + MQ throughput)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3. ⚠️ WARNING: Hardcoded IP Address di MAPPING_REQUEST.esql
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/MAPPING_REQUEST.esql`
|
|
|
+**Line:** 97
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Line 97
|
|
|
+SET om.metadata.IPClient = '172.18.30.203';
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Configuration**: Hardcoded IP tidak bisa berubah tanpa redeploy
|
|
|
+- **Environment**: IP berbeda di dev/staging/prod
|
|
|
+- **Security**: Internal IP exposed di code
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk)
|
|
|
+SET om.metadata.IPClient = '172.18.30.203';
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal): Gunakan EXTERNAL variable
|
|
|
+DECLARE coreClientIp EXTERNAL CHARACTER;
|
|
|
+
|
|
|
+-- Di subflow properties:
|
|
|
+-- defaultValueLiteral="172.18.30.203"
|
|
|
+
|
|
|
+-- Usage:
|
|
|
+SET om.metadata.IPClient = coreClientIp;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Configuration management)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 4. ⚠️ WARNING: Hardcoded Channel Name
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/MAPPING_REQUEST.esql`
|
|
|
+**Line:** 98
|
|
|
+
|
|
|
+```esql
|
|
|
+SET om.metadata.Channel = 'NewMb';
|
|
|
+```
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+DECLARE coreChannel EXTERNAL CHARACTER;
|
|
|
+-- defaultValueLiteral="NewMb"
|
|
|
+SET om.metadata.Channel = coreChannel;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 5. ⚠️ WARNING: Hardcoded ResponseBodyLength
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/MAPPING_REQUEST.esql`
|
|
|
+**Line:** 95
|
|
|
+
|
|
|
+```esql
|
|
|
+SET om.metadata.ResponseBodyLength = '162';
|
|
|
+-- Commented out: --SET om.header.ResponseBodyLength = '162';
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- Magic number tanpa penjelasan
|
|
|
+- Jika Core response format berubah, harus cari di code
|
|
|
+- Commented-out code menunjukkan ada kebingungan tentang field placement
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk)
|
|
|
+SET om.metadata.ResponseBodyLength = '162';
|
|
|
+--SET om.header.ResponseBodyLength = '162';
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal)
|
|
|
+DECLARE coreResponseBodyLength EXTERNAL CHARACTER;
|
|
|
+-- defaultValueLiteral="162"
|
|
|
+-- description: "Expected response body length from Core system for service 428101"
|
|
|
+SET om.metadata.ResponseBodyLength = coreResponseBodyLength;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. ⚙️ CONFIGURATION MANAGEMENT
|
|
|
+
|
|
|
+**Status: ❌ CRITICAL - Multiple Hardcoded Values**
|
|
|
+
|
|
|
+### Summary of Hardcoded Values Found:
|
|
|
+
|
|
|
+| File | Line | Value | Type |
|
|
|
+|------|------|-------|------|
|
|
|
+| MAPPING_REQUEST.esql | 91 | `'CORE_MQ_REPLY.MB'` | Queue name |
|
|
|
+| MAPPING_REQUEST.esql | 95 | `'162'` | Magic number |
|
|
|
+| MAPPING_REQUEST.esql | 96 | `'NONFIN'` | Core type |
|
|
|
+| MAPPING_REQUEST.esql | 97 | `'172.18.30.203'` | IP Address |
|
|
|
+| MAPPING_REQUEST.esql | 98 | `'NewMb'` | Channel name |
|
|
|
+| MAPPING_REQUEST.esql | 100 | `'428101'` | Service code |
|
|
|
+| postLogin.subflow | property | `'aaa;bbb;ccc;ddd;eee'` | API Keys (default!) |
|
|
|
+
|
|
|
+#### 1. ❌ CRITICAL: Default API Keys Exposed di Subflow Properties
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/postLogin.subflow`
|
|
|
+**Line:** Property definition
|
|
|
+
|
|
|
+```xml
|
|
|
+<eStructuralFeatures xmi:type="ecore:EAttribute" xmi:id="Property.keyList"
|
|
|
+ name="keyList" defaultValueLiteral="aaa;bbb;ccc;ddd;eee">
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Security**: Default keys `aaa;bbb;ccc;ddd;eee` sangat lemah dan predictable
|
|
|
+- **Risk**: Jika production deployment lupa override, API terbuka dengan weak keys
|
|
|
+- **Audit**: Keys visible di source code (version control exposure)
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```xml
|
|
|
+<!-- BEFORE (❌ Buruk) -->
|
|
|
+<eStructuralFeatures defaultValueLiteral="aaa;bbb;ccc;ddd;eee">
|
|
|
+
|
|
|
+<!-- AFTER (✅ Optimal) -->
|
|
|
+<!-- 1. Default value kosong - force explicit configuration -->
|
|
|
+<eStructuralFeatures defaultValueLiteral="">
|
|
|
+
|
|
|
+<!-- 2. Tambahkan startup validation di REQUEST_IN.esql -->
|
|
|
+```
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Di REQUEST_IN.esql, tambahkan startup check:
|
|
|
+CREATE FUNCTION Main() RETURNS BOOLEAN
|
|
|
+BEGIN
|
|
|
+ -- Validate critical configuration
|
|
|
+ IF keyList IS NULL OR LENGTH(TRIM(keyList)) = 0 THEN
|
|
|
+ THROW USER EXCEPTION MESSAGE 2951 VALUES('FATAL: keyList not configured. Deployment aborted.');
|
|
|
+ END IF;
|
|
|
+ -- ... rest of logic
|
|
|
+END;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Security)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 2. ⚠️ WARNING: Queue Name Hardcoded di ESQL
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/MAPPING_REQUEST.esql`
|
|
|
+**Line:** 91
|
|
|
+
|
|
|
+```esql
|
|
|
+SET OutputRoot.MQMD.ReplyToQ = 'CORE_MQ_REPLY.MB';
|
|
|
+```
|
|
|
+
|
|
|
+**Issue:** Queue name sudah ada sebagai subflow property (`queueReply`), tapi di ESQL di-hardcode lagi.
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk)
|
|
|
+SET OutputRoot.MQMD.ReplyToQ = 'CORE_MQ_REPLY.MB';
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal): Gunakan EXTERNAL variable yang sudah ada
|
|
|
+DECLARE queueReply EXTERNAL CHARACTER;
|
|
|
+-- Ini sudah ada di subflow properties! Tinggal pakai:
|
|
|
+SET OutputRoot.MQMD.ReplyToQ = queueReply;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Configuration consistency)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 🔒 SECURITY & CREDENTIALS
|
|
|
+
|
|
|
+**Status: ❌ CRITICAL - Multiple Security Issues**
|
|
|
+
|
|
|
+### Issues Found:
|
|
|
+
|
|
|
+#### 1. ❌ CRITICAL: SQL Injection via PASSTHRU
|
|
|
+
|
|
|
+*(Sudah dibahas di Kategori 1 dan 4 - lihat GEN_ERROR.esql)*
|
|
|
+
|
|
|
+#### 2. ❌ CRITICAL: Sensitive Data di Debug Logs
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/REQUEST_IN.esql`, `MAPPING_REQUEST.esql`, `RESPONSE_IN.esql`
|
|
|
+
|
|
|
+**Problem:** `ACE_LIB.logDebug` dipanggil dengan full `InputRoot` dan `OutputRoot` yang berisi:
|
|
|
+- `imb.nid` (Nomor ID / KTP)
|
|
|
+- `imb.cif` (Customer ID)
|
|
|
+- `imb.hp` (Nomor HP)
|
|
|
+- `InputRoot.HTTPInputHeader.Key` (API Key)
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Semua file memanggil ini:
|
|
|
+CALL ACE_LIB.logDebug('Input', InputLocalEnvironment, InputExceptionList, InputRoot);
|
|
|
+CALL ACE_LIB.logDebug('Output', OutputLocalEnvironment, OutputExceptionList, OutputRoot);
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **PII Exposure**: Nomor HP, CIF, NIK ter-log di plaintext
|
|
|
+- **Security**: API Key ter-log (bisa digunakan untuk unauthorized access)
|
|
|
+- **Compliance**: Melanggar PDP (Perlindungan Data Pribadi) Indonesia
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk): Log full message termasuk sensitive data
|
|
|
+CALL ACE_LIB.logDebug('Input', InputLocalEnvironment, InputExceptionList, InputRoot);
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal): Mask sensitive fields sebelum logging
|
|
|
+CREATE PROCEDURE logDebugMasked(IN id CHARACTER, IN LocalEnvironment REFERENCE,
|
|
|
+ IN ExceptionList REFERENCE, IN Root REFERENCE)
|
|
|
+BEGIN
|
|
|
+ -- Create sanitized copy untuk logging
|
|
|
+ DECLARE sanitized ROW;
|
|
|
+ SET sanitized = Root;
|
|
|
+
|
|
|
+ -- Mask sensitive fields
|
|
|
+ IF sanitized.JSON.Data.body.hp IS NOT NULL THEN
|
|
|
+ DECLARE hp CHARACTER sanitized.JSON.Data.body.hp;
|
|
|
+ SET sanitized.JSON.Data.body.hp = SUBSTRING(hp FROM 1 FOR 3) || '****' || SUBSTRING(hp FROM LENGTH(hp) - 2);
|
|
|
+ END IF;
|
|
|
+ IF sanitized.JSON.Data.body.nid IS NOT NULL THEN
|
|
|
+ SET sanitized.JSON.Data.body.nid = '****MASKED****';
|
|
|
+ END IF;
|
|
|
+ IF sanitized.JSON.Data.body.cif IS NOT NULL THEN
|
|
|
+ DECLARE cif CHARACTER sanitized.JSON.Data.body.cif;
|
|
|
+ SET sanitized.JSON.Data.body.cif = SUBSTRING(cif FROM 1 FOR 3) || '****';
|
|
|
+ END IF;
|
|
|
+ -- Mask API key dari header
|
|
|
+ IF sanitized.HTTPInputHeader.Key IS NOT NULL THEN
|
|
|
+ SET sanitized.HTTPInputHeader.Key = '****MASKED****';
|
|
|
+ END IF;
|
|
|
+
|
|
|
+ CALL ACE_LIB.logDebug(id, LocalEnvironment, ExceptionList, sanitized);
|
|
|
+END;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Security + Compliance)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3. ⚠️ WARNING: No Input Sanitization untuk Nomor HP
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/MAPPING_REQUEST.esql`
|
|
|
+**Lines:** 40-51
|
|
|
+
|
|
|
+**Problem:** Validasi hanya cek length, tidak cek format/karakter.
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Hanya cek length, tidak cek apakah berisi angka
|
|
|
+ELSEIF (imb.hp IS NULL) OR (LENGTH(imb.hp) < 1) OR (LENGTH(imb.hp) > 15) THEN
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- Nomor HP bisa berisi karakter special, script injection, dll
|
|
|
+- Downstream system (Core) mungkin tidak handle invalid characters
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- AFTER (✅ Optimal): Tambahkan format validation
|
|
|
+-- Cek apakah hp hanya berisi digit
|
|
|
+IF NOT (imb.hp LIKE '%[^0-9]%' = FALSE) THEN
|
|
|
+ -- Contains non-numeric characters
|
|
|
+ -- Handle error
|
|
|
+END IF;
|
|
|
+
|
|
|
+-- ATAU gunakan Java procedure untuk regex validation:
|
|
|
+CREATE PROCEDURE isNumeric(IN value CHARACTER) RETURNS BOOLEAN
|
|
|
+ LANGUAGE JAVA EXTERNAL NAME "id.co.hanoman.Util.isNumeric";
|
|
|
+
|
|
|
+IF NOT ACE_LIB.isNumeric(imb.hp) THEN
|
|
|
+ -- Handle invalid format
|
|
|
+END IF;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM (Input validation)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 🧩 MODULARITY & REUSABILITY
|
|
|
+
|
|
|
+**Status: ⚠️ NEEDS IMPROVEMENT**
|
|
|
+
|
|
|
+### Issues Found:
|
|
|
+
|
|
|
+#### 1. ❌ CRITICAL: Typo di Error Message
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/MAPPING_REQUEST.esql`
|
|
|
+**Line:** 63
|
|
|
+
|
|
|
+```esql
|
|
|
+-- Line 63: "AKses" seharusnya "Akses"
|
|
|
+SET om.brokerlog = timenode || brokerLabel || '.' || node || '(Invalid Kode AKses)' || '&&&';
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:** Inconsistency di log messages, sulit untuk grep/filter logs.
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk)
|
|
|
+'(Invalid Kode AKses)'
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal)
|
|
|
+'(Invalid Kode Akses)'
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟢 LOW (Quick win)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 2. ⚠️ WARNING: Timestamp Format Inconsistency
|
|
|
+
|
|
|
+**Problem:** Dua format timestamp berbeda digunakan di project yang sama:
|
|
|
+
|
|
|
+| File | Format | Usage |
|
|
|
+|------|--------|-------|
|
|
|
+| REQUEST_IN.esql line 9 | `'YYYY/MM/dd HH:mm:ss:SSS '` | brokerlog |
|
|
|
+| MAPPING_REQUEST.esql line 12 | `'YYYY/MM/dd HH:mm:ss:SSS '` | brokerlog |
|
|
|
+| MAPPING_REQUEST.esql line 30 | `'YYYY-MM-dd HH:mm:ss.SSS'` | omd.timestamp |
|
|
|
+| RESPONSE_IN.esql line 9 | `'YYYY/MM/dd HH:mm:ss:SSS '` | brokerlog |
|
|
|
+| RESPONSE_IN.esql line 42 | `'YYYY-MM-dd HH:mm:ss.SSS'` | omd.timestamp |
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- Standardize di ACE_LIB/COMMON.esql:
|
|
|
+CREATE FUNCTION getLogTimestamp() RETURNS CHARACTER
|
|
|
+BEGIN
|
|
|
+ RETURN CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+END;
|
|
|
+
|
|
|
+CREATE FUNCTION getBrokerTimestamp() RETURNS CHARACTER
|
|
|
+BEGIN
|
|
|
+ RETURN CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY/MM/dd HH:mm:ss:SSS ');
|
|
|
+END;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟢 LOW (Standardization)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3. ⚠️ WARNING: Commented-Out Code
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/MAPPING_REQUEST.esql`
|
|
|
+**Line:** 93
|
|
|
+
|
|
|
+```esql
|
|
|
+--SET om.header.ResponseBodyLength = '162';
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:** Dead code menambah confusion. Tidak jelas apakah ini intentional atau lupa dihapus.
|
|
|
+
|
|
|
+**Fix:** Hapus commented-out code, atau tambahkan comment yang jelas:
|
|
|
+```esql
|
|
|
+-- ResponseBodyLength dipindah ke metadata (bukan header) per Core spec v2.1
|
|
|
+-- SET om.header.ResponseBodyLength = '162';
|
|
|
+SET om.metadata.ResponseBodyLength = coreResponseBodyLength;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟢 LOW (Code cleanliness)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 📊 MONITORING & OBSERVABILITY
|
|
|
+
|
|
|
+**Status: ⚠️ NEEDS IMPROVEMENT**
|
|
|
+
|
|
|
+### Issues Found:
|
|
|
+
|
|
|
+#### 1. ⚠️ WARNING: correlId Generation Tidak Unique Enough
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/REQUEST_IN.esql`
|
|
|
+**Line:** 31
|
|
|
+
|
|
|
+```esql
|
|
|
+SET omd.correlId = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'MMddHHmmss') || '.' || InputRoot.JSON.Data.ref || ' ';
|
|
|
+```
|
|
|
+
|
|
|
+**Problem:**
|
|
|
+- Format `MMddHHmmss` hanya 10 karakter, resolusi detik
|
|
|
+- Jika 2 request dengan `ref` sama masuk dalam detik yang sama → **duplicate correlId**
|
|
|
+- Trailing space `' '` di akhir correlId tidak perlu dan bisa menyebabkan comparison issues
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- Log correlation bisa salah di high-throughput scenarios
|
|
|
+- Debugging menjadi sulit karena correlId tidak unique
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- BEFORE (❌ Buruk): Tidak unique, ada trailing space
|
|
|
+SET omd.correlId = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'MMddHHmmss') || '.' || InputRoot.JSON.Data.ref || ' ';
|
|
|
+
|
|
|
+-- AFTER (✅ Optimal): UUID-based atau high-resolution timestamp
|
|
|
+-- Option 1: Tambahkan milliseconds + random suffix
|
|
|
+SET omd.correlId = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'yyyyMMddHHmmssSSS')
|
|
|
+ || '.' || COALESCE(InputRoot.JSON.Data.ref, 'NOREF');
|
|
|
+
|
|
|
+-- Option 2: Gunakan Java UUID generator (lebih robust)
|
|
|
+-- Di ACE_LIB/COMMON.esql:
|
|
|
+CREATE PROCEDURE generateUUID() RETURNS CHARACTER
|
|
|
+ LANGUAGE JAVA EXTERNAL NAME "id.co.hanoman.Util.generateUUID";
|
|
|
+
|
|
|
+-- Usage:
|
|
|
+SET omd.correlId = ACE_LIB.generateUUID();
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM (Observability)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 2. ⚠️ WARNING: Tidak Ada Response Time Tracking
|
|
|
+
|
|
|
+**Problem:** Tidak ada pengukuran response time end-to-end.
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- Di REQUEST_IN.esql, simpan start time:
|
|
|
+SET omd.reqtimestamp = CURRENT_TIMESTAMP; -- Sudah ada, tapi tipe TIMESTAMP bukan CHARACTER
|
|
|
+
|
|
|
+-- Di RESPONSE_IN.esql, hitung elapsed time:
|
|
|
+DECLARE startTime TIMESTAMP InputRoot.JSON.Data.OriginalDlog.reqtimestamp;
|
|
|
+DECLARE elapsedMs INTEGER CAST(CURRENT_TIMESTAMP - startTime AS INTEGER SECOND) * 1000;
|
|
|
+SET omd.elapsedMs = elapsedMs;
|
|
|
+
|
|
|
+-- Log warning jika response time > threshold
|
|
|
+IF elapsedMs > 5000 THEN
|
|
|
+ CALL ACE_LIB.logWarning('SLOW_RESPONSE', OutputLocalEnvironment, OutputExceptionList, OutputRoot);
|
|
|
+END IF;
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM (Observability)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3. ⚠️ WARNING: brokerlog Format Tidak Konsisten
|
|
|
+
|
|
|
+**Problem:** `brokerlog` field diset di setiap module dengan pattern yang sama tapi tidak di-standardize.
|
|
|
+
|
|
|
+```esql
|
|
|
+-- REQUEST_IN.esql line 9
|
|
|
+SET InputLocalEnvironment.brokerlog = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY/MM/dd HH:mm:ss:SSS ') || brokerLabel || '.' || node || '&&&';
|
|
|
+
|
|
|
+-- MAPPING_REQUEST.esql line 12
|
|
|
+DECLARE timenode CHARACTER CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY/MM/dd HH:mm:ss:SSS ');
|
|
|
+-- ... dipakai di banyak tempat sebagai: timenode || brokerLabel || '.' || node || '(context)' || '&&&'
|
|
|
+```
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```esql
|
|
|
+-- Standardize di ACE_LIB:
|
|
|
+CREATE FUNCTION buildBrokerLog(IN context CHARACTER) RETURNS CHARACTER
|
|
|
+BEGIN
|
|
|
+ DECLARE node CHARACTER MessageFlowLabel || '.' || NodeLabel;
|
|
|
+ DECLARE brokerLabel CHARACTER BrokerName || '.' || ExecutionGroupLabel;
|
|
|
+ DECLARE ts CHARACTER CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY/MM/dd HH:mm:ss:SSS ');
|
|
|
+ IF context IS NOT NULL AND context <> '' THEN
|
|
|
+ RETURN ts || brokerLabel || '.' || node || '(' || context || ')' || '&&&';
|
|
|
+ ELSE
|
|
|
+ RETURN ts || brokerLabel || '.' || node || '&&&';
|
|
|
+ END IF;
|
|
|
+END;
|
|
|
+
|
|
|
+-- Usage:
|
|
|
+SET om.brokerlog = ACE_LIB.buildBrokerLog('');
|
|
|
+SET om.brokerlog = ACE_LIB.buildBrokerLog('Invalid Kode Referensi');
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟢 LOW (Standardization)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 🚀 STARTUP TIME & RESOURCE EFFICIENCY
|
|
|
+
|
|
|
+**Status: ⚠️ NEEDS IMPROVEMENT**
|
|
|
+
|
|
|
+### Issues Found:
|
|
|
+
|
|
|
+#### 1. ⚠️ WARNING: HTTPS Disabled
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/restapi.descriptor`
|
|
|
+**Line:** `https="false"`
|
|
|
+
|
|
|
+```xml
|
|
|
+<ns2:restapiDescriptor ... https="false" ...>
|
|
|
+```
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- **Security**: Traffic tidak terenkripsi
|
|
|
+- **Compliance**: Untuk banking application, HTTPS wajib
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```xml
|
|
|
+<!-- AFTER (✅ Optimal) -->
|
|
|
+<ns2:restapiDescriptor ... https="true" ...>
|
|
|
+<!-- Pastikan SSL certificate sudah dikonfigurasi di ACE server -->
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🔴 HIGH (Security)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 2. ⚠️ WARNING: Swagger Spec Tidak Lengkap
|
|
|
+
|
|
|
+**File:** `REST_MB_RA/swagger.json`
|
|
|
+
|
|
|
+**Problems:**
|
|
|
+- Request body schema hanya `"type": "string"` — tidak ada field definitions
|
|
|
+- Tidak ada response schema definition
|
|
|
+- Tidak ada error response definitions (400, 401, 500)
|
|
|
+- `info.title` = "REST_MQ_RA" tapi project name = "REST_MB_RA" (inconsistency!)
|
|
|
+
|
|
|
+```json
|
|
|
+// BEFORE (❌ Buruk)
|
|
|
+"parameters": [{
|
|
|
+ "name": "body",
|
|
|
+ "in": "body",
|
|
|
+ "schema": { "type": "string" }
|
|
|
+}]
|
|
|
+
|
|
|
+// AFTER (✅ Optimal)
|
|
|
+"parameters": [{
|
|
|
+ "name": "body",
|
|
|
+ "in": "body",
|
|
|
+ "required": true,
|
|
|
+ "schema": {
|
|
|
+ "type": "object",
|
|
|
+ "required": ["ref", "cif", "hp", "nid"],
|
|
|
+ "properties": {
|
|
|
+ "ref": { "type": "string", "minLength": 1, "maxLength": 12, "description": "Kode Referensi" },
|
|
|
+ "cif": { "type": "string", "minLength": 7, "maxLength": 19, "description": "Nomor CIF" },
|
|
|
+ "hp": { "type": "string", "minLength": 1, "maxLength": 15, "description": "Nomor HP" },
|
|
|
+ "nid": { "type": "string", "minLength": 1, "maxLength": 16, "description": "Kode Akses" }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}],
|
|
|
+"responses": {
|
|
|
+ "200": {
|
|
|
+ "description": "Success",
|
|
|
+ "schema": {
|
|
|
+ "type": "object",
|
|
|
+ "properties": {
|
|
|
+ "rsp": { "type": "string" },
|
|
|
+ "rspdesc": { "type": "string" },
|
|
|
+ "ref": { "type": "string" },
|
|
|
+ "hp": { "type": "string" },
|
|
|
+ "cardnumber": { "type": "string" },
|
|
|
+ "cardstatus": { "type": "string" },
|
|
|
+ "carddesc": { "type": "string" },
|
|
|
+ "cardexpired": { "type": "string" }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "400": { "description": "Invalid request parameters" },
|
|
|
+ "401": { "description": "Unauthorized - Invalid API Key" },
|
|
|
+ "500": { "description": "Internal server error" }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM (API documentation + validation)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+#### 3. ⚠️ WARNING: `ibmint optimize server` Belum Dijalankan
|
|
|
+
|
|
|
+**Problem:** Tidak ada `server.components.yaml` di project, menandakan `ibmint optimize server` belum pernah dijalankan.
|
|
|
+
|
|
|
+**Impact:**
|
|
|
+- ACE server load semua components (NodeJS, GlobalCache, dll) meskipun tidak dipakai
|
|
|
+- Startup time lebih lama
|
|
|
+- Memory footprint lebih besar
|
|
|
+
|
|
|
+**Fix:**
|
|
|
+```bash
|
|
|
+# Jalankan di ACE environment:
|
|
|
+ibmint optimize server --work-directory /path/to/work/dir \
|
|
|
+ --output-file server.components.yaml
|
|
|
+
|
|
|
+# Hasilnya akan disable unused components seperti:
|
|
|
+# - NodeJS runtime (jika tidak dipakai)
|
|
|
+# - GlobalCache (jika tidak dipakai)
|
|
|
+# - DFDL parser (jika tidak dipakai)
|
|
|
+```
|
|
|
+
|
|
|
+**Priority:** 🟡 MEDIUM (Resource efficiency)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📊 CODE METRICS (Lanjutan)
|
|
|
+
|
|
|
+```
|
|
|
+REFERENCE Variable Usage:
|
|
|
+- REQUEST_IN.esql: 80% ✅ (om, omd dipakai konsisten)
|
|
|
+- MAPPING_REQUEST.esql: 55% ⚠️ (im, imb, om, omb, omd ada, tapi
|
|
|
+ banyak re-declare di setiap IF block)
|
|
|
+- RESPONSE_IN.esql: 75% ✅ (im, imb, om, omb, omd dipakai)
|
|
|
+- RESPONSE_OUT.esql: 60% ⚠️ (minimal code, acceptable)
|
|
|
+- Overall: 68% ⚠️ (Target: >80%)
|
|
|
+
|
|
|
+Hardcoded Values: 7 instances ❌ (Target: 0)
|
|
|
+Security Issues: 4 instances ❌ (Target: 0)
|
|
|
+Code Duplication: ~52 lines ❌ (validation block 4x copy-paste)
|
|
|
+Missing Error Handlers: 2 terminals ❌ (MAPPING_REQUEST out1 tidak ke error handler)
|
|
|
+Dead Code: 1 instance ⚠️ (commented-out line 93)
|
|
|
+Typos di code: 1 instance ⚠️ (AKses → Akses)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📄 PER-FILE DETAILED REVIEW
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 📄 REQUEST_IN.esql
|
|
|
+
|
|
|
+```
|
|
|
+✅ Strengths:
|
|
|
+- REFERENCE variables dipakai dengan baik (om, omd)
|
|
|
+- logDebug dipanggil di Input dan Output
|
|
|
+- EXTERNAL variables untuk source, msgType, keyList
|
|
|
+- Struktur module bersih dan singkat (33 lines)
|
|
|
+
|
|
|
+❌ Issues:
|
|
|
+1. Line 9: CAST timestamp di-compute tapi disimpan ke LocalEnvironment,
|
|
|
+ bukan di-reuse. Bisa jadi shared function.
|
|
|
+2. Line 27: Nested SUBSTRING untuk extract serverIp — buat intermediate variable
|
|
|
+3. Line 31: correlId format tidak unique enough (resolusi detik, ada trailing space)
|
|
|
+4. Line 22: SET OutputRoot.JSON.Data.body = InputRoot.JSON.Data
|
|
|
+ — ini copy seluruh body, termasuk field yang mungkin tidak perlu
|
|
|
+5. Missing: Tidak ada validasi bahwa InputRoot.JSON.Data ada (null check)
|
|
|
+```
|
|
|
+
|
|
|
+**Recommended refactoring REQUEST_IN.esql:**
|
|
|
+```esql
|
|
|
+CREATE COMPUTE MODULE "REQUEST_IN"
|
|
|
+ DECLARE keyList EXTERNAL CHARACTER;
|
|
|
+ DECLARE source EXTERNAL CHARACTER;
|
|
|
+ DECLARE msgType EXTERNAL CHARACTER;
|
|
|
+
|
|
|
+ CREATE FUNCTION Main() RETURNS BOOLEAN
|
|
|
+ BEGIN
|
|
|
+ -- Guard: pastikan JSON body ada
|
|
|
+ IF InputRoot.JSON.Data IS NULL THEN
|
|
|
+ THROW USER EXCEPTION MESSAGE 2951 VALUES('Empty or invalid JSON body');
|
|
|
+ END IF;
|
|
|
+
|
|
|
+ DECLARE node CHARACTER MessageFlowLabel || '.' || NodeLabel;
|
|
|
+ DECLARE brokerLabel CHARACTER BrokerName || '.' || ExecutionGroupLabel;
|
|
|
+ DECLARE currentTs TIMESTAMP CURRENT_TIMESTAMP;
|
|
|
+
|
|
|
+ SET InputLocalEnvironment.brokerlog =
|
|
|
+ CAST(currentTs AS CHARACTER FORMAT 'YYYY/MM/dd HH:mm:ss:SSS ')
|
|
|
+ || brokerLabel || '.' || node || '&&&';
|
|
|
+
|
|
|
+ CALL ACE_LIB.logDebug('Input', InputLocalEnvironment, InputExceptionList, InputRoot);
|
|
|
+
|
|
|
+ -- Copy headers
|
|
|
+ CREATE LASTCHILD OF OutputRoot DOMAIN 'HTTPInputHeader' NAME 'HTTPInputHeader';
|
|
|
+ SET OutputRoot.HTTPInputHeader = InputRoot.HTTPInputHeader;
|
|
|
+
|
|
|
+ -- Build output JSON
|
|
|
+ CREATE LASTCHILD OF OutputRoot DOMAIN 'JSON' NAME 'JSON';
|
|
|
+ CREATE FIELD OutputRoot.JSON.Data;
|
|
|
+ CREATE FIELD OutputRoot.JSON.Data.dlog;
|
|
|
+
|
|
|
+ DECLARE om REFERENCE TO OutputRoot.JSON.Data;
|
|
|
+ DECLARE omd REFERENCE TO om.dlog;
|
|
|
+
|
|
|
+ SET om.body = InputRoot.JSON.Data;
|
|
|
+
|
|
|
+ -- Extract serverIp dengan intermediate variable (lebih readable)
|
|
|
+ DECLARE uriAfterProtocol CHARACTER
|
|
|
+ SUBSTRING(InputLocalEnvironment.REST.Input.URI AFTER '//');
|
|
|
+
|
|
|
+ -- Set DLOG
|
|
|
+ SET omd.type = msgType;
|
|
|
+ SET omd.dir = 'I';
|
|
|
+ SET omd.source = source;
|
|
|
+ SET omd.clientIp = InputRoot.HTTPInputHeader."X-Client-Ip"
|
|
|
+ || ':' || InputRoot.HTTPInputHeader."X-Server-Port";
|
|
|
+ SET omd.serverIp = SUBSTRING(uriAfterProtocol BEFORE '/');
|
|
|
+ SET omd.code = InputLocalEnvironment.REST.Input.URI;
|
|
|
+ SET omd.timestamp = CAST(currentTs AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+ SET omd.reqtimestamp = currentTs; -- Simpan sebagai TIMESTAMP, bukan CHARACTER
|
|
|
+ -- correlId: pakai milliseconds untuk uniqueness lebih baik
|
|
|
+ SET omd.correlId = CAST(currentTs AS CHARACTER FORMAT 'yyyyMMddHHmmssSSS')
|
|
|
+ || '.' || COALESCE(InputRoot.JSON.Data.ref, 'NOREF');
|
|
|
+ SET om.brokerlog = InputLocalEnvironment.brokerlog;
|
|
|
+
|
|
|
+ CALL ACE_LIB.logDebug('Output', OutputLocalEnvironment, OutputExceptionList, OutputRoot);
|
|
|
+ RETURN TRUE;
|
|
|
+ END;
|
|
|
+END MODULE;
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 📄 MAPPING_REQUEST.esql
|
|
|
+
|
|
|
+```
|
|
|
+✅ Strengths:
|
|
|
+- REFERENCE variables dipakai (im, imb, om, omb, omd)
|
|
|
+- logDebug dipanggil di Input dan Output
|
|
|
+- EXTERNAL variables untuk errCodeInvMsg, errCodeInvKey, cacheExpiry, Group
|
|
|
+- Validasi field dilakukan sebelum processing
|
|
|
+
|
|
|
+❌ Issues:
|
|
|
+1. Lines 16-67: Validation block di-copy-paste 4x (52 lines duplikasi)
|
|
|
+2. Lines 68-84: Manual key parsing loop — inefficient, security risk
|
|
|
+3. Line 91: Queue name hardcoded 'CORE_MQ_REPLY.MB' (sudah ada queueReply property)
|
|
|
+4. Line 95: Magic number '162' tanpa penjelasan
|
|
|
+5. Line 96: Hardcoded 'NONFIN'
|
|
|
+6. Line 97: Hardcoded IP '172.18.30.203'
|
|
|
+7. Line 98: Hardcoded 'NewMb'
|
|
|
+8. Line 100: Service code '428101' hardcoded
|
|
|
+9. Lines 103-105: OriginalMsg = InputRoot (copy full message, memory overhead)
|
|
|
+10. Line 63: Typo 'AKses'
|
|
|
+11. Lines 30,41,52,63,75: DECLARE om/omb/omd di-repeat di setiap IF block
|
|
|
+ (bisa di-declare sekali di awal)
|
|
|
+12. Line 93: Commented-out dead code
|
|
|
+```
|
|
|
+
|
|
|
+**Recommended refactoring MAPPING_REQUEST.esql:**
|
|
|
+```esql
|
|
|
+CREATE COMPUTE MODULE MAPPING_REQUEST
|
|
|
+ DECLARE keyList EXTERNAL CHARACTER;
|
|
|
+ DECLARE errCodeInvMsg EXTERNAL CHARACTER;
|
|
|
+ DECLARE errCodeInvKey EXTERNAL CHARACTER;
|
|
|
+ DECLARE cacheExpiry EXTERNAL INTEGER;
|
|
|
+ DECLARE Group EXTERNAL CHARACTER;
|
|
|
+ DECLARE queueReply EXTERNAL CHARACTER; -- Ganti hardcode 'CORE_MQ_REPLY.MB'
|
|
|
+ DECLARE coreClientIp EXTERNAL CHARACTER; -- Ganti hardcode '172.18.30.203'
|
|
|
+ DECLARE coreChannel EXTERNAL CHARACTER; -- Ganti hardcode 'NewMb'
|
|
|
+ DECLARE coreRespLen EXTERNAL CHARACTER; -- Ganti hardcode '162'
|
|
|
+ DECLARE coreSvcCode EXTERNAL CHARACTER; -- Ganti hardcode '428101'
|
|
|
+ DECLARE coreMsgType EXTERNAL CHARACTER; -- Ganti hardcode 'NONFIN'
|
|
|
+
|
|
|
+ -- Reusable validation procedure
|
|
|
+ CREATE PROCEDURE sendValidationError(
|
|
|
+ IN errorMsg CHARACTER,
|
|
|
+ IN timenode CHARACTER,
|
|
|
+ IN brokerLabel CHARACTER,
|
|
|
+ IN node CHARACTER,
|
|
|
+ INOUT OutputRoot REFERENCE,
|
|
|
+ IN InputRoot REFERENCE,
|
|
|
+ INOUT OutputLocalEnvironment REFERENCE
|
|
|
+ )
|
|
|
+ BEGIN
|
|
|
+ SET OutputRoot = InputRoot;
|
|
|
+ DECLARE om REFERENCE TO OutputRoot.JSON.Data;
|
|
|
+ DECLARE omb REFERENCE TO OutputRoot.JSON.Data.body;
|
|
|
+ DECLARE omd REFERENCE TO OutputRoot.JSON.Data.dlog;
|
|
|
+ SET omb.rsp = errCodeInvMsg;
|
|
|
+ CALL ACE_LIB.generateError(Group, omb.rsp, omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ SET omb.rspdesc = errorMsg;
|
|
|
+ SET omd.dir = 'O';
|
|
|
+ SET omd.timestamp = CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+ SET om.brokerlog = timenode || brokerLabel || '.' || node || '(' || errorMsg || ')' || '&&&';
|
|
|
+ PROPAGATE TO TERMINAL 'out1';
|
|
|
+ END;
|
|
|
+
|
|
|
+ CREATE FUNCTION Main() RETURNS BOOLEAN
|
|
|
+ BEGIN
|
|
|
+ DECLARE node CHARACTER MessageFlowLabel || '.' || NodeLabel;
|
|
|
+ DECLARE brokerLabel CHARACTER BrokerName || '.' || ExecutionGroupLabel;
|
|
|
+ DECLARE timenode CHARACTER CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY/MM/dd HH:mm:ss:SSS ');
|
|
|
+
|
|
|
+ CALL ACE_LIB.logDebug('Input', InputLocalEnvironment, InputExceptionList, InputRoot);
|
|
|
+
|
|
|
+ DECLARE im REFERENCE TO InputRoot.JSON.Data;
|
|
|
+ DECLARE imb REFERENCE TO im.body;
|
|
|
+
|
|
|
+ -- Validasi fields (DRY - tidak ada duplikasi)
|
|
|
+ IF (imb.ref IS NULL) OR (LENGTH(imb.ref) < 1) OR (LENGTH(imb.ref) > 12) THEN
|
|
|
+ CALL sendValidationError('Invalid Kode Referensi', timenode, brokerLabel, node,
|
|
|
+ OutputRoot, InputRoot, OutputLocalEnvironment);
|
|
|
+ RETURN FALSE;
|
|
|
+ END IF;
|
|
|
+ IF (imb.cif IS NULL) OR (LENGTH(imb.cif) < 7) OR (LENGTH(imb.cif) > 19) THEN
|
|
|
+ CALL sendValidationError('Invalid Nomor CIF', timenode, brokerLabel, node,
|
|
|
+ OutputRoot, InputRoot, OutputLocalEnvironment);
|
|
|
+ RETURN FALSE;
|
|
|
+ END IF;
|
|
|
+ IF (imb.hp IS NULL) OR (LENGTH(imb.hp) < 1) OR (LENGTH(imb.hp) > 15) THEN
|
|
|
+ CALL sendValidationError('Invalid Nomor HP', timenode, brokerLabel, node,
|
|
|
+ OutputRoot, InputRoot, OutputLocalEnvironment);
|
|
|
+ RETURN FALSE;
|
|
|
+ END IF;
|
|
|
+ IF (imb.nid IS NULL) OR (LENGTH(imb.nid) < 1) OR (LENGTH(imb.nid) > 16) THEN
|
|
|
+ CALL sendValidationError('Invalid Kode Akses', timenode, brokerLabel, node,
|
|
|
+ OutputRoot, InputRoot, OutputLocalEnvironment);
|
|
|
+ RETURN FALSE;
|
|
|
+ END IF;
|
|
|
+
|
|
|
+ -- Validasi API Key (gunakan cache, bukan loop setiap request)
|
|
|
+ IF NOT ACE_LIB.isValidApiKey(InputRoot.HTTPInputHeader.Key, keyList, cacheExpiry) THEN
|
|
|
+ CALL sendValidationError('Not Authorize', timenode, brokerLabel, node,
|
|
|
+ OutputRoot, InputRoot, OutputLocalEnvironment);
|
|
|
+ RETURN FALSE;
|
|
|
+ END IF;
|
|
|
+
|
|
|
+ -- Mapping ke Core MQ format
|
|
|
+ CREATE LASTCHILD OF OutputRoot DOMAIN 'MQMD' NAME 'MQMD';
|
|
|
+ SET OutputRoot.MQMD.ReplyToQ = queueReply; -- Dari EXTERNAL variable
|
|
|
+
|
|
|
+ CREATE LASTCHILD OF OutputRoot DOMAIN 'JSON' NAME 'JSON';
|
|
|
+ CREATE FIELD OutputRoot.JSON.Data;
|
|
|
+ CREATE FIELD OutputRoot.JSON.Data.body;
|
|
|
+ DECLARE om REFERENCE TO OutputRoot.JSON.Data;
|
|
|
+ DECLARE omb REFERENCE TO OutputRoot.JSON.Data.body;
|
|
|
+
|
|
|
+ SET omb.ABMBERCD = '';
|
|
|
+ SET omb.ABMBERDS = '';
|
|
|
+ SET omb.ABMBREFF = imb.ref;
|
|
|
+ SET omb.ABMBCIFN = imb.cif;
|
|
|
+ SET omb.ABMBPHON = imb.hp;
|
|
|
+ SET omb.ABMBACID = imb.nid;
|
|
|
+
|
|
|
+ -- Header
|
|
|
+ SET om.header.HDRTRN = 'M' || CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYMMddHHmmss') || imb.ref;
|
|
|
+ SET om.header.HDRSVC = coreSvcCode; -- Dari EXTERNAL variable
|
|
|
+
|
|
|
+ -- Metadata (semua dari EXTERNAL variables)
|
|
|
+ SET om.metadata.ResponseBodyLength = coreRespLen;
|
|
|
+ SET om.metadata.CoreType = coreMsgType;
|
|
|
+ SET om.metadata.IPClient = coreClientIp;
|
|
|
+ SET om.metadata.Channel = coreChannel;
|
|
|
+
|
|
|
+ -- Simpan HANYA dlog (bukan full OriginalMsg) untuk response path
|
|
|
+ SET om.dlog = InputRoot.JSON.Data.dlog;
|
|
|
+ SET om.OriginalDlog = InputRoot.JSON.Data.dlog;
|
|
|
+ SET om.RequestIdentifier = BASE64ENCODE(InputLocalEnvironment.Destination.HTTP.RequestIdentifier);
|
|
|
+ SET om.brokerlog = timenode || brokerLabel || '.' || node || '&&&';
|
|
|
+
|
|
|
+ CALL ACE_LIB.logDebug('Output', OutputLocalEnvironment, OutputExceptionList, OutputRoot);
|
|
|
+ RETURN TRUE;
|
|
|
+ END;
|
|
|
+END MODULE;
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 📄 RESPONSE_IN.esql
|
|
|
+
|
|
|
+```
|
|
|
+✅ Strengths:
|
|
|
+- REFERENCE variables dipakai dengan baik (im, imb, om, omb, omd)
|
|
|
+- logDebug dipanggil di Input dan Output
|
|
|
+- EXTERNAL variables untuk semua configurable values
|
|
|
+- Error code mapping logic cukup jelas
|
|
|
+
|
|
|
+❌ Issues:
|
|
|
+1. Lines 35-46: REPLACE(im.header.HDRERR, ' ', '') dipanggil 2x
|
|
|
+ — extract ke variable sekali
|
|
|
+2. Lines 35-50: Nested IF-ELSE bisa disederhanakan
|
|
|
+3. Lines 55-63: Deep path reference ke OriginalMsg (akan fix setelah
|
|
|
+ MAPPING_REQUEST direfactor ke OriginalDlog)
|
|
|
+4. Line 9: CAST timestamp format berbeda dengan omd.timestamp di line 42
|
|
|
+5. Line 30: SUBSTRING bersarang untuk cardexpired — bisa lebih readable
|
|
|
+6. Missing: Tidak ada null check untuk im.header sebelum akses HDRRSP/HDRERR
|
|
|
+```
|
|
|
+
|
|
|
+**Recommended refactoring RESPONSE_IN.esql:**
|
|
|
+```esql
|
|
|
+CREATE COMPUTE MODULE "RESPONSE_IN"
|
|
|
+ DECLARE source EXTERNAL CHARACTER;
|
|
|
+ DECLARE msgType EXTERNAL CHARACTER;
|
|
|
+ DECLARE cacheExpiry EXTERNAL INTEGER;
|
|
|
+ DECLARE Group EXTERNAL CHARACTER;
|
|
|
+ DECLARE errCodeNotFound EXTERNAL CHARACTER;
|
|
|
+ DECLARE errCoreNoRC EXTERNAL CHARACTER;
|
|
|
+
|
|
|
+ CREATE FUNCTION Main() RETURNS BOOLEAN
|
|
|
+ BEGIN
|
|
|
+ DECLARE node CHARACTER MessageFlowLabel || '.' || NodeLabel;
|
|
|
+ DECLARE brokerLabel CHARACTER BrokerName || '.' || ExecutionGroupLabel;
|
|
|
+ DECLARE currentTs CHARACTER CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY-MM-dd HH:mm:ss.SSS');
|
|
|
+
|
|
|
+ SET InputLocalEnvironment.brokerlog =
|
|
|
+ CAST(CURRENT_TIMESTAMP AS CHARACTER FORMAT 'YYYY/MM/dd HH:mm:ss:SSS ')
|
|
|
+ || brokerLabel || '.' || node || '&&&';
|
|
|
+
|
|
|
+ CALL ACE_LIB.logDebug('Input', InputLocalEnvironment, InputExceptionList, InputRoot);
|
|
|
+
|
|
|
+ SET OutputLocalEnvironment.Destination.HTTP.RequestIdentifier =
|
|
|
+ BASE64DECODE(InputRoot.JSON.Data.RequestIdentifier);
|
|
|
+ SET OutputRoot.HTTPResponseHeader."Content-Type" = 'application/json';
|
|
|
+
|
|
|
+ CREATE LASTCHILD OF OutputRoot DOMAIN 'JSON' NAME 'JSON';
|
|
|
+ CREATE FIELD OutputRoot.JSON.Data;
|
|
|
+ CREATE FIELD OutputRoot.JSON.Data.body;
|
|
|
+ CREATE FIELD OutputRoot.JSON.Data.dlog;
|
|
|
+
|
|
|
+ DECLARE im REFERENCE TO InputRoot.JSON.Data;
|
|
|
+ DECLARE imb REFERENCE TO im.body;
|
|
|
+ DECLARE om REFERENCE TO OutputRoot.JSON.Data;
|
|
|
+ DECLARE omb REFERENCE TO om.body;
|
|
|
+ DECLARE omd REFERENCE TO om.dlog;
|
|
|
+
|
|
|
+ -- Guard: null check untuk header
|
|
|
+ IF im.header IS NULL THEN
|
|
|
+ CALL ACE_LIB.generateError(Group, errCoreNoRC, omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ CALL ACE_LIB.generateErrorDesc(Group, errCoreNoRC, omb.rspdesc, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ ELSE
|
|
|
+ -- Tentukan error code SEKALI, bukan 4x
|
|
|
+ DECLARE errorCode CHARACTER;
|
|
|
+ IF im.header.HDRRSP = '00' THEN
|
|
|
+ SET errorCode = CASE WHEN im.header.HDRERR = '' THEN '00'
|
|
|
+ ELSE REPLACE(im.header.HDRERR, ' ', '')
|
|
|
+ END;
|
|
|
+ ELSE
|
|
|
+ SET errorCode = CASE WHEN im.header.HDRERR = '' THEN errCoreNoRC
|
|
|
+ ELSE REPLACE(im.header.HDRERR, ' ', '')
|
|
|
+ END;
|
|
|
+ END IF;
|
|
|
+
|
|
|
+ CALL ACE_LIB.generateError(Group, errorCode, omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ CALL ACE_LIB.generateErrorDesc(Group, errorCode, omb.rspdesc, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+
|
|
|
+ -- Override jika rsp = '99'
|
|
|
+ IF omb.rsp = '99' THEN
|
|
|
+ CALL ACE_LIB.generateError(Group, errCodeNotFound, omb.rsp, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ CALL ACE_LIB.generateErrorDesc(Group, errCodeNotFound, omb.rspdesc, OutputLocalEnvironment.ERROR, cacheExpiry);
|
|
|
+ END IF;
|
|
|
+ END IF;
|
|
|
+
|
|
|
+ -- Map response fields
|
|
|
+ SET omb.ref = imb.ABMBREFF;
|
|
|
+ SET omb.hp = imb.ABMBPHON;
|
|
|
+ SET omb.cardnumber = '';
|
|
|
+ SET omb.cardstatus = imb.ABMBSTAT;
|
|
|
+ SET omb.carddesc = imb.ABMBDESC;
|
|
|
+ -- cardexpired: lebih readable dengan variable intermediate
|
|
|
+ DECLARE rawExpiry CHARACTER imb.ABMBEXCR;
|
|
|
+ SET omb.cardexpired = SUBSTRING(rawExpiry FROM 5 FOR 2) -- MM
|
|
|
+ || SUBSTRING(rawExpiry FROM 3 FOR 2) -- YY
|
|
|
+ || SUBSTRING(rawExpiry FROM 1 FOR 2); -- DD
|
|
|
+
|
|
|
+ -- Set DLOG — pakai OriginalDlog (bukan full OriginalMsg)
|
|
|
+ DECLARE origDlog REFERENCE TO im.OriginalDlog;
|
|
|
+ SET omd.type = msgType;
|
|
|
+ SET omd.dir = 'O';
|
|
|
+ SET omd.source = source;
|
|
|
+ SET omd.clientIp = origDlog.clientIp;
|
|
|
+ SET omd.serverIp = origDlog.serverIp;
|
|
|
+ SET omd.code = origDlog.code;
|
|
|
+ SET omd.timestamp = currentTs;
|
|
|
+ SET omd.reqtimestamp = origDlog.reqtimestamp;
|
|
|
+ SET omd.correlId = origDlog.correlId;
|
|
|
+ SET om.brokerlog = InputLocalEnvironment.brokerlog;
|
|
|
+
|
|
|
+ CALL ACE_LIB.logDebug('Output', OutputLocalEnvironment, OutputExceptionList, OutputRoot);
|
|
|
+ RETURN TRUE;
|
|
|
+ END;
|
|
|
+END MODULE;
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 📄 RESPONSE_OUT.esql
|
|
|
+
|
|
|
+```
|
|
|
+✅ Strengths:
|
|
|
+- Module sangat singkat dan focused (10 lines) — single responsibility
|
|
|
+- logDebug dipanggil di Input dan Output
|
|
|
+- Tidak ada hardcoded values
|
|
|
+
|
|
|
+⚠️ Minor Issues:
|
|
|
+1. Tidak ada null check untuk InputRoot.JSON.Data.body
|
|
|
+2. SET OutputLocalEnvironment = InputLocalEnvironment — perlu dipastikan
|
|
|
+ tidak ada data sensitif yang ikut ter-copy ke LocalEnvironment output
|
|
|
+```
|
|
|
+
|
|
|
+**Recommended minor fix:**
|
|
|
+```esql
|
|
|
+CREATE COMPUTE MODULE RESPONSE_OUT
|
|
|
+ CREATE FUNCTION Main() RETURNS BOOLEAN
|
|
|
+ BEGIN
|
|
|
+ CALL ACE_LIB.logDebug('Input', InputLocalEnvironment, InputExceptionList, InputRoot);
|
|
|
+
|
|
|
+ SET OutputLocalEnvironment = InputLocalEnvironment;
|
|
|
+
|
|
|
+ -- Guard: pastikan body ada
|
|
|
+ IF InputRoot.JSON.Data.body IS NULL THEN
|
|
|
+ THROW USER EXCEPTION MESSAGE 2951 VALUES('RESPONSE_OUT: body is null');
|
|
|
+ END IF;
|
|
|
+
|
|
|
+ CREATE LASTCHILD OF OutputRoot DOMAIN 'JSON' NAME 'JSON';
|
|
|
+ CREATE FIELD OutputRoot.JSON.Data;
|
|
|
+ SET OutputRoot.JSON.Data = InputRoot.JSON.Data.body;
|
|
|
+
|
|
|
+ CALL ACE_LIB.logDebug('Output', OutputLocalEnvironment, OutputExceptionList, OutputRoot);
|
|
|
+ RETURN TRUE;
|
|
|
+ END;
|
|
|
+END MODULE;
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🚀 PRIORITIZED ACTION PLAN
|
|
|
+
|
|
|
+### 🔴 CRITICAL FIXES — Must-do Sprint Ini
|
|
|
+
|
|
|
+| # | Action | File | Effort | Impact |
|
|
|
+|---|--------|------|--------|--------|
|
|
|
+| 1 | Ganti PASSTHRU dengan parameterized SELECT di GEN_ERROR.esql | GEN_ERROR.esql | 1 jam | Security + Performance |
|
|
|
+| 2 | Merge generateError + generateErrorDesc jadi single DB query | GEN_ERROR.esql | 2 jam | 50% DB query reduction |
|
|
|
+| 3 | Hapus hardcoded IP `172.18.30.203`, ganti EXTERNAL variable | MAPPING_REQUEST.esql | 30 menit | Config management |
|
|
|
+| 4 | Ganti default keyList `aaa;bbb;ccc;ddd;eee` dengan empty + startup validation | postLogin.subflow | 30 menit | Security |
|
|
|
+| 5 | Enable HTTPS di restapi.descriptor | restapi.descriptor | 15 menit | Security |
|
|
|
+| 6 | Ganti `SET om.OriginalMsg = InputRoot` dengan `SET om.OriginalDlog = InputRoot.JSON.Data.dlog` | MAPPING_REQUEST.esql | 1 jam | Memory + MQ throughput |
|
|
|
+| 7 | Tambahkan TryCatch node di sekitar MQ operations | postLogin.subflow | 2 jam | Resilience |
|
|
|
+| 8 | Tambahkan timeout handling untuk MQ request-reply | postLogin.subflow | 2 jam | Resilience |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### ⚡ QUICK WINS — Impact Tinggi, Effort Rendah
|
|
|
+
|
|
|
+| # | Action | File | Effort | Impact |
|
|
|
+|---|--------|------|--------|--------|
|
|
|
+| 1 | Fix typo `AKses` → `Akses` | MAPPING_REQUEST.esql | 5 menit | Log consistency |
|
|
|
+| 2 | Hapus commented-out dead code line 93 | MAPPING_REQUEST.esql | 5 menit | Code cleanliness |
|
|
|
+| 3 | Extract `REPLACE(im.header.HDRERR, ' ', '')` ke variable | RESPONSE_IN.esql | 15 menit | Readability + minor perf |
|
|
|
+| 4 | Ganti `SET OutputRoot.MQMD.ReplyToQ = 'CORE_MQ_REPLY.MB'` dengan EXTERNAL variable | MAPPING_REQUEST.esql | 15 menit | Config consistency |
|
|
|
+| 5 | Tambahkan milliseconds ke correlId, hapus trailing space | REQUEST_IN.esql | 15 menit | Observability |
|
|
|
+| 6 | Fix swagger.json title `REST_MQ_RA` → `REST_MB_RA` | swagger.json | 5 menit | Documentation |
|
|
|
+| 7 | Tambahkan intermediate variable untuk nested SUBSTRING | REQUEST_IN.esql | 15 menit | Readability |
|
|
|
+| 8 | Standardize timestamp format (pilih satu format, pakai di semua file) | Semua ESQL | 30 menit | Consistency |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 📈 STRATEGIC IMPROVEMENTS — Long-term
|
|
|
+
|
|
|
+| # | Action | Effort | Impact |
|
|
|
+|---|--------|--------|--------|
|
|
|
+| 1 | Refactor validation block di MAPPING_REQUEST jadi reusable procedure (eliminasi 52 lines duplikasi) | 3 jam | Maintainability |
|
|
|
+| 2 | Buat `isValidApiKey()` di ACE_LIB dengan caching (eliminasi loop per-request) | 4 jam | Performance + Security |
|
|
|
+| 3 | Tambahkan PII masking di logDebug calls (hp, cif, nid, API key) | 4 jam | Compliance (PDP) |
|
|
|
+| 4 | Lengkapi swagger.json dengan request/response schema | 2 jam | API documentation |
|
|
|
+| 5 | Buat standardized error handler subflow (reusable across flows) | 1 hari | Consistency |
|
|
|
+| 6 | Tambahkan response time tracking (elapsedMs di dlog) | 2 jam | Observability |
|
|
|
+| 7 | Jalankan `ibmint optimize server` dan commit hasilnya | 1 jam | Startup time + Memory |
|
|
|
+| 8 | Buat `buildBrokerLog()` shared function di ACE_LIB | 1 jam | Standardization |
|
|
|
+| 9 | Tambahkan input format validation (nomor HP hanya digit) | 2 jam | Data quality |
|
|
|
+| 10 | Pisahkan validation logic ke `validateRequest.subflow` yang reusable | 1 hari | Reusability |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 🏁 FINAL SUMMARY
|
|
|
+
|
|
|
+```
|
|
|
+🎯 OPTIMASI SCORE: 52/100
|
|
|
+
|
|
|
+Breakdown per kategori:
|
|
|
+┌─────────────────────────────────┬────────┬────────┐
|
|
|
+│ Kategori │ Score │ Status │
|
|
|
+├─────────────────────────────────┼────────┼────────┤
|
|
|
+│ 1. ESQL Code Quality │ 55/100 │ ⚠️ │
|
|
|
+│ 2. Message Flow Design │ 60/100 │ ⚠️ │
|
|
|
+│ 3. Error Handling & Resilience │ 40/100 │ ❌ │
|
|
|
+│ 4. Performance Optimization │ 45/100 │ ❌ │
|
|
|
+│ 5. Configuration Management │ 35/100 │ ❌ │
|
|
|
+│ 6. Security & Credentials │ 30/100 │ ❌ │
|
|
|
+│ 7. Modularity & Reusability │ 55/100 │ ⚠️ │
|
|
|
+│ 8. Monitoring & Observability │ 65/100 │ ⚠️ │
|
|
|
+│ 9. Startup & Resource Efficiency│ 50/100 │ ⚠️ │
|
|
|
+└─────────────────────────────────┴────────┴────────┘
|
|
|
+
|
|
|
+Top 3 yang paling urgent:
|
|
|
+1. 🔴 Security: SQL injection + HTTPS disabled + weak default API keys
|
|
|
+2. 🔴 Memory: OriginalMsg = InputRoot (full message copy ke MQ)
|
|
|
+3. 🔴 Resilience: Tidak ada TryCatch + timeout di MQ operations
|
|
|
+
|
|
|
+Setelah semua Critical Fixes diimplementasi, estimasi score naik ke: 78/100
|
|
|
+Setelah semua Strategic Improvements: 90/100
|
|
|
+```
|