Skip to content

Directive @selection

dermakov edited this page Aug 29, 2021 · 3 revisions

The @selection directive provides an ability to move optional arguments of GraphQL field to a lambda block in generated projection interface. Let define a GraphQL schema:

type Query {
    films(
        genre: Genre!,
        title: String,
        offset: Int! = 0,
        limit: Int! = 30
    ): [Film!]!
}

type Film {
    id: ID!
    genre: Genre!
    title: String!
}

enum Genre {
    DRAMA
    COMEDY
    THRILLER
    HORROR
}

This schema allows us to write a query using the generated DSL that looks like this:

val context: ExampleContext = exampleContextOf(createMyAdapter())
val response = context.query {
    films(Genre.DRAMA, title = "*Dream*", offset = 100, limit = 100) {
        id()
        genre()
        title()
    }
}

response.films.forEach { film ->
    println("Film [${film.id}]: ${film.title} (${film.genre})")
}

The films projection function has 3 optional arguments:

  • The title is optional because it is nullable.
  • The offset is optional because it has a default value.
  • The limit is optional because it has a default value.

We can move these arguments to the lambda block of the films projection function using the @selection directive. Let's modify our schema:

directive @selection on FIELD_DEFINITION

type Query {
    films(
        genre: Genre!,
        title: String,
        offset: Int! = 0,
        limit: Int! = 30
    ): [Film!]! @selection
}

type Film {
    id: ID!
    genre: Genre!
    title: String!
}

enum Genre {
    DRAMA
    COMEDY
    THRILLER
    HORROR
}

Now we can write our query like this:

val context: ExampleContext = exampleContextOf(createMyAdapter())
val response = context.query {
    films(Genre.DRAMA) {
        title = "*Dream*"
        offset = 100
        limit = 100

        id()
        genre()
        title()
    }
}

response.films.forEach { film ->
    println("Film [${film.id}]: ${film.title} (${film.genre})")
}

The @selection directive improves readability of DSL queries in case the GraphQL field contains many optional arguments.

How it works?

Without @selection directive for the GraphQL type Query, Kobby generates a projection interface that looks like this:

@ExampleDSL
interface QueryProjection {
    /**
     * @param offset Default: 0
     * @param limit Default: 30
     */
    fun films(
        genre: Genre,
        title: String? = null,
        offset: Int? = null,
        limit: Int? = null,
        __projection: FilmProjection.() -> Unit
    ): Unit
}

After applying @selection directive, the generated interface will change as follows:

@ExampleDSL
interface QueryProjection {
    fun films(genre: Genre, __query: QueryFilmsQuery.() -> Unit): Unit
}

@ExampleDSL
interface QueryFilmsSelection {
    var title: String?

    /**
     * Default: 0
     */
    var offset: Int?

    /**
     * Default: 30
     */
    var limit: Int?
}

@ExampleDSL
interface QueryFilmsQuery : QueryFilmsSelection, FilmProjection

For the films field marked with the @selection directive, Kobby generates an additional QueryFilmsSelection interface that contains all the optional arguments. Since the films field returns a list of films, its lambda must also contain the projection of the Film type. To do this, Kobby generates a third interface, QueryFilmsQuery, which is simply QueryFilmsSelection + FilmProjection (naming convention is simple: "selection" + "projection" = "query"). And the QueryFilmsQuery interface is used as a receiver of the QueryProjection.films lambda expression.

Restrictions

  • The @selection directive can only be applied to a field that contains optional arguments - nullable arguments or arguments with default value.
  • The @selection directive cannot be applied to overridden fields. In this case, apply the directive to the base interface field.

In case of violation of any restriction, the directive will be ignored.

Clone this wiki locally