Compare commits

..

No commits in common. "main" and "kickstart" have entirely different histories.

78 changed files with 249 additions and 12771 deletions

View File

@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gradle" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

26
.gitignore vendored
View File

@ -1,4 +1,4 @@
../HELP.md
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
@ -35,27 +35,3 @@ out/
### VS Code ###
.vscode/
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

8
.gitmodules vendored
View File

@ -1,8 +0,0 @@
[submodule "cubetiq-security-core"]
path = cubetiq-security-core
url = https://git.cubetiqs.com/cubetiq/cubetiq-security-core.git
branch = main
[submodule "cubetiq-security-web"]
path = cubetiq-security-web
url = https://git.cubetiqs.com/cubetiq/cubetiq-security-web.git
branch = main

View File

@ -1,7 +1,7 @@
# GraphQL & Spring Boot (Demo Project)
- Spring Boot
- DGS Framework
- Kotlin
- Spring Boot (2.6)
- GraphQL (11)
- Kotlin (1.5.21)
# Contributors
- Sambo Chea <sombochea@cubetiqs.com>
- Sambo Chea <sombochea@cubetiqs.com>

View File

@ -1,45 +1,53 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.0.2" apply false
id("io.spring.dependency-management") version "1.1.0" apply false
kotlin("jvm") version "1.8.0" apply false
kotlin("plugin.spring") version "1.8.0" apply false
kotlin("plugin.jpa") version "1.8.0" apply false
id("com.netflix.dgs.codegen") version "5.6.6" apply false
id("org.springframework.boot") version "2.6.0-SNAPSHOT"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.5.21"
kotlin("plugin.spring") version "1.5.21"
kotlin("plugin.jpa") version "1.5.21"
}
allprojects {
repositories {
mavenCentral()
}
group = "com.cubetiqs"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
group = "com.cubetiqs"
version = "0.0.1-SNAPSHOT"
val javaVersion = "17"
tasks.withType<JavaCompile> {
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = javaVersion
}
}
repositories {
maven { url = uri("https://m.ctdn.net") }
}
subprojects {
apply {
plugin("io.spring.dependency-management")
}
extra["graphqlVersion"] = "11.0.0"
the<io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension>().apply {
imports {
mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
}
}
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")}")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
testImplementation("com.graphql-java-kickstart:graphql-spring-boot-starter-test:${property("graphqlVersion")}")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}

23
client/.gitignore vendored
View File

@ -1,23 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1,3 +0,0 @@
# React Client App
- Apollo Client
- GraphQL

View File

@ -1,17 +0,0 @@
const CracoLessPlugin = require('craco-less');
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { '@primary-color': '#7c2cdb' },
javascriptEnabled: true,
},
},
},
},
],
};

View File

@ -1,51 +0,0 @@
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.4.5",
"antd": "^5.0.0",
"graphql": "^16.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"subscriptions-transport-ws": "^0.11.0",
"web-vitals": "^3.0.0"
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@craco/craco": "^7.0.0",
"craco-less": "^2.0.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^29.0.0",
"@types/node": "^18.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"react-scripts": "5.0.1",
"typescript": "^4.1.2"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1 +0,0 @@
@import '~antd/dist/antd.less';

View File

@ -1,99 +0,0 @@
import React from 'react';
import './App.less';
import {gql, useQuery, useSubscription} from "@apollo/client";
import { Table } from 'antd';
interface User {
id: number
code: string
name: string
}
interface Account {
id: number
code: string
balance: number
user: User
}
interface AccountResult {
fetchAccounts: Array<Account>
}
const ACCOUNTS = gql`
{
fetchAccounts {
id
code
balance
user {
code
name
}
}
}
`
const SUB_ACCOUNTS = gql`
subscription {
fetchAccounts {
id
code
balance
user {
code
name
}
}
}
`
const HELLO = gql`
subscription {
hello
}
`
const accountColumns = [
{
title: "Account ID",
dataIndex: "id",
key: "id",
},
{
title: "Account Code",
dataIndex: "code",
key: "code",
},
{
title: "User",
dataIndex: ["user", "name"],
key: "user.name",
}
]
function App() {
// const {error, loading, data} = useQuery<AccountResult>(ACCOUNTS)
const {error, loading, data} = useSubscription<AccountResult>(SUB_ACCOUNTS)
// const {error, loading, data} = useSubscription(HELLO)
console.log(data)
return (
<>
<h1>Accounts</h1>
{
loading || !data ? <p>Loading...</p> :
// data.fetchAccounts.map(account => (
// <>
// <div>Account ID: {account.id}</div>
// <div>Account Code: {account.code}</div>
// <div>Account User: {account.user.name}</div>
// </>
// )
// )
<Table dataSource={data.fetchAccounts} columns={accountColumns}/>
// <p>{`${data.hello}`}</p>
}
</>
);
}
export default App;

