Remove samples from main
We no longer build the samples in the main branch due to dependency issues between Boot starter dependencies and the project sources. The samples really need to move out to an independent repository. For now, this commit removes them from the main branch. See gh-208
This commit is contained in:
@@ -9,7 +9,7 @@ This project is governed by the [Spring Code of Conduct](CODE_OF_CONDUCT.adoc).
|
||||
## Documentation
|
||||
|
||||
This project has reference documentation ([published](https://docs.spring.io/spring-graphql/docs/current-SNAPSHOT/reference/html/) and [source](spring-graphql-docs/src/docs/asciidoc)), an
|
||||
[API reference](https://docs.spring.io/spring-graphql/docs/current-SNAPSHOT/api/), and [samples](samples).
|
||||
[API reference](https://docs.spring.io/spring-graphql/docs/current-SNAPSHOT/api/). The are [samples](https://github.com/spring-projects/spring-graphql/tree/1.0.x/samples) in the 1.0.x branch that will be [moved out](https://github.com/spring-projects/spring-graphql/issues/208) into a separate repository.
|
||||
|
||||
## Continuous Integration Builds
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
This directory contains samples to test scenarios and features with.
|
||||
|
||||
All samples have integration tests you can run or debug, and expose a GraphiQL page at "/graphiql".
|
||||
|
||||
To run a sample from the command line:
|
||||
```shell script
|
||||
$ ./gradlew :samples:{sample-directory-name}:bootRun
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Spring WebFlux Security Sample Schema",
|
||||
"schemaPath": "src/main/resources/graphql/schema.graphqls",
|
||||
"extensions": {}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
**Spring Security for GraphQL HTTP Endpoint with WebFlux**
|
||||
|
||||
- Spring Security [config](src/main/java/io/spring/sample/graphql/SecurityConfig.java) secures GraphQL HTTP endpoint.
|
||||
- Fine-grained, method-level security on [SalaryService](src/main/java/io/spring/sample/graphql/SalaryService.java).
|
||||
- `AuthenticationException` and `AccessDeniedException` resolved to GraphQL errors.
|
||||
- [Tests](src/test/java/io/spring/sample/graphql/WebFluxSecuritySampleTests.java) with `WebGraphQlTester` and WebFlux without a server.
|
||||
@@ -1,31 +0,0 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version "${bootVersion}"
|
||||
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
|
||||
id 'java'
|
||||
}
|
||||
group = 'com.example'
|
||||
description = "GraphQL webflux security example"
|
||||
sourceCompatibility = '1.8'
|
||||
|
||||
ext['spring-graphql.version'] = version
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.spring.io/milestone' }
|
||||
maven { url 'https://repo.spring.io/snapshot' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-graphql'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
testImplementation project(':spring-graphql')
|
||||
testImplementation project(':spring-graphql-test')
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
}
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
public class Employee {
|
||||
|
||||
private String id;
|
||||
|
||||
private String name;
|
||||
|
||||
public Employee(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class EmployeeService {
|
||||
|
||||
public List<Employee> getAllEmployees() {
|
||||
return Collections.singletonList(new Employee("1", "Andi"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class SalaryController {
|
||||
|
||||
private final EmployeeService employeeService;
|
||||
|
||||
private final SalaryService salaryService;
|
||||
|
||||
public SalaryController(EmployeeService employeeService, SalaryService salaryService) {
|
||||
this.employeeService = employeeService;
|
||||
this.salaryService = salaryService;
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public List<Employee> employees() {
|
||||
return this.employeeService.getAllEmployees();
|
||||
}
|
||||
|
||||
@SchemaMapping
|
||||
public Mono<BigDecimal> salary(Employee employee) {
|
||||
return this.salaryService.getSalaryForEmployee(employee);
|
||||
}
|
||||
|
||||
@MutationMapping
|
||||
public Mono<Void> updateSalary(@Argument("input") SalaryInput salaryInput) {
|
||||
String employeeId = salaryInput.getEmployeeId();
|
||||
BigDecimal salary = salaryInput.getNewSalary();
|
||||
return this.salaryService.updateSalary(employeeId, salary);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class SalaryInput {
|
||||
|
||||
private String employeeId;
|
||||
|
||||
private BigDecimal newSalary;
|
||||
|
||||
public SalaryInput(String employeeId, BigDecimal newSalary) {
|
||||
this.employeeId = employeeId;
|
||||
this.newSalary = newSalary;
|
||||
}
|
||||
|
||||
public String getEmployeeId() {
|
||||
return employeeId;
|
||||
}
|
||||
|
||||
public void setEmployeeId(String employeeId) {
|
||||
this.employeeId = employeeId;
|
||||
}
|
||||
|
||||
public BigDecimal getNewSalary() {
|
||||
return newSalary;
|
||||
}
|
||||
|
||||
public void setNewSalary(BigDecimal newSalary) {
|
||||
this.newSalary = newSalary;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SalaryService {
|
||||
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Mono<BigDecimal> getSalaryForEmployee(Employee employee) {
|
||||
return Mono.just(new BigDecimal("42"));
|
||||
}
|
||||
|
||||
@Secured("ROLE_HR")
|
||||
public Mono<Void> updateSalary(String employeeId, BigDecimal newSalary) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SampleApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
@EnableReactiveMethodSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
|
||||
return http
|
||||
.csrf(c -> c.disable())
|
||||
// Demonstrate that method security works
|
||||
// Best practice to use both for defense in depth
|
||||
.authorizeExchange(requests -> requests.anyExchange().permitAll())
|
||||
.httpBasic(withDefaults())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("deprecation")
|
||||
public MapReactiveUserDetailsService userDetailsService() {
|
||||
User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
|
||||
UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
|
||||
UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build();
|
||||
return new MapReactiveUserDetailsService(rob, admin);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
spring.graphql.websocket.path=/graphql
|
||||
management.endpoints.web.exposure.include=health,metrics,info
|
||||
logging.level.org.springframework.web=debug
|
||||
logging.level.org.springframework.http=debug
|
||||
logging.level.org.springframework.graphql=debug
|
||||
logging.level.org.springframework.security=debug
|
||||
logging.level.reactor.netty=debug
|
||||
@@ -1,22 +0,0 @@
|
||||
type Query {
|
||||
employees: [Employee]
|
||||
}
|
||||
type Mutation {
|
||||
# restricted
|
||||
updateSalary(input: UpdateSalaryInput!): UpdateSalaryPayload
|
||||
}
|
||||
type Employee {
|
||||
id: ID!
|
||||
name: String
|
||||
# restricted
|
||||
salary: String
|
||||
}
|
||||
|
||||
input UpdateSalaryInput {
|
||||
employeeId: ID!
|
||||
newSalary: String!
|
||||
}
|
||||
type UpdateSalaryPayload {
|
||||
success: Boolean!
|
||||
employee: Employee
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.graphql.execution.ErrorType;
|
||||
import org.springframework.graphql.test.tester.WebGraphQlTester;
|
||||
import org.springframework.graphql.test.tester.WebSocketGraphQlTester;
|
||||
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
class WebFluxSecuritySampleTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
private WebSocketGraphQlTester graphQlTester;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
URI url = URI.create("http://localhost:" + this.port + "/graphql");
|
||||
this.graphQlTester = WebSocketGraphQlTester.create(url, new ReactorNettyWebSocketClient());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
this.graphQlTester.stop().block(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void printError() {
|
||||
this.graphQlTester.documentName("employeesNamesAndSalaries")
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy(System.out::println);
|
||||
}
|
||||
|
||||
@Test
|
||||
void anonymousThenUnauthorized() {
|
||||
this.graphQlTester.documentName("employeesNamesAndSalaries")
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy(errors -> {
|
||||
assertThat(errors).hasSize(1);
|
||||
assertThat(errors.get(0).getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void userRoleThenForbidden() {
|
||||
|
||||
WebGraphQlTester authTester = this.graphQlTester.mutate()
|
||||
.headers(headers -> headers.setBasicAuth("rob", "rob"))
|
||||
.build();
|
||||
|
||||
authTester.documentName("employeesNamesAndSalaries")
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy(errors -> {
|
||||
assertThat(errors).hasSize(1);
|
||||
assertThat(errors.get(0).getErrorType()).isEqualTo(ErrorType.FORBIDDEN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void canQueryName() {
|
||||
this.graphQlTester.documentName("employeesNames")
|
||||
.execute()
|
||||
.path("employees[0].name").entity(String.class).isEqualTo("Andi");
|
||||
}
|
||||
|
||||
@Test
|
||||
void canNotQuerySalary() {
|
||||
this.graphQlTester.documentName("employeesNamesAndSalaries")
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy(errors -> {
|
||||
assertThat(errors).hasSize(1);
|
||||
assertThat(errors.get(0).getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED);
|
||||
});
|
||||
}
|
||||
|
||||
@Disabled // This does not work currently
|
||||
@Test
|
||||
void canNotMutateUpdateSalary() {
|
||||
SalaryInput salaryInput = new SalaryInput("1", BigDecimal.valueOf(44));
|
||||
|
||||
this.graphQlTester.documentName("updateSalary")
|
||||
.variable("salaryInput", salaryInput)
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy(errors -> {
|
||||
assertThat(errors).hasSize(1);
|
||||
assertThat(errors.get(0).getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void canQuerySalaryAsAdmin() {
|
||||
|
||||
WebGraphQlTester authTester = this.graphQlTester.mutate()
|
||||
.headers(headers -> headers.setBasicAuth("admin", "admin"))
|
||||
.build();
|
||||
|
||||
authTester.documentName("employeesNamesAndSalaries")
|
||||
.execute()
|
||||
.path("employees[0].name").entity(String.class).isEqualTo("Andi")
|
||||
.path("employees[0].salary").entity(int.class).isEqualTo(42);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidCredentials() {
|
||||
assertThatThrownBy(() ->
|
||||
this.graphQlTester.mutate()
|
||||
.headers(headers -> headers.setBasicAuth("admin", "INVALID"))
|
||||
.build()
|
||||
.documentName("employeesNamesAndSalaries")
|
||||
.executeAndVerify())
|
||||
.hasMessage(
|
||||
"GraphQlTransport error: Invalid handshake response getStatus: 401 Unauthorized; " +
|
||||
"nested exception is io.netty.handler.codec.http.websocketx.WebSocketClientHandshakeException: " +
|
||||
"Invalid handshake response getStatus: 401 Unauthorized");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
query {
|
||||
employees {
|
||||
name
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
query {
|
||||
employees {
|
||||
name,
|
||||
salary
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
mutation updateSalary($salaryInput: UpdateSalaryInput!) {
|
||||
updateSalary(input: $salaryInput) {
|
||||
success
|
||||
employee {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Spring WebFlux WebSocket Sample Schema",
|
||||
"schemaPath": "src/main/resources/graphql/schema.graphqls",
|
||||
"extensions": {}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
**GraphQL WebSocket Endpoint with Spring WebFlux**
|
||||
|
||||
- [Data Controller](src/main/java/io/spring/sample/graphql/SampleController.java) with reactive methods for queries and subscriptions.
|
||||
- [WebFilter](src/main/java/io/spring/sample/graphql/ContextWebFilter.java) to insert Reactor `Context` and check it can be accessed in [DataRepository](src/main/java/io/spring/sample/graphql/DataRepository.java).
|
||||
- [Server tests](src/test/java/io/spring/sample/graphql/WebFluxWebSocketSampleTests.java) without a client.
|
||||
- [Integration tests](src/test/java/io/spring/sample/graphql/WebFluxWebSocketSampleIntegrationTests.java) via `WebSocketGraphQlTester`.
|
||||
|
||||
GraphiQL does not support subscriptions. There is a basic index.html page that logs subscriptions the console.
|
||||
@@ -1,30 +0,0 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version "${bootVersion}"
|
||||
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
|
||||
id 'java'
|
||||
}
|
||||
group = 'com.example'
|
||||
description = "GraphQL over WebSocket With Spring WebFlux Sample"
|
||||
sourceCompatibility = '1.8'
|
||||
|
||||
ext['spring-graphql.version'] = version
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.spring.io/milestone' }
|
||||
maven { url 'https://repo.spring.io/snapshot' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-graphql'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
testImplementation project(':spring-graphql')
|
||||
testImplementation project(':spring-graphql-test')
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
}
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Repository with data fetcher methods.
|
||||
*/
|
||||
@Repository
|
||||
public class DataRepository {
|
||||
|
||||
public String getBasic() {
|
||||
return "Hello world!";
|
||||
}
|
||||
|
||||
public Mono<String> getGreeting() {
|
||||
return Mono.delay(Duration.ofMillis(50)).map(aLong -> "Hello!");
|
||||
}
|
||||
|
||||
public Flux<String> getGreetings() {
|
||||
return Mono.delay(Duration.ofMillis(50))
|
||||
.flatMapMany(aLong -> Flux.just("Hi!", "Bonjour!", "Hola!", "Ciao!", "Zdravo!"));
|
||||
}
|
||||
|
||||
public Flux<String> getGreetingsStream() {
|
||||
return Mono.delay(Duration.ofMillis(50))
|
||||
.flatMapMany(aLong -> Flux.just("Hi!", "Bonjour!", "Hola!", "Ciao!", "Zdravo!"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SampleApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class SampleController {
|
||||
|
||||
private final DataRepository repository;
|
||||
|
||||
public SampleController(DataRepository dataRepository) {
|
||||
this.repository = dataRepository;
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public String greeting() {
|
||||
return this.repository.getBasic();
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public Mono<String> greetingMono() {
|
||||
return this.repository.getGreeting();
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public Flux<String> greetingsFlux() {
|
||||
return this.repository.getGreetings();
|
||||
}
|
||||
|
||||
@SubscriptionMapping
|
||||
public Flux<String> greetings() {
|
||||
return this.repository.getGreetingsStream();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
spring.graphql.websocket.path=/graphql
|
||||
spring.graphql.schema.printer.enabled=true
|
||||
|
||||
management.endpoints.web.exposure.include=health,metrics,info
|
||||
|
||||
logging.level.org.springframework.web=debug
|
||||
logging.level.org.springframework.http=debug
|
||||
logging.level.org.springframework.graphql=debug
|
||||
logging.level.reactor.netty=debug
|
||||
@@ -1,8 +0,0 @@
|
||||
type Query {
|
||||
greeting: String
|
||||
greetingMono : String
|
||||
greetingsFlux : [String]
|
||||
}
|
||||
type Subscription {
|
||||
greetings: String
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>GraphQL over WebSocket</title>
|
||||
<script type="text/javascript" src="https://unpkg.com/graphql-ws/umd/graphql-ws.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Check the console for subscription messages.</p>
|
||||
<script type="text/javascript">
|
||||
const client = graphqlWs.createClient({
|
||||
url: 'ws://localhost:8080/graphql',
|
||||
});
|
||||
|
||||
// query
|
||||
(async () => {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
let result;
|
||||
client.subscribe(
|
||||
{
|
||||
query: '{ greeting }',
|
||||
},
|
||||
{
|
||||
next: (data) => (result = data),
|
||||
error: reject,
|
||||
complete: () => resolve(result),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
console.log("Query result: " + result);
|
||||
})();
|
||||
|
||||
// subscription
|
||||
(async () => {
|
||||
const onNext = (data) => {
|
||||
console.log("Subscription data:", data);
|
||||
};
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
client.subscribe(
|
||||
{
|
||||
query: 'subscription { greetings }',
|
||||
},
|
||||
{
|
||||
next: onNext,
|
||||
error: reject,
|
||||
complete: resolve,
|
||||
},
|
||||
);
|
||||
});
|
||||
})();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||
import org.springframework.graphql.test.tester.WebSocketGraphQlTester;
|
||||
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
|
||||
|
||||
/**
|
||||
* GraphQL over WebSocket integration tests.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
public class WebFluxWebSocketSampleIntegrationTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Value("http://localhost:${local.server.port}${spring.graphql.websocket.path}")
|
||||
private String baseUrl;
|
||||
|
||||
private GraphQlTester graphQlTester;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
URI url = URI.create(baseUrl);
|
||||
this.graphQlTester = WebSocketGraphQlTester.builder(url, new ReactorNettyWebSocketClient()).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void greetingMono() {
|
||||
this.graphQlTester.document("{greetingMono}")
|
||||
.execute()
|
||||
.path("greetingMono")
|
||||
.entity(String.class)
|
||||
.isEqualTo("Hello!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void greetingsFlux() {
|
||||
this.graphQlTester.document("{greetingsFlux}")
|
||||
.execute()
|
||||
.path("greetingsFlux")
|
||||
.entityList(String.class)
|
||||
.containsExactly("Hi!", "Bonjour!", "Hola!", "Ciao!", "Zdravo!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscriptionWithEntityPath() {
|
||||
Flux<String> result = this.graphQlTester.document("subscription { greetings }")
|
||||
.executeSubscription()
|
||||
.toFlux("greetings", String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext("Hi!")
|
||||
.expectNext("Bonjour!")
|
||||
.expectNext("Hola!")
|
||||
.expectNext("Ciao!")
|
||||
.expectNext("Zdravo!")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscriptionWithResponse() {
|
||||
Flux<GraphQlTester.Response> result = this.graphQlTester.document("subscription { greetings }")
|
||||
.executeSubscription()
|
||||
.toFlux();
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(response -> response.path("greetings").hasValue())
|
||||
.consumeNextWith(response -> response.path("greetings").matchesJson("\"Bonjour!\""))
|
||||
.consumeNextWith(response -> response.path("greetings").matchesJson("\"Hola!\""))
|
||||
.expectNextCount(2)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||
|
||||
/**
|
||||
* GraphQL over WebSocket, server-side tests, i.e. without a client.
|
||||
*/
|
||||
@GraphQlTest(SampleController.class)
|
||||
@Import(DataRepository.class)
|
||||
public class WebFluxWebSocketSampleTests {
|
||||
|
||||
@Autowired
|
||||
private GraphQlTester graphQlTester;
|
||||
|
||||
|
||||
@Test
|
||||
void greetingMono() {
|
||||
this.graphQlTester.document("{greetingMono}")
|
||||
.execute()
|
||||
.path("greetingMono")
|
||||
.entity(String.class)
|
||||
.isEqualTo("Hello!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void greetingsFlux() {
|
||||
this.graphQlTester.document("{greetingsFlux}")
|
||||
.execute()
|
||||
.path("greetingsFlux")
|
||||
.entityList(String.class)
|
||||
.containsExactly("Hi!", "Bonjour!", "Hola!", "Ciao!", "Zdravo!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscriptionWithEntityPath() {
|
||||
Flux<String> result = this.graphQlTester.document("subscription { greetings }")
|
||||
.executeSubscription()
|
||||
.toFlux("greetings", String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext("Hi!")
|
||||
.expectNext("Bonjour!")
|
||||
.expectNext("Hola!")
|
||||
.expectNext("Ciao!")
|
||||
.expectNext("Zdravo!")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscriptionWithResponse() {
|
||||
Flux<GraphQlTester.Response> result = this.graphQlTester.document("subscription { greetings }")
|
||||
.executeSubscription()
|
||||
.toFlux();
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(response -> response.path("greetings").hasValue())
|
||||
.consumeNextWith(response -> response.path("greetings").matchesJson("\"Bonjour!\""))
|
||||
.consumeNextWith(response -> response.path("greetings").matchesJson("\"Hola!\""))
|
||||
.expectNextCount(2)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Spring MVC HTTP Security Sample Schema",
|
||||
"schemaPath": "src/main/resources/graphql/schema.graphqls",
|
||||
"extensions": {}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
**Spring Security for GraphQL HTTP Endpoint with Spring MVC**
|
||||
|
||||
- Spring Security [config](src/main/java/io/spring/sample/graphql/SecurityConfig.java) secures GraphQL HTTP endpoint.
|
||||
- Fine-grained, method-level security on [SalaryService](src/main/java/io/spring/sample/graphql/SalaryService.java).
|
||||
- `AuthenticationException` and `AccessDeniedException` resolved to GraphQL errors.
|
||||
- [Tests](src/test/java/io/spring/sample/graphql/WebMvcHttpSecuritySampleTests.java) with `WebGraphQlTester` without a server.
|
||||
@@ -1,31 +0,0 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version "${bootVersion}"
|
||||
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
|
||||
id 'java'
|
||||
}
|
||||
group = 'com.example'
|
||||
description = "Secure GraphQL over HTTP with Spring MVC Sample"
|
||||
sourceCompatibility = '1.8'
|
||||
|
||||
ext['spring-graphql.version'] = version
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.spring.io/milestone' }
|
||||
maven { url 'https://repo.spring.io/snapshot' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-graphql'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
testImplementation project(':spring-graphql')
|
||||
testImplementation project(':spring-graphql-test')
|
||||
testImplementation 'org.springframework:spring-webflux'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
}
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
public class Employee {
|
||||
|
||||
private String id;
|
||||
|
||||
private String name;
|
||||
|
||||
public Employee(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class EmployeeService {
|
||||
|
||||
public List<Employee> getAllEmployees() {
|
||||
return Arrays.asList(new Employee("1", "Andi"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.MutationMapping;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class SalaryController {
|
||||
|
||||
private final EmployeeService employeeService;
|
||||
|
||||
private final SalaryService salaryService;
|
||||
|
||||
public SalaryController(EmployeeService employeeService, SalaryService salaryService) {
|
||||
this.employeeService = employeeService;
|
||||
this.salaryService = salaryService;
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public List<Employee> employees() {
|
||||
return this.employeeService.getAllEmployees();
|
||||
}
|
||||
|
||||
@SchemaMapping
|
||||
public BigDecimal salary(Employee employee) {
|
||||
return this.salaryService.getSalaryForEmployee(employee);
|
||||
}
|
||||
|
||||
@MutationMapping
|
||||
public void updateSalary(@Argument("input") SalaryInput salaryInput) {
|
||||
String employeeId = salaryInput.getEmployeeId();
|
||||
BigDecimal salary = salaryInput.getNewSalary();
|
||||
this.salaryService.updateSalary(employeeId, salary);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class SalaryInput {
|
||||
|
||||
private String employeeId;
|
||||
|
||||
private BigDecimal newSalary;
|
||||
|
||||
public SalaryInput(String employeeId, BigDecimal newSalary) {
|
||||
this.employeeId = employeeId;
|
||||
this.newSalary = newSalary;
|
||||
}
|
||||
|
||||
public String getEmployeeId() {
|
||||
return employeeId;
|
||||
}
|
||||
|
||||
public void setEmployeeId(String employeeId) {
|
||||
this.employeeId = employeeId;
|
||||
}
|
||||
|
||||
public BigDecimal getNewSalary() {
|
||||
return newSalary;
|
||||
}
|
||||
|
||||
public void setNewSalary(BigDecimal newSalary) {
|
||||
this.newSalary = newSalary;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SalaryService {
|
||||
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public BigDecimal getSalaryForEmployee(Employee employee) {
|
||||
return new BigDecimal("42");
|
||||
}
|
||||
|
||||
@Secured("ROLE_HR")
|
||||
public void updateSalary(String employeeId, BigDecimal newSalary) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.graphql.server.WebGraphQlInterceptor;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SampleApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebGraphQlInterceptor interceptor() {
|
||||
return (webInput, interceptorChain) -> {
|
||||
// Switch threads to prove ThreadLocal context propagation works
|
||||
return Mono.delay(Duration.ofMillis(10)).flatMap(aLong -> interceptorChain.next(webInput));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
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.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
DefaultSecurityFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.csrf(c -> c.disable())
|
||||
// Demonstrate that method security works
|
||||
// Best practice to use both for defense in depth
|
||||
.authorizeRequests(requests -> requests.anyRequest().permitAll())
|
||||
.httpBasic(withDefaults())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public static InMemoryUserDetailsManager userDetailsService() {
|
||||
User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
|
||||
UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
|
||||
UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build();
|
||||
return new InMemoryUserDetailsManager(rob, admin);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
management.endpoints.web.exposure.include=health,metrics,info
|
||||
|
||||
spring.graphql.schema.printer.enabled=true
|
||||
@@ -1,22 +0,0 @@
|
||||
type Query {
|
||||
employees: [Employee]
|
||||
}
|
||||
type Mutation {
|
||||
# restricted
|
||||
updateSalary(input: UpdateSalaryInput!): UpdateSalaryPayload
|
||||
}
|
||||
type Employee {
|
||||
id: ID!
|
||||
name: String
|
||||
# restricted
|
||||
salary: String
|
||||
}
|
||||
|
||||
input UpdateSalaryInput {
|
||||
employeeId: ID!
|
||||
newSalary: String!
|
||||
}
|
||||
type UpdateSalaryPayload {
|
||||
success: Boolean!
|
||||
employee: Employee
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.graphql.execution.ErrorType;
|
||||
import org.springframework.graphql.test.tester.WebGraphQlTester;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureHttpGraphQlTester
|
||||
class WebMvcHttpSecuritySampleTests {
|
||||
|
||||
@Autowired
|
||||
private WebGraphQlTester graphQlTester;
|
||||
|
||||
|
||||
@Test
|
||||
void printError() {
|
||||
this.graphQlTester.documentName("employeesNamesAndSalaries")
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy(System.out::println);
|
||||
}
|
||||
|
||||
@Test
|
||||
void anonymousThenUnauthorized() {
|
||||
this.graphQlTester.documentName("employeesNamesAndSalaries")
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy(errors -> {
|
||||
assertThat(errors).hasSize(1);
|
||||
assertThat(errors.get(0).getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void userRoleThenForbidden() {
|
||||
|
||||
WebGraphQlTester tester = this.graphQlTester.mutate()
|
||||
.headers(headers -> headers.setBasicAuth("rob", "rob"))
|
||||
.build();
|
||||
|
||||
tester.documentName("employeesNamesAndSalaries")
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy(errors -> {
|
||||
assertThat(errors).hasSize(1);
|
||||
assertThat(errors.get(0).getErrorType()).isEqualTo(ErrorType.FORBIDDEN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void candocumentName() {
|
||||
this.graphQlTester.documentName("employeesNames")
|
||||
.execute()
|
||||
.path("employees[0].name").entity(String.class).isEqualTo("Andi");
|
||||
}
|
||||
|
||||
@Test
|
||||
void canNotQuerySalary() {
|
||||
this.graphQlTester.documentName("employeesNamesAndSalaries")
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy(errors -> {
|
||||
assertThat(errors).hasSize(1);
|
||||
assertThat(errors.get(0).getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void canNotMutateUpdateSalary() {
|
||||
SalaryInput salaryInput = new SalaryInput("1", BigDecimal.valueOf(44));
|
||||
|
||||
this.graphQlTester.documentName("updateSalary")
|
||||
.variable("salaryInput", salaryInput)
|
||||
.execute()
|
||||
.errors()
|
||||
.satisfy(errors -> {
|
||||
assertThat(errors).hasSize(1);
|
||||
assertThat(errors.get(0).getErrorType()).isEqualTo(ErrorType.UNAUTHORIZED);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void canQuerySalaryAsAdmin() {
|
||||
|
||||
WebGraphQlTester tester = this.graphQlTester.mutate()
|
||||
.headers(headers -> headers.setBasicAuth("admin", "admin"))
|
||||
.build();
|
||||
|
||||
tester.documentName("employeesNamesAndSalaries")
|
||||
.execute()
|
||||
.path("employees[0].name").entity(String.class).isEqualTo("Andi")
|
||||
.path("employees[0].salary").entity(int.class).isEqualTo(42);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidCredentials() {
|
||||
assertThatThrownBy(() ->
|
||||
this.graphQlTester.mutate()
|
||||
.headers(headers -> headers.setBasicAuth("admin", "INVALID"))
|
||||
.build()
|
||||
.documentName("employeesNamesAndSalaries")
|
||||
.executeAndVerify())
|
||||
.hasMessage("Status expected:<200 OK> but was:<401 UNAUTHORIZED>");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
query {
|
||||
employees {
|
||||
name
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
query {
|
||||
employees {
|
||||
name,
|
||||
salary
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
mutation updateSalary($salaryInput: UpdateSalaryInput!) {
|
||||
updateSalary(input: $salaryInput) {
|
||||
success
|
||||
employee {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Spring MVC HTTP Sample Schema",
|
||||
"schemaPath": "src/main/resources/graphql/schema.graphqls",
|
||||
"extensions": {}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
**GraphQL HTTP Endpoint with Spring MVC**
|
||||
|
||||
- [Data Controller](src/main/java/io/spring/sample/graphql/project/ProjectController.java) with Spring HATEOAS calls to spring.io.
|
||||
- Querydsl [GraphQlRepository](src/main/java/io/spring/sample/graphql/repository/ArtifactRepositories.java) making JPA queries.
|
||||
- Use of [ThreadLocalAccessor](src/main/java/io/spring/sample/graphql/greeting/RequestAttributesAccessor.java) to propagate context to data fetchers.
|
||||
- Schema printing enabled at "/graphql/schema".
|
||||
- [Tests](src/test/java/io/spring/sample/graphql/project/ProjectControllerTests.java) with `GraphQlTester`.
|
||||
@@ -1,44 +0,0 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version "${bootVersion}"
|
||||
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
|
||||
id 'java'
|
||||
}
|
||||
group = 'com.example'
|
||||
description = "GraphQL over HTTP with Spring MVC Sample"
|
||||
sourceCompatibility = '1.8'
|
||||
|
||||
ext['spring-graphql.version'] = version
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.spring.io/milestone' }
|
||||
maven { url 'https://repo.spring.io/snapshot' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-graphql'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'com.querydsl:querydsl-core'
|
||||
implementation 'com.querydsl:querydsl-jpa'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
testImplementation project(':spring-graphql')
|
||||
testImplementation project(':spring-graphql-test')
|
||||
testImplementation 'org.springframework:spring-webflux'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
|
||||
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa',
|
||||
'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final',
|
||||
'javax.annotation:javax.annotation-api'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.annotationProcessorPath = configurations.annotationProcessor
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SampleApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql.greeting;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
import static org.springframework.web.context.request.RequestAttributes.SCOPE_REQUEST;
|
||||
|
||||
@Controller
|
||||
public class GreetingController {
|
||||
|
||||
@QueryMapping
|
||||
public String greeting() {
|
||||
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
|
||||
return "Hello " + attributes.getAttribute(RequestAttributeFilter.NAME_ATTRIBUTE, SCOPE_REQUEST);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql.greeting;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Servlet Filter that adds a Servlet request attribute.
|
||||
*/
|
||||
@Component
|
||||
public class RequestAttributeFilter implements Filter {
|
||||
|
||||
public static final String NAME_ATTRIBUTE = RequestAttributeFilter.class.getName() + ".name";
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
request.setAttribute(NAME_ATTRIBUTE, "007");
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql.greeting;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.graphql.execution.ThreadLocalAccessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
/**
|
||||
* {@link ThreadLocalAccessor} to expose a thread-bound RequestAttributes object to data
|
||||
* fetchers in Spring for GraphQL.
|
||||
*/
|
||||
@Component
|
||||
public class RequestAttributesAccessor implements ThreadLocalAccessor {
|
||||
|
||||
private static final String KEY = RequestAttributesAccessor.class.getName();
|
||||
|
||||
@Override
|
||||
public void extractValues(Map<String, Object> container) {
|
||||
container.put(KEY, RequestContextHolder.getRequestAttributes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreValues(Map<String, Object> values) {
|
||||
if (values.containsKey(KEY)) {
|
||||
RequestContextHolder.setRequestAttributes((RequestAttributes) values.get(KEY));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetValues(Map<String, Object> values) {
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package io.spring.sample.graphql.greeting;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -1,6 +0,0 @@
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package io.spring.sample.graphql;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -1,67 +0,0 @@
|
||||
package io.spring.sample.graphql.project;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Project {
|
||||
|
||||
private String slug;
|
||||
|
||||
private String name;
|
||||
|
||||
private String repositoryUrl;
|
||||
|
||||
private ProjectStatus status;
|
||||
|
||||
private List<Release> releases;
|
||||
|
||||
public Project() {
|
||||
}
|
||||
|
||||
public Project(String slug, String name, String repositoryUrl, ProjectStatus status) {
|
||||
this.slug = slug;
|
||||
this.name = name;
|
||||
this.repositoryUrl = repositoryUrl;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return this.slug;
|
||||
}
|
||||
|
||||
public void setSlug(String slug) {
|
||||
this.slug = slug;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getRepositoryUrl() {
|
||||
return this.repositoryUrl;
|
||||
}
|
||||
|
||||
public void setRepositoryUrl(String repositoryUrl) {
|
||||
this.repositoryUrl = repositoryUrl;
|
||||
}
|
||||
|
||||
public ProjectStatus getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public void setStatus(ProjectStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public List<Release> getReleases() {
|
||||
return this.releases;
|
||||
}
|
||||
|
||||
public void setReleases(List<Release> releases) {
|
||||
this.releases = releases;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql.project;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class ProjectController {
|
||||
|
||||
private final SpringProjectsClient client;
|
||||
|
||||
public ProjectController(SpringProjectsClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public Project project(@Argument String slug) {
|
||||
return client.fetchProject(slug);
|
||||
}
|
||||
|
||||
@SchemaMapping
|
||||
public List<Release> releases(Project project) {
|
||||
return client.fetchProjectReleases(project.getSlug());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package io.spring.sample.graphql.project;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
public enum ProjectStatus {
|
||||
|
||||
ACTIVE, COMMUNITY, INCUBATING, ATTIC;
|
||||
|
||||
@JsonCreator
|
||||
public static ProjectStatus fromName(String name) {
|
||||
return Arrays.stream(ProjectStatus.values())
|
||||
.filter(type -> type.name().equals(name))
|
||||
.findFirst()
|
||||
.orElse(ProjectStatus.ACTIVE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package io.spring.sample.graphql.project;
|
||||
|
||||
public class Release {
|
||||
|
||||
private String version;
|
||||
|
||||
private ReleaseStatus status;
|
||||
|
||||
private String referenceDocUrl;
|
||||
|
||||
private String apiDocUrl;
|
||||
|
||||
private boolean current;
|
||||
|
||||
public Release() {
|
||||
}
|
||||
|
||||
public Release(Project project, String version, ReleaseStatus status) {
|
||||
this.version = version;
|
||||
this.status = status;
|
||||
this.apiDocUrl = String.format("https://docs.spring.io/%s/docs/%s/javadoc-api/", project.getSlug(), version);
|
||||
this.referenceDocUrl = String.format("https://docs.spring.io/%s/docs/%s/reference/html/", project.getSlug(), version);
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public ReleaseStatus getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public void setStatus(ReleaseStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getReferenceDocUrl() {
|
||||
return this.referenceDocUrl;
|
||||
}
|
||||
|
||||
public void setReferenceDocUrl(String referenceDocUrl) {
|
||||
this.referenceDocUrl = referenceDocUrl;
|
||||
}
|
||||
|
||||
public String getApiDocUrl() {
|
||||
return this.apiDocUrl;
|
||||
}
|
||||
|
||||
public void setApiDocUrl(String apiDocUrl) {
|
||||
this.apiDocUrl = apiDocUrl;
|
||||
}
|
||||
|
||||
public boolean isCurrent() {
|
||||
return this.current;
|
||||
}
|
||||
|
||||
public void setCurrent(boolean current) {
|
||||
this.current = current;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package io.spring.sample.graphql.project;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
public enum ReleaseStatus {
|
||||
|
||||
GENERAL_AVAILABILITY, MILESTONE, SNAPSHOT;
|
||||
|
||||
@JsonCreator
|
||||
public static ReleaseStatus fromName(String name) {
|
||||
return Arrays.stream(ReleaseStatus.values())
|
||||
.filter(type -> type.name().equals(name))
|
||||
.findFirst()
|
||||
.orElse(ReleaseStatus.GENERAL_AVAILABILITY);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package io.spring.sample.graphql.project;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.hateoas.CollectionModel;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.client.Hop;
|
||||
import org.springframework.hateoas.client.Traverson;
|
||||
import org.springframework.hateoas.server.core.TypeReferences;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Component
|
||||
public class SpringProjectsClient {
|
||||
|
||||
private static final TypeReferences.CollectionModelType<Release> releaseCollection =
|
||||
new TypeReferences.CollectionModelType<Release>() {};
|
||||
|
||||
private final Traverson traverson;
|
||||
|
||||
public SpringProjectsClient(RestTemplateBuilder builder) {
|
||||
List<HttpMessageConverter<?>> converters = Traverson.getDefaultMessageConverters(MediaTypes.HAL_JSON);
|
||||
RestTemplate restTemplate = builder.messageConverters(converters).build();
|
||||
this.traverson = new Traverson(URI.create("https://spring.io/api/"), MediaTypes.HAL_JSON);
|
||||
this.traverson.setRestOperations(restTemplate);
|
||||
}
|
||||
|
||||
public Project fetchProject(String projectSlug) {
|
||||
return this.traverson.follow("projects")
|
||||
.follow(Hop.rel("project").withParameter("id", projectSlug))
|
||||
.toObject(Project.class);
|
||||
}
|
||||
|
||||
public List<Release> fetchProjectReleases(String projectSlug) {
|
||||
CollectionModel<Release> releases = this.traverson.follow("projects")
|
||||
.follow(Hop.rel("project").withParameter("id", projectSlug)).follow(Hop.rel("releases"))
|
||||
.toObject(releaseCollection);
|
||||
return new ArrayList(releases.getContent());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package io.spring.sample.graphql.project;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -1,11 +0,0 @@
|
||||
package io.spring.sample.graphql.repository;
|
||||
|
||||
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.graphql.data.GraphQlRepository;
|
||||
|
||||
@GraphQlRepository
|
||||
public interface ArtifactRepositories extends
|
||||
CrudRepository<ArtifactRepository, String>, QuerydslPredicateExecutor<ArtifactRepository> {
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package io.spring.sample.graphql.repository;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ArtifactRepositoriesInitializer implements ApplicationRunner {
|
||||
|
||||
private final ArtifactRepositories repositories;
|
||||
|
||||
public ArtifactRepositoriesInitializer(ArtifactRepositories repositories) {
|
||||
this.repositories = repositories;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
List<ArtifactRepository> repositoryList = Arrays.asList(
|
||||
new ArtifactRepository("spring-releases", "Spring Releases", "https://repo.spring.io/libs-releases"),
|
||||
new ArtifactRepository("spring-milestones", "Spring Milestones", "https://repo.spring.io/libs-milestones"),
|
||||
new ArtifactRepository("spring-snapshots", "Spring Snapshots", "https://repo.spring.io/libs-snapshots"));
|
||||
repositories.saveAll(repositoryList);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package io.spring.sample.graphql.repository;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
|
||||
@Entity
|
||||
public class ArtifactRepository {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String url;
|
||||
|
||||
private boolean snapshotsEnabled;
|
||||
|
||||
public ArtifactRepository(String id, String name, String url) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public ArtifactRepository() {
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public boolean isSnapshotsEnabled() {
|
||||
return snapshotsEnabled;
|
||||
}
|
||||
|
||||
public void setSnapshotsEnabled(boolean snapshotsEnabled) {
|
||||
this.snapshotsEnabled = snapshotsEnabled;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package io.spring.sample.graphql.repository;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -1,3 +0,0 @@
|
||||
management.endpoints.web.exposure.include=health,metrics,info
|
||||
|
||||
spring.graphql.schema.printer.enabled=true
|
||||
@@ -1,40 +0,0 @@
|
||||
type Query {
|
||||
greeting: String
|
||||
artifactRepositories : [ArtifactRepository]
|
||||
artifactRepository(id : ID!) : ArtifactRepository
|
||||
project(slug: ID!): Project
|
||||
}
|
||||
|
||||
type ArtifactRepository {
|
||||
id: ID!
|
||||
name: String!
|
||||
url: String!
|
||||
snapshotsEnabled: Boolean
|
||||
}
|
||||
|
||||
type Project {
|
||||
slug: ID!
|
||||
name: String!
|
||||
repositoryUrl: String!
|
||||
status: ProjectStatus!
|
||||
releases: [Release]
|
||||
}
|
||||
|
||||
type Release {
|
||||
version: String!
|
||||
status: ReleaseStatus!
|
||||
current: Boolean
|
||||
}
|
||||
|
||||
enum ProjectStatus {
|
||||
ACTIVE
|
||||
COMMUNITY
|
||||
INCUBATING
|
||||
ATTIC
|
||||
}
|
||||
|
||||
enum ReleaseStatus {
|
||||
GENERAL_AVAILABILITY
|
||||
MILESTONE
|
||||
SNAPSHOT
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.spring.sample.graphql.project;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* GraphQL requests via {@link GraphQlTester}.
|
||||
*/
|
||||
@GraphQlTest(ProjectController.class)
|
||||
public class ProjectControllerTests {
|
||||
|
||||
@Autowired
|
||||
private GraphQlTester graphQlTester;
|
||||
|
||||
@MockBean
|
||||
private SpringProjectsClient projectsClient;
|
||||
|
||||
private Project springFramework;
|
||||
|
||||
private Release latestRelease;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.springFramework = new Project("spring-framework", "Spring Framework",
|
||||
"http://github.com/spring-projects/spring-framework", ProjectStatus.ACTIVE);
|
||||
this.latestRelease = new Release(this.springFramework, "5.3.0", ReleaseStatus.GENERAL_AVAILABILITY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonPath() {
|
||||
|
||||
given(this.projectsClient.fetchProject(eq("spring-framework")))
|
||||
.willReturn(this.springFramework);
|
||||
|
||||
given(this.projectsClient.fetchProjectReleases(eq("spring-framework")))
|
||||
.willReturn(Collections.singletonList(this.latestRelease));
|
||||
|
||||
this.graphQlTester.documentName("projectReleases")
|
||||
.variable("slug", "spring-framework")
|
||||
.execute()
|
||||
.path("project.releases[*].version")
|
||||
.entityList(String.class)
|
||||
.hasSize(1);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void jsonContent() {
|
||||
given(this.projectsClient.fetchProject(eq("spring-framework"))).willReturn(this.springFramework);
|
||||
this.graphQlTester.documentName("projectRepositoryUrl")
|
||||
.variable("slug", "spring-framework")
|
||||
.execute()
|
||||
.path("project")
|
||||
.matchesJson("{\"repositoryUrl\":\"http://github.com/spring-projects/spring-framework\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void decodedResponse() {
|
||||
|
||||
given(this.projectsClient.fetchProject(eq("spring-framework")))
|
||||
.willReturn(this.springFramework);
|
||||
|
||||
given(this.projectsClient.fetchProjectReleases(eq("spring-framework")))
|
||||
.willReturn(Collections.singletonList(this.latestRelease));
|
||||
|
||||
this.graphQlTester.documentName("projectReleases")
|
||||
.variable("slug", "spring-framework")
|
||||
.execute()
|
||||
.path("project")
|
||||
.entity(Project.class)
|
||||
.satisfies(project -> assertThat(project.getReleases()).hasSize(1));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.spring.sample.graphql.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureGraphQlTester;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.graphql.test.tester.GraphQlTester;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureGraphQlTester
|
||||
class ArtifactRepositoriesTests {
|
||||
|
||||
@Autowired
|
||||
private GraphQlTester graphQlTester;
|
||||
|
||||
@Test
|
||||
void querydslRepositorySingle() {
|
||||
this.graphQlTester.documentName("artifactRepository")
|
||||
.variable("id", "spring-releases")
|
||||
.execute()
|
||||
.path("artifactRepository.name")
|
||||
.entity(String.class).isEqualTo("Spring Releases");
|
||||
}
|
||||
|
||||
@Test
|
||||
void querydslRepositoryMany() {
|
||||
this.graphQlTester.documentName("artifactRepositories")
|
||||
.execute()
|
||||
.path("artifactRepositories[*].id")
|
||||
.entityList(String.class).containsExactly("spring-releases", "spring-milestones", "spring-snapshots");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
logging.level.org.springframework.web=DEBUG
|
||||
@@ -1,5 +0,0 @@
|
||||
query {
|
||||
artifactRepositories {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
query artifactRepository($id: ID!) {
|
||||
artifactRepository(id: $id) {
|
||||
name
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
query project($slug: ID!) {
|
||||
project(slug: $slug) {
|
||||
repositoryUrl
|
||||
releases {
|
||||
version
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
query project($slug: ID!) {
|
||||
project(slug: $slug) {
|
||||
repositoryUrl
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
:github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag}
|
||||
:github-issues: https://github.com/{github-repo}/issues/
|
||||
:github-main-branch: https://github.com/{github-repo}/tree/main
|
||||
:github-10x-branch: https://github.com/{github-repo}/tree/1.0.x
|
||||
:github-wiki: https://github.com/{github-repo}/wiki
|
||||
:graphql-java-docs: https://www.graphql-java.com/documentation
|
||||
:javadoc: https://docs.spring.io/spring-graphql/docs/{spring-graphql-version}/api
|
||||
|
||||
@@ -21,8 +21,8 @@ Please, see our
|
||||
https://github.com/spring-projects/spring-graphql/wiki[Wiki].
|
||||
for what's new, baseline requirements, and upgrade notes, and other cross-version information.
|
||||
|
||||
To get started, check the Spring GraphQL starter on https://start.spring.io and the
|
||||
<<samples>> in this repository.
|
||||
To get started, check the Spring GraphQL starter on https://start.spring.io.
|
||||
The are also https://github.com/spring-projects/spring-graphql/tree/1.0.x/samples[samples] in the 1.0.x branch, which will be https://github.com/spring-projects/spring-graphql/issues/208[moved out] into a separate repository.
|
||||
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@ Boot starter does this, see the
|
||||
details, or check `GraphQlWebMvcAutoConfiguration` or `GraphQlWebFluxAutoConfiguration`
|
||||
it contains, for the actual config.
|
||||
|
||||
The Spring for GraphQL repository contains a Spring MVC
|
||||
{github-main-branch}/samples/webmvc-http[HTTP sample] application.
|
||||
The 1.0.x branch of this repository contains a Spring MVC
|
||||
{github-10x-branch}/samples/webmvc-http[HTTP sample] application.
|
||||
|
||||
|
||||
|
||||
@@ -103,8 +103,8 @@ enable it by adding a property for the endpoint path. Please, see the
|
||||
section for details, or check the `GraphQlWebMvcAutoConfiguration` or the
|
||||
`GraphQlWebFluxAutoConfiguration` for the actual Boot starter config.
|
||||
|
||||
The Spring for GraphQL repository contains a WebFlux
|
||||
{github-main-branch}/samples/webflux-websocket[WebSocket sample] application.
|
||||
The 1.0.x branch of this repository contains a WebFlux
|
||||
{github-10x-branch}/samples/webflux-websocket[WebSocket sample] application.
|
||||
|
||||
|
||||
|
||||
@@ -892,7 +892,7 @@ compileJava {
|
||||
</plugins>
|
||||
----
|
||||
|
||||
The {github-main-branch}/samples/webmvc-http[webmvc-http] sample uses Querydsl for
|
||||
The {github-10x-branch}/samples/webmvc-http[webmvc-http] sample uses Querydsl for
|
||||
`artifactRepositories`.
|
||||
|
||||
|
||||
@@ -1748,9 +1748,9 @@ To apply more fine-grained security, add Spring Security annotations such as
|
||||
the GraphQL response. This should work due to <<execution-context>> that aims to make
|
||||
Security, and other context, available at the data fetching level.
|
||||
|
||||
The Spring for GraphQL repository contains samples for
|
||||
{github-main-branch}/samples/webmvc-http-security[Spring MVC] and for
|
||||
{github-main-branch}/samples/webflux-security[WebFlux].
|
||||
The 1.0.x branch of this repository contains samples for
|
||||
{github-10x-branch}/samples/webmvc-http-security[Spring MVC] and for
|
||||
{github-10x-branch}/samples/webflux-security[WebFlux].
|
||||
|
||||
|
||||
|
||||
@@ -1768,11 +1768,9 @@ include::testing.adoc[leveloffset=+1]
|
||||
[[samples]]
|
||||
== Samples
|
||||
|
||||
This Spring for GraphQL repository contains {github-main-branch}/samples[sample applications] for
|
||||
various scenarios.
|
||||
The 1.0.x branch of this repository contains {github-10x-branch}/samples[sample applications] for various scenarios.
|
||||
|
||||
You can run those by cloning this repository and running main application classes from
|
||||
your IDE or by typing the following on the command line:
|
||||
You can run those by cloning this repository and running main application classes from your IDE or by typing the following on the command line:
|
||||
|
||||
[source,bash,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
|
||||
Reference in New Issue
Block a user