Mit CQRS findet eine Architektur immer mehr Anklang, die sich von einer klassischen CRUD-Abbildung von Fachlichkeiten löst. Statt verschiedene Entitäten zu ändern, um beispielsweise einen Buchungsvorgang umzusetzen, wird ein Command verwendet, der alle notwendigen Informationen bündelt und die Aufgabe fachlich abbildet. Wie lassen sich solche Commands jedoch in einem API abbilden? REST konzentriert sich auf Entitäten und bildet CRUD für diese Entitäten ab. Wie bildet man jedoch Commands in einem API ab? Genau hier ist GraphQL hervorragend geeignet. In dieser Session betrachten wir, wie GraphQL für eine fachlich getriebene Interaktion verwendet werden kann und dadurch mit CQRS harmoniert. An konkreten Beispielen wird ersichtlich, wo die Stärken von GraphQL gegenüber einem REST API für CQRS liegen.
3. 3
Was ist denn eigentlich REST?
• Alle relevanten Daten sind über eine URI erreichbar
http://smart.service/api/resources/<resource-id>
• Änderungen an den Daten werden mittels HTTP Requests und
passenden Verben durchgeführt
POST http://smart.service/api/resources/
GET http://smart.service/api/resources/<resource-id>
DELETE http://smart.service/api/resources/<resource-id>
4. 3
Was ist denn eigentlich REST?
• Alle relevanten Daten sind über eine URI erreichbar
http://smart.service/api/resources/<resource-id>
• Änderungen an den Daten werden mittels HTTP Requests und
passenden Verben durchgeführt
POST http://smart.service/api/resources/
GET http://smart.service/api/resources/<resource-id>
DELETE http://smart.service/api/resources/<resource-id>
Kein REST ist:
GET http://dumb.service/api/resources/<id>/applyColorBlue
15. 8
Guthaben erhöhen
• Ausstellen neuer Karten
• Guthaben einlösen
• Guthaben erhöhen
• Karte stornieren
• Einlösung stornieren
PUT http://svc.local/api/giftcards/102
Accept: application/json
Content-Type: application/json
{
"amount": 80
}
Request HTTP/1.1 201 CREATED
Content-Type: application/json
{
"id": 102,
"amount": 80
}
Response
Ist das der Betrag um
den das Guthaben
erhöht werden soll?
Oder ist es der neue
Zielbetrag?
16. 9
Karte stornieren
• Ausstellen neuer Karten
• Guthaben einlösen
• Guthaben erhöhen
• Karte stornieren
• Einlösung stornieren
Was ist eigentlich eine Stornierung?
• Löschung mittels DELETE?
Der Datensatz sollte weiterhin existieren für
eine Nachverfolgung
• Eine Aktualisierung mittels PUT?
Das steht in Konkurrenz zu Erhöhung des
Guthabens
20. 13
Einlösung stornieren
• Ausstellen neuer Karten
• Guthaben einlösen
• Guthaben erhöhen
• Karte stornieren
• Einlösung stornieren
DELETE http://svc.local/api/giftcards/102/transactions/203
• Der Datensatz soll nicht gelöscht werden
• Auch wird der Datensatz selbst nicht geändert, sondern ein
neuer angelegt
21. 14
Einlösung stornieren
• Ausstellen neuer Karten
• Guthaben einlösen
• Guthaben erhöhen
• Karte stornieren
• Einlösung stornieren
POST http://svc.local/api/giftcards/102/transactions
• Der Body des POST könnte ein Flag tragen, das den Datensatz
als Stornierung kennzeichnet
• Steht in Konkurrenz zu normalen Transaktionen
POST http://svc.local/api/giftcards/102/transactions
Accept: application/json
Content-Type: application/json
{
"amount": -11,
„revokedTransactionId": 203
}
Request
23. 16
GraphQL Schnelleinstieg
• GraphQL ist ein Mechanismus für die Kommunikation
zwischen Client und Server
• Die technische Übertragung ist typischerweise HTTP
• Kern von GraphQL ist das GraphQL Schema
• Das GraphQL Schema definiert, welche Daten der Server
bereitstellen kann
24. 17
GraphQL Schnelleinstieg: Das Schema
• Das GraphQL Schema basiert auf
Typen
type Type {
field: StrictType
}
type StrictType {
valueNeverNull: String!
}
type PrimitiveFields {
identifier: ID!
numeric: Int!
}
type TypeWithArrays {
strings: [String]
strictList: [StrictType!]!
}
25. 18
GraphQL Schnelleinstieg: Das Schema
• Das GraphQL Schema basiert auf
Typen
• Ein Typ besteht aus Feldern und
deren Typisierung
type Type {
field: StrictType
}
type StrictType {
valueNeverNull: String!
}
type PrimitiveFields {
identifier: ID!
numeric: Int!
}
type TypeWithArrays {
strings: [String]
strictList: [StrictType!]!
}
26. 19
GraphQL Schnelleinstieg: Das Schema
• Das GraphQL Schema basiert auf
Typen
• Ein Typ besteht aus Feldern und
deren Typisierung
• Felder können explizit als non-
nullable deklariert werden
type Type {
field: StrictType
}
type StrictType {
valueNeverNull: String!
}
type PrimitiveFields {
identifier: ID!
numeric: Int!
}
type TypeWithArrays {
strings: [String]
strictList: [StrictType!]!
}
27. 20
GraphQL Schnelleinstieg: Das Schema
• Das GraphQL Schema basiert auf
Typen
• Ein Typ besteht aus Feldern und
deren Typisierung
• Felder können explizit als non-
nullable deklariert werden
• Die Typen der Felder sind entweder
deklarierte Typen oder Scalars
type Type {
field: StrictType
}
type StrictType {
valueNeverNull: String!
}
type PrimitiveFields {
identifier: ID!
numeric: Int!
}
type TypeWithArrays {
strings: [String]
strictList: [StrictType!]!
}
28. 21
GraphQL Schnelleinstieg: Das Schema
• Das GraphQL Schema basiert auf
Typen
• Ein Typ besteht aus Feldern und
deren Typisierung
• Felder können explizit als non-
nullable deklariert werden
• Die Typen der Felder sind entweder
deklarierte Typen oder Scalars
• Listen werden ebenfalls typisiert
definiert
type Type {
field: StrictType
}
type StrictType {
valueNeverNull: String!
}
type PrimitiveFields {
identifier: ID!
numeric: Int!
}
type TypeWithArrays {
strings: [String]
strictList: [StrictType!]!
}
29. 22
GraphQL Schnelleinstieg: Das Schema
• Felder können Argumente erhalten
• Anhand der Argumente kann der
Zugriff auf das Feld konfiguriert
werden
type Type {
field(filter: String!): ResultType
other(filter: Filter!): ResultType
}
input Filter {
text: String
maxResults: Int
}
30. 23
GraphQL Schnelleinstieg: Das Schema
• Felder können Argumente erhalten
• Anhand der Argumente kann der
Zugriff auf das Feld konfiguriert
werden
• Komplexere Argumente können in
eigenen Input-Typen definiert
werden
type Type {
field(filter: String!): ResultType
other(filter: Filter!): ResultType
}
input Filter {
text: String
maxResults: Int
}
31. 24
GraphQL Schnelleinstieg: Das Schema
Das GraphQL Schema bietet noch weitere Möglichkeiten
• Union Types
• Enum
• Eigene Skalare
• Interfaces
33. 26
GraphQL Schnelleinstieg: Das Schema
Das Schema definiert drei
Einstiegspunkte in den Graphen:
• Query
type Query {
#...
}
34. 27
GraphQL Schnelleinstieg: Das Schema
Das Schema definiert drei
Einstiegspunkte in den Graphen:
• Query
• Mutation
type Query {
#...
}
type Mutation {
#...
}
35. 28
GraphQL Schnelleinstieg: Das Schema
Das Schema definiert drei
Einstiegspunkte in den Graphen:
• Query
• Mutation
• Subscription
type Query {
#...
}
type Mutation {
#...
}
type Subscription {
#...
}
36. 29
GraphQL Schnelleinstieg: Das Schema
• Query ist der Einstieg in den
Graphen
• Daten die für den Client abrufbar
sein sollen, müssen direkt oder
indirekt über Query erreichbar sein
• Die Komplexität des Graphen ist
wenig limitiert
type Query {
companies: [Company!]!
company(id: ID!): Company
}
type Company {
id: ID!
name: String!
locations: [Location!]!
}
type Location {
name: String!
address: Address
}
type Address {
addressLines: [String!]!
city: String!
postalCode: String!
Country: String!
}
39. 32
GraphQL Schnelleinstieg: Queries
Eine Query definiert, welche Daten aus dem Grafen
ausgelesen werden sollen
type Query {
companies: [Company!]!
company(id: ID!): Company
}
type Company {
id: ID!
name: String!
locations: [Location!]!
}
type Location {
name: String!
address: Address
}
type Address {
addressLines: [String!]!
city: String!
postalCode: String!
Country: String!
}
40. 32
GraphQL Schnelleinstieg: Queries
Eine Query definiert, welche Daten aus dem Grafen
ausgelesen werden sollen
type Query {
companies: [Company!]!
company(id: ID!): Company
}
type Company {
id: ID!
name: String!
locations: [Location!]!
}
type Location {
name: String!
address: Address
}
type Address {
addressLines: [String!]!
city: String!
postalCode: String!
Country: String!
}
query {
companies {
name
locations {
name
}
}
}
41. 32
GraphQL Schnelleinstieg: Queries
Eine Query definiert, welche Daten aus dem Grafen
ausgelesen werden sollen
type Query {
companies: [Company!]!
company(id: ID!): Company
}
type Company {
id: ID!
name: String!
locations: [Location!]!
}
type Location {
name: String!
address: Address
}
type Address {
addressLines: [String!]!
city: String!
postalCode: String!
Country: String!
}
query {
companies {
name
locations {
name
}
}
}
{
"data": {
"companies": [
{
"name": "Apple",
"locations": [
{
"name": "Campus"
}
]
}
]
}
}
42. 32
GraphQL Schnelleinstieg: Queries
Eine Query definiert, welche Daten aus dem Grafen
ausgelesen werden sollen
type Query {
companies: [Company!]!
company(id: ID!): Company
}
type Company {
id: ID!
name: String!
locations: [Location!]!
}
type Location {
name: String!
address: Address
}
type Address {
addressLines: [String!]!
city: String!
postalCode: String!
Country: String!
}
query {
companies {
name
locations {
name
}
}
}
{
"data": {
"companies": [
{
"name": "Apple",
"locations": [
{
"name": "Campus"
}
]
}
]
}
}
Ein GraphQL Server antwortet nur mit den
Daten, die explizit angefordert wurden
64. 45
Klassische Architekturen
• Es gibt ein zentrales Daten-Modell
• Der Zugriff auf alle relevanten Daten der
Applikation erfolgt über das Daten-Modell
• Das Daten-Modell wird sowohl für
schreibende als auch lesende Zugriffe
verwendet
• Damit verbunden muss dieses Datenmodell
auch alle Use Cases abbilden können
Data Model
Data
Store
W
r
i
t
e
R
e
a
d
65. 46
Command & Query Responsibility Separation
Write
Data Model
Data
Store
Write
(Comma
nd)
66. 46
Command & Query Responsibility Separation
• Mit CQRS, sind schreibende (commands)
und lesende (queries) Zugriffe getrennt
• Commands definieren, welche Änderung
durchgeführt werden sollen
• Die zugehörige Command Handler
implementiert die fachliche Logik
Write
Data Model
Data
Store
Write
(Comma
nd)
67. 46
Command & Query Responsibility Separation
• Mit CQRS, sind schreibende (commands)
und lesende (queries) Zugriffe getrennt
• Commands definieren, welche Änderung
durchgeführt werden sollen
• Die zugehörige Command Handler
implementiert die fachliche Logik
• War ein Command erfolgreich, wird aus
dem neuen Zustand ein Read Model
abgeleitet (Projection)
Write
Data Model
Data
Store
Write
(Comma
nd)
Read
Data Model
Data
Store
Read
(Query
Projection
68. 47
Read Model Projections
• Es gibt keine zwingende 1:1
Beziehung zwischen Write und Read
Model
• Es können beliebig viele Read
Models existieren
• Ein Read Model kann für einen
spezifischen Einsatzzweck optimiert
werden
Read
Data Model Y
Data
Store
Read
(Query
Read
Data Model X
Data
Store
Read
(Query
Projection
70. 49
Sequenz der Events
• Beim Event Sourcing wird der Zustand der Daten in
Form einer Sequenz von Events abgebildet
• Diese Events werden nicht geändert
• Änderungen erfolgen durch das Hinzufügen neuer
Events
71. 49
Sequenz der Events
• Beim Event Sourcing wird der Zustand der Daten in
Form einer Sequenz von Events abgebildet
• Diese Events werden nicht geändert
• Änderungen erfolgen durch das Hinzufügen neuer
Events
{
"FirstName": "Maria",
"LastName": "Strong"
}
CreateContactEvent
72. 49
Sequenz der Events
• Beim Event Sourcing wird der Zustand der Daten in
Form einer Sequenz von Events abgebildet
• Diese Events werden nicht geändert
• Änderungen erfolgen durch das Hinzufügen neuer
Events
{
"FirstName": "Maria",
"LastName": "Strong"
}
CreateContactEvent
{
"LastName": "Fisher"
}
NameChangedEvent
73. 49
Sequenz der Events
• Beim Event Sourcing wird der Zustand der Daten in
Form einer Sequenz von Events abgebildet
• Diese Events werden nicht geändert
• Änderungen erfolgen durch das Hinzufügen neuer
Events
{
"FirstName": "Maria",
"LastName": "Strong"
}
CreateContactEvent
{
"LastName": "Fisher"
}
NameChangedEvent
{
"email": "m.f@aol.com"
}
EMailAddressChangedEvent
76. 51
Command Handling mit Events
Write Model
Command
Handling
Logic
Event
Store
1
1. Schreiboperation wird
gestartet (Command)
77. 51
Command Handling mit Events
Write Model
Command
Handling
Logic
Event
Store
1
2
1. Schreiboperation wird
gestartet (Command)
2. Der Zustand des Write Model
wird aus der Sequenz der
Events wiederhergestellt
(sourced)
78. 51
Command Handling mit Events
Write Model
Command
Handling
Logic
Event
Store
1
2
3
1. Schreiboperation wird
gestartet (Command)
2. Der Zustand des Write Model
wird aus der Sequenz der
Events wiederhergestellt
(sourced)
3. Die Command Handling
Logic vergleicht das
Command mit dem Write
Model und führt
79. 51
Command Handling mit Events
Write Model
Command
Handling
Logic
Event
Store
1
2
3
4
1. Schreiboperation wird
gestartet (Command)
2. Der Zustand des Write Model
wird aus der Sequenz der
Events wiederhergestellt
(sourced)
3. Die Command Handling
Logic vergleicht das
Command mit dem Write
Model und führt
4. Änderungen werden als
Events abgebildet und in
einem Event Store
gespeichert
81. 52
Mehr Möglichkeiten mit Projections
Write Model
Command
Handling
Logic
Event
Store
1
1. Events werden, wie
bereits beschrieben, im
Event Store gespeichert
82. 52
Mehr Möglichkeiten mit Projections
Write Model
Command
Handling
Logic
Event
Store
2
1
1. Events werden, wie
bereits beschrieben, im
Event Store gespeichert
2. Bei der erfolgreichen
Persistierung, wird der
Event auch über einen
Event Bus verteilt
Event Bus
83. 52
Mehr Möglichkeiten mit Projections
Write Model
Command
Handling
Logic
Event
Store
2
3
1
1. Events werden, wie
bereits beschrieben, im
Event Store gespeichert
2. Bei der erfolgreichen
Persistierung, wird der
Event auch über einen
Event Bus verteilt
3. Basierend auf diesen
Events, können viele
verschiedene
Projektionen erstellt oder
auch andere Integration
geschaffen werden
Event Bus
Read Model X Read Model Y Read Model Z
eMail
Notification
Analytics
101. 58
@Controller
class MutationController(
private val gateway: CommandGateway
) {
@MutationMapping
fun issueCard(@Argument amount: Int): IssueCardResult {
val id = UUID.randomUUID().toString()
gateway.sendAndWait<Unit>(
IssueCardCommand(
id = id,
amount = amount
)
)
return IssueCardResult(id, amount)
}
}
type Mutation {
issueCard(amount: Int!): IssueCardResult
# ...
}
102. 59
@Controller
class MutationController(
private val gateway: CommandGateway
) {
@MutationMapping
fun issueCard(@Argument amount: Int): IssueCardResult {
val id = UUID.randomUUID().toString()
gateway.sendAndWait<Unit>(
IssueCardCommand(
id = id,
amount = amount
)
)
return IssueCardResult(id, amount)
}
}
type Mutation {
issueCard(amount: Int!): IssueCardResult
# ...
} • Die Spring Boot Integration
für GraphQL basiert auf
dem Controller-Konzept
103. 60
@Controller
class MutationController(
private val gateway: CommandGateway
) {
@MutationMapping
fun issueCard(@Argument amount: Int): IssueCardResult {
val id = UUID.randomUUID().toString()
gateway.sendAndWait<Unit>(
IssueCardCommand(
id = id,
amount = amount
)
)
return IssueCardResult(id, amount)
}
}
type Mutation {
issueCard(amount: Int!): IssueCardResult
# ...
} • Die Spring Boot Integration
für GraphQL basiert auf
dem Controller-Konzept
104. 61
@Controller
class MutationController(
private val gateway: CommandGateway
) {
@MutationMapping
fun issueCard(@Argument amount: Int): IssueCardResult {
val id = UUID.randomUUID().toString()
gateway.sendAndWait<Unit>(
IssueCardCommand(
id = id,
amount = amount
)
)
return IssueCardResult(id, amount)
}
}
type Mutation {
issueCard(amount: Int!): IssueCardResult
# ...
} • Die Spring Boot Integration
für GraphQL basiert auf
dem Controller-Konzept
• GraphQL-Spezifische
Mapping-Annotationen
definieren die Handler
105. 62
@Controller
class MutationController(
private val gateway: CommandGateway
) {
@MutationMapping
fun issueCard(@Argument amount: Int): IssueCardResult {
val id = UUID.randomUUID().toString()
gateway.sendAndWait<Unit>(
IssueCardCommand(
id = id,
amount = amount
)
)
return IssueCardResult(id, amount)
}
}
type Mutation {
issueCard(amount: Int!): IssueCardResult
# ...
} • Die Spring Boot Integration
für GraphQL basiert auf
dem Controller-Konzept
• GraphQL-Spezifische
Mapping-Annotationen
definieren die Handler
• Zentrales Element sind die
Commands
106. 63
@Controller
class MutationController(
private val gateway: CommandGateway
) {
@MutationMapping
fun issueCard(@Argument amount: Int): IssueCardResult {
val id = UUID.randomUUID().toString()
gateway.sendAndWait<Unit>(
IssueCardCommand(
id = id,
amount = amount
)
)
return IssueCardResult(id, amount)
}
}
type Mutation {
issueCard(amount: Int!): IssueCardResult
# ...
} • Die Spring Boot Integration
für GraphQL basiert auf
dem Controller-Konzept
• GraphQL-Spezifische
Mapping-Annotationen
definieren die Handler
• Zentrales Element sind die
Commands
• Commands werden über
den CommandGateway
verschickt
107. 64
@Controller
class MutationController(
private val gateway: CommandGateway
) {
@MutationMapping
fun issueCard(@Argument amount: Int): IssueCardResult {
val id = UUID.randomUUID().toString()
gateway.sendAndWait<Unit>(
IssueCardCommand(
id = id,
amount = amount
)
)
return IssueCardResult(id, amount)
}
}
type Mutation {
issueCard(amount: Int!): IssueCardResult
# ...
} • Die Spring Boot Integration
für GraphQL basiert auf
dem Controller-Konzept
• GraphQL-Spezifische
Mapping-Annotationen
definieren die Handler
• Zentrales Element sind die
Commands
• Commands werden über
den CommandGateway
verschickt
108. 65
@MutationMapping
fun redeemCard(
@Argument cardId: String,
@Argument amount: Int): RedeemCardResult {
val transaction =
gateway.sendAndWait<GiftCardTransaction>(
RedeemCardCommand(
id = cardId,
amount = amount
)
)
return RedeemCardResult(transaction.remainingAmount)
}
type Mutation {
redeemCard(cardId: ID!, amount: Int!): RedeemCardResult
# ...
}
110. 67
var amount by Delegates.notNull<Int>()
var revoked = false
@CommandHandler
fun handle(cmd: RedeemCardCommand): GiftCardTransaction {
if (revoked)
throw IllegalStateException("card has been revoked")
if (cmd.amount <= 0)
throw IllegalArgumentException(
"invalid amount ${cmd.amount}")
if (cmd.amount > amount)
throw IllegalArgumentException(
„insufficient card credit")
val transactionId = newTransactionId()
val e = RedeemedEvent(
id = id,
transactionId = transactionId,
amount = -cmd.amount,
remainingAmount = amount - cmd.amount
)
applyEvent(e)
return e
}
111. 68
var amount by Delegates.notNull<Int>()
var revoked = false
@CommandHandler
fun handle(cmd: RedeemCardCommand): GiftCardTransaction {
if (revoked)
throw IllegalStateException("card has been revoked")
if (cmd.amount <= 0)
throw IllegalArgumentException(
"invalid amount ${cmd.amount}")
if (cmd.amount > amount)
throw IllegalArgumentException(
„insufficient card credit")
val transactionId = newTransactionId()
val e = RedeemedEvent(
id = id,
transactionId = transactionId,
amount = -cmd.amount,
remainingAmount = amount - cmd.amount
)
applyEvent(e)
return e
}
• Der Command Handler führ
eine Validierung des
Commands durch
112. 69
var amount by Delegates.notNull<Int>()
var revoked = false
@CommandHandler
fun handle(cmd: RedeemCardCommand): GiftCardTransaction {
if (revoked)
throw IllegalStateException("card has been revoked")
if (cmd.amount <= 0)
throw IllegalArgumentException(
"invalid amount ${cmd.amount}")
if (cmd.amount > amount)
throw IllegalArgumentException(
„insufficient card credit")
val transactionId = newTransactionId()
val e = RedeemedEvent(
id = id,
transactionId = transactionId,
amount = -cmd.amount,
remainingAmount = amount - cmd.amount
)
applyEvent(e)
return e
}
• Der Command Handler führ
eine Validierung des
Commands durch
• Nach erfolgter Validierung
wird ein Event mit dem
neuen Zustand erzeugt
113. 70
var amount by Delegates.notNull<Int>()
var revoked = false
@CommandHandler
fun handle(cmd: RedeemCardCommand): GiftCardTransaction {
if (revoked)
throw IllegalStateException("card has been revoked")
if (cmd.amount <= 0)
throw IllegalArgumentException(
"invalid amount ${cmd.amount}")
if (cmd.amount > amount)
throw IllegalArgumentException(
„insufficient card credit")
val transactionId = newTransactionId()
val e = RedeemedEvent(
id = id,
transactionId = transactionId,
amount = -cmd.amount,
remainingAmount = amount - cmd.amount
)
applyEvent(e)
return e
}
• Der Command Handler führ
eine Validierung des
Commands durch
• Nach erfolgter Validierung
wird ein Event mit dem
neuen Zustand erzeugt
116. 73
@Entity
data class GiftCardTransactionEntity(
@Id
@GeneratedValue
var id: Long? = null,
var giftCardId: String,
var transactionId: Int,
var amount: Int,
@Column(length = 100)
var description: String
)
@Entity
data class GiftCardEntity(
@Id
var id: String,
var revoked: Boolean = false,
var amount: Int = 0
)
type Query {
giftCard(id: ID!): GiftCard
}
type GiftCard {
id: ID!
amount: Int!
revoked: Boolean!
transactions: [GiftCardTransaction!]!
}
type GiftCardTransaction {
transactionId: ID!
amount: Int!
description: String!
}
117. 74
class GiftCardTransactionRepositoryUpdater(private val repo: GiftCardTransactionRepository) {
@EventHandler
fun on(e: CardIssuedEvent) {
// we create a "virtual" transaction that represents the issuing
repo.save(
GiftCardTransactionEntity(
giftCardId = e.id,
transactionId = 0,
amount = e.amount,
remainingAmount = e.amount,
description = "Gift Card issued"
)
)
}
@EventHandler
fun on(e: GiftCardTransaction) {
repo.save(
GiftCardTransactionEntity(
giftCardId = e.id,
transactionId = e.transactionId,
amount = e.amount,
description = when(e) {
is RedeemedEvent -> "Redeemed"
is CreditIncreasedEvent -> "Credit increased"
is TransactionRevokedEvent -> "Transaction ${e.revokedTransactionId} revoked"
else -> ""
}
)
)
}
}
118. 75
@Controller
class QueryController(
private val giftCardRepository: GiftCardRepository,
private val giftCardTransactionRepository: GiftCardTransactionRepository
) {
@QueryMapping
fun giftCard(@Argument id: String) = giftCardRepository
.findById(id).orElseThrow {
GraphqlErrorException.Builder()
.errorClassification(ErrorType.NOT_FOUND)
.message("Gift card with id $id not found")
.build()
}
@SchemaMapping(typeName = "GiftCard", field = "transactions")
fun transactions(parent: GiftCardEntity) =
giftCardTransactionRepository
.findByGiftCardIdOrderByTransactionId(parent.id)
}
119. 76
Thank you for listening!
Any Questions?
https://blog.digitalfrontiers.de
https://www.digitalfrontiers.de
@tellme_francois
github.com/fernanfs
François Fernandès
Senior Solution Architect
Photo by Matt Walsh on Unsplash