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:
rstoyanchev
2022-11-24 11:06:05 +00:00
parent 2f27cc7c8e
commit 0baceda48c
76 changed files with 14 additions and 2282 deletions

View File

@@ -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

View File

@@ -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
```

View File

@@ -1,5 +0,0 @@
{
"name": "Spring WebFlux Security Sample Schema",
"schemaPath": "src/main/resources/graphql/schema.graphqls",
"extensions": {}
}

View File

@@ -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.

View File

@@ -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()
}

View File

@@ -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;
}
}

View File

@@ -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"));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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");
}
}

View File

@@ -1,5 +0,0 @@
query {
employees {
name
}
}

View File

@@ -1,6 +0,0 @@
query {
employees {
name,
salary
}
}

View File

@@ -1,9 +0,0 @@
mutation updateSalary($salaryInput: UpdateSalaryInput!) {
updateSalary(input: $salaryInput) {
success
employee {
id
name
}
}
}

View File

@@ -1,5 +0,0 @@
{
"name": "Spring WebFlux WebSocket Sample Schema",
"schemaPath": "src/main/resources/graphql/schema.graphqls",
"extensions": {}
}

View File

@@ -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.

View File

@@ -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()
}

View File

@@ -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!"));
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -1,8 +0,0 @@
type Query {
greeting: String
greetingMono : String
greetingsFlux : [String]
}
type Subscription {
greetings: String
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -1,5 +0,0 @@
{
"name": "Spring MVC HTTP Security Sample Schema",
"schemaPath": "src/main/resources/graphql/schema.graphqls",
"extensions": {}
}

View File

@@ -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.

View File

@@ -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()
}

View File

@@ -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;
}
}

View File

@@ -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"));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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) {
}
}

View File

@@ -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));
};
}
}

View File

@@ -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);
}
}

View File

@@ -1,3 +0,0 @@
management.endpoints.web.exposure.include=health,metrics,info
spring.graphql.schema.printer.enabled=true

View File

@@ -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
}

View File

@@ -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>");
}
}

View File

@@ -1,5 +0,0 @@
query {
employees {
name
}
}

View File

@@ -1,6 +0,0 @@
query {
employees {
name,
salary
}
}

View File

@@ -1,9 +0,0 @@
mutation updateSalary($salaryInput: UpdateSalaryInput!) {
updateSalary(input: $salaryInput) {
success
employee {
id
name
}
}
}

View File

@@ -1,5 +0,0 @@
{
"name": "Spring MVC HTTP Sample Schema",
"schemaPath": "src/main/resources/graphql/schema.graphqls",
"extensions": {}
}

View File

@@ -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`.

View File

@@ -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()
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -1,6 +0,0 @@
@NonNullApi
@NonNullFields
package io.spring.sample.graphql.greeting;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,6 +0,0 @@
@NonNullApi
@NonNullFields
package io.spring.sample.graphql;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -1,6 +0,0 @@
@NonNullApi
@NonNullFields
package io.spring.sample.graphql.project;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -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> {
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -1,6 +0,0 @@
@NonNullApi
@NonNullFields
package io.spring.sample.graphql.repository;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -1,3 +0,0 @@
management.endpoints.web.exposure.include=health,metrics,info
spring.graphql.schema.printer.enabled=true

View File

@@ -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
}

View File

@@ -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));
}
}

View File

@@ -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");
}
}

View File

@@ -1 +0,0 @@
logging.level.org.springframework.web=DEBUG

View File

@@ -1,5 +0,0 @@
query {
artifactRepositories {
id
}
}

View File

@@ -1,5 +0,0 @@
query artifactRepository($id: ID!) {
artifactRepository(id: $id) {
name
}
}

View File

@@ -1,8 +0,0 @@
query project($slug: ID!) {
project(slug: $slug) {
repositoryUrl
releases {
version
}
}
}

View File

@@ -1,5 +0,0 @@
query project($slug: ID!) {
project(slug: $slug) {
repositoryUrl
}
}

View File

@@ -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

View File

@@ -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"]
----