View File

View File

@ -1,54 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.less';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {split, HttpLink, ApolloClient, InMemoryCache} from '@apollo/client';
import {ApolloProvider} from '@apollo/client/react';
import {getMainDefinition} from '@apollo/client/utilities';
import {WebSocketLink} from '@apollo/client/link/ws';
const APP_HOST = process.env.APP_HOST || 'localhost:8081'
const httpLink = new HttpLink({
uri: `http://${APP_HOST}/graphql`
});
const wsLink = new WebSocketLink({
uri: `ws://${APP_HOST}/subscriptions`,
options: {
reconnect: true
}
});
const splitLink = split(
({query}) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache()
});
ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App/>
</ApolloProvider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

File diff suppressed because it is too large Load Diff

@ -1 +0,0 @@
Subproject commit 612bafe9af476798a40a536c82112c63c8627f4f

@ -1 +0,0 @@
Subproject commit 62d0e718e59af79db15871bde67affd8c38b15e5

View File

@ -1,37 +0,0 @@
../HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

View File

@ -1,43 +0,0 @@
plugins {
id("org.springframework.boot")
id("io.spring.dependency-management")
kotlin("jvm")
kotlin("plugin.spring")
kotlin("plugin.jpa")
id("com.netflix.dgs.codegen")
}
dependencies {
api(project(":cubetiq-security-web"))
implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:6.0.0"))
implementation("com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter")
runtimeOnly("com.netflix.graphql.dgs:graphql-dgs-subscriptions-websockets-autoconfigure")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<com.netflix.graphql.dgs.codegen.gradle.GenerateJavaTask> {
packageName = "com.cubetiqs.graphql.demo.dgmodel"
schemaPaths = mutableListOf("${projectDir}/src/main/resources/schema")
generateClient = true
}

View File

@ -1,20 +0,0 @@
package com.cubetiqs.graphql.demo
import com.cubetiqs.sp.security.util.PasswordUtils
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.security.crypto.password.PasswordEncoder
@SpringBootApplication
class GraphqlDemoApplication @Autowired constructor(
passwordEncoder: PasswordEncoder,
) {
init {
PasswordUtils.setEncoder(passwordEncoder)
}
}
fun main(args: Array<String>) {
runApplication<GraphqlDemoApplication>(*args)
}

View File

@ -1,6 +0,0 @@
package com.cubetiqs.graphql.demo.config
import org.springframework.context.annotation.Configuration
@Configuration
class GraphQLConfig

View File

@ -1,17 +0,0 @@
package com.cubetiqs.graphql.demo.config
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.EnableWebMvc
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(corsRegistry: CorsRegistry) {
corsRegistry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.maxAge(3600)
}
}

View File

@ -1,44 +0,0 @@
package com.cubetiqs.graphql.demo.config
import com.cubetiqs.graphql.demo.security.AuthService
import com.cubetiqs.sp.security.EnableCubetiqSecurityModule
import com.cubetiqs.sp.security.jwt.CubetiqJwtProperties
import com.cubetiqs.sp.security.jwt.JwtSecurityConfigurer
import com.cubetiqs.sp.security.support.AuthenticationExceptionEntryPoint
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@EnableCubetiqSecurityModule
class WebSecurityConfig @Autowired constructor(
private val authService: AuthService,
private val cubetiqJwtProperties: CubetiqJwtProperties,
) {
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder(10)
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
http.exceptionHandling().authenticationEntryPoint(AuthenticationExceptionEntryPoint())
http.apply(JwtSecurityConfigurer(authService, cubetiqJwtProperties))
http.authorizeHttpRequests().anyRequest().permitAll()
return http.build()
}
}

View File

@ -1,8 +0,0 @@
package com.cubetiqs.graphql.demo.context
import com.netflix.graphql.dgs.DgsComponent
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@DgsComponent
annotation class GMutation

View File

