$data * @return int|null Inserted ID or null on failure */ public function insert(int $tenantId, array $data): ?int { $result = DB::insert( 'INSERT INTO security_check_templates' . ' (tenant_id, product_code, product_name, tech_schema_json, active, version, created_by, updated_by)' . ' VALUES (?, ?, ?, ?, ?, ?, ?, ?)', (string) $tenantId, (string) ($data['product_code'] ?? ''), (string) ($data['product_name'] ?? ''), $data['tech_schema_json'] ?? null, (string) ((int) ($data['active'] ?? 1)), (string) ((int) ($data['version'] ?? 1)), (string) ((int) ($data['created_by'] ?? 0)), (string) ((int) ($data['created_by'] ?? 0)) ); return $result === false ? null : (int) $result; } public function findById(int $tenantId, int $id): ?array { if ($id <= 0) { return null; } $row = DB::selectOne( 'SELECT t.*, uc.display_name AS created_by_name, uu.display_name AS updated_by_name' . ' FROM security_check_templates t' . ' LEFT JOIN users uc ON uc.id = t.created_by' . ' LEFT JOIN users uu ON uu.id = t.updated_by' . ' WHERE t.id = ? AND t.tenant_id = ? LIMIT 1', (string) $id, (string) $tenantId ); return $this->normalizeRow($row); } public function findByCode(int $tenantId, string $code): ?array { $code = trim($code); if ($code === '') { return null; } $row = DB::selectOne( 'SELECT * FROM security_check_templates WHERE tenant_id = ? AND product_code = ? LIMIT 1', (string) $tenantId, $code ); return $this->normalizeRow($row); } public function codeExists(int $tenantId, string $code, int $exceptId = 0): bool { $code = trim($code); if ($code === '') { return false; } $count = (int) (DB::selectValue( 'SELECT COUNT(*) FROM security_check_templates WHERE tenant_id = ? AND product_code = ? AND id <> ?', (string) $tenantId, $code, (string) $exceptId ) ?? 0); return $count > 0; } /** * Active templates for the wizard product picker. * * @return list> */ public function listActive(int $tenantId): array { $rows = DB::select( 'SELECT id, product_code, product_name, tech_schema_json FROM security_check_templates' . ' WHERE tenant_id = ? AND active = 1 ORDER BY product_name ASC', (string) $tenantId ); $normalized = []; if (is_array($rows)) { foreach ($rows as $row) { $item = $this->normalizeRow($row); if ($item !== null) { $normalized[] = $item; } } } return $normalized; } /** * @param array $filters * @return array{total: int, rows: list>} */ public function listPaged(int $tenantId, array $filters): array { $search = trim((string) ($filters['search'] ?? '')); $active = trim((string) ($filters['active'] ?? '')); [$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 100, 0); [$order, $dir] = RepoQuery::sanitizeOrder( $filters, ['id', 'product_code', 'product_name', 'active', 'version', 'updated_at', 'created_at'], 'product_name', 'asc' ); $where = ['t.tenant_id = ?']; $params = [(string) $tenantId]; RepoQuery::addLikeFilter($where, $params, ['t.product_code', 't.product_name'], $search); if ($active === '1' || $active === '0') { $where[] = 't.active = ?'; $params[] = $active; } $whereSql = ' WHERE ' . implode(' AND ', $where); $total = (int) (DB::selectValue('SELECT COUNT(*) FROM security_check_templates t' . $whereSql, ...$params) ?? 0); $rows = DB::select( 'SELECT t.*, uu.display_name AS updated_by_name' . ' FROM security_check_templates t' . ' LEFT JOIN users uu ON uu.id = t.updated_by' . $whereSql . sprintf(' ORDER BY t.`%s` %s LIMIT ? OFFSET ?', $order, $dir), ...array_merge($params, [(string) $limit, (string) $offset]) ); $normalized = []; if (is_array($rows)) { foreach ($rows as $row) { $item = $this->normalizeRow($row); if ($item !== null) { $normalized[] = $item; } } } return ['total' => $total, 'rows' => $normalized]; } public function updateMeta(int $tenantId, int $id, string $name, bool $active, int $updatedBy): bool { $result = DB::update( 'UPDATE security_check_templates SET product_name = ?, active = ?, updated_by = ? WHERE id = ? AND tenant_id = ?', $name, (string) ($active ? 1 : 0), (string) $updatedBy, (string) $id, (string) $tenantId ); return $result !== false; } public function updateSchema(int $tenantId, int $id, string $schemaJson, int $version, int $updatedBy): bool { $result = DB::update( 'UPDATE security_check_templates SET tech_schema_json = ?, version = ?, updated_by = ? WHERE id = ? AND tenant_id = ?', $schemaJson, (string) $version, (string) $updatedBy, (string) $id, (string) $tenantId ); return $result !== false; } private function normalizeRow(mixed $row): ?array { if (!is_array($row)) { return null; } // JOIN queries return nested arrays keyed by table name plus scalar aliased // columns — merge sub-arrays and copy scalars (same convention as core repos). $item = []; foreach ($row as $key => $value) { if (is_array($value)) { $item = array_merge($item, $value); } else { $item[$key] = $value; } } return isset($item['id']) ? $item : null; } }