Skip to content

Commit

Permalink
Support GraphQL Upload spec as proposed at https://github.com/jaydens…
Browse files Browse the repository at this point in the history
  • Loading branch information
truongsinh authored and micimize committed Mar 30, 2019
1 parent 21bff7c commit c020d12
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 162 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "example/server"]
path = example/server
url = https://github.com/jaydenseric/apollo-upload-examples.git
40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# GraphQL Flutter
# GraphQL Flutter <!-- omit in toc -->

[![Build Status][build-status-badge]][build-status-link]
[![Coverage][coverage-badge]][coverage-link]
Expand All @@ -10,7 +10,7 @@
[![Watch on GitHub][github-watch-badge]][github-watch]
[![Star on GitHub][github-star-badge]][github-star]

## Table of Contents
## Table of Contents <!-- omit in toc -->

- [About this project](#about-this-project)
- [Installation](#installation)
Expand All @@ -23,6 +23,7 @@
- [Mutations](#mutations)
- [Subscriptions (Experimental)](#subscriptions-experimental)
- [GraphQL Consumer](#graphql-consumer)
- [Graphql Upload](#graphql-upload)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [Contributors](#contributors)
Expand Down Expand Up @@ -411,7 +412,7 @@ class _MyHomePageState extends State<MyHomePage> {
}
```

### Graphql Consumer
### GraphQL Consumer

You can always access the client directly from the `GraphQLProvider` but to make it even easier you can also use the `GraphQLConsumer` widget.

Expand All @@ -431,6 +432,38 @@ You can always access the client directly from the `GraphQLProvider` but to make
...
```

### Graphql Upload

We support GraphQL Upload spec as proposed at
https://github.com/jaydenseric/graphql-multipart-request-spec

```grapql
mutation($files: [Upload!]!) {
multipleUpload(files: $files) {
id
filename
mimetype
path
}
}
```

```dart
import 'dart:io' show File;
// ...
String filePath = '/aboslute/path/to/file.ext';
final QueryResult r = await graphQLClientClient.mutate(
MutationOptions(
document: uploadMutation,
variables: {
'files': [File(filePath)],
},
)
);
```

## Roadmap

This is currently our roadmap, please feel free to request additions/changes.
Expand All @@ -443,6 +476,7 @@ This is currently our roadmap, please feel free to request additions/changes.
| Query polling ||
| In memory cache ||
| Offline cache sync ||
| GraphQL pload ||
| Optimistic results | 🔜 |
| Client state management | 🔜 |
| Modularity | 🔜 |
Expand Down
2 changes: 1 addition & 1 deletion example/lib/bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class Bloc {
getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
);

static final Link _link = _authLink.concat(_httpLink);
static final Link _link = _authLink.concat(_httpLink as Link);

static final GraphQLClient _client = GraphQLClient(
cache: NormalizedInMemoryCache(
Expand Down
7 changes: 5 additions & 2 deletions lib/src/core/query_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,11 @@ class QueryManager {
).first;

// save the data from fetchResult to the cache
if (fetchResult.data != null &&
options.fetchPolicy != FetchPolicy.noCache) {
if (
// should never cache a mutation
!(options is MutationOptions) &&
fetchResult.data != null &&
options.fetchPolicy != FetchPolicy.noCache) {
cache.write(
operation.toKey(),
fetchResult.data,
Expand Down
10 changes: 5 additions & 5 deletions lib/src/link/http/http_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ class HttpConfig {
Map<String, String> headers;
}

class HttpOptionsAndBody {
HttpOptionsAndBody({
this.options,
class HttpHeadersAndBody {
HttpHeadersAndBody({
this.headers,
this.body,
});

final Map<String, dynamic> options;
final String body;
final Map<String, String> headers;
final Map<String, dynamic> body;
}
134 changes: 107 additions & 27 deletions lib/src/link/http/link_http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'dart:io';
import 'package:meta/meta.dart';
import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart';
import 'package:mime/mime.dart';

import 'package:graphql_flutter/src/link/link.dart';
import 'package:graphql_flutter/src/link/operation.dart';
Expand Down Expand Up @@ -53,35 +55,33 @@ class HttpLink extends Link {
);
}

final HttpOptionsAndBody httpOptionsAndBody =
final HttpHeadersAndBody httpHeadersAndBody =
_selectHttpOptionsAndBody(
operation,
fallbackHttpConfig,
linkConfig,
contextConfig,
);

final Map<String, dynamic> options = httpOptionsAndBody.options;
final Map<String, String> httpHeaders =
options['headers'] as Map<String, String>;
final Map<String, String> httpHeaders = httpHeadersAndBody.headers;

StreamController<FetchResult> controller;

Future<void> onListen() async {
StreamedResponse response;

try {
// httpOptionsAndBody.body as String
final BaseRequest request = await _prepareRequest(
uri, httpHeadersAndBody.body, httpHeaders);

Request r = Request('post', Uri.parse(uri))
..headers.addAll(httpHeaders)
;
r.body = httpOptionsAndBody.body as String;
response = await fetcher.send(r);
response = await fetcher.send(request);

operation.setContext(<String, StreamedResponse>{
'response': response,
});
final FetchResult parsedResponse = await _parseResponse(response);
final FetchResult parsedResponse =
await _parseResponse(response);

controller.add(parsedResponse);
} catch (error) {
Expand All @@ -98,7 +98,95 @@ class HttpLink extends Link {
);
}

HttpOptionsAndBody _selectHttpOptionsAndBody(
Map<String, File> _getFileMap(
dynamic body, {
Map<String, File> currentMap,
List<String> currentPath = const <String>[],
}) {
currentMap ??= <String, File>{};
if (body is Map<String, dynamic>) {
final entries = body.entries;
for (MapEntry<String, dynamic> element in entries) {
currentMap.addAll(_getFileMap(
element.value,
currentMap: currentMap,
currentPath: List<String>.from(currentPath)..add(element.key),
));
}
return currentMap;
}
if (body is List<dynamic>) {
for (int i = 0; i < body.length; i++) {
currentMap.addAll(_getFileMap(
body[i],
currentMap: currentMap,
currentPath: List<String>.from(currentPath)..add(i.toString()),
));
}
return currentMap;
}
if (body is File) {
return currentMap..addAll(<String, File>{currentPath.join('.'): body});
}
// else should only be either String, num, null; NOTHING else
return currentMap;
}

Future<BaseRequest> _prepareRequest(
String url,
Map<String, dynamic> body,
Map<String, String> httpHeaders,
) async {
final Map<String, File> fileMap = _getFileMap(body);
if (fileMap.isEmpty) {
final Request r = Request('post', Uri.parse(url));
r.headers.addAll(httpHeaders);
r.body = json.encode(body);
return r;
}

final MultipartRequest r = MultipartRequest('post', Uri.parse(url));
r.headers.addAll(httpHeaders);
r.fields['operations'] = json.encode(body, toEncodable: (dynamic object) {
if (object is File) {
return null;
}
return object.toJson();
});

// @todo fileMap.keys.toList() and fileMap.values.toList() same order????
final Map<String, List<String>> adasd =
{}; // fileMap.keys.toList().asMap().map((int index, String filePath) => MapEntry(index.toString(),[filePath]));
final List<MultipartFile> fileList = [];

final fEn = fileMap.entries.toList(growable: false);

for (int i = 0; i < fEn.length; i++) {
final MapEntry<String, File> entry = fEn[i];
final String indexString = i.toString();
adasd.addAll({
indexString: [entry.key]
});
final File f = entry.value;
final String fileName = basename(f.path);
fileList.add(MultipartFile(
indexString,
f.openRead(),
await f.length(),
contentType: MediaType.parse(lookupMimeType(fileName)),
filename: fileName,
));
}

final rfieldsmap = json.encode(adasd);

r.fields['map'] = rfieldsmap;

r.files.addAll(fileList);
return r;
}

HttpHeadersAndBody _selectHttpOptionsAndBody(
Operation operation,
HttpConfig fallbackConfig, [
HttpConfig linkConfig,
Expand All @@ -112,7 +200,7 @@ HttpOptionsAndBody _selectHttpOptionsAndBody(

// http options

// initialze with fallback http options
// initialize with fallback http options
http.addAll(fallbackConfig.http);

// inject the configured http options
Expand All @@ -127,7 +215,7 @@ HttpOptionsAndBody _selectHttpOptionsAndBody(

// options

// initialze with fallback options
// initialize with fallback options
options.addAll(fallbackConfig.options);

// inject the configured options
Expand Down Expand Up @@ -185,30 +273,21 @@ HttpOptionsAndBody _selectHttpOptionsAndBody(
body['query'] = operation.document;
}

return HttpOptionsAndBody(
options: options,
body: encodeBody(body),
return HttpHeadersAndBody(
headers: options['headers'] as Map<String, String>,
body: body,
);
}

dynamic encodeBody(dynamic body) {
final encodedBody = json.encode(body, toEncodable: (dynamic object) {
if(object is File){
return null;
}
return object.toJson();
});
return encodedBody;
}

Future<FetchResult> _parseResponse(StreamedResponse response) async {
final int statusCode = response.statusCode;
final String reasonPhrase = response.reasonPhrase;

try {
final Encoding encoding = _determineEncodingFromResponse(response);
// @todo limit bodyBytes
final String decodedBody = encoding.decode(await response.stream.toBytes());
final reponseByte = await response.stream.toBytes();
final String decodedBody = encoding.decode(reponseByte);

final Map<String, dynamic> jsonResponse =
json.decode(decodedBody) as Map<String, dynamic>;
Expand All @@ -231,6 +310,7 @@ Future<FetchResult> _parseResponse(StreamedResponse response) async {
'Network Error: $statusCode $reasonPhrase',
);
}
rethrow;
}
}

Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies:
sdk: flutter
meta: ^1.1.6
http: ^0.12.0
mime: 0.9.6+2
http_parser: ^3.1.3
path_provider: ^0.5.0
uuid: ^2.0.0
Expand Down
Loading

0 comments on commit c020d12

Please sign in to comment.