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
@Inject
constructor(
    private val characterRepository: CharacterRepository,
    private val securityAccessContext: SecurityAccessContext
) : MutationResolvers.CreateCharacter() {
    override suspend fun resolve(ctx: Context): Character =
        securityAccessContext.validateAccess {
            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
            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.
  • Validates security access using request-scoped context (see Request Context for details).

Update character name

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

Implementation notes:

@Resolver
class UpdateCharacterNameMutation
@Inject
constructor(
    private val characterRepository: CharacterRepository,
    private val securityAccessService: SecurityAccessContext
) : MutationResolvers.UpdateCharacterName() {
    override suspend fun resolve(ctx: Context): Character? =
        securityAccessService.validateAccess {
            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)

  • 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.

Using request context

Operations often need access to request-specific data like authentication, authorization, or tenant information. While this example focuses on mutations, request context is available to all operations — queries, mutations, and subscriptions may all use request context as needed.

Viaduct fully supports request context management through your framework’s dependency injection. For detailed information on different approaches and best practices, see the Request Context documentation.

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.
  7. Leverage request context: use framework-provided request scoping for authentication, authorization, and tenant isolation.
  8. Inject dependencies: prefer dependency injection over global state or manual context threading.

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