Mutations

Implementing mutation operations in the Star Wars demo app using Viaduct.

The Star Wars demo app includes several mutation operations that allow you to modify data. All mutations are available under the Mutation root type and demonstrate how to implement data modification operations in Viaduct.

Mutation implementation patterns

Mutations in Viaduct follow similar patterns to queries but focus on data modification operations. Each mutation resolver typically:

  1. Validates input data using input types with appropriate constraints.
  2. Performs the data modification on the underlying data store.
  3. Returns updated entities that can be further resolved with additional fields.
  4. Maintains data consistency and referential integrity.

Available mutations

Create a new character

input CreateCharacterInput @scope(to: ["default"]) {
 name: String!
 birthYear: String
 eyeColor: String
 gender: String
 hairColor: String
 height: Int
 mass: Float
 homeworldId: ID @idOf(type: "Planet")
 speciesId: ID @idOf(type: "Species")
}

"""
Create a new Character.
"""
createCharacter(input: CreateCharacterInput!): Character @resolver

Implementation:

@Resolver
class CreateCharacterMutation : MutationResolvers.CreateCharacter() {
   override suspend fun resolve(ctx: Context): Character {
       val input = ctx.arguments.input
       val homeworldId = input.homeworldId
       val speciesId = input.speciesId

       // TODO: Validate homeworld and species are valid ids

       // Create and store the new character
       val character = CharacterRepository.add(
           com.example.starwars.modules.filmography.characters.models.Character(
               id = "",
               name = input.name,
               birthYear = input.birthYear,
               eyeColor = input.eyeColor,
               gender = input.gender,
               hairColor = input.hairColor,
               height = input.height,
               mass = input.mass?.toFloat(),
               homeworldId = homeworldId?.internalID,
               speciesId = speciesId?.internalID,
           )
       )

       // Build and return the GraphQL Character object from the created entity
       return CharacterBuilder(ctx).build(character)
   }
}

Execution :

mutation {
  createCharacter(input: {
    name: "New Jedi"
    birthYear: "19BBY"
    eyeColor: "blue"
    gender: "male"
    hairColor: "brown"
    height: 180
    mass: 75.5
    homeworldId: "UGxhbmV0OjE="  # Tatooine
    speciesId: "U3BlY2llczox"    # Human
  }) {
    id
    name
    birthYear
    homeworld { name }
    species { name }
  }
}

Implementation notes:

  • Uses input types for structured data validation.
  • Generates new GlobalIDs for created entities.
  • Supports relationship creation via reference IDs.
  • Returns the full created entity for immediate use.

Update character name

"""
Update an existing Character's name.
"""
updateCharacterName(id: ID! @idOf(type: "Character"), name: String!): Character @resolver

Implementation notes:

@Resolver
class UpdateCharacterNameMutation : MutationResolvers.UpdateCharacterName() {
   override suspend fun resolve(ctx: Context): Character? {
       val id = ctx.arguments.id
       val name = ctx.arguments.name

       // Fetch existing character
       val character = CharacterRepository.findById(id.internalID)
           ?: throw IllegalArgumentException("Character with ID ${id.internalID} not found")

       // Update character's name
       val updatedCharacter = character.copy(name = name)

       val newCharacter = CharacterRepository.update(updatedCharacter)

       // Return the updated character as a GraphQL object
       return CharacterBuilder(ctx).build(newCharacter)
   }
}

  • Uses GlobalIDs for entity identification.
  • Performs atomic field updates.
  • Returns updated entity for verification.

Add character to film

 """
 Link a Character to a Film (adds the character to that film's cast).
 Allows locating the character either directly by ID or via the existing search input.
 """
 addCharacterToFilm(input: AddCharacterToFilmInput!): AddCharacterToFilmPayload @resolver
}

mutation {
  addCharacterToFilm(input: {
    filmId: "RmlsbTox"           # A New Hope
    characterId: "Q2hhcmFjdGVyOjU="  # Obi-Wan Kenobi
  }) {
    character {
      name
    }
    film {
      title
    }
  }
}

Implementation notes:

  • Manages many-to-many relationships.
  • Uses input types for relationship data.
  • Returns both related entities for verification.
  • Maintains bidirectional relationship consistency.

Delete character

"""
Delete a Character by ID. Returns true if a record was removed.
"""
deleteCharacter(id: ID! @idOf(type: "Character")): Boolean @resolver

Implementation notes:

  • Uses GlobalIDs for entity identification.
  • Returns boolean success indicator.
  • Handles cascading relationship cleanup.
  • Maintains data integrity during deletion.

Mutation best practices

  1. Use input types: structure mutation arguments with dedicated input types for validation and clarity.
  2. GlobalID consistency: always use encoded GlobalIDs for entity references.
  3. Return useful data: return updated entities or relationship objects, not just success flags.
  4. Validate relationships: ensure referenced entities exist before creating relationships.
  5. Handle errors gracefully: provide meaningful error messages for invalid operations.
  6. Maintain consistency: update all related data structures atomically.

Note: when using mutations, make sure to use properly encoded GlobalIDs.