@ -1,8 +0,0 @@
package com.cubetiqs.graphql.demo.context
import com.netflix.graphql.dgs.DgsComponent
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@DgsComponent
annotation class GQuery

View File

@ -1,8 +0,0 @@
package com.cubetiqs.graphql.demo.context
import com.netflix.graphql.dgs.DgsComponent
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@DgsComponent
annotation class GSubscription

View File

@ -1,13 +0,0 @@
package com.cubetiqs.graphql.demo.repository
import com.cubetiqs.graphql.demo.domain.account.Account
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Repository
import java.util.concurrent.CompletableFuture
@Repository
interface AccountRepository : JpaRepository<Account, Long> {
@Async
fun readAllBy(): CompletableFuture<Collection<Account>>
}

View File

@ -1,25 +0,0 @@
package com.cubetiqs.graphql.demo.resolver.mutation
import com.cubetiqs.graphql.demo.context.GMutation
import com.cubetiqs.graphql.demo.dgmodel.DgsConstants
import com.cubetiqs.graphql.demo.dgmodel.types.LoginResponse
import com.cubetiqs.graphql.demo.security.AuthService
import com.cubetiqs.sp.security.jwt.util.JwtTokenUtils
import com.netflix.graphql.dgs.DgsMutation
import org.springframework.beans.factory.annotation.Autowired
@GMutation
class LoginMutationResolver {
@Autowired
private lateinit var authService: AuthService
@DgsMutation(field = DgsConstants.MUTATION.Login)
fun login(username: String, password: String): LoginResponse {
val auth = authService.login(username, password)
val token = JwtTokenUtils.createTokens(auth)
return LoginResponse(
accessToken = token.accessToken,
refreshToken = token.refreshToken,
)
}
}

View File

@ -1,22 +0,0 @@
package com.cubetiqs.graphql.demo.resolver.query
import com.cubetiqs.graphql.demo.context.GQuery
import com.cubetiqs.graphql.demo.dgmodel.DgsConstants
import com.netflix.graphql.dgs.DgsQuery
import org.springframework.security.access.prepost.PreAuthorize
import reactor.core.publisher.Mono
import java.util.concurrent.CompletableFuture
@GQuery
class HelloQueryResolver {
@DgsQuery(field = "hello")
fun hello(): CompletableFuture<String> {
return Mono.just("Hello Query...!").toFuture()
}
@PreAuthorize("hasAnyRole('ADMIN')")
@DgsQuery(field = DgsConstants.QUERY.HelloByName)
fun helloByName(name: String): CompletableFuture<String> {
return Mono.just("Hello $name...!").toFuture()
}
}

View File

@ -1,34 +0,0 @@
package com.cubetiqs.graphql.demo.resolver.subscription
import com.cubetiqs.graphql.demo.context.GSubscription
import com.cubetiqs.graphql.demo.domain.account.Account
import com.cubetiqs.graphql.demo.repository.AccountRepository
import com.netflix.graphql.dgs.DgsSubscription
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.reactor.awaitSingle
import org.reactivestreams.Publisher
import org.springframework.beans.factory.annotation.Autowired
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toFlux
import reactor.kotlin.core.publisher.toMono
import java.time.Duration
@GSubscription
class HelloSubscriptionResolver {
@Autowired
private lateinit var accountRepository: AccountRepository
@DgsSubscription(field = "hello")
fun hello(env: DataFetchingEnvironment): Publisher<Int> {
return Flux.range(1, 10).delayElements(Duration.ofSeconds(1))
}
@DgsSubscription(field = "fetchAccounts")
fun fetchAccounts(env: DataFetchingEnvironment): Publisher<Collection<Account>> {
return Flux.generate<Collection<Account>?> { sink ->
sink.next(accountRepository.findAll())
}.delayElements(Duration.ofSeconds(1))
// return Mono.fromFuture(accounts).toFlux().delayElements(Duration.ofSeconds(1))
}
}

View File

