Compare commits

..

13 Commits

Author SHA1 Message Date
ea9c700f37 Task: Add query for account with completable future and want to test with flux and fetch accounts with subscriptions 2021-08-08 19:48:50 +07:00
bfff91a8df Task: Add Ant Design and Craco for react scripts ejected 2021-08-08 19:08:10 +07:00
96781943e4 Updated readme 2021-08-08 19:00:13 +07:00
ceaacc9685 Task: Completed login and get token from user and able to change user password by username and updated the security filters and add auth service and auth details. And more add login mutation in resolver 2021-08-08 18:55:52 +07:00
9202c52640 Task: Add security configuration and implement for auth service and configs 2021-08-08 17:40:08 +07:00
0c2669d153 Task: Add and updated module for security in graphql demo 2021-08-08 13:23:01 +07:00
94552197dc Task: Add cubetiq security jwt module 2021-08-08 12:39:19 +07:00
32f356569d Task: Upgraded the dgs graphql to submodules and parent modules for extends projects 2021-08-08 12:33:34 +07:00
83477a9946 Task: Changed types for DgsConstants support 2021-08-08 11:51:24 +07:00
b1d9f8eba6 Updated and add dgs websocket 2021-08-08 11:01:26 +07:00
53d35264e4 Task: Fixed websocket for subscriptions in DGS framework and add netflix dgs subscription websocket autoconfigure 2021-08-08 11:00:39 +07:00
8660f35410 Task: Changed the client to frontend package in graphq demo 2021-08-08 10:39:48 +07:00
626f8b9fc5 Task: Add Netflix DGS framework and DGS codegen for GraphQL and still issued with subscriptions that not work on server and add some configurations and add webflux included web starter and removed all graphql kickstart and add frontend client with apollo client and react in typescript for graphql demo 2021-08-07 19:56:39 +07:00
72 changed files with 13151 additions and 225 deletions

26
.gitignore vendored
View File

@@ -1,4 +1,4 @@
HELP.md ../HELP.md
.gradle .gradle
build/ build/
!gradle/wrapper/gradle-wrapper.jar !gradle/wrapper/gradle-wrapper.jar
@@ -35,3 +35,27 @@ out/
### VS Code ### ### VS Code ###
.vscode/ .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*

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "cubetiq-security-jwt"]
path = cubetiq-security-jwt
url = https://git.cubetiqs.com/CUBETIQ/cubetiq-security-jwt.git

View File

@@ -1,6 +1,6 @@
# GraphQL & Spring Boot (Demo Project) # GraphQL & Spring Boot (Demo Project)
- Spring Boot (2.6) - Spring Boot (2.6)
- GraphQL (11) - DGS Framework (4.5.0)
- Kotlin (1.5.21) - Kotlin (1.5.21)
# Contributors # Contributors

View File

@@ -1,53 +1,45 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("org.springframework.boot") version "2.6.0-SNAPSHOT" id("org.springframework.boot") version "2.5.3" apply false
id("io.spring.dependency-management") version "1.0.11.RELEASE" id("io.spring.dependency-management") version "1.0.11.RELEASE" apply false
kotlin("jvm") version "1.5.21" kotlin("jvm") version "1.5.21" apply false
kotlin("plugin.spring") version "1.5.21" kotlin("plugin.spring") version "1.5.21" apply false
kotlin("plugin.jpa") version "1.5.21" kotlin("plugin.jpa") version "1.5.21" apply false
id("com.netflix.dgs.codegen") version "5.0.5" apply false
} }
group = "com.cubetiqs" allprojects {
version = "0.0.1-SNAPSHOT" repositories {
java.sourceCompatibility = JavaVersion.VERSION_11 maven { url = uri("https://m.ctdn.net") }
}
repositories { group = "com.cubetiqs"
maven { url = uri("https://m.ctdn.net") } version = "0.0.1-SNAPSHOT"
val javaVersion = "11"
tasks.withType<JavaCompile> {
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = javaVersion
}
}
} }
extra["graphqlVersion"] = "11.0.0" subprojects {
apply {
plugin("io.spring.dependency-management")
}
dependencies { the<io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension>().apply {
implementation("com.graphql-java:graphql-java-extended-scalars:16.0.0") imports {
implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:${property("graphqlVersion")}") mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
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 Normal file
View File

@@ -0,0 +1,23 @@
# 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*

3
client/README.md Normal file
View File

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

17
client/craco.config.js Normal file
View File

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

51
client/package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.4.5",
"antd": "^4.16.10",
"graphql": "^15.5.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"subscriptions-transport-ws": "^0.9.19",
"web-vitals": "^1.0.1"
},
"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": "^6.2.0",
"craco-less": "^1.18.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"react-scripts": "4.0.3",
"typescript": "^4.1.2"
}
}

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

43
client/public/index.html Normal file
View File

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

BIN
client/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
client/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"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"
}

3
client/public/robots.txt Normal file
View File

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

1
client/src/App.less Normal file
View File

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

99
client/src/App.tsx Normal file
View File

@@ -0,0 +1,99 @@
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;

0
client/src/index.less Normal file
View File

54
client/src/index.tsx Normal file
View File

@@ -0,0 +1,54 @@
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();

1
client/src/react-app-env.d.ts vendored Normal file
View File

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

View File

@@ -0,0 +1,15 @@
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;

5
client/src/setupTests.ts Normal file
View File

@@ -0,0 +1,5 @@
// 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';

26
client/tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"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"
]
}

12171
client/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

1
cubetiq-security-jwt Submodule

Submodule cubetiq-security-jwt added at d69f52fee0

37
dgs-graphql/.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
../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

