Add a new GraphQL type or operation to a Spring Boot + Kotlin + Netflix DGS + JPA repository service. Covers DataFetcher / Mutation / Repository / JPA Entity wiring.
Trigger when the user asks to:
workflow-api@DgsComponent, @DgsQuery, @DgsMutation, @InputArgument, JpaRepository, @EntityThis skill is intentionally CONVERGED — one skill covers Query, Mutation, Repository AND Entity work because the patterns repeat. Per the slide thesis: backend collapses into a small number of high-coverage skills.
workflow-api is Spring Boot 3 + Kotlin + Netflix DGS GraphQL + JPA (Postgres). The full server's GraphQL surface is two files plus two repository interfaces:
workflow-api/src/main/kotlin/com/workflow/graphql/WorkflowDataFetcher.kt:13-34 — @DgsQuery methods (workflows, workflow(id), myApprovals(assigneeId))workflow-api/src/main/kotlin/com/workflow/graphql/WorkflowMutation.kt:15-65 — @DgsMutation methods (createWorkflow, approveStep, rejectStep)workflow-api/src/main/kotlin/com/workflow/repository/WorkflowRepository.kt:10-27 — JpaRepository<T, UUID> interfaces with spring-data method-name queries (findByRequesterId, findByOrgId, findByStatus, findByAssigneeIdAndStatus)workflow-api/src/main/kotlin/com/workflow/domain/model/Workflow.kt:21-42 — @Entity data class Workflow(@Id @GeneratedValue(strategy = GenerationType.UUID) val id: UUID? = null, ...)Conventions:
UUID with @GeneratedValue(strategy = GenerationType.UUID). GraphQL accepts them as String (@InputArgument id: String) and converts via UUID.fromString(id) inside the resolver.repository.findById(UUID.fromString(id)).orElseThrow { IllegalArgumentException("...") }, mutate fields, then repository.save(it).@Query annotations) — findByXxxAndYyy, findByXxxOrderByZzz. Compose method names rather than writing JPQL.@Enumerated(EnumType.STRING) so the DB stores readable values.java.time.Instant not LocalDateTime.When adding a new entity Foo with list query, findById query, and create mutation:
domain/model/Foo.kt as a @Entity @Table(name = "foos") data class Foo(...). Use the field annotations from Workflow.kt. Always:
@Id @GeneratedValue(strategy = GenerationType.UUID) val id: UUID? = null@Column(nullable = false, updatable = false) val createdAt: Instant = Instant.now()@Column(nullable = false) var updatedAt: Instant = Instant.now()repository/FooRepository.kt:
@Repository
interface FooRepository : JpaRepository<Foo, UUID> {
fun findByOrgId(orgId: UUID): List<Foo>
}
graphql/FooDataFetcher.kt:
@DgsComponent
class FooDataFetcher(private val repo: FooRepository) {
@DgsQuery fun foos(): List<Foo> = repo.findAll()
@DgsQuery fun foo(@InputArgument id: String): Foo? = repo.findById(UUID.fromString(id)).orElse(null)
}
graphql/FooMutation.kt mirroring WorkflowMutation.createWorkflow (line 20-36)..graphqls file under src/main/resources/schema/) declaring type Foo, extend type Query { foos: [Foo!]! ... }, extend type Mutation { createFoo(...): Foo! }.src/main/resources/db/migration/ with V<n>__create_foos.sql matching the entity columns.Entity skeleton:
package com.workflow.domain.model
import jakarta.persistence.*
import java.time.Instant
import java.util.UUID
enum class FooStatus { DRAFT, ACTIVE, ARCHIVED }
@Entity
@Table(name = "foos")
data class Foo(
@Id
@GeneratedValue(strategy = GenerationType.UUID)
val id: UUID? = null,
@Column(nullable = false)
var name: String = "",
@Enumerated(EnumType.STRING)
@Column(nullable = false)
var status: FooStatus = FooStatus.DRAFT,
@Column(nullable = false)
val orgId: UUID = UUID.randomUUID(),
@Column(nullable = false, updatable = false)
val createdAt: Instant = Instant.now(),
@Column(nullable = false)
var updatedAt: Instant = Instant.now(),
)
Repository (spring-data method names only):
@Repository
interface FooRepository : JpaRepository<Foo, UUID> {
fun findByOrgId(orgId: UUID): List<Foo>
fun findByStatus(status: FooStatus): List<Foo>
}
DataFetcher + Mutation:
@DgsComponent
class FooDataFetcher(private val repo: FooRepository) {
@DgsQuery fun foos(): List<Foo> = repo.findAll()
@DgsQuery fun foo(@InputArgument id: String): Foo? = repo.findById(UUID.fromString(id)).orElse(null)
}
@DgsComponent
class FooMutation(private val repo: FooRepository) {
@DgsMutation
fun createFoo(@InputArgument name: String, @InputArgument orgId: String): Foo {
return repo.save(Foo(name = name, orgId = UUID.fromString(orgId)))
}
@DgsMutation
fun archiveFoo(@InputArgument id: String): Foo {
val foo = repo.findById(UUID.fromString(id)).orElseThrow { IllegalArgumentException("Foo not found: $id") }
foo.status = FooStatus.ARCHIVED
foo.updatedAt = Instant.now()
return repo.save(foo)
}
}
LocalDateTime. Always Instant for timestamps.@Query JPQL — compose spring-data method names instead.Optional<T> from a @DgsQuery — return T? and handle null with .orElse(null).@InputArgument parameters per field.@Enumerated(EnumType.ORDINAL) — always STRING so the DB stays human-readable.GenerationType.UUID.workflow-api/src/main/kotlin/com/workflow/graphql/WorkflowDataFetcher.kt:13-34workflow-api/src/main/kotlin/com/workflow/graphql/WorkflowMutation.kt:15-65workflow-api/src/main/kotlin/com/workflow/repository/WorkflowRepository.kt:10-27workflow-api/src/main/kotlin/com/workflow/domain/model/Workflow.kt:21-42workflow-api/src/main/kotlin/com/workflow/config/GraphQLConfig.kt — DGS config