@ -1,65 +0,0 @@
package com.cubetiqs.graphql.demo.security
import com.cubetiqs.graphql.demo.domain.user.User
import com.cubetiqs.sp.security.jwt.util.JwtTokenUtils
import com.cubetiqs.sp.security.util.PasswordUtils
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
data class AuthDetails(
private var code: String? = null,
private var name: String? = null,
private var username: String? = null,
private var password: String? = null,
private var authorities: Collection<String>? = null,
private var enabled: Boolean? = null,
) : UserDetails {
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
return authorities?.map { SimpleGrantedAuthority(it) }?.toMutableList() ?: mutableListOf(
SimpleGrantedAuthority(
"USER"
)
)
}
override fun getPassword(): String {
return password ?: ""
}
override fun getUsername(): String {
return username ?: ""
}
override fun isAccountNonExpired(): Boolean {
return true
}
override fun isAccountNonLocked(): Boolean {
return true
}
override fun isCredentialsNonExpired(): Boolean {
return true
}
override fun isEnabled(): Boolean {
return enabled ?: false
}
fun isPasswordValid(password: String): Boolean {
return PasswordUtils.matches(password, this.getPassword())
}
companion object {
fun fromUser(user: User): AuthDetails {
return AuthDetails(
code = user.code,
name = user.name,
username = user.username,
password = user.password,
enabled = user.enabled,
)
}
}
}

View File

@ -1,32 +0,0 @@
package com.cubetiqs.graphql.demo.security
import com.cubetiqs.graphql.demo.repository.UserRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.stereotype.Service
@Service
class AuthService : UserDetailsService {
@Autowired
private lateinit var userRepository: UserRepository
private fun findAuthDetailsByUsername(username: String): AuthDetails {
val user = userRepository.queryByUsername(username).orElse(null) ?: throw Exception("User not found!")
return AuthDetails.fromUser(user)
}
override fun loadUserByUsername(username: String?): UserDetails {
return findAuthDetailsByUsername(username ?: "")
}
fun login(username: String, password: String): AuthDetails {
val auth = findAuthDetailsByUsername(username)
if (auth.isPasswordValid(password)) {
return auth
} else {
throw AccessDeniedException("Username and password is incorrect!")
}
}
}

View File

@ -1 +0,0 @@
application-local.yml

View File

@ -1,26 +0,0 @@
enum AccountType {
BASIC
PREMIUM
BUSINESS
}
enum AccountCurrency {
USD
KHR
}
type Account {
id: ID
code: String
balance: Float
currentBalance: Float
type: AccountType
currency: AccountCurrency
user: User!
}
input AccountInput {
userId: Int
type: AccountType
currency: AccountCurrency
}

View File

@ -1,16 +0,0 @@
input ExpenseFilter {
isIncome: Boolean
}
type Expense {
id: ID
description: String
amount: Int
isIncome: Boolean
}
type ExpensePage {
list: [Expense]
totalPages: Int
currentPage: Int
}

View File

@ -1,26 +0,0 @@
type Query {
hello: String
helloByName(name: String!): String
fetchUsers: [User]!
fetchAccounts: [Account]!
fetchExpenses(filter: ExpenseFilter, pageNumber: Int, pageSize: Int) : ExpensePage
}
type Subscription {
hello: Int
fetchAccounts: [Account]
}
type Mutation {
login(username: String!, password: String!): LoginResponse
createUser(input: UserInput): User!
changeUserPassword(input: UserChangePasswordInput): User!
openAccount(input: AccountInput): Account!
}

View File

@ -1,24 +0,0 @@
type User {
id: ID
code: String
username: String
name: String
enabled: Boolean
}
input UserInput {
username: String
password: String
name: String
enabled: Boolean
}
type LoginResponse {
accessToken: String
refreshToken: String
}
input UserChangePasswordInput {
username: String!
password: String!
}

View File

@ -1,55 +0,0 @@
package com.cubetiqs.graphql.demo
import com.cubetiqs.graphql.demo.resolver.subscription.HelloSubscriptionResolver
import com.netflix.graphql.dgs.DgsQueryExecutor
import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration
import graphql.ExecutionResult
import org.junit.jupiter.api.Test
import org.reactivestreams.Publisher
import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest(
classes = [
DgsAutoConfiguration::class,
HelloSubscriptionResolver::class,
]
)
class GraphqlDemoApplicationTests {
@Autowired
lateinit var dgsQueryExecutor: DgsQueryExecutor
@Test
fun helloSubscription() {
dgsQueryExecutor.execute(
"""
subscription {
hello
}
""".trimIndent()
)
.getData<Publisher<ExecutionResult>>()
.subscribe(object : Subscriber<ExecutionResult> {
override fun onSubscribe(s: Subscription) {
s.request(2)
}
override fun onNext(t: ExecutionResult) {
println(t.getData<Any?>())
}
override fun onError(t: Throwable?) {
}
override fun onComplete() {
println("Hello World")
}
}
)
}
}

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
}