@@ -0,0 +1,44 @@
plugins {
id("org.springframework.boot")
id("io.spring.dependency-management")
kotlin("jvm")
kotlin("plugin.spring")
kotlin("plugin.jpa")
id("com.netflix.dgs.codegen")
}
extra["dgsVersion"] = "4.5.0"
dependencies {
api(project(":cubetiq-security-jwt"))
implementation("com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:${property("dgsVersion")}")
runtimeOnly("com.netflix.graphql.dgs:graphql-dgs-subscriptions-websockets-autoconfigure:${property("dgsVersion")}")
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

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

View File

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

View File

@@ -0,0 +1,17 @@
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

@@ -0,0 +1,37 @@
package com.cubetiqs.graphql.demo.config
import com.cubetiqs.graphql.demo.security.AuthService
import com.cubetiqs.security.jwt.AuthenticationExceptionEntryPoint
import com.cubetiqs.security.jwt.JwtSecurityConfigurer
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class WebSecurityConfig : WebSecurityConfigurerAdapter() {
@Autowired
private lateinit var authService: AuthService
override fun configure(http: HttpSecurity) {
http.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
http
.exceptionHandling()
.authenticationEntryPoint(AuthenticationExceptionEntryPoint())
http
.apply(JwtSecurityConfigurer(authService))
http
.authorizeRequests()
.anyRequest().permitAll()
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
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

@@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.util.*
@Repository @Repository
interface UserRepository : JpaRepository<User, Long> { interface UserRepository : JpaRepository<User, Long> {
@@ -14,4 +15,7 @@ interface UserRepository : JpaRepository<User, Long> {
@Query("select (count(u) > 0) from User u where u.username = ?1") @Query("select (count(u) > 0) from User u where u.username = ?1")
fun existsAllByUsername(username: String): Boolean fun existsAllByUsername(username: String): Boolean
@Query("select u from User u where u.username = ?1")
fun queryByUsername(username: String): Optional<User>
} }

View File

@@ -1,25 +1,31 @@
package com.cubetiqs.graphql.demo.resolver.mutation package com.cubetiqs.graphql.demo.resolver.mutation
import com.cubetiqs.graphql.demo.context.GMutation 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.Account
import com.cubetiqs.graphql.demo.domain.account.AccountInput import com.cubetiqs.graphql.demo.domain.account.AccountInput
import com.cubetiqs.graphql.demo.domain.account.AccountMapper import com.cubetiqs.graphql.demo.domain.account.AccountMapper
import com.cubetiqs.graphql.demo.repository.AccountRepository import com.cubetiqs.graphql.demo.repository.AccountRepository
import com.cubetiqs.graphql.demo.repository.UserRepository import com.cubetiqs.graphql.demo.repository.UserRepository
import graphql.kickstart.tools.GraphQLMutationResolver import com.netflix.graphql.dgs.DgsMutation
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@GMutation @GMutation
class AccountMutationResolver @Autowired constructor( class AccountMutationResolver {
private val accountRepository: AccountRepository, @Autowired
private val userRepository: UserRepository, private lateinit var accountRepository: AccountRepository
) : GraphQLMutationResolver {
@Autowired
private lateinit var userRepository: UserRepository
@DgsMutation(field = DgsConstants.MUTATION.OpenAccount)
@Transactional(propagation = Propagation.REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRES_NEW)
fun openAccount(input: AccountInput): Account { fun openAccount(input: AccountInput): Account {
val account = AccountMapper.fromInputToAccount(input) 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 account.user = user
return accountRepository.save(account) return accountRepository.save(account)
} }

View File

@@ -0,0 +1,22 @@
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.security.jwt.util.JwtUtils
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 = JwtUtils.encryptToken(auth)
return LoginResponse(token)
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
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

@@ -0,0 +1,64 @@
package com.cubetiqs.graphql.demo.security
import com.cubetiqs.graphql.demo.domain.user.User
import com.cubetiqs.security.jwt.util.JwtUtils
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 JwtUtils.passwordEncoder().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

@@ -0,0 +1,32 @@
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

@@ -17,7 +17,8 @@ spring:
properties: properties:
hibernate: hibernate:
enable_lazy_load_no_trans: ${HIBERNATE_LAZY_NO_TRANS:true} enable_lazy_load_no_trans: ${HIBERNATE_LAZY_NO_TRANS:true}
main:
allow-bean-definition-overriding: true
# Spring Boot Actuator # Spring Boot Actuator
management: management:
endpoints: endpoints:
@@ -25,15 +26,8 @@ management:
exposure: exposure:
include: health,info,metrics include: health,info,metrics
# GraphQL Voyager # DGS GraphQL
voyager: dgs:
enabled: true graphql:
cdn: graphiql:
enabled: false enabled: true
# GraphQL Playground
graphql:
playground:
enabled: true
cdn:
enabled: false

View File

@@ -0,0 +1,26 @@
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

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

View File

@@ -0,0 +1,26 @@
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

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

View File

@@ -0,0 +1,55 @@
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,8 +1,4 @@
pluginManagement {
repositories {
maven { url = uri("https://repo.spring.io/milestone") }
maven { url = uri("https://repo.spring.io/snapshot") }
gradlePluginPortal()
}
}
rootProject.name = "graphql-demo" rootProject.name = "graphql-demo"
include("dgs-graphql")
include("cubetiq-security-jwt")

View File

@@ -1,15 +0,0 @@
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,12 +0,0 @@
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,12 +0,0 @@
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

@@ -1,8 +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.stereotype.Repository
@Repository
interface AccountRepository : JpaRepository<Account, Long>

View File

@@ -1,13 +0,0 @@
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,15 +0,0 @@
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

@@ -1,59 +0,0 @@
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

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