|
12 | 12 | import java.util.Arrays;
|
13 | 13 | import java.util.Collections;
|
14 | 14 | import java.util.List;
|
| 15 | +import java.util.function.IntFunction; |
15 | 16 | import lombok.Getter;
|
16 | 17 | import lombok.NonNull;
|
17 | 18 | import org.springframework.beans.factory.annotation.Value;
|
18 | 19 | import org.springframework.boot.test.web.client.TestRestTemplate;
|
| 20 | +import org.springframework.core.io.ClassPathResource; |
19 | 21 | import org.springframework.core.io.Resource;
|
20 | 22 | import org.springframework.core.io.ResourceLoader;
|
21 | 23 | import org.springframework.http.HttpEntity;
|
22 | 24 | import org.springframework.http.HttpHeaders;
|
23 | 25 | import org.springframework.http.HttpMethod;
|
24 | 26 | import org.springframework.http.ResponseEntity;
|
25 | 27 | import org.springframework.lang.Nullable;
|
| 28 | +import org.springframework.util.LinkedMultiValueMap; |
26 | 29 | import org.springframework.util.MultiValueMap;
|
27 | 30 | import org.springframework.util.StreamUtils;
|
28 | 31 |
|
@@ -239,13 +242,33 @@ public GraphQLResponse perform(
|
239 | 242 | ObjectNode variables,
|
240 | 243 | List<String> fragmentResources)
|
241 | 244 | throws IOException {
|
| 245 | + String payload = getPayload(graphqlResource, operationName, variables, fragmentResources); |
| 246 | + return post(payload); |
| 247 | + } |
| 248 | + |
| 249 | + /** |
| 250 | + * Generate GraphQL payload, which consist of 3 elements: query, operationName and variables |
| 251 | + * |
| 252 | + * @param graphqlResource path to the classpath resource containing the GraphQL query |
| 253 | + * @param operationName the name of the GraphQL operation to be executed |
| 254 | + * @param variables the input variables for the GraphQL query |
| 255 | + * @param fragmentResources an ordered list of classpath resources containing GraphQL fragment |
| 256 | + * definitions. |
| 257 | + * @return the payload |
| 258 | + * @throws IOException if the resource cannot be loaded from the classpath |
| 259 | + */ |
| 260 | + private String getPayload( |
| 261 | + String graphqlResource, |
| 262 | + String operationName, |
| 263 | + ObjectNode variables, |
| 264 | + List<String> fragmentResources) |
| 265 | + throws IOException { |
242 | 266 | StringBuilder sb = new StringBuilder();
|
243 | 267 | for (String fragmentResource : fragmentResources) {
|
244 | 268 | sb.append(loadQuery(fragmentResource));
|
245 | 269 | }
|
246 | 270 | String graphql = sb.append(loadQuery(graphqlResource)).toString();
|
247 |
| - String payload = createJsonQuery(graphql, operationName, variables); |
248 |
| - return post(payload); |
| 271 | + return createJsonQuery(graphql, operationName, variables); |
249 | 272 | }
|
250 | 273 |
|
251 | 274 | /**
|
@@ -279,6 +302,115 @@ public GraphQLResponse postMultipart(String query, String variables) {
|
279 | 302 | return postRequest(RequestFactory.forMultipart(query, variables, headers));
|
280 | 303 | }
|
281 | 304 |
|
| 305 | + /** |
| 306 | + * Handle the multipart files upload request to GraphQL servlet |
| 307 | + * |
| 308 | + * <p>In contrast with usual the GraphQL request with body as json payload (consist of query, |
| 309 | + * operationName and variables), multipart file upload request will use multipart/form-data body |
| 310 | + * with the following structure: |
| 311 | + * |
| 312 | + * <ul> |
| 313 | + * <li><b>operations</b> the payload that we used to use for the normal GraphQL request |
| 314 | + * <li><b>map</b> a map for referencing between one part of multi-part request and the |
| 315 | + * corresponding <i>Upload</i> element inside <i>variables</i> |
| 316 | + * <li>a consequence of upload files embedded into the multi-part request, keyed as numeric |
| 317 | + * number starting from 1, valued as File payload of usual multipart file upload |
| 318 | + * </ul> |
| 319 | + * |
| 320 | + * <p>Example uploading two files: |
| 321 | + * |
| 322 | + * <p>* Please note that we can't embed binary data into json. Clients library supporting graphql |
| 323 | + * file upload will set variable.files to null for every element inside the array, but each file |
| 324 | + * will be a part of multipart request. GraphQL Servlet will use <i>map</i> part to walk through |
| 325 | + * variables.files and validate the request in combination with other binary file parts |
| 326 | + * |
| 327 | + * <p>----------------------------dummyid |
| 328 | + * |
| 329 | + * <p>Content-Disposition: form-data; name="operations" |
| 330 | + * |
| 331 | + * <p>{ "query": "mutation($files:[Upload]!) {uploadFiles(files:$files)}", "operationName": |
| 332 | + * "uploadFiles", "variables": { "files": [null, null] } } |
| 333 | + * |
| 334 | + * <p>----------------------------dummyid |
| 335 | + * |
| 336 | + * <p>Content-Disposition: form-data; name="map" |
| 337 | + * |
| 338 | + * <p>map: { "1":["variables.files.0"], "2":["variables.files.1"] } |
| 339 | + * |
| 340 | + * <p>----------------------------dummyid |
| 341 | + * |
| 342 | + * <p>Content-Disposition: form-data; name="1"; filename="file1.pdf" |
| 343 | + * |
| 344 | + * <p>Content-Type: application/octet-stream |
| 345 | + * |
| 346 | + * <p>--file 1 binary code-- |
| 347 | + * |
| 348 | + * <p>----------------------------dummyid |
| 349 | + * |
| 350 | + * <p>Content-Disposition: form-data; name="2"; filename="file2.pdf" |
| 351 | + * |
| 352 | + * <p>Content-Type: application/octet-stream |
| 353 | + * |
| 354 | + * <p>2: --file 2 binary code-- |
| 355 | + * |
| 356 | + * <p> |
| 357 | + * |
| 358 | + * @param graphqlResource path to the classpath resource containing the GraphQL query |
| 359 | + * @param variables the input variables for the GraphQL query |
| 360 | + * @param files ClassPathResource instance for each file that will be uploaded to GraphQL server. |
| 361 | + * When Spring RestTemplate processes the request, it will automatically produce a valid part |
| 362 | + * representing given file inside multipart request (including size, submittedFileName, etc.) |
| 363 | + * @return {@link GraphQLResponse} containing the result of query execution |
| 364 | + * @throws IOException if the resource cannot be loaded from the classpath |
| 365 | + */ |
| 366 | + public GraphQLResponse postFiles( |
| 367 | + String graphqlResource, ObjectNode variables, List<ClassPathResource> files) |
| 368 | + throws IOException { |
| 369 | + |
| 370 | + return postFiles( |
| 371 | + graphqlResource, variables, files, index -> String.format("variables.files.%d", index)); |
| 372 | + } |
| 373 | + |
| 374 | + /** |
| 375 | + * Handle the multipart files upload request to GraphQL servlet |
| 376 | + * |
| 377 | + * @param graphqlResource path to the classpath resource containing the GraphQL query |
| 378 | + * @param variables the input variables for the GraphQL query |
| 379 | + * @param files ClassPathResource instance for each file that will be uploaded to GraphQL server. |
| 380 | + * When Spring RestTemplate processes the request, it will automatically produce a valid part |
| 381 | + * representing given file inside multipart request (including size, submittedFileName, etc.) |
| 382 | + * @param pathFunc function to generate the path to file inside variables. For example: |
| 383 | + * <ul> |
| 384 | + * <li>index -> String.format("variables.files.%d", index) for multiple files |
| 385 | + * <li>index -> "variables.file" for single file |
| 386 | + * </ul> |
| 387 | + * |
| 388 | + * @return {@link GraphQLResponse} containing the result of query execution |
| 389 | + * @throws IOException if the resource cannot be loaded from the classpath |
| 390 | + */ |
| 391 | + public GraphQLResponse postFiles( |
| 392 | + String graphqlResource, |
| 393 | + ObjectNode variables, |
| 394 | + List<ClassPathResource> files, |
| 395 | + IntFunction<String> pathFunc) |
| 396 | + throws IOException { |
| 397 | + MultiValueMap<String, Object> values = new LinkedMultiValueMap<>(); |
| 398 | + MultiValueMap<String, Object> map = new LinkedMultiValueMap<>(); |
| 399 | + |
| 400 | + for (int i = 0; i < files.size(); i++) { |
| 401 | + String valueKey = String.valueOf(i + 1); // map value and part index starts at 1 |
| 402 | + map.add(valueKey, pathFunc.apply(i)); |
| 403 | + |
| 404 | + values.add(valueKey, files.get(i)); |
| 405 | + } |
| 406 | + |
| 407 | + String payload = getPayload(graphqlResource, null, variables, Collections.emptyList()); |
| 408 | + values.add("operations", payload); |
| 409 | + values.add("map", map); |
| 410 | + |
| 411 | + return postRequest(RequestFactory.forMultipart(values, headers)); |
| 412 | + } |
| 413 | + |
282 | 414 | /**
|
283 | 415 | * Performs a GraphQL request with the provided payload.
|
284 | 416 | *
|
|
0 commit comments