question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Reactor - Flux and Mono can't get them to work at all

See original GitHub issue

So I’ve been hammering away at this for a day and I can’t seem to get even basic queries to work with mono or flux.

Below is an example of a query to a user profile from the identity store. If I convert this into a future it works out of the box, but the moment I use a mono i get all my fields as null

{"data":{"currentUserProfile":{"firstname":null,"lastname":null,"username":null,"emailAddress":null}}}

And the query code:

@PreAuthorize("isAuthenticated()")
    fun currentUserProfile(context: GraphQLSecurityContext): Mono<UserProfile> {
        return context.securityContext.flatMap { sc ->
            val details = sc.authentication.details as UserCredential
            userIdentityService.findById(details.id).map { UserProfile.fromUserIdentity(it) }
        }

I don’t seem to have a problem generating schema with mono. I have registered a monad hook, but it seems to do nothing.

Is there something I’m doing wrong? I know it does call the query method, but I can’t seem to figure out I get a null response.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:1
  • Comments:32 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
ianmichellcommented, May 7, 2020

Anyway, thanks for the help.

Here is the code for anyone that wants it. Obviously a todo will be to add expression support:

class AuthorisedDataFetcher(private val originDataFetcher: DataFetcher<Any?>, val roles: Array<out String>) : DataFetcher<Any?> {

    val logger = LoggerFactory.getLogger(AuthorisedDataFetcher::class.java)

    @Throws(AccessDeniedException::class)
    override fun get(environment: DataFetchingEnvironment): Any? {
        val securityContext = environment.getContext<GraphQLSecurityContext>().securityContext
        val authentication = securityContext.authentication
        if (!authentication.isAuthenticated || (roles.isNotEmpty() && authentication.authorities.filter {
                    roles.contains(it.authority) }.none())) {
            throw AccessDeniedException("Access denied") // TODO Make sure this gets logged for audit purposes
        }
        return originDataFetcher.get(environment)
    }
}
@Component
class ReactiveDataFactoryProvider(val dataFetcherFactory: ReactiveDataFetcherFactory, val objectMapper: ObjectMapper) :
        SimpleKotlinDataFetcherFactoryProvider(objectMapper) {

    override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>) = DataFetcherFactory {
        val authorised = kFunction.findAnnotation<Authorised>()
        val reactiveDataFetcher = ReactiveFunctionDataFetcher(
                target = target,
                fn = kFunction,
                objectMapper = objectMapper)
        when {
            authorised != null -> AuthorisedDataFetcher(reactiveDataFetcher, authorised.roles)
            else -> reactiveDataFetcher
        }
    }

    override fun propertyDataFetcherFactory(kClass: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any?> =
            if (kProperty.isLateinit) {
                dataFetcherFactory
            } else {
                super.propertyDataFetcherFactory(kClass, kProperty)
            }
}
@Component
class ReactiveDataFetcherFactory: DataFetcherFactory<Any?>, BeanFactoryAware {
    private lateinit var beanFactory: BeanFactory

    override fun setBeanFactory(beanFactory: BeanFactory) {
        this.beanFactory = beanFactory
    }

    @Suppress("UNCHECKED_CAST")
    override fun get(environment: DataFetcherFactoryEnvironment?): DataFetcher<Any?> {

        // Strip out possible `Input` and `!` suffixes added to by the SchemaGenerator
        val targetedTypeName = environment?.fieldDefinition?.type?.deepName?.removeSuffix("!")?.removeSuffix("Input")
        return beanFactory.getBean("${targetedTypeName}DataFetcher") as DataFetcher<Any?>
    }
}
class ReactiveFunctionDataFetcher(target: Any?, fn: KFunction<*>, objectMapper: ObjectMapper): FunctionDataFetcher(target, fn, objectMapper) {

    override fun get(environment: DataFetchingEnvironment): Any? = when (val result = super.get(environment)) {
        is Mono<*> -> result.toFuture()
        else -> result
    }
}

Edit: Forgot the annotation

@GraphQLDirective(
        name = "authorised",
        description = "Used to check authorisation"
)
annotation class Authorised(vararg val roles: String)

Example:

@Authorised()
    suspend fun currentUserProfile(context: GraphQLSecurityContext): UserProfile? {
        logger.debug("Query executed")
        return mono { context.securityContext }.flatMap { sc ->
            val details = sc.authentication.details as UserCredential
            userIdentityService.findById(details.id).map { UserProfile.fromUserIdentity(it) }
        }.awaitFirst()
    }
0reactions
fhonercommented, Jan 3, 2021

Hi, thanks for providing the examples, it is indeed very helpful. Regarding the code from @thunderbird do I understand correctly, the @Authorised annotation only works on top-level object like query or mutation?
This works for me:

class PlanQuery(
	private val planService: PlanService
): Query {

	@Authorised("USER")
	fun getPlans(): List<PlanDto> = planService.getAll()

}

Now I annotated a field on the type the query returns, like follows:

@GraphQLName("Plan")
data class PlanDto(
	@GraphQLIgnore val id: Long,
	val uuid: String,
	val name: String,
	@Authorised("MANAGER")
	val secret: String
)

Expected behaviour would be that a user without the role ‘Manager’ could not retrieve the field ‘secret’.
Any ideas how to achieve this?

EDIT: One approach that works for me is to go the same way on the PropertyDataFetcherFactory:

override fun propertyDataFetcherFactory(kClass: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any?> = DataFetcherFactory {
    val authorized = kProperty.findAnnotation<Authorized>()
    if (kProperty.isLateinit) {
       // ...or whatever you named your lateinit property fetcher beans
        val targetedTypeName = it?.fieldDefinition?.type?.deepName?.removeSuffix("!")?.removeSuffix("Input")
        val dataFetcherBean = beanFactory.getBean("${targetedTypeName}DataFetcher") as DataFetcher<Any?>
        when {
            authorized != null -> AuthorizedDataFetcher(dataFetcherBean, authorized.roles.toList())
            else               -> dataFetcherBean
        }
    } else {
        val reactiveDataFetcher = ReactivePropertyDataFetcher(kProperty)
        when {
            authorized != null -> AuthorizedDataFetcher(reactiveDataFetcher, authorized.roles.toList())
            else               -> reactiveDataFetcher
        }
    }
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Reactor 3 Reference Guide
It offers composable asynchronous sequence APIs — Flux (for [N] elements) and Mono (for [0|1] elements) — and extensively implements the ...
Read more >
[Reactor Java #4] How to take control over the execution of ...
Reactor is a Java library for creating reactive non-blocking ... Mono and Flux have two methods to configure the scheduler to use.
Read more >
project reactor - How to combine a Mono and a Flux?
While Kevin Hussey's solution is a correct one, I think it is better to have it another way around: Mono<String> mono1 = Mono....
Read more >
Intro To Reactor Core - Baeldung
We'll take small steps through Reactor until we've built a picture of how to compose reactive code, laying the foundation for more advanced ......
Read more >
The 5 basic topics you should know about project reactor
For example, wrapping every external service call into an ... To convert a Mono into a Flux that repeats that value we can...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found