Task: A completed guide for graphql with query, mutation for user and acccount and with entity and relationships and configs and add schema.graphqls for user and account too in graphql demo

This commit is contained in:
Sambo Chea 2021-08-07 12:20:52 +07:00
parent 7293a05766
commit 66defb6669
22 changed files with 331 additions and 26 deletions

View File

@ -19,6 +19,7 @@ repositories {
extra["graphqlVersion"] = "11.0.0"
dependencies {
implementation("com.graphql-java:graphql-java-extended-scalars:16.0.0")
implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:${property("graphqlVersion")}")
implementation("com.graphql-java-kickstart:playground-spring-boot-starter:${property("graphqlVersion")}")
implementation("com.graphql-java-kickstart:voyager-spring-boot-starter:${property("graphqlVersion")}")

View File

@ -0,0 +1,15 @@
package com.cubetiqs.graphql.demo.config
import graphql.scalars.ExtendedScalars
import graphql.schema.idl.RuntimeWiring
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class GraphQLConfig {
@Bean
fun extendsScalars(): RuntimeWiring.Builder {
return RuntimeWiring.newRuntimeWiring()
.scalar(ExtendedScalars.DateTime)
}
}

View File

@ -0,0 +1,8 @@
package com.cubetiqs.graphql.demo.config
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.annotation.EnableTransactionManagement
@Configuration
@EnableTransactionManagement
class ManagementConfig

View File

@ -0,0 +1,12 @@
package com.cubetiqs.graphql.demo.context
import org.springframework.core.annotation.AliasFor
import org.springframework.stereotype.Component
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@Component
annotation class GMutation(
@get:AliasFor(annotation = Component::class)
val value: String = "",
)

View File

@ -1,7 +1,10 @@
package com.cubetiqs.graphql.demo.domain
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.io.Serializable
import javax.persistence.EntityListeners
import javax.persistence.MappedSuperclass
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class AbstractEntity<T, ID : Serializable> : Serializable

View File

@ -0,0 +1,5 @@
package com.cubetiqs.graphql.demo.domain
import java.io.Serializable
abstract class AbstractInput<T> : Serializable

View File

@ -1,6 +1,8 @@
package com.cubetiqs.graphql.demo.domain.account
import com.cubetiqs.graphql.demo.domain.AbstractEntity
import com.cubetiqs.graphql.demo.domain.user.User
import com.fasterxml.jackson.annotation.JsonBackReference
import org.hibernate.Hibernate
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
@ -9,41 +11,47 @@ import java.util.*
import javax.persistence.*
@Entity
@Table(
name = "accounts", indexes = [
Index(name = "idx_account_id", columnList = "id")
]
)
@EntityListeners(AccountEntityListener::class)
@Table(name = "accounts", indexes = [
Index(name = "idx_account_code", columnList = "code")
])
@EntityListeners(value = [AccountEntityListener::class])
open class Account(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
open var id: Long? = null,
@Column(length = 9)
open var code: String? = null,
@Column
var balance: BigDecimal = BigDecimal.ZERO,
open var balance: BigDecimal = BigDecimal.ZERO,
@Column
var currentBalance: BigDecimal = BigDecimal.ZERO,
open var currentBalance: BigDecimal = BigDecimal.ZERO,
@Column(length = 10)
@Enumerated(EnumType.STRING)
var accountType: AccountType = AccountType.BASIC,
open var type: AccountType = AccountType.BASIC,
@Column(length = 3)
@Enumerated(EnumType.STRING)
var currency: AccountCurrency = AccountCurrency.USD,
open var currency: AccountCurrency = AccountCurrency.USD,
@Version
var version: Long? = null,
open var version: Long? = null,
@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
var createdDate: Date? = null,
open var createdDate: Date? = null,
@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
var updatedDate: Date? = null,
open var updatedDate: Date? = null,
@JsonBackReference
@ManyToOne(fetch = FetchType.LAZY,cascade = [CascadeType.REFRESH, CascadeType.DETACH])
@JoinColumn(name = "user_id")
open var user: User? = null,
) : AbstractEntity<Account, Long>() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -57,6 +65,6 @@ open class Account(
@Override
override fun toString(): String {
return this::class.simpleName + "(id = $id , balance = $balance , currentBalance = $currentBalance , accountType = $accountType , currency = $currency , version = $version , createdDate = $createdDate , updatedDate = $updatedDate )"
return this::class.simpleName + "(id = $id , balance = $balance , currentBalance = $currentBalance , type = $type , currency = $currency , version = $version , createdDate = $createdDate , updatedDate = $updatedDate , user = ${user?.id} )"
}
}

View File

@ -1,10 +1,25 @@
package com.cubetiqs.graphql.demo.domain.account
import java.util.*
import javax.persistence.PostPersist
import javax.persistence.PrePersist
import javax.persistence.PreUpdate
class AccountEntityListener {
@PrePersist
fun beforeSave(account: Account) {
if (account.createdDate == null) {
account.createdDate = Date()
}
}
@PreUpdate
fun beforeUpdate(account: Account) {
account.updatedDate = Date()
}
@PostPersist
fun afterSaved(account: Account) {
account.code = account.id.toString().padStart(9, '0')
}
}

View File

@ -0,0 +1,7 @@
package com.cubetiqs.graphql.demo.domain.account
data class AccountInput(
var userId: Long? = null,
var type: AccountType? = null,
var currency: AccountCurrency? = null,
)

View File

@ -0,0 +1,10 @@
package com.cubetiqs.graphql.demo.domain.account
object AccountMapper {
fun fromInputToAccount(input: AccountInput): Account {
return Account(
type = input.type ?: AccountType.BASIC,
currency = input.currency ?: AccountCurrency.USD,
)
}
}

View File

@ -1,6 +1,9 @@
package com.cubetiqs.graphql.demo.domain.user
import com.cubetiqs.graphql.demo.domain.AbstractEntity
import com.cubetiqs.graphql.demo.domain.account.Account
import com.fasterxml.jackson.annotation.JsonManagedReference
import org.hibernate.Hibernate
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.util.*
@ -9,43 +12,64 @@ import javax.persistence.*
@Entity
@Table(
name = "users", indexes = [
Index(name = "idx_user_id", columnList = "id"),
Index(name = "idx_user_code", columnList = "code"),
Index(name = "idx_user_username", columnList = "username"),
]
)
@EntityListeners(UserEntityListener::class)
@EntityListeners(value = [UserEntityListener::class])
open class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
open var id: Long? = null,
@Column(unique = true, length = 65, nullable = false)
var code: String? = null,
open var code: String? = null,
@Column(unique = true, length = 35, nullable = false)
var username: String? = null,
open var username: String? = null,
@Column(length = 100)
var password: String? = null,
open var password: String? = null,
@Column(length = 50)
var name: String? = null,
open var name: String? = null,
@Version
var version: Long? = null,
open var version: Long? = null,
@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
var createdDate: Date? = null,
open var createdDate: Date? = null,
@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
var updatedDate: Date? = null,
open var updatedDate: Date? = null,
@JsonManagedReference
@OneToMany(fetch = FetchType.LAZY,mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true)
open var accounts: MutableSet<Account> = mutableSetOf(),
@Basic
open var enabled: Boolean = true,
) : AbstractEntity<User, Long>() {
@Transient
fun updatePassword(newPassword: String) {
// hash it here
this.password = newPassword
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false
other as User
return id != null && id == other.id
}
override fun hashCode(): Int = 562048007
@Override
override fun toString(): String {
return this::class.simpleName + "(id = $id , code = $code , username = $username , password = $password , name = $name , version = $version , createdDate = $createdDate , updatedDate = $updatedDate , enabled = $enabled )"
}
}

View File

@ -2,10 +2,21 @@ package com.cubetiqs.graphql.demo.domain.user
import java.util.*
import javax.persistence.PrePersist
import javax.persistence.PreUpdate
class UserEntityListener {
@PrePersist
fun beforeSave(user: User) {
user.id = null
user.code = UUID.randomUUID().toString()
if (user.createdDate == null) {
user.createdDate = Date()
}
}
@PreUpdate
fun beforeUpdate(user: User) {
user.updatedDate = Date()
}
}

View File

@ -0,0 +1,10 @@
package com.cubetiqs.graphql.demo.domain.user
import com.cubetiqs.graphql.demo.domain.AbstractInput
data class UserInput(
val username: String? = null,
val password: String? = null,
val name: String? = null,
val enabled: Boolean? = null,
) : AbstractInput<UserInput>()

View File

@ -0,0 +1,12 @@
package com.cubetiqs.graphql.demo.domain.user
object UserMapper {
fun fromInputToUser(input: UserInput): User {
return User(
username = input.username,
password = input.password,
name = input.name,
enabled = input.enabled ?: true,
)
}
}

View File

@ -0,0 +1,26 @@
package com.cubetiqs.graphql.demo.mutation
import com.cubetiqs.graphql.demo.context.GMutation
import com.cubetiqs.graphql.demo.domain.account.Account
import com.cubetiqs.graphql.demo.domain.account.AccountInput
import com.cubetiqs.graphql.demo.domain.account.AccountMapper
import com.cubetiqs.graphql.demo.repository.AccountRepository
import com.cubetiqs.graphql.demo.repository.UserRepository
import graphql.kickstart.tools.GraphQLMutationResolver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
@GMutation
class AccountMutation @Autowired constructor(
private val accountRepository: AccountRepository,
private val userRepository: UserRepository,
) : GraphQLMutationResolver {
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun openAccount(input: AccountInput): Account {
val account = AccountMapper.fromInputToAccount(input)
val user = userRepository.findById(input.userId ?: 0).orElse(null) ?: throw Exception("User not found to open an account!")
account.user = user
return accountRepository.save(account)
}
}

View File

@ -0,0 +1,22 @@
package com.cubetiqs.graphql.demo.mutation
import com.cubetiqs.graphql.demo.context.GMutation
import com.cubetiqs.graphql.demo.domain.user.User
import com.cubetiqs.graphql.demo.domain.user.UserInput
import com.cubetiqs.graphql.demo.domain.user.UserMapper
import com.cubetiqs.graphql.demo.repository.UserRepository
import graphql.kickstart.tools.GraphQLMutationResolver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
@GMutation
class UserMutation @Autowired constructor(
private val userRepository: UserRepository,
) : GraphQLMutationResolver {
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun createUser(input: UserInput): User {
val user = UserMapper.fromInputToUser(input)
return userRepository.save(user)
}
}

View File

@ -0,0 +1,18 @@
package com.cubetiqs.graphql.demo.query
import com.cubetiqs.graphql.demo.context.GQuery
import com.cubetiqs.graphql.demo.domain.account.Account
import com.cubetiqs.graphql.demo.repository.AccountRepository
import graphql.kickstart.tools.GraphQLQueryResolver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.Pageable
@GQuery
class AccountQuery @Autowired constructor(
private val accountRepository: AccountRepository,
) : GraphQLQueryResolver {
fun fetchAccounts(): Collection<Account> {
val accounts = accountRepository.findAll(Pageable.unpaged())
return accounts.content
}
}

View File

@ -0,0 +1,18 @@
package com.cubetiqs.graphql.demo.query
import com.cubetiqs.graphql.demo.context.GQuery
import com.cubetiqs.graphql.demo.domain.user.User
import com.cubetiqs.graphql.demo.repository.UserRepository
import graphql.kickstart.tools.GraphQLQueryResolver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.Pageable
@GQuery
class UserQuery @Autowired constructor(
private val userRepository: UserRepository,
) : GraphQLQueryResolver {
fun fetchUsers(): Collection<User> {
val users = userRepository.queryAllByEnabledIsTrue(Pageable.unpaged())
return users.content
}
}

View File

@ -0,0 +1,8 @@
package com.cubetiqs.graphql.demo.repository
import com.cubetiqs.graphql.demo.domain.account.Account
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface AccountRepository : JpaRepository<Account, Long>

View File

@ -0,0 +1,14 @@
package com.cubetiqs.graphql.demo.repository
import com.cubetiqs.graphql.demo.domain.user.User
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
@Repository
interface UserRepository : JpaRepository<User, Long> {
@Query("select u from User u where u.enabled = true")
fun queryAllByEnabledIsTrue(pageable: Pageable): Page<User>
}

View File

@ -9,8 +9,14 @@ spring:
password: ${DB_PASSWORD:your-password}
driver-class-name: org.postgresql.Driver
jpa:
database: postgresql
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
ddl-auto: ${HIBERNATE_DDL:update}
show-sql: ${JPA_SHOW_SQL:true}
properties:
hibernate:
enable_lazy_load_no_trans: ${HIBERNATE_LAZY_NO_TRANS:true}
# Spring Boot Actuator
management:

View File

@ -1,7 +1,59 @@
enum AccountType {
BASIC
PREMIUM
BUSINESS
}
enum AccountCurrency {
USD
KHR
}
type User {
id: ID
code: String
username: String
name: String
enabled: Boolean
}
type Account {
id: ID
code: String
balance: Float
currentBalance: Float
type: AccountType
currency: AccountCurrency
user: User!
}
input UserInput {
username: String
password: String
name: String
enabled: Boolean
}
input AccountInput {
userId: Int
type: AccountType
currency: AccountCurrency
}
type Query {
hello: String
fetchUsers: [User]!
fetchAccounts: [Account]!
}
type Subscription {
hello: Int
}
type Mutation {
createUser(input: UserInput): User!
openAccount(input: AccountInput): Account!
}