ruleDefModel = new RuleDefModel(); } public function index() { try { $eventCode = $this->request->getGet('EventCode'); $testSiteID = $this->request->getGet('TestSiteID'); $search = $this->request->getGet('search'); $builder = $this->ruleDefModel->where('ruledef.EndDate', null); if ($eventCode !== null && $eventCode !== '') { $builder->where('ruledef.EventCode', $eventCode); } if ($search !== null && $search !== '') { $builder->like('ruledef.RuleName', $search); } // Filter by TestSiteID - join with mapping table if ($testSiteID !== null && $testSiteID !== '' && is_numeric($testSiteID)) { $builder->join('testrule', 'testrule.RuleID = ruledef.RuleID', 'inner'); $builder->where('testrule.TestSiteID', (int) $testSiteID); $builder->where('testrule.EndDate IS NULL'); } $rows = $builder ->orderBy('ruledef.RuleID', 'ASC') ->findAll(); return $this->respond([ 'status' => 'success', 'message' => 'fetch success', 'data' => $rows, ], 200); } catch (\Throwable $e) { log_message('error', 'RuleController::index error: ' . $e->getMessage()); return $this->respond([ 'status' => 'failed', 'message' => 'Failed to fetch rules', 'data' => [], ], 500); } } public function show($id = null) { try { if (!$id || !is_numeric($id)) { return $this->failValidationErrors('RuleID is required'); } $rule = $this->ruleDefModel->where('EndDate', null)->find((int) $id); if (!$rule) { return $this->respond([ 'status' => 'failed', 'message' => 'Rule not found', 'data' => [], ], 404); } $linkedTests = $this->ruleDefModel->getLinkedTests((int) $id); $rule['linkedTests'] = $linkedTests; return $this->respond([ 'status' => 'success', 'message' => 'fetch success', 'data' => $rule, ], 200); } catch (\Throwable $e) { log_message('error', 'RuleController::show error: ' . $e->getMessage()); return $this->respond([ 'status' => 'failed', 'message' => 'Failed to fetch rule', 'data' => [], ], 500); } } public function create() { $input = $this->request->getJSON(true) ?? []; $validation = service('validation'); $validation->setRules([ 'RuleCode' => 'required|max_length[50]', 'RuleName' => 'required|max_length[100]', 'EventCode' => 'required|max_length[50]', 'TestSiteIDs' => 'required', 'TestSiteIDs.*' => 'is_natural_no_zero', 'ConditionExpr' => 'permit_empty|max_length[1000]', ]); if (!$validation->run($input)) { return $this->failValidationErrors($validation->getErrors()); } $testSiteIDs = $input['TestSiteIDs'] ?? []; if (!is_array($testSiteIDs) || empty($testSiteIDs)) { return $this->failValidationErrors(['TestSiteIDs' => 'At least one TestSiteID is required']); } // Validate all TestSiteIDs exist $testDef = new TestDefSiteModel(); foreach ($testSiteIDs as $testSiteID) { $exists = $testDef->where('EndDate', null)->find((int) $testSiteID); if (!$exists) { return $this->failValidationErrors(['TestSiteIDs' => "TestSiteID {$testSiteID} not found"]); } } $db = \Config\Database::connect(); $db->transStart(); try { $ruleData = [ 'RuleCode' => $input['RuleCode'], 'RuleName' => $input['RuleName'], 'Description' => $input['Description'] ?? null, 'EventCode' => $input['EventCode'], 'ConditionExpr' => $input['ConditionExpr'] ?? null, 'ConditionExprCompiled' => $input['ConditionExprCompiled'] ?? null, ]; $ruleID = $this->ruleDefModel->insert($ruleData, true); if (!$ruleID) { throw new \Exception('Failed to create rule'); } // Link rule to test sites foreach ($testSiteIDs as $testSiteID) { $this->ruleDefModel->linkTest($ruleID, (int) $testSiteID); } $db->transComplete(); if ($db->transStatus() === false) { throw new \Exception('Transaction failed'); } return $this->respondCreated([ 'status' => 'success', 'message' => 'Rule created successfully', 'data' => ['RuleID' => $ruleID], ], 201); } catch (\Throwable $e) { $db->transRollback(); log_message('error', 'RuleController::create error: ' . $e->getMessage()); return $this->failServerError('Something went wrong: ' . $e->getMessage()); } } public function update($id = null) { $input = $this->request->getJSON(true) ?? []; if (!$id || !is_numeric($id)) { $id = $input['RuleID'] ?? null; } if (!$id || !is_numeric($id)) { return $this->failValidationErrors('RuleID is required'); } $existing = $this->ruleDefModel->where('EndDate', null)->find((int) $id); if (!$existing) { return $this->respond([ 'status' => 'failed', 'message' => 'Rule not found', 'data' => [], ], 404); } $validation = service('validation'); $validation->setRules([ 'RuleCode' => 'permit_empty|max_length[50]', 'RuleName' => 'permit_empty|max_length[100]', 'EventCode' => 'permit_empty|max_length[50]', 'TestSiteIDs' => 'permit_empty', 'TestSiteIDs.*' => 'is_natural_no_zero', 'ConditionExpr' => 'permit_empty|max_length[1000]', ]); if (!$validation->run($input)) { return $this->failValidationErrors($validation->getErrors()); } $db = \Config\Database::connect(); $db->transStart(); try { $updateData = []; foreach (['RuleCode', 'RuleName', 'Description', 'EventCode', 'ConditionExpr', 'ConditionExprCompiled'] as $field) { if (array_key_exists($field, $input)) { $updateData[$field] = $input[$field]; } } if (!empty($updateData)) { $this->ruleDefModel->update((int) $id, $updateData); } // Update test site mappings if provided if (isset($input['TestSiteIDs']) && is_array($input['TestSiteIDs'])) { $testSiteIDs = $input['TestSiteIDs']; // Validate all TestSiteIDs exist $testDef = new TestDefSiteModel(); foreach ($testSiteIDs as $testSiteID) { $exists = $testDef->where('EndDate', null)->find((int) $testSiteID); if (!$exists) { throw new \Exception("TestSiteID {$testSiteID} not found"); } } // Get current linked tests $currentLinks = $this->ruleDefModel->getLinkedTests((int) $id); // Unlink tests that are no longer in the list foreach ($currentLinks as $currentTestSiteID) { if (!in_array($currentTestSiteID, $testSiteIDs)) { $this->ruleDefModel->unlinkTest((int) $id, $currentTestSiteID); } } // Link new tests foreach ($testSiteIDs as $testSiteID) { if (!in_array($testSiteID, $currentLinks)) { $this->ruleDefModel->linkTest((int) $id, (int) $testSiteID); } } } $db->transComplete(); if ($db->transStatus() === false) { throw new \Exception('Transaction failed'); } return $this->respond([ 'status' => 'success', 'message' => 'Rule updated successfully', 'data' => ['RuleID' => (int) $id], ], 200); } catch (\Throwable $e) { $db->transRollback(); log_message('error', 'RuleController::update error: ' . $e->getMessage()); return $this->failServerError('Something went wrong: ' . $e->getMessage()); } } public function delete($id = null) { try { if (!$id || !is_numeric($id)) { return $this->failValidationErrors('RuleID is required'); } $existing = $this->ruleDefModel->where('EndDate', null)->find((int) $id); if (!$existing) { return $this->respond([ 'status' => 'failed', 'message' => 'Rule not found', 'data' => [], ], 404); } $this->ruleDefModel->delete((int) $id); return $this->respondDeleted([ 'status' => 'success', 'message' => 'Rule deleted successfully', 'data' => ['RuleID' => (int) $id], ]); } catch (\Throwable $e) { log_message('error', 'RuleController::delete error: ' . $e->getMessage()); return $this->failServerError('Something went wrong: ' . $e->getMessage()); } } public function validateExpr() { $input = $this->request->getJSON(true) ?? []; $expr = $input['expr'] ?? ''; $context = $input['context'] ?? []; if (!is_string($expr) || trim($expr) === '') { return $this->failValidationErrors(['expr' => 'expr is required']); } if (!is_array($context)) { return $this->failValidationErrors(['context' => 'context must be an object']); } try { $svc = new RuleExpressionService(); $result = $svc->evaluate($expr, $context); return $this->respond([ 'status' => 'success', 'data' => [ 'valid' => true, 'result' => $result, ], ], 200); } catch (\Throwable $e) { return $this->respond([ 'status' => 'failed', 'data' => [ 'valid' => false, 'error' => $e->getMessage(), ], ], 200); } } /** * Compile DSL expression to engine-compatible structure. * Frontend calls this when user clicks "Compile" button. */ public function compile() { $input = $this->request->getJSON(true) ?? []; $expr = $input['expr'] ?? ''; if (!is_string($expr) || trim($expr) === '') { return $this->failValidationErrors(['expr' => 'Expression is required']); } try { $svc = new RuleExpressionService(); $compiled = $svc->compile($expr); return $this->respond([ 'status' => 'success', 'data' => [ 'raw' => $expr, 'compiled' => $compiled, 'conditionExprCompiled' => json_encode($compiled), ], ], 200); } catch (\Throwable $e) { return $this->respond([ 'status' => 'failed', 'message' => 'Compilation failed', 'data' => [ 'error' => $e->getMessage(), ], ], 400); } } }