View File

@ -1,3 +1,8 @@
rootProject.name = "spring-graphql-demo"
include("cubetiq-security-core", "cubetiq-security-web", "dgs-graphql")
pluginManagement {
repositories {
maven { url = uri("https://repo.spring.io/milestone") }
maven { url = uri("https://repo.spring.io/snapshot") }
gradlePluginPortal()
}
}
rootProject.name = "graphql-demo"

View File

@ -0,0 +1,11 @@
package com.cubetiqs.graphql.demo
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class GraphqlDemoApplication
fun main(args: Array<String>) {
runApplication<GraphqlDemoApplication>(*args)
}

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

@ -1,10 +1,8 @@
package com.cubetiqs.graphql.demo.config
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.transaction.annotation.EnableTransactionManagement
@Configuration
@EnableTransactionManagement
@EnableAsync
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

@ -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 GQuery(
@get:AliasFor(annotation = Component::class)
val value: String = "",
)

View File

@ -2,8 +2,8 @@ package com.cubetiqs.graphql.demo.domain
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.io.Serializable
import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import javax.persistence.EntityListeners
import javax.persistence.MappedSuperclass
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)

View File

@ -8,7 +8,7 @@ import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.math.BigDecimal
import java.util.*
import jakarta.persistence.*
import javax.persistence.*
@Entity
@Table(name = "accounts", indexes = [

View File

@ -1,9 +1,9 @@
package com.cubetiqs.graphql.demo.domain.account
import java.util.*
import jakarta.persistence.PostPersist
import jakarta.persistence.PrePersist
import jakarta.persistence.PreUpdate
import javax.persistence.PostPersist
import javax.persistence.PrePersist
import javax.persistence.PreUpdate
class AccountEntityListener {
@PrePersist

View File

@ -7,7 +7,7 @@ import org.hibernate.Hibernate
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.util.*
import jakarta.persistence.*
import javax.persistence.*
@Entity
@Table(

View File

@ -1,8 +1,8 @@
package com.cubetiqs.graphql.demo.domain.user
import java.util.*
import jakarta.persistence.PrePersist
import jakarta.persistence.PreUpdate
import javax.persistence.PrePersist
import javax.persistence.PreUpdate
class UserEntityListener {
@PrePersist

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

@ -6,7 +6,6 @@ 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
import java.util.*
@Repository
interface UserRepository : JpaRepository<User, Long> {
@ -15,7 +14,4 @@ interface UserRepository : JpaRepository<User, Long> {
@Query("select (count(u) > 0) from User u where u.username = ?1")
fun existsAllByUsername(username: String): Boolean
@Query("select u from User u where u.username = ?1")
fun queryByUsername(username: String): Optional<User>
}

View File

@ -1,31 +1,25 @@
package com.cubetiqs.graphql.demo.resolver.mutation
import com.cubetiqs.graphql.demo.context.GMutation
import com.cubetiqs.graphql.demo.dgmodel.DgsConstants
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 com.netflix.graphql.dgs.DgsMutation
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 AccountMutationResolver {
@Autowired
private lateinit var accountRepository: AccountRepository
@Autowired
private lateinit var userRepository: UserRepository
@DgsMutation(field = DgsConstants.MUTATION.OpenAccount)
class AccountMutationResolver @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!")
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

@ -1,15 +1,11 @@
package com.cubetiqs.graphql.demo.resolver.mutation
import com.cubetiqs.graphql.demo.context.GMutation
import com.cubetiqs.graphql.demo.dgmodel.DgsConstants
import com.cubetiqs.graphql.demo.dgmodel.types.UserChangePasswordInput
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 com.cubetiqs.sp.security.util.PasswordUtils
import com.netflix.graphql.dgs.DgsMutation
import com.netflix.graphql.dgs.exceptions.DgsEntityNotFoundException
import graphql.kickstart.tools.GraphQLMutationResolver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
@ -17,20 +13,12 @@ import org.springframework.transaction.annotation.Transactional
@GMutation
class UserMutationResolver @Autowired constructor(
private val userRepository: UserRepository,
) {
@DgsMutation(field = DgsConstants.MUTATION.CreateUser)
) : GraphQLMutationResolver {
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun createUser(input: UserInput): User {
if (userRepository.existsAllByUsername(input.username ?: "")) throw DgsEntityNotFoundException("Username has been already existed!")
if (userRepository.existsAllByUsername(input.username ?: "")) throw Exception("Username has been already existed!")
val user = UserMapper.fromInputToUser(input)
return userRepository.save(user)
}
@DgsMutation(field = DgsConstants.MUTATION.ChangeUserPassword)
fun changePassword(input: UserChangePasswordInput): User {
val user = userRepository.queryByUsername(input.username).orElse(null) ?: throw DgsEntityNotFoundException("User not found!")
user.password = PasswordUtils.encode(input.password)
return userRepository.save(user)
}
}

View File

@ -1,18 +1,16 @@
package com.cubetiqs.graphql.demo.resolver.query
import com.cubetiqs.graphql.demo.context.GQuery
import com.cubetiqs.graphql.demo.dgmodel.DgsConstants
import com.cubetiqs.graphql.demo.domain.account.Account
import com.cubetiqs.graphql.demo.repository.AccountRepository
import com.netflix.graphql.dgs.DgsQuery
import graphql.kickstart.tools.GraphQLQueryResolver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.Pageable
@GQuery
class AccountQueryResolver @Autowired constructor(
private val accountRepository: AccountRepository,
) {
@DgsQuery(field = DgsConstants.QUERY.FetchAccounts)
) : GraphQLQueryResolver {
fun fetchAccounts(): Collection<Account> {
val accounts = accountRepository.findAll(Pageable.unpaged())
return accounts.content

View File

@ -0,0 +1,13 @@
package com.cubetiqs.graphql.demo.resolver.query
import com.cubetiqs.graphql.demo.context.GQuery
import graphql.kickstart.tools.GraphQLQueryResolver
import reactor.core.publisher.Mono
import java.util.concurrent.CompletableFuture
@GQuery
class HelloQueryResolver : GraphQLQueryResolver {
fun hello(): CompletableFuture<String> {
return Mono.just("Hello Query...!").toFuture()
}
}

View File

@ -1,18 +1,16 @@
package com.cubetiqs.graphql.demo.resolver.query
import com.cubetiqs.graphql.demo.context.GQuery
import com.cubetiqs.graphql.demo.dgmodel.DgsConstants
import com.cubetiqs.graphql.demo.domain.user.User
import com.cubetiqs.graphql.demo.repository.UserRepository
import com.netflix.graphql.dgs.DgsQuery
import graphql.kickstart.tools.GraphQLQueryResolver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.domain.Pageable
@GQuery
class UserQueryResolver @Autowired constructor(
private val userRepository: UserRepository,
) {
@DgsQuery(field = DgsConstants.QUERY.FetchUsers)
) : GraphQLQueryResolver {
fun fetchUsers(): Collection<User> {
val users = userRepository.queryAllByEnabledIsTrue(Pageable.unpaged())
return users.content

View File

@ -0,0 +1,15 @@
package com.cubetiqs.graphql.demo.resolver.subscription
import graphql.kickstart.tools.GraphQLSubscriptionResolver
import graphql.schema.DataFetchingEnvironment
import org.reactivestreams.Publisher
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import java.time.Duration
@Service
class HelloSubscriptionResolver : GraphQLSubscriptionResolver {
fun hello(env: DataFetchingEnvironment): Publisher<Int> {
return Flux.range(1, 10).delayElements(Duration.ofSeconds(1))
}
}

View File

@ -3,8 +3,6 @@ server:
# Spring Boot
spring:
profiles:
active: ${APP_PROFILE:}
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:graphql-demo}
username: ${DB_USER:your-username}
@ -19,8 +17,7 @@ spring:
properties:
hibernate:
enable_lazy_load_no_trans: ${HIBERNATE_LAZY_NO_TRANS:true}
main:
allow-bean-definition-overriding: true
# Spring Boot Actuator
management:
endpoints:
@ -28,9 +25,15 @@ management:
exposure:
include: health,info,metrics
# DGS GraphQL
dgs:
graphql:
graphiql:
# GraphQL Voyager
voyager:
enabled: true
cdn:
enabled: false
# GraphQL Playground
graphql:
playground:
enabled: true
path: /graphiql
cdn:
enabled: false

View File

@ -0,0 +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!
}

View File

@ -0,0 +1,13 @@
package com.cubetiqs.graphql.demo
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class GraphqlDemoApplicationTests {
@Test
fun contextLoads() {
}
}