Link Search Menu Expand Document

Data Transfer Object (DTO)

KDone supports DTO pattern, which allows controlling API’s input and output data. It is possible to specify the output model of reading operations and the input model of creating and update operations. To configure DTO add dto inside module configuration DSL

module<Game>("games") {
...

   dto {
      ...
   }
}

Read

Let’s say we have API configured for this model:

data class Game(
    val name: String,
    val players: Int?,
    val image: ResourceFile?): Identifiable()

If we call a GET API, the result will be

  {
    "name": "Days Gone",
    "players": 1,
    "image": {
      "url": "https://dariopellegrini.ams3.digitaloceanspaces.com/kdone/game/1578676851711aozI0SMf9C-Days-Gone-768x432.jpg",
      "mimeType": "image/jpeg"
    },
    "_id": "5e18b27480c3a94962fa09a9"
  }

Now we would like to specify a custom model for GET APIs, different from the previous one, such as the output has only name and players parameters.

This can be achieved by configuring KDone DTO.

First create the desired model

data class GameOutput(val name: String, val players: Int)

Then configure inside dto

dto {
   read<GameOutput> {
      game ->
      GameOutput(game.name, game.players)
   }

Inside read DSL is specified the rule used to go from Game to GameOutput. It is possible to omit the rule and KDone will make the transfer automatically, using the class attributes’ name.

dto {
   read<GameOutput>()
}

Now each KDone GET API for Game model will have the desired output.

{
    "name": "Days Gone",
    "players": 1
}

Create

Create DTO works opposite from read one: it is possible to specify the input model of create APIs, in order to control the JSON parameters of POST calls.

Again let’s take as an example Game class define previously. We would like to let POST specify only the parameter name.

To do this start creating a model

data class GameInput(val name: String)

Then configure create DTO

create<GameInput> {
   gameInput ->
   Game(gameInput.name, 1, null)
}

The rule is used to go from GameInput to Game.

Like for read DTO, it is possible to omit the rule and let KDone perform an automatic transfer from GameInput to Game using class attributes’ names.

Now the new POST supported body will be

{
   "name":"Days Gone"
}

Update

Similarly to create one, update DTO works on input, but doesn’t need any rule. It only needs the input model that PATCH calls’ body should have.

Create the model

data class GameUpdate(val name: String)

Configure update DTO

dto {
   update<GameUpdate>()
}

Now each PATCH API should have a body like

{
   "name":"Days Gone"
}

Role-based DTO

It is possible to configure DTO for each user role. This gives a more specific control on user’s input and output.


// Guest DTO used when the call has no authorization Bearer token
guest {
   read<GameOutput> {
      game ->
     GameOutput(game.name)
   }

   create<GameInput>() {
      gameInput ->
      Game(gameInput.name, null, null)
   }

   update<GameUpdate>()
}

// Registered DTO used when the call has authorization Bearer token
registered {
   read<GameOutput> {
      game ->
      GameOutput(game.name, game.players)
   }

   create<GameInput> {
      gameInput ->
      Game(gameInput.name, gameInput.players, null)
   }

   update<GameUpdate>()
}

// Owner DTO used when user owns a specific resource
// Owner doesn't need any create DTO, cause owner is assigned after creation
owner {
   read<GameOutput> {
      game ->
      GameOutput(game.name, game.players, game.image)
   }

   update<GameUpdateOwner>()
}

// Custom role DTO used when a user has this role
"admin".dto {
   read<GameOutput> {
      game ->
      GameOutput(game.name, game.players, game.image)
   }

   create<GameInput> {
      gameInput ->
      Game(gameInput.name, gameInput.players, gameInput.image)
   }

   update<GameUpdate>()
}

Priority

If DTO authorizations overlap, KDone uses priority algorithms.

Read

if (userRole != null)
   if (dtoIsPresentForRole(userRole)) useDTOForRole(userRole)
   else if (user.isOwnerOf(resource)) useDTOForOwner()
   else useGlobalDTOIfPresent()
else if (userToken != null)
   if (user.isOwnerOf(resource)) useDTOForOwner()
   else if (dtoForRegisteredUserIsPresent()) useRegisteredUserDTO()
   else useGlobalDTOIfPresent()
else if (dtoForGuestUserIsPresent) useGuestUserDTO()
else useGlobalDTO()

Create

if (userRole != null)
   if (dtoIsPresentForRole(userRole)) useDTOForRole(userRole)
   else useGlobalDTOIfPresent()
else if (userToken != null)
   if (user.isOwnerOf(resource)) useDTOForOwner()
   else useGlobalDTOIfPresent()
else if (dtoForGuestUserIsPresent) useGuestUserDTO()
else useGlobalDTO()

Update

if (userRole != null)
   if (dtoIsPresentForRole(userRole)) useDTOForRole(userRole)
   else if (user.isOwnerOf(resource)) useDTOForOwner()
   else useGlobalDTOIfPresent()
else if (userToken != null)
   if (user.isOwnerOf(resource)) useDTOForOwner()
   else if (dtoForRegisteredUserIsPresent()) useRegisteredUserDTO()
   else useGlobalDTOIfPresent()
else if (dtoForGuestUserIsPresent) useGuestUserDTO()
else useGlobalDTO()

Copyright © 2021 Dario Pellegrini