Add 'packages/graphql/' from commit '33e2f86ba73d559197b6270df036256104726aca'

git-subtree-dir: packages/graphql
git-subtree-mainline: ac29392d7f
git-subtree-split: 33e2f86ba7
This commit is contained in:
Tobe O 2020-02-15 18:22:07 -05:00
commit 4e69153e3e
185 changed files with 10757 additions and 0 deletions

56
packages/graphql/.gitignore vendored Normal file
View file

@ -0,0 +1,56 @@
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/dictionaries
.idea/**/shelf
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
cmake-build-release/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
.dart_tool

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/angel_graphql/angel_graphql.iml" filepath="$PROJECT_DIR$/angel_graphql/angel_graphql.iml" />
<module fileurl="file://$PROJECT_DIR$/example_star_wars/example_star_wars.iml" filepath="$PROJECT_DIR$/example_star_wars/example_star_wars.iml" />
<module fileurl="file://$PROJECT_DIR$/graphql.iml" filepath="$PROJECT_DIR$/graphql.iml" />
<module fileurl="file://$PROJECT_DIR$/graphql_parser/graphql_parser.iml" filepath="$PROJECT_DIR$/graphql_parser/graphql_parser.iml" />
<module fileurl="file://$PROJECT_DIR$/graphql_schema/graphql_schema.iml" filepath="$PROJECT_DIR$/graphql_schema/graphql_schema.iml" />
<module fileurl="file://$PROJECT_DIR$/graphql_server/graphql_server.iml" filepath="$PROJECT_DIR$/graphql_server/graphql_server.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/angel_graphql/example/main.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$/angel_graphql" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="objects in equality_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/graphql_schema/test/equality_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="objects" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="server.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="VMOptions" value="--observe" />
<option name="filePath" value="$PROJECT_DIR$/example_star_wars/bin/server.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$/example_star_wars" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in comment_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/graphql_parser/test/comment_test.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in graphql_parser" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/graphql_parser" />
<option name="scope" value="FOLDER" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in graphql_schema" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/graphql_schema" />
<option name="scope" value="FOLDER" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in mirrors_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/graphql_server/test/mirrors_test.dart" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in query_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/graphql_server/test/query_test.dart" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in validation_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/graphql_schema/test/validation_test.dart" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in value_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/graphql_parser/test/value_test.dart" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -0,0 +1,6 @@
language: dart
dart:
- dev
- stable
script:
- bash -ex travis.sh

21
packages/graphql/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,39 @@
![Logo](https://github.com/angel-dart/graphql/raw/master/img/angel_logo.png)
<div style="text-align: center">
<hr>
<a href="https://pub.dartlang.org/packages/angel_graphql" rel="nofollow"><img src="https://img.shields.io/pub/v/angel_graphql.svg" alt="Pub" data-canonical-src="https://img.shields.io/pub/v/angel_graphql.svg" style="max-width:100%;"></a>
<a href="https://travis-ci.org/angel-dart/graphql" rel="nofollow"><img src="https://travis-ci.org/angel-dart/graphql.svg" alt="Pub" data-canonical-src="https://img.shields.io/pub/v/angel_graphql.svg" style="max-width:100%;"></a>
</div>
A complete implementation of the official
[GraphQL specification](https://graphql.github.io/graphql-spec/June2018/),
in the Dart programming language.
The goal of this project is to provide to server-side
users of Dart an alternative to REST API's.
Included is also
`package:angel_graphql`, which, when combined with the
[Angel](https://github.com/angel-dart) framework, allows
server-side Dart users to build backends with GraphQL and
virtually any database imaginable.
## Tutorial Demo (click to watch)
[![Youtube thumbnail](video.png)](https://youtu.be/5x6S4kDODa8)
## Projects
This mono repo is split into several sub-projects,
each with its own detailed documentation and examples:
* `angel_graphql` - Support for handling GraphQL via HTTP and
WebSockets in the [Angel](https://angel-dart.dev) framework. Also serves as the `package:graphql_server` reference implementation.
* `data_loader` - A Dart port of [`graphql/data_loader`](https://github.com/graphql/dataloader).
* `example_star_wars`: An example GraphQL API built using
`package:angel_graphql`.
* `graphql_generator`: Generates `package:graphql_schema` object types from concrete Dart classes.
* `graphql_parser`: A recursive descent parser for the GraphQL language.
* `graphql_schema`: An implementation of GraphQL's type system. This, combined with `package:graphql_parser`,
powers `package:graphql_server`.
* `graphql_server`: Base functionality for implementing GraphQL servers in Dart. Has no dependency on any
framework.

View file

@ -0,0 +1,94 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.buildlog
.packages
.project
.pub/
.scripts-bin/
build/
**/packages/
posts.json
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
*.dart.js
*.part.js
*.js.deps
*.js.map
*.info.json
# Directory created by dartdoc
doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
# SDK 1.20 and later (no longer creates packages directories)
# Older SDK versions
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
# Directory created by dartdoc
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
../.idea/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

View file

@ -0,0 +1,17 @@
# 1.1.0
* Support the GraphQL multipart spec: https://github.com/jaydenseric/graphql-multipart-request-spec
# 1.0.0
* Apply `package:pedantic`.
# 1.0.0-rc.0
* Finish `graphQLWS`.
# 1.0.0-beta.1
* Add `graphQLWS` handler, and support subscriptions.
# 1.0.0-beta
* Angel RC updates.
# 1.0.0-alpha
* First official release.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,297 @@
![Logo](https://github.com/angel-dart/graphql/raw/master/img/angel_logo.png)
<div style="text-align: center">
<hr>
<a href="https://pub.dartlang.org/packages/angel_graphql" rel="nofollow"><img src="https://img.shields.io/pub/v/angel_graphql.svg" alt="Pub" data-canonical-src="https://img.shields.io/pub/v/angel_graphql.svg" style="max-width:100%;"></a>
<a href="https://travis-ci.org/angel-dart/graphql" rel="nofollow"><img src="https://travis-ci.org/angel-dart/graphql.svg" alt="Pub" data-canonical-src="https://img.shields.io/pub/v/angel_graphql.svg" style="max-width:100%;"></a>
</div>
* [Installation](#installation)
* [Usage](#usage)
* [Subscriptions](#subscriptions)
* [Integration with Angel `Service`s](#using-services)
* [Documenting API's](#documentation)
* [Deprecated - Mirrors Usage](#mirrors)
A complete implementation of the official
[GraphQL specification](http://facebook.github.io/graphql/October2016/#sec-Language) - these
are the [Angel framework](https://angel-dart.github.io)-specific
bindings.
The goal of this project is to provide to server-side
users of Dart an alternative to REST API's. `package:angel_graphql`, which, when combined with the allows
server-side Dart users to build backends with GraphQL and
virtually any database imaginable.
## Installation
To install `package:angel_graphql`, add the following to your
`pubspec.yaml`:
```yaml
dependencies:
angel_framework: ^2.0.0-alpha
angel_graphql: ^1.0.0-alpha
```
## Usage
Using this package is very similar to GraphQL.js - you define
a schema, and then mount `graphQLHttp` in your router to start
serving. This implementation supports GraphQL features like
introspection, so you can play around with `graphiql` as well!
Firstly, define your schema. A GraphQL schema contains an
*object type* that defines all querying operations that can be
applied to the backend.
A GraphQL schema may also have a *mutation* object type,
which defines operations that change the backend's state, and
optionally a *subscription* type, which defines real-time
interactions (coming soon!).
You can use the `convertDartType` helper to wrap your existing
`Model`/PODO classes, and make GraphQL aware of them without duplicated
effort.
```dart
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_graphql/angel_graphql.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
import 'package:graphql_server/mirrors.dart';
Future configureServer(Angel app) async {
var queryType = objectType(
'Query',
description: 'A simple API that manages your to-do list.',
fields: [
field(
'todos',
listOf(convertDartType(Todo).nonNullable()),
resolve: resolveViaServiceIndex(todoService),
),
field(
'todo',
convertDartType(Todo),
resolve: resolveViaServiceRead(todoService),
inputs: [
GraphQLFieldInput('id', graphQLId.nonNullable()),
],
),
],
);
var mutationType = objectType(
'Mutation',
description: 'Modify the to-do list.',
fields: [
field(
'create',
graphQLString,
),
],
);
var schema = graphQLSchema(
queryType: queryType,
mutationType: mutationType,
);
}
```
After you've created your `GraphQLSchema`, you just need to
wrap in a call to `graphQLHttp`, a request handler that responds
to GraphQL.
In *development*, it's also highly recommended to mount the
`graphiQL` handler, which serves GraphQL's official visual
interface, for easy querying and feedback.
```dart
app.all('/graphql', graphQLHttp(GraphQL(schema)));
app.get('/graphiql', graphiQL());
```
All that's left now is just to start the server!
```dart
var server = await http.startServer('127.0.0.1', 3000);
var uri =
Uri(scheme: 'http', host: server.address.address, port: server.port);
var graphiqlUri = uri.replace(path: 'graphiql');
print('Listening at $uri');
print('Access graphiql at $graphiqlUri');
```
Visit your `/graphiql` endpoint, and you'll see the `graphiql`
UI, ready-to-go!
![Graphiql screenshot](https://github.com/angel-dart/graphql/raw/master/img/angel_graphql.png)
Now you're ready to build a GraphQL API!
## Subscriptions
Example:
https://github.com/angel-dart/graphql/blob/master/angel_graphql/example/subscription.dart
In GraphQL, as of the June 2018 spec, clients can subscribe to streams of events
from the server. In your schema, all you need to do is return a `Stream` from a `resolve`
callback, rather than a plain object:
```dart
var postAdded = postService.afterCreated
.asStream()
.map((e) => {'postAdded': e.result})
.asBroadcastStream();
var schema = graphQLSchema(
// ...
subscriptionType: objectType(
'Subscription',
fields: [
field('postAdded', postType, resolve: (_, __) => postAdded),
],
),
);
```
By default, `graphQLHttp` has no support for subscriptions, because regular
HTTP requests are stateless, and are not ideal for continuous data pushing.
You can add your own handler:
```dart
graphQLHttp(graphQL, onSubscription: (req, res, stream) {
// Do something with the stream here. It's up to you.
});
```
There is, however, `graphQLWS`, which implements Apollo's
`subscriptions-transport-ws` protocol:
```dart
app.get('/subscriptions', graphQLWS(GraphQL(schema)));
```
You can then use existing JavaScript clients to handle subscriptions.
The `graphiQL` handler also supports using subscriptions. In the following snippet, the
necessary scripts will be added to the rendered page, so that the `subscriptions-transport-ws`
client can be used by GraphiQL:
```dart
app.get('/graphiql',
graphiQL(subscriptionsEndpoint: 'ws://localhost:3000/subscriptions'));
```
**NOTE: Apollo's spec for the aforementioned protocol is very far outdated, and completely inaccurate,**
**See this issue for more:**
**https://github.com/apollographql/subscriptions-transport-ws/issues/551**
## Using Services
What would Angel be without services? For those unfamiliar - in Angel,
`Service` is a base class that implements CRUD functionality, and serves
as the database interface within an Angel application. They are well-suited
for NoSQL or other databases without a schema (they can be used with
SQL, but that's not their primary focus).
`package:angel_graphql` has functionality to resolve fields by interacting with
services.
Consider our previous example, and note the calls to
`resolveViaServiceIndex` and `resolveViaServiceRead`:
```dart
var queryType = objectType(
'Query',
description: 'A simple API that manages your to-do list.',
fields: [
field(
'todos',
listOf(convertDartType(Todo).nonNullable()),
resolve: resolveViaServiceIndex(todoService),
),
field(
'todo',
convertDartType(Todo),
resolve: resolveViaServiceRead(todoService),
inputs: [
GraphQLFieldInput('id', graphQLId.nonNullable()),
],
),
],
);
```
In all, there are:
* `resolveViaServiceIndex`
* `resolveViaServiceFindOne`
* `resolveViaServiceRead`
* `resolveViaServiceCreate`
* `resolveViaServiceModify`
* `resolveViaServiceUpdate`
* `resolveViaServiceRemove`
As one might imagine, using these convenience helpers makes
it much quicker to implement CRUD functionality in a GraphQL
API.
## Documentation
Using `package:graphql_generator`, you can generate GraphQL schemas for concrete Dart
types:
```dart
configureServer(Angel app) async {
var schema = graphQLSchema(
queryType: objectType('Query', fields: [
field('todos', listOf(todoGraphQLType), resolve: (_, __) => ...)
]);
);
}
@graphQLClass
class Todo {
String text;
@GraphQLDocumentation(description: 'Whether this item is complete.')
bool isComplete;
}
```
For more documentation, see:
https://pub.dartlang.org/packages/graphql_generator
## Mirrors
**NOTE: Mirrors support is deprecated, and will not be updated further.**
The `convertDartType` function can automatically read the documentation
from a type like the following:
```dart
@GraphQLDocumentation(description: 'Any object with a .text (String) property.')
abstract class HasText {
String get text;
}
@serializable
@GraphQLDocumentation(
description: 'A task that might not be completed yet. **Yay! Markdown!**')
class Todo extends Model implements HasText {
String text;
@GraphQLDocumentation(deprecationReason: 'Use `completion_status` instead.')
bool completed;
CompletionStatus completionStatus;
Todo({this.text, this.completed, this.completionStatus});
}
@GraphQLDocumentation(description: 'The completion status of a to-do item.')
enum CompletionStatus { COMPLETE, INCOMPLETE }
```
You can also manually provide documentation for
parameters and endpoints, via a `description` parameter on almost
all related functions.
See [`package:graphql_schema`](https://github.com/angel-dart/graphql/tree/master/graphql_schema)
for more documentation.

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,105 @@
// ignore_for_file: deprecated_member_use
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_graphql/angel_graphql.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
import 'package:graphql_server/mirrors.dart';
import 'package:logging/logging.dart';
main() async {
var logger = Logger('angel_graphql');
var app = Angel(
logger: logger
..onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
}));
var http = AngelHttp(app);
var todoService = app.use('api/todos', MapService());
var queryType = objectType(
'Query',
description: 'A simple API that manages your to-do list.',
fields: [
field(
'todos',
listOf(convertDartType(Todo).nonNullable()),
resolve: resolveViaServiceIndex(todoService),
),
field(
'todo',
convertDartType(Todo),
resolve: resolveViaServiceRead(todoService),
inputs: [
GraphQLFieldInput('id', graphQLId.nonNullable()),
],
),
],
);
var mutationType = objectType(
'Mutation',
description: 'Modify the to-do list.',
fields: [
field(
'createTodo',
convertDartType(Todo),
inputs: [
GraphQLFieldInput(
'data', convertDartType(Todo).coerceToInputObject()),
],
resolve: resolveViaServiceCreate(todoService),
),
],
);
var schema = graphQLSchema(
queryType: queryType,
mutationType: mutationType,
);
app.all('/graphql', graphQLHttp(GraphQL(schema)));
app.get('/graphiql', graphiQL());
await todoService
.create({'text': 'Clean your room!', 'completion_status': 'COMPLETE'});
await todoService.create(
{'text': 'Take out the trash', 'completion_status': 'INCOMPLETE'});
await todoService.create({
'text': 'Become a billionaire at the age of 5',
'completion_status': 'INCOMPLETE'
});
var server = await http.startServer('127.0.0.1', 3000);
var uri =
Uri(scheme: 'http', host: server.address.address, port: server.port);
var graphiqlUri = uri.replace(path: 'graphiql');
print('Listening at $uri');
print('Access graphiql at $graphiqlUri');
}
@GraphQLDocumentation(description: 'Any object with a .text (String) property.')
abstract class HasText {
String get text;
}
@serializable
@GraphQLDocumentation(
description: 'A task that might not be completed yet. **Yay! Markdown!**')
class Todo extends Model implements HasText {
String text;
@GraphQLDocumentation(deprecationReason: 'Use `completion_status` instead.')
bool completed;
CompletionStatus completionStatus;
Todo({this.text, this.completed, this.completionStatus});
}
@GraphQLDocumentation(description: 'The completion status of a to-do item.')
enum CompletionStatus { COMPLETE, INCOMPLETE }

View file

@ -0,0 +1,98 @@
// Inspired by:
// https://www.apollographql.com/docs/apollo-server/features/subscriptions/#subscriptions-example
import 'package:angel_file_service/angel_file_service.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_graphql/angel_graphql.dart';
import 'package:file/local.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
import 'package:logging/logging.dart';
main() async {
var logger = Logger('angel_graphql');
var app = Angel(logger: logger);
var http = AngelHttp(app);
app.logger.onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
// Create an in-memory service.
var fs = LocalFileSystem();
var postService =
app.use('/api/posts', JsonFileService(fs.file('posts.json')));
// Also get a [Stream] of item creation events.
var postAdded = postService.afterCreated
.asStream()
.map((e) => {'postAdded': e.result})
.asBroadcastStream();
// GraphQL setup.
var postType = objectType('Post', fields: [
field('author', graphQLString),
field('comment', graphQLString),
]);
var schema = graphQLSchema(
// Hooked up to the postService:
// type Query { posts: [Post] }
queryType: objectType(
'Query',
fields: [
field(
'posts',
listOf(postType),
resolve: resolveViaServiceIndex(postService),
),
],
),
// Hooked up to the postService:
// type Mutation {
// addPost(author: String!, comment: String!): Post
// }
mutationType: objectType(
'Mutation',
fields: [
field(
'addPost',
postType,
inputs: [
GraphQLFieldInput(
'data', postType.toInputObject('PostInput').nonNullable()),
],
resolve: resolveViaServiceCreate(postService),
),
],
),
// Hooked up to `postAdded`:
// type Subscription { postAdded: Post }
subscriptionType: objectType(
'Subscription',
fields: [
field('postAdded', postType, resolve: (_, __) => postAdded),
],
),
);
// Mount GraphQL routes; we'll support HTTP and WebSockets transports.
app.all('/graphql', graphQLHttp(GraphQL(schema)));
app.get('/subscriptions',
graphQLWS(GraphQL(schema), keepAliveInterval: Duration(seconds: 3)));
app.get('/graphiql',
graphiQL(subscriptionsEndpoint: 'ws://localhost:3000/subscriptions'));
var server = await http.startServer('127.0.0.1', 3000);
var uri =
Uri(scheme: 'http', host: server.address.address, port: server.port);
var graphiqlUri = uri.replace(path: 'graphiql');
var postsUri = uri.replace(pathSegments: ['api', 'posts']);
print('Listening at $uri');
print('Access graphiql at $graphiqlUri');
print('Access posts service at $postsUri');
}

View file

@ -0,0 +1,46 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:graphql_schema/graphql_schema.dart';
export 'src/graphiql.dart';
export 'src/graphql_http.dart';
export 'src/graphql_ws.dart';
export 'src/resolvers.dart';
/// The canonical [GraphQLUploadType] instance.
final GraphQLUploadType graphQLUpload = GraphQLUploadType();
/// A [GraphQLScalarType] that is used to read uploaded files from
/// `multipart/form-data` requests.
class GraphQLUploadType extends GraphQLScalarType<UploadedFile, UploadedFile> {
@override
String get name => 'Upload';
@override
String get description =>
'Represents a file that has been uploaded to the server.';
@override
GraphQLType<UploadedFile, UploadedFile> coerceToInputObject() => this;
@override
UploadedFile deserialize(UploadedFile serialized) => serialized;
@override
UploadedFile serialize(UploadedFile value) => value;
@override
ValidationResult<UploadedFile> validate(String key, UploadedFile input) {
if (input != null && input is! UploadedFile) {
return _Vr(false, errors: ['Expected "$key" to be a boolean.']);
}
return _Vr(true, value: input);
}
}
// TODO: Really need to make the validation result constructors *public*
class _Vr<T> implements ValidationResult<T> {
final bool successful;
final List<String> errors;
final T value;
_Vr(this.successful, {this.errors, this.value});
}

View file

@ -0,0 +1,89 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:http_parser/http_parser.dart';
/// Returns a simple [RequestHandler] that renders the GraphiQL visual interface for GraphQL.
///
/// By default, the interface expects your backend to be mounted at `/graphql`; this is configurable
/// via [graphQLEndpoint].
RequestHandler graphiQL(
{String graphQLEndpoint = '/graphql', String subscriptionsEndpoint}) {
return (req, res) {
res
..contentType = MediaType('text', 'html')
..write(renderGraphiql(
graphqlEndpoint: graphQLEndpoint,
subscriptionsEndpoint: subscriptionsEndpoint))
..close();
};
}
String renderGraphiql(
{String graphqlEndpoint = '/graphql', String subscriptionsEndpoint}) {
var subscriptionsScripts = '',
subscriptionsFetcher = '',
fetcherName = 'graphQLFetcher';
if (subscriptionsEndpoint != null) {
fetcherName = 'subscriptionsFetcher';
subscriptionsScripts = '''
<script src="//unpkg.com/subscriptions-transport-ws@0.8.3/browser/client.js"></script>
<script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script>
''';
subscriptionsFetcher = '''
let subscriptionsClient = window.SubscriptionsTransportWs.SubscriptionClient('$subscriptionsEndpoint', {
reconnect: true
});
let $fetcherName = window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher);
''';
}
return '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Angel GraphQL</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css">
<style>
html, body {
margin: 0;
padding: 0;
}
html, body, #app {
height: 100%;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.js"></script>
$subscriptionsScripts
<script>
window.onload = function() {
function graphQLFetcher(graphQLParams) {
return fetch('$graphqlEndpoint', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(graphQLParams)
}).then(function(response) {
return response.json();
});
}
$subscriptionsFetcher
ReactDOM.render(
React.createElement(
GraphiQL,
{fetcher: $fetcherName}
),
document.getElementById('app')
);
};
</script>
</body>
</html>
'''
.trim();
}

View file

@ -0,0 +1,182 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_validate/server.dart';
import 'package:graphql_parser/graphql_parser.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
final ContentType graphQlContentType = ContentType('application', 'graphql');
final Validator graphQlPostBody = Validator({
'query*': isNonEmptyString,
'operation_name': isNonEmptyString,
'variables': predicate((v) => v == null || v is String || v is Map),
});
final RegExp _num = RegExp(r'^[0-9]+$');
/// A [RequestHandler] that serves a spec-compliant GraphQL backend.
///
/// Follows the guidelines listed here:
/// https://graphql.org/learn/serving-over-http/
RequestHandler graphQLHttp(GraphQL graphQL,
{Function(RequestContext, ResponseContext, Stream<Map<String, dynamic>>)
onSubscription}) {
return (req, res) async {
var globalVariables = <String, dynamic>{
'__requestctx': req,
'__responsectx': res,
};
sendGraphQLResponse(result) async {
if (result is Stream<Map<String, dynamic>>) {
if (onSubscription == null) {
throw StateError(
'The GraphQL backend returned a Stream, but no `onSubscription` callback was provided.');
} else {
return await onSubscription(req, res, result);
}
}
return {
'data': result,
};
}
executeMap(Map map) async {
var body = await req.parseBody().then((_) => req.bodyAsMap);
var text = body['query'] as String;
var operationName = body['operation_name'] as String;
var variables = body['variables'];
if (variables is String) {
variables = json.decode(variables as String);
}
return await sendGraphQLResponse(await graphQL.parseAndExecute(
text,
sourceUrl: 'input',
operationName: operationName,
variableValues: foldToStringDynamic(variables as Map),
globalVariables: globalVariables,
));
}
try {
if (req.method == 'GET') {
if (await validateQuery(graphQlPostBody)(req, res) as bool) {
return await executeMap(req.queryParameters);
}
} else if (req.method == 'POST') {
if (req.headers.contentType?.mimeType == graphQlContentType.mimeType) {
var text = await req.body.transform(utf8.decoder).join();
return sendGraphQLResponse(await graphQL.parseAndExecute(
text,
sourceUrl: 'input',
globalVariables: globalVariables,
));
} else if (req.headers.contentType?.mimeType == 'application/json') {
if (await validate(graphQlPostBody)(req, res) as bool) {
return await executeMap(req.bodyAsMap);
}
} else if (req.headers.contentType?.mimeType == 'multipart/form-data') {
var fields = await req.parseBody().then((_) => req.bodyAsMap);
var operations = fields['operations'] as String;
if (operations == null) {
throw AngelHttpException.badRequest(
message: 'Missing "operations" field.');
}
var map = fields.containsKey('map')
? json.decode(fields['map'] as String)
: null;
if (map is! Map) {
throw AngelHttpException.badRequest(
message: '"map" field must decode to a JSON object.');
}
var variables = Map<String, dynamic>.from(globalVariables);
for (var entry in (map as Map).entries) {
var file = req.uploadedFiles
.firstWhere((f) => f.name == entry.key, orElse: () => null);
if (file == null) {
throw AngelHttpException.badRequest(
message:
'"map" contained key "${entry.key}", but no uploaded file '
'has that name.');
}
if (entry.value is! List) {
throw AngelHttpException.badRequest(
message:
'The value for "${entry.key}" in the "map" field was not a JSON array.');
}
var objectPaths = entry.value as List;
for (var objectPath in objectPaths) {
var subPaths = (objectPath as String).split('.');
if (subPaths[0] == 'variables') {
Object current = variables;
for (int i = 1; i < subPaths.length; i++) {
var name = subPaths[i];
var parent = subPaths.take(i).join('.');
if (_num.hasMatch(name)) {
if (current is! List) {
throw AngelHttpException.badRequest(
message:
'Object "$parent" is not a JSON array, but the '
'"map" field contained a mapping to $parent.$name.');
}
(current as List)[int.parse(name)] = file;
} else {
if (current is! Map) {
throw AngelHttpException.badRequest(
message:
'Object "$parent" is not a JSON object, but the '
'"map" field contained a mapping to $parent.$name.');
}
(current as Map)[name] = file;
}
}
} else {
throw AngelHttpException.badRequest(
message:
'All array values in the "map" field must begin with "variables.".');
}
}
}
return await sendGraphQLResponse(await graphQL.parseAndExecute(
operations,
sourceUrl: 'input',
globalVariables: variables,
));
} else {
throw AngelHttpException.badRequest();
}
} else {
throw AngelHttpException.badRequest();
}
} on ValidationException catch (e) {
var errors = <GraphQLExceptionError>[GraphQLExceptionError(e.message)];
errors.addAll(e.errors.map((ee) => GraphQLExceptionError(ee)).toList());
return GraphQLException(errors).toJson();
} on AngelHttpException catch (e) {
var errors = <GraphQLExceptionError>[GraphQLExceptionError(e.message)];
errors.addAll(e.errors.map((ee) => GraphQLExceptionError(ee)).toList());
return GraphQLException(errors).toJson();
} on SyntaxError catch (e) {
return GraphQLException.fromSourceSpan(e.message, e.span);
} on GraphQLException catch (e) {
return e.toJson();
} catch (e, st) {
if (req.app?.logger != null) {
req.app.logger.severe(
'An error occurred while processing GraphQL query at ${req.uri}.',
e,
st);
}
return GraphQLException.fromMessage(e.toString()).toJson();
}
};
}

View file

@ -0,0 +1,80 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
import 'package:graphql_server/subscriptions_transport_ws.dart' as stw;
import 'package:web_socket_channel/io.dart';
/// A [RequestHandler] that serves a spec-compliant GraphQL backend, over WebSockets.
/// This endpoint only supports WebSockets, and can be used to deliver subscription events.
///
/// `graphQLWS` uses the Apollo WebSocket protocol, for the sake of compatibility with
/// existing tooling.
///
/// See:
/// * https://github.com/apollographql/subscriptions-transport-ws
RequestHandler graphQLWS(GraphQL graphQL, {Duration keepAliveInterval}) {
return (req, res) async {
if (req is HttpRequestContext) {
if (WebSocketTransformer.isUpgradeRequest(req.rawRequest)) {
await res.detach();
var socket = await WebSocketTransformer.upgrade(req.rawRequest,
protocolSelector: (protocols) {
if (protocols.contains('graphql-ws')) {
return 'graphql-ws';
} else {
throw AngelHttpException.badRequest(
message: 'Only the "graphql-ws" protocol is allowed.');
}
});
var channel = IOWebSocketChannel(socket);
var client = stw.RemoteClient(channel.cast<String>());
var server =
_GraphQLWSServer(client, graphQL, req, res, keepAliveInterval);
await server.done;
} else {
throw AngelHttpException.badRequest(
message: 'The `graphQLWS` endpoint only accepts WebSockets.');
}
} else {
throw AngelHttpException.badRequest(
message: 'The `graphQLWS` endpoint only accepts HTTP/1.1 requests.');
}
};
}
class _GraphQLWSServer extends stw.Server {
final GraphQL graphQL;
final RequestContext req;
final ResponseContext res;
_GraphQLWSServer(stw.RemoteClient client, this.graphQL, this.req, this.res,
Duration keepAliveInterval)
: super(client, keepAliveInterval: keepAliveInterval);
@override
bool onConnect(stw.RemoteClient client, [Map connectionParams]) => true;
@override
Future<stw.GraphQLResult> onOperation(String id, String query,
[Map<String, dynamic> variables, String operationName]) async {
try {
var globalVariables = <String, dynamic>{
'__requestctx': req,
'__responsectx': res,
};
var data = await graphQL.parseAndExecute(
query,
operationName: operationName,
sourceUrl: 'input',
globalVariables: globalVariables,
variableValues: variables,
);
return stw.GraphQLResult(data);
} on GraphQLException catch (e) {
return stw.GraphQLResult(null, errors: e.errors);
}
}
}

View file

@ -0,0 +1,143 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:graphql_schema/graphql_schema.dart';
Map<String, dynamic> _fetchRequestInfo(Map<String, dynamic> arguments) {
return <String, dynamic>{
'__requestctx': arguments.remove('__requestctx'),
'__responsectx': arguments.remove('__responsectx'),
};
}
Map<String, dynamic> _getQuery(Map<String, dynamic> arguments) {
var f = Map<String, dynamic>.from(arguments)..remove('id')..remove('data');
return f.isEmpty ? null : f;
}
/// A GraphQL resolver that `index`es an Angel service.
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<List<Value>, Serialized>
resolveViaServiceIndex<Value, Serialized>(Service<dynamic, Value> service) {
return (_, arguments) async {
var _requestInfo = _fetchRequestInfo(arguments);
var params = {'query': _getQuery(arguments), 'provider': Providers.graphQL}
..addAll(_requestInfo);
return await service.index(params);
};
}
/// A GraphQL resolver that calls `findOne` on an Angel service.
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceFindOne<Value, Serialized>(
Service<dynamic, Value> service) {
return (_, arguments) async {
var _requestInfo = _fetchRequestInfo(arguments);
var params = {'query': _getQuery(arguments), 'provider': Providers.graphQL}
..addAll(_requestInfo);
return await service.findOne(params);
};
}
/// A GraphQL resolver that `read`s a single value from an Angel service.
///
/// This resolver should be used on a field with at least the following inputs:
/// * `id`: a [graphQLId] or [graphQLString]
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceRead<Value, Serialized>(Service<dynamic, Value> service,
{String idField = 'id'}) {
return (_, arguments) async {
var _requestInfo = _fetchRequestInfo(arguments);
var params = {'query': _getQuery(arguments), 'provider': Providers.graphQL}
..addAll(_requestInfo);
var id = arguments.remove(idField);
return await service.read(id, params);
};
}
/// A GraphQL resolver that `creates` a single value in an Angel service.
///
/// This resolver should be used on a field with at least the following input:
/// * `data`: a [GraphQLObjectType] corresponding to the format of `data` to be passed to `create`
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceCreate<Value, Serialized>(
Service<dynamic, Value> service) {
return (_, arguments) async {
var _requestInfo = _fetchRequestInfo(arguments);
var params = {'query': _getQuery(arguments), 'provider': Providers.graphQL}
..addAll(_requestInfo);
return await service.create(arguments['data'] as Value, params);
};
}
/// A GraphQL resolver that `modifies` a single value from an Angel service.
///
/// This resolver should be used on a field with at least the following inputs:
/// * `id`: a [graphQLId] or [graphQLString]
/// * `data`: a [GraphQLObjectType] corresponding to the format of `data` to be passed to `modify`
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceModify<Value, Serialized>(Service<dynamic, Value> service,
{String idField = 'id'}) {
return (_, arguments) async {
var _requestInfo = _fetchRequestInfo(arguments);
var params = {'query': _getQuery(arguments), 'provider': Providers.graphQL}
..addAll(_requestInfo);
var id = arguments.remove(idField);
return await service.modify(id, arguments['data'] as Value, params);
};
}
/// A GraphQL resolver that `update`s a single value from an Angel service.
///
/// This resolver should be used on a field with at least the following inputs:
/// * `id`: a [graphQLId] or [graphQLString]
/// * `data`: a [GraphQLObjectType] corresponding to the format of `data` to be passed to `update`
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
///
/// Keep in mind that `update` **overwrites** existing contents.
/// To avoid this, use [resolveViaServiceModify] instead.
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceUpdate<Value, Serialized>(Service<dynamic, Value> service,
{String idField = 'id'}) {
return (_, arguments) async {
var _requestInfo = _fetchRequestInfo(arguments);
var params = {'query': _getQuery(arguments), 'provider': Providers.graphQL}
..addAll(_requestInfo);
var id = arguments.remove(idField);
return await service.update(id, arguments['data'] as Value, params);
};
}
/// A GraphQL resolver that `remove`s a single value from an Angel service.
///
/// This resolver should be used on a field with at least the following inputs:
/// * `id`: a [graphQLId] or [graphQLString]
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceRemove<Value, Serialized>(Service<dynamic, Value> service,
{String idField = 'id'}) {
return (_, arguments) async {
var _requestInfo = _fetchRequestInfo(arguments);
var params = {'query': _getQuery(arguments), 'provider': Providers.graphQL}
..addAll(_requestInfo);
var id = arguments.remove(idField);
return await service.remove(id, params);
};
}

View file

@ -0,0 +1,22 @@
name: angel_graphql
version: 1.1.0
description: The fastest + easiest way to get a GraphQL backend in Dart, using Angel.
homepage: https://github.com/angel-dart/graphql
author: Tobe O <thosakwe@gmail.com>
environment:
sdk: ">=2.0.0-dev <3.0.0"
dependencies:
angel_file_service: ^2.0.0
angel_framework: ^2.0.0
angel_websocket: ^2.0.0
angel_validate: ^2.0.0
graphql_parser: ^1.0.0
graphql_schema: ^1.0.0
graphql_server: ^1.0.0
http_parser: ^3.0.0
web_socket_channel: ^1.0.0
dev_dependencies:
angel_serialize: ^2.0.0
file: ^5.0.0
logging: ^0.11.0
pedantic: ^1.0.0

View file

@ -0,0 +1,4 @@
.packages
pubspec.lock
.dart_tool
doc/api

View file

@ -0,0 +1,2 @@
# 1.0.0
* Initial version.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,26 @@
# data_loader
[![Pub](https://img.shields.io/pub/v/data_loader.svg)](https://pub.dartlang.org/packages/data_loader)
[![build status](https://travis-ci.org/angel-dart/graphql.svg)](https://travis-ci.org/angel-dart/graphql)
Batch and cache database lookups. Works well with GraphQL.
Ported from the original JS version:
https://github.com/graphql/dataloader
## Installation
In your pubspec.yaml:
```yaml
dependencies:
data_loader: ^1.0.0
```
## Usage
Complete example:
https://github.com/angel-dart/graphql/blob/master/data_loader/example/main.dart
```dart
var userLoader = new DataLoader((key) => myBatchGetUsers(keys));
var invitedBy = await userLoader.load(1)then(user => userLoader.load(user.invitedByID))
print('User 1 was invited by $invitedBy'));
```

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,44 @@
import 'dart:async';
import 'package:data_loader/data_loader.dart';
import 'package:graphql_schema/graphql_schema.dart';
external Future<List<Todo>> fetchTodos(Iterable<int> ids);
main() async {
// Create a DataLoader. By default, it caches lookups.
var todoLoader = DataLoader(fetchTodos); // DataLoader<int, Todo>
// type Todo { id: Int, text: String, is_complete: Boolean }
var todoType = objectType(
'Todo',
fields: [
field('id', graphQLInt),
field('text', graphQLString),
field('is_complete', graphQLBoolean),
],
);
// type Query { todo($id: Int!) Todo }
// ignore: unused_local_variable
var schema = graphQLSchema(
queryType: objectType(
'Query',
fields: [
field(
'todo',
listOf(todoType),
inputs: [GraphQLFieldInput('id', graphQLInt.nonNullable())],
resolve: (_, args) => todoLoader.load(args['id'] as int),
),
],
),
);
// Do something with your schema...
}
abstract class Todo {
int get id;
String get text;
bool get isComplete;
}

View file

@ -0,0 +1,94 @@
import 'dart:async';
import 'dart:collection';
/// A utility for batching multiple requests together, to improve application performance.
///
/// Enqueues batches of requests until the next tick, when they are processed in bulk.
///
/// Port of Facebook's `DataLoader`:
/// https://github.com/graphql/dataloader
class DataLoader<Id, Data> {
/// Invoked to fetch a batch of keys simultaneously.
final FutureOr<Iterable<Data>> Function(Iterable<Id>) loadMany;
/// Whether to use a memoization cache to store the results of past lookups.
final bool cache;
var _cache = <Id, Data>{};
var _queue = Queue<_QueueItem<Id, Data>>();
bool _started = false;
DataLoader(this.loadMany, {this.cache = true});
Future<void> _onTick() async {
if (_queue.isNotEmpty) {
var current = _queue.toList();
_queue.clear();
List<Id> loadIds =
current.map((i) => i.id).toSet().toList(growable: false);
var data = await loadMany(
loadIds,
);
for (int i = 0; i < loadIds.length; i++) {
var id = loadIds[i];
var value = data.elementAt(i);
if (cache) _cache[id] = value;
current
.where((item) => item.id == id)
.forEach((item) => item.completer.complete(value));
}
}
_started = false;
// if (!_closed) scheduleMicrotask(_onTick);
}
/// Clears the value at [key], if it exists.
void clear(Id key) => _cache.remove(key);
/// Clears the entire cache.
void clearAll() => _cache.clear();
/// Primes the cache with the provided key and value. If the key already exists, no change is made.
///
/// To forcefully prime the cache, clear the key first with
/// `loader..clear(key)..prime(key, value)`.
void prime(Id key, Data value) => _cache.putIfAbsent(key, () => value);
/// Closes this [DataLoader], cancelling all pending requests.
void close() {
while (_queue.isNotEmpty) {
_queue.removeFirst().completer.completeError(
StateError('The DataLoader was closed before the item was loaded.'));
}
_queue.clear();
}
/// Returns a [Future] that completes when the next batch of requests completes.
Future<Data> load(Id id) {
if (cache && _cache.containsKey(id)) {
return Future<Data>.value(_cache[id]);
} else {
var item = _QueueItem<Id, Data>(id);
_queue.add(item);
if (!_started) {
_started = true;
scheduleMicrotask(_onTick);
}
return item.completer.future;
}
}
}
class _QueueItem<Id, Data> {
final Id id;
final Completer<Data> completer = Completer();
_QueueItem(this.id);
}

View file

@ -0,0 +1,11 @@
name: data_loader
version: 1.0.0
author: Tobe O <thosakwe@gmail.com>
description: Batch and cache database lookups. Works well with GraphQL. Ported from JS.
homepage: https://github.com/angel-dart/graphql
environment:
sdk: ">=2.0.0-dev <3.0.0"
dev_dependencies:
graphql_schema: ^1.0.0
pedantic: ^1.0.0
test: ">=0.12.0 <2.0.0"

View file

@ -0,0 +1,100 @@
import 'dart:async';
import 'package:data_loader/data_loader.dart';
import 'package:test/test.dart';
void main() {
var numbers = List.generate(10, (i) => i.toStringAsFixed(2));
var numberLoader = DataLoader<int, String>((ids) {
print('ID batch: $ids');
return ids.map((i) => numbers[i]);
});
test('batch', () async {
var zero = numberLoader.load(0);
var one = numberLoader.load(1);
var two = numberLoader.load(2);
var batch = await Future.wait([zero, one, two]);
print('Fetched result: $batch');
expect(batch, ['0.00', '1.00', '2.00']);
});
test('dedupe', () async {
var loader = DataLoader<int, Map<int, List<int>>>((ids) {
return ids.map(
(i) => {i: ids.toList()},
);
});
var zero = loader.load(0);
var one = loader.load(1);
var two = loader.load(2);
var anotherZero = loader.load(0);
var batch = await Future.wait([zero, one, two, anotherZero]);
expect(
batch,
[
{ 0: [0, 1, 2]},
{ 1: [0, 1, 2]},
{ 2: [0, 1, 2]},
{ 0: [0, 1, 2]},
],
);
});
group('cache', () {
DataLoader<int, _Unique> uniqueLoader, noCache;
setUp(() {
uniqueLoader = DataLoader<int, _Unique>((ids) async {
var numbers = await numberLoader.loadMany(ids);
return numbers.map((s) => _Unique(s));
});
noCache = DataLoader(uniqueLoader.loadMany, cache: false);
});
tearDown(() {
uniqueLoader.close();
noCache.close();
});
test('only lookup once', () async {
var a = await uniqueLoader.load(3);
var b = await uniqueLoader.load(3);
expect(a, b);
});
test('can be disabled', () async {
var a = await noCache.load(3);
var b = await noCache.load(3);
expect(a, isNot(b));
});
test('clear', () async {
var a = await uniqueLoader.load(3);
uniqueLoader.clear(3);
var b = await uniqueLoader.load(3);
expect(a, isNot(b));
});
test('clearAll', () async {
var a = await uniqueLoader.load(3);
uniqueLoader.clearAll();
var b = await uniqueLoader.load(3);
expect(a, isNot(b));
});
test('prime', () async {
uniqueLoader.prime(3, _Unique('hey'));
var a = await uniqueLoader.load(3);
expect(a.value, 'hey');
});
});
}
class _Unique {
final String value;
_Unique(this.value);
}

View file

@ -0,0 +1,93 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.buildlog
.packages
.project
.pub/
.scripts-bin/
build/
**/packages/
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
*.dart.js
*.part.js
*.js.deps
*.js.map
*.info.json
# Directory created by dartdoc
doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
# SDK 1.20 and later (no longer creates packages directories)
# Older SDK versions
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
# Directory created by dartdoc
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
../.idea/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,28 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_hot/angel_hot.dart';
import 'package:logging/logging.dart';
import 'package:star_wars/src/pretty_logging.dart' as star_wars;
import 'package:star_wars/star_wars.dart' as star_wars;
main() async {
Future<Angel> createServer() async {
hierarchicalLoggingEnabled = true;
var logger = Logger.detached('star_wars')
..onRecord.listen(star_wars.prettyLog);
var app = Angel(logger: logger);
await app.configure(star_wars.configureServer);
return app;
}
var hot = HotReloader(createServer, [Directory('lib')]);
var server = await hot.startServer('127.0.0.1', 3000);
var serverUrl =
Uri(scheme: 'http', host: server.address.address, port: server.port);
var graphiQLUrl = serverUrl.replace(path: '/graphiql');
print('Listening at $serverUrl');
print('GraphiQL endpoint: $graphiQLUrl');
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,12 @@
import 'package:graphql_schema/graphql_schema.dart';
import 'episode.dart';
part 'character.g.dart';
@graphQLClass
abstract class Character {
String get id;
String get name;
// List<Episode> get appearsIn;
}

View file

@ -0,0 +1,13 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'character.dart';
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Character].
final GraphQLObjectType characterGraphQLType = objectType('Character',
isInterface: true,
interfaces: [],
fields: [field('id', graphQLString), field('name', graphQLString)]);

View file

@ -0,0 +1,23 @@
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:collection/collection.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'character.dart';
import 'episode.dart';
part 'droid.g.dart';
@serializable
@graphQLClass
@GraphQLDocumentation(description: 'Beep! Boop!')
abstract class _Droid extends Model implements Character {
String get id;
String get name;
@GraphQLDocumentation(
description: 'The list of episodes this droid appears in.')
List<Episode> get appearsIn;
/// Doc comments automatically become GraphQL descriptions.
List<Character> get friends;
}

View file

@ -0,0 +1,164 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'droid.dart';
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Droid extends _Droid {
Droid(
{this.id,
this.name,
List<Episode> appearsIn,
List<Character> friends,
this.createdAt,
this.updatedAt})
: this.appearsIn = new List.unmodifiable(appearsIn ?? []),
this.friends = new List.unmodifiable(friends ?? []);
@override
final String id;
@override
final String name;
@override
final List<Episode> appearsIn;
@override
final List<Character> friends;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
Droid copyWith(
{String id,
String name,
List<Episode> appearsIn,
List<Character> friends,
DateTime createdAt,
DateTime updatedAt}) {
return new Droid(
id: id ?? this.id,
name: name ?? this.name,
appearsIn: appearsIn ?? this.appearsIn,
friends: friends ?? this.friends,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);
}
bool operator ==(other) {
return other is _Droid &&
other.id == id &&
other.name == name &&
const ListEquality<Episode>(const DefaultEquality<Episode>())
.equals(other.appearsIn, appearsIn) &&
const ListEquality<Character>(const DefaultEquality<Character>())
.equals(other.friends, friends) &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return hashObjects([id, name, appearsIn, friends, createdAt, updatedAt]);
}
Map<String, dynamic> toJson() {
return DroidSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
abstract class DroidSerializer {
static Droid fromMap(Map map) {
return new Droid(
id: map['id'] as String,
name: map['name'] as String,
appearsIn: map['appears_in'] is Iterable
? (map['appears_in'] as Iterable).cast<Episode>().toList()
: null,
friends: map['friends'] is Iterable
? (map['friends'] as Iterable).cast<Character>().toList()
: null,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null);
}
static Map<String, dynamic> toMap(_Droid model) {
if (model == null) {
return null;
}
return {
'id': model.id,
'name': model.name,
'appears_in': model.appearsIn,
'friends': model.friends,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String()
};
}
}
abstract class DroidFields {
static const List<String> allFields = <String>[
id,
name,
appearsIn,
friends,
createdAt,
updatedAt
];
static const String id = 'id';
static const String name = 'name';
static const String appearsIn = 'appears_in';
static const String friends = 'friends';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
}
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Droid].
final GraphQLObjectType droidGraphQLType = objectType('Droid',
isInterface: false,
description: 'Beep! Boop!',
interfaces: [
characterGraphQLType
],
fields: [
field('id', graphQLString),
field('name', graphQLString),
field('appears_in', listOf(episodeGraphQLType),
description: 'The list of episodes this droid appears in.'),
field('friends', listOf(characterGraphQLType),
description:
'Doc comments automatically become GraphQL descriptions.'),
field('created_at', graphQLDate),
field('updated_at', graphQLDate),
field('idAsInt', graphQLInt)
]);

View file

@ -0,0 +1,11 @@
import 'package:graphql_schema/graphql_schema.dart';
part 'episode.g.dart';
@GraphQLDocumentation(
description: 'The episodes of the Star Wars original trilogy.')
@graphQLClass
enum Episode {
NEWHOPE,
EMPIRE,
JEDI,
}

View file

@ -0,0 +1,12 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'episode.dart';
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Episode].
final GraphQLEnumType<String> episodeGraphQLType = enumTypeFromStrings(
'Episode', const ['NEWHOPE', 'EMPIRE', 'JEDI'],
description: 'The episodes of the Star Wars original trilogy.');

View file

@ -0,0 +1,35 @@
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:collection/collection.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'character.dart';
import 'episode.dart';
part 'human.g.dart';
@serializable
@graphQLClass
abstract class _Human extends Model implements Character {
// @GraphQLDocumentation(description: "This human's name, of course.")
// String name;
// List<Character> friends;
// List<Episode> appearsIn;
// List<Starship> starships;
// int totalCredits;
String get id;
String get name;
List<Episode> get appearsIn;
List<Character> get friends;
int get totalCredits;
// Human(
// {this.name,
// this.friends,
// this.appearsIn,
// this.starships,
// this.totalCredits});
}

View file

@ -0,0 +1,172 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'human.dart';
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Human extends _Human {
Human(
{this.id,
this.name,
List<Episode> appearsIn,
List<Character> friends,
this.totalCredits,
this.createdAt,
this.updatedAt})
: this.appearsIn = new List.unmodifiable(appearsIn ?? []),
this.friends = new List.unmodifiable(friends ?? []);
@override
final String id;
@override
final String name;
@override
final List<Episode> appearsIn;
@override
final List<Character> friends;
@override
final int totalCredits;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
Human copyWith(
{String id,
String name,
List<Episode> appearsIn,
List<Character> friends,
int totalCredits,
DateTime createdAt,
DateTime updatedAt}) {
return new Human(
id: id ?? this.id,
name: name ?? this.name,
appearsIn: appearsIn ?? this.appearsIn,
friends: friends ?? this.friends,
totalCredits: totalCredits ?? this.totalCredits,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);
}
bool operator ==(other) {
return other is _Human &&
other.id == id &&
other.name == name &&
const ListEquality<Episode>(const DefaultEquality<Episode>())
.equals(other.appearsIn, appearsIn) &&
const ListEquality<Character>(const DefaultEquality<Character>())
.equals(other.friends, friends) &&
other.totalCredits == totalCredits &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return hashObjects(
[id, name, appearsIn, friends, totalCredits, createdAt, updatedAt]);
}
Map<String, dynamic> toJson() {
return HumanSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
abstract class HumanSerializer {
static Human fromMap(Map map) {
return new Human(
id: map['id'] as String,
name: map['name'] as String,
appearsIn: map['appears_in'] is Iterable
? (map['appears_in'] as Iterable).cast<Episode>().toList()
: null,
friends: map['friends'] is Iterable
? (map['friends'] as Iterable).cast<Character>().toList()
: null,
totalCredits: map['total_credits'] as int,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null);
}
static Map<String, dynamic> toMap(_Human model) {
if (model == null) {
return null;
}
return {
'id': model.id,
'name': model.name,
'appears_in': model.appearsIn,
'friends': model.friends,
'total_credits': model.totalCredits,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String()
};
}
}
abstract class HumanFields {
static const List<String> allFields = <String>[
id,
name,
appearsIn,
friends,
totalCredits,
createdAt,
updatedAt
];
static const String id = 'id';
static const String name = 'name';
static const String appearsIn = 'appears_in';
static const String friends = 'friends';
static const String totalCredits = 'total_credits';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
}
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Human].
final GraphQLObjectType humanGraphQLType =
objectType('Human', isInterface: false, interfaces: [
characterGraphQLType
], fields: [
field('id', graphQLString),
field('name', graphQLString),
field('appears_in', listOf(episodeGraphQLType)),
field('friends', listOf(characterGraphQLType)),
field('total_credits', graphQLInt),
field('created_at', graphQLDate),
field('updated_at', graphQLDate),
field('idAsInt', graphQLInt)
]);

View file

@ -0,0 +1,5 @@
export 'character.dart';
export 'droid.dart';
export 'episode.dart';
export 'human.dart';
export 'starship.dart';

View file

@ -0,0 +1,11 @@
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:graphql_schema/graphql_schema.dart';
part 'starship.g.dart';
@serializable
@graphQLClass
abstract class _Starship extends Model {
String get name;
int get length;
}

View file

@ -0,0 +1,130 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'starship.dart';
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Starship extends _Starship {
Starship({this.id, this.name, this.length, this.createdAt, this.updatedAt});
@override
final String id;
@override
final String name;
@override
final int length;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
Starship copyWith(
{String id,
String name,
int length,
DateTime createdAt,
DateTime updatedAt}) {
return new Starship(
id: id ?? this.id,
name: name ?? this.name,
length: length ?? this.length,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);
}
bool operator ==(other) {
return other is _Starship &&
other.id == id &&
other.name == name &&
other.length == length &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return hashObjects([id, name, length, createdAt, updatedAt]);
}
Map<String, dynamic> toJson() {
return StarshipSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
abstract class StarshipSerializer {
static Starship fromMap(Map map) {
return new Starship(
id: map['id'] as String,
name: map['name'] as String,
length: map['length'] as int,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null);
}
static Map<String, dynamic> toMap(_Starship model) {
if (model == null) {
return null;
}
return {
'id': model.id,
'name': model.name,
'length': model.length,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String()
};
}
}
abstract class StarshipFields {
static const List<String> allFields = <String>[
id,
name,
length,
createdAt,
updatedAt
];
static const String id = 'id';
static const String name = 'name';
static const String length = 'length';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
}
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Starship].
final GraphQLObjectType starshipGraphQLType =
objectType('Starship', isInterface: false, interfaces: [], fields: [
field('id', graphQLString),
field('name', graphQLString),
field('length', graphQLInt),
field('created_at', graphQLDate),
field('updated_at', graphQLDate),
field('idAsInt', graphQLInt)
]);

View file

@ -0,0 +1,35 @@
import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:logging/logging.dart';
import 'package:io/ansi.dart';
/// Prints the contents of a [LogRecord] with pretty colors.
void prettyLog(LogRecord record) {
var code = chooseLogColor(record.level);
if (record.error == null) print(code.wrap(record.toString()));
if (record.error != null) {
var err = record.error;
if (err is AngelHttpException && err.statusCode != 500) return;
print(code.wrap(record.toString() + '\n'));
print(code.wrap(err.toString()));
if (record.stackTrace != null) {
print(code.wrap(record.stackTrace.toString()));
}
}
}
/// Chooses a color based on the logger [level].
AnsiCode chooseLogColor(Level level) {
if (level == Level.SHOUT)
return backgroundRed;
else if (level == Level.SEVERE)
return red;
else if (level == Level.WARNING)
return yellow;
else if (level == Level.INFO)
return cyan;
else if (level == Level.FINER || level == Level.FINEST) return lightGray;
return resetAll;
}

View file

@ -0,0 +1,154 @@
import 'dart:async';
import 'dart:math';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_graphql/angel_graphql.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
import 'package:graphql_server/mirrors.dart';
import 'src/models/models.dart';
Future configureServer(Angel app) async {
// Create standard Angel services. Note that these will also *automatically* be
// exposed via a REST API as well.
var droidService = app.use('/api/droids', MapService());
var humansService = app.use('/api/humans', MapService());
var starshipService = app.use('/api/starships', MapService());
var rnd = Random();
// Create the GraphQL schema.
// `package:graphql_generator` has generated schemas for some of our
// classes.
// A Hero can be either a Droid or Human; create a union type that represents this.
var heroType = GraphQLUnionType('Hero', [droidGraphQLType, humanGraphQLType]);
// Create the query type.
//
// Use the `resolveViaServiceIndex` helper to load data directly from an
// Angel service.
var queryType = objectType(
'StarWarsQuery',
description: 'A long time ago, in a galaxy far, far away...',
fields: [
field(
'droids',
listOf(droidGraphQLType.nonNullable()),
description: 'All droids in the known galaxy.',
resolve: resolveViaServiceIndex(droidService),
),
field(
'humans',
listOf(humanGraphQLType.nonNullable()),
description: 'All humans in the known galaxy.',
resolve: resolveViaServiceIndex(humansService),
),
field(
'starships',
listOf(starshipGraphQLType.nonNullable()),
description: 'All starships in the known galaxy.',
resolve: resolveViaServiceIndex(starshipService),
),
field(
'hero',
heroType,
description:
'Finds a random hero within the known galaxy, whether a Droid or Human.',
inputs: [
GraphQLFieldInput('ep', episodeGraphQLType),
],
resolve: randomHeroResolver(droidService, humansService, rnd),
),
],
);
// Convert our object types to input objects, so that they can be passed to
// mutations.
var humanChangesType = humanGraphQLType.toInputObject('HumanChanges');
// Create the mutation type.
var mutationType = objectType(
'StarWarsMutation',
fields: [
// We'll use the `modify_human` mutation to modify a human in the database.
field(
'modify_human',
humanGraphQLType.nonNullable(),
description: 'Modifies a human in the database.',
inputs: [
GraphQLFieldInput('id', graphQLId.nonNullable()),
GraphQLFieldInput('data', humanChangesType.nonNullable()),
],
resolve: resolveViaServiceModify(humansService),
),
],
);
// Finally, create the schema.
var schema = graphQLSchema(
queryType: queryType,
mutationType: mutationType,
);
// Next, create a GraphQL object, which will be passed to `graphQLHttp`, and
// used to mount a spec-compliant GraphQL endpoint on the server.
//
// The `mirrorsFieldResolver` is unnecessary in this case, because we are using
// `Map`s only, but if our services returned concrete Dart objects, we'd need
// this to allow GraphQL to read field values.
var graphQL = GraphQL(schema, defaultFieldResolver: mirrorsFieldResolver);
// Mount the GraphQL endpoint.
app.all('/graphql', graphQLHttp(graphQL));
// In development, we'll want to mount GraphiQL, for easy management of the database.
if (!app.isProduction) {
app.get('/graphiql', graphiQL());
}
// Seed the database.
var leia = await humansService.create({
'name': 'Leia Organa',
'appears_in': ['NEWHOPE', 'EMPIRE', 'JEDI'],
'total_credits': 520,
});
var lando = await humansService.create({
'name': 'Lando Calrissian',
'appears_in': ['EMPIRE', 'JEDI'],
'total_credits': 525430,
});
var hanSolo = await humansService.create({
'name': 'Han Solo',
'appears_in': ['NEWHOPE', 'EMPIRE', 'JEDI'],
'total_credits': 23,
'friends': [leia, lando],
});
// Luke, of course.
await humansService.create({
'name': 'Luke Skywalker',
'appears_in': ['NEWHOPE', 'EMPIRE', 'JEDI'],
'total_credits': 682,
'friends': [leia, hanSolo, lando],
});
}
GraphQLFieldResolver randomHeroResolver(
Service droidService, Service humansService, Random rnd) {
return (_, args) async {
var allHeroes = [];
var allDroids = await droidService.index();
var allHumans = await humansService.index();
allHeroes..addAll(allDroids)..addAll(allHumans);
// Ignore the annoying cast here, hopefully Dart 2 fixes cases like this
allHeroes = allHeroes
.where((m) =>
!args.containsKey('ep') ||
(m['appears_in'].contains(args['ep']) as bool))
.toList();
return allHeroes.isEmpty ? null : allHeroes[rnd.nextInt(allHeroes.length)];
};
}

View file

@ -0,0 +1,21 @@
name: star_wars
publish_to: none
dependencies:
#angel_file_service: ^1.0.0
angel_graphql:
path: ../angel_graphql
angel_hot: ^2.0.0-alpha
angel_serialize: ^2.0.0
io: ^0.3.2
dev_dependencies:
angel_serialize_generator: ^2.0.0
build_runner: ^1.0.0
graphql_generator:
path: ../graphql_generator
dependency_overrides:
graphql_parser:
path: ../graphql_parser
graphql_schema:
path: ../graphql_schema
graphql_server:
path: ../graphql_server

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,93 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.buildlog
.packages
.project
.pub/
.scripts-bin/
build/
**/packages/
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
*.dart.js
*.part.js
*.js.deps
*.js.map
*.info.json
# Directory created by dartdoc
doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
# SDK 1.20 and later (no longer creates packages directories)
# Older SDK versions
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
# Directory created by dartdoc
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
../.idea/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

View file

@ -0,0 +1,9 @@
# 1.0.0+1
* Replace `snakeCase` with `camelCase`.
# 1.0.0
* Apply `package:pedantic`.
# 1.0.0-rc.1
* Add `CHANGELOG.md`, `example/main.dart`.
* Add documentation to `README.md`.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,50 @@
# graphql_generator
[![Pub](https://img.shields.io/pub/v/graphql_generator.svg)](https://pub.dartlang.org/packages/graphql_generator)
[![build status](https://travis-ci.org/angel-dart/graphql.svg)](https://travis-ci.org/angel-dart/graphql)
Generates `package:graphql_schema` schemas for
annotated class.
Replaces `convertDartType` from `package:graphql_server`.
## Usage
Usage is very simple. You just need a `@graphQLClass` or `@GraphQLClass()` annotation
on any class you want to generate an object type for.
Individual fields can have a `@GraphQLDocumentation()` annotation, to provide information
like descriptions, deprecation reasons, etc.
```dart
@graphQLClass
class Todo {
String text;
@GraphQLDocumentation(description: 'Whether this item is complete.')
bool isComplete;
}
void main() {
print(todoGraphQLType.fields.map((f) => f.name));
}
```
The following is generated (as of April 18th, 2019):
```dart
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main.dart';
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [Todo].
final GraphQLObjectType todoGraphQLType = objectType('Todo',
isInterface: false,
interfaces: [],
fields: [
field('text', graphQLString),
field('isComplete', graphQLBoolean)
]);
```

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,14 @@
builders:
graphql:
import: "package:graphql_generator/graphql_generator.dart"
builder_factories:
- graphQLBuilder
auto_apply: root_package
build_to: cache
build_extensions:
.dart:
- graphql_generator.g.part
required_inputs:
- angel_serialize.g.part
applies_builders:
- source_gen|combining_builder

View file

@ -0,0 +1,14 @@
import 'package:graphql_schema/graphql_schema.dart';
part 'main.g.dart';
@graphQLClass
class TodoItem {
String text;
@GraphQLDocumentation(description: 'Whether this item is complete.')
bool isComplete;
}
void main() {
print(todoItemGraphQLType.fields.map((f) => f.name));
}

View file

@ -0,0 +1,16 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main.dart';
// **************************************************************************
// _GraphQLGenerator
// **************************************************************************
/// Auto-generated from [TodoItem].
final GraphQLObjectType todoItemGraphQLType = objectType('TodoItem',
isInterface: false,
interfaces: [],
fields: [
field('text', graphQLString),
field('isComplete', graphQLBoolean)
]);

View file

@ -0,0 +1,233 @@
import 'dart:async';
import 'dart:mirrors';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize_generator/build_context.dart';
import 'package:angel_serialize_generator/context.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:recase/recase.dart';
import 'package:source_gen/source_gen.dart';
/// Generates GraphQL schemas, statically.
Builder graphQLBuilder(_) {
return SharedPartBuilder([_GraphQLGenerator()], 'graphql_generator');
}
var _docComment = RegExp(r'^/// ', multiLine: true);
var _graphQLDoc = TypeChecker.fromRuntime(GraphQLDocumentation);
var _graphQLClassTypeChecker = TypeChecker.fromRuntime(GraphQLClass);
class _GraphQLGenerator extends GeneratorForAnnotation<GraphQLClass> {
@override
Future<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) async {
if (element is ClassElement) {
var ctx = element.isEnum
? null
: await buildContext(
element,
annotation,
buildStep,
buildStep.resolver,
serializableTypeChecker.hasAnnotationOf(element));
var lib = buildSchemaLibrary(element, ctx, annotation);
return lib.accept(DartEmitter()).toString();
} else {
throw UnsupportedError('@GraphQLClass() is only supported on classes.');
}
}
bool isInterface(ClassElement clazz) {
return clazz.isAbstract && !serializableTypeChecker.hasAnnotationOf(clazz);
}
bool _isGraphQLClass(InterfaceType clazz) {
var search = clazz;
while (search != null) {
if (_graphQLClassTypeChecker.hasAnnotationOf(search.element)) return true;
search = search.superclass;
}
return false;
}
Expression _inferType(String className, String name, DartType type) {
// Firstly, check if it's a GraphQL class.
if (type is InterfaceType && _isGraphQLClass(type)) {
var c = type;
var name = serializableTypeChecker.hasAnnotationOf(c.element) &&
c.name.startsWith('_')
? c.name.substring(1)
: c.name;
var rc = ReCase(name);
return refer('${rc.camelCase}GraphQLType');
}
// Next, check if this is the "id" field of a `Model`.
if (TypeChecker.fromRuntime(Model).isAssignableFromType(type) &&
name == 'id') {
return refer('graphQLId');
}
var primitive = {
String: 'graphQLString',
int: 'graphQLInt',
double: 'graphQLFloat',
bool: 'graphQLBoolean',
DateTime: 'graphQLDate'
};
// Check to see if it's a primitive type.
for (var entry in primitive.entries) {
if (TypeChecker.fromRuntime(entry.key).isAssignableFromType(type)) {
return refer(entry.value);
}
}
// Next, check to see if it's a List.
if (type is InterfaceType &&
type.typeArguments.isNotEmpty &&
TypeChecker.fromRuntime(Iterable).isAssignableFromType(type)) {
var arg = type.typeArguments[0];
var inner = _inferType(className, name, arg);
return refer('listOf').call([inner]);
}
// Nothing else is allowed.
throw 'Cannot infer the GraphQL type for field $className.$name (type=$type).';
}
void _applyDescription(
Map<String, Expression> named, Element element, String docComment) {
String docString = docComment;
if (docString == null && _graphQLDoc.hasAnnotationOf(element)) {
var ann = _graphQLDoc.firstAnnotationOf(element);
var cr = ConstantReader(ann);
docString = cr.peek('description')?.stringValue;
}
if (docString != null) {
named['description'] = literalString(
docString.replaceAll(_docComment, '').replaceAll('\n', '\\n'));
}
}
Library buildSchemaLibrary(
ClassElement clazz, BuildContext ctx, ConstantReader ann) {
return Library((b) {
// Generate a top-level xGraphQLType object
if (clazz.isEnum) {
b.body.add(Field((b) {
// enumTypeFromStrings(String name, List<String> values, {String description}
var args = <Expression>[literalString(clazz.name)];
var values =
clazz.fields.where((f) => f.isEnumConstant).map((f) => f.name);
var named = <String, Expression>{};
_applyDescription(named, clazz, clazz.documentationComment);
args.add(literalConstList(values.map(literalString).toList()));
b
..name = ReCase(clazz.name).camelCase + 'GraphQLType'
..docs.add('/// Auto-generated from [${clazz.name}].')
..type = TypeReference((b) => b
..symbol = 'GraphQLEnumType'
..types.add(refer('String')))
..modifier = FieldModifier.final$
..assignment = refer('enumTypeFromStrings').call(args, named).code;
}));
} else {
b.body.add(Field((b) {
var args = <Expression>[literalString(ctx.modelClassName)];
var named = <String, Expression>{
'isInterface': literalBool(isInterface(clazz))
};
// Add documentation
_applyDescription(named, clazz, clazz.documentationComment);
// Add interfaces
var interfaces = clazz.interfaces.where(_isGraphQLClass).map((c) {
var name = serializableTypeChecker.hasAnnotationOf(c.element) &&
c.name.startsWith('_')
? c.name.substring(1)
: c.name;
var rc = ReCase(name);
return refer('${rc.camelCase}GraphQLType');
});
named['interfaces'] = literalList(interfaces);
// Add fields
var ctxFields = ctx.fields.toList();
// Also incorporate parent fields.
var search = clazz.type;
while (search != null &&
!TypeChecker.fromRuntime(Object).isExactlyType(search)) {
for (var field in search.element.fields) {
if (!ctxFields.any((f) => f.name == field.name)) {
ctxFields.add(field);
}
}
search = search.superclass;
}
var fields = <Expression>[];
for (var field in ctxFields) {
var named = <String, Expression>{};
var originalField = clazz.fields
.firstWhere((f) => f.name == field.name, orElse: () => null);
// Check if it is deprecated.
var depEl = originalField?.getter ?? originalField ?? field;
var depAnn =
TypeChecker.fromRuntime(Deprecated).firstAnnotationOf(depEl);
if (depAnn != null) {
var dep = ConstantReader(depAnn);
var reason = dep.peek('messages')?.stringValue ??
dep.peek('expires')?.stringValue ??
'Expires: ${deprecated.message}.';
named['deprecationReason'] = literalString(reason);
}
// Description finder...
_applyDescription(
named,
originalField?.getter ?? originalField ?? field,
originalField?.getter?.documentationComment ??
originalField?.documentationComment);
// Pick the type.
var doc = _graphQLDoc.firstAnnotationOf(depEl);
Expression type;
if (doc != null) {
var cr = ConstantReader(doc);
var typeName = cr.peek('typeName')?.symbolValue;
if (typeName != null)
type = refer(MirrorSystem.getName(typeName));
}
fields.add(refer('field').call([
literalString(ctx.resolveFieldName(field.name)),
type ??= _inferType(clazz.name, field.name, field.type)
], named));
}
named['fields'] = literalList(fields);
b
..name = ctx.modelClassNameRecase.camelCase + 'GraphQLType'
..docs.add('/// Auto-generated from [${ctx.modelClassName}].')
..type = refer('GraphQLObjectType')
..modifier = FieldModifier.final$
..assignment = refer('objectType').call(args, named).code;
}));
}
});
}
}

View file

@ -0,0 +1,20 @@
name: graphql_generator
version: 1.0.0+1
description: Generates GraphQL schemas from Dart classes, for use with pkg:graphql_server.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/graphql
environment:
sdk: ">=2.0.0 <3.0.0"
dependencies:
analyzer: ">=0.27.1 <2.0.0"
angel_model: ^1.0.0
angel_serialize_generator: ^2.0.0
build: ^1.0.0
build_config: ^0.3.0
code_builder: ^3.0.0
graphql_schema: ^1.0.2
recase: ^2.0.0
source_gen: ^0.9.4
dev_dependencies:
build_runner: ^1.0.0
pedantic: ^1.0.0

View file

@ -0,0 +1,95 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.buildlog
.packages
.project
.pub/
.scripts-bin/
build/
**/packages/
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
*.dart.js
*.part.js
*.js.deps
*.js.map
*.info.json
# Directory created by dartdoc
doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
# SDK 1.20 and later (no longer creates packages directories)
# Older SDK versions
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
# Directory created by dartdoc
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
../.idea/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.dart_tool

View file

@ -0,0 +1,28 @@
# 1.2.0
* Combine `ValueContext` and `VariableContext` into a single `InputValueContext` supertype.
* Add `T computeValue(Map<String, dynamic> variables);`
* Resolve [#23](https://github.com/angel-dart/graphql/issues/23).
* Deprecate old `ValueOrVariable` class, and parser/AST methods related to it.
# 1.1.4
* Fix broken int variable parsing - https://github.com/angel-dart/graphql/pull/32
# 1.1.3
* Add `Parser.nextName`, and remove all formerly-reserved words from the lexer.
Resolves [#19](https://github.com/angel-dart/graphql/issues/19).
# 1.1.2
* Parse the `subscription` keyword.
# 1.1.1
* Pubspec updates for Dart 2.
# 1.1.0
* Removed `GraphQLVisitor`.
* Enable parsing operations without an explicit
name.
* Parse `null`.
* Completely ignore commas.
* Ignore Unicode BOM, as per the spec.
* Parse object values.
* Parse enum values.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,47 @@
# graphql_parser
[![Pub](https://img.shields.io/pub/v/graphql_parser.svg)](https://pub.dartlang.org/packages/graphql_parser)
[![build status](https://travis-ci.org/angel-dart/graphql.svg)](https://travis-ci.org/angel-dart/graphql)
Parses GraphQL queries and schemas.
*This library is merely a parser/visitor*. Any sort of actual GraphQL API functionality must be implemented by you,
or by a third-party package.
[Angel framework](https://angel-dart.github.io)
users should consider
[`package:angel_graphql`](https://pub.dartlang.org/packages/angel_graphql)
as a dead-simple way to add GraphQL functionality to their servers.
# Installation
Add `graphql_parser` as a dependency in your `pubspec.yaml` file:
```yaml
dependencies:
graphql_parser: ^1.0.0
```
# Usage
The AST featured in this library was originally directly based off this ANTLR4 grammar created by Joseph T. McBride:
https://github.com/antlr/grammars-v4/blob/master/graphql/GraphQL.g4
It has since been updated to reflect upon the grammar in the official GraphQL
specification (
[June 2018](https://facebook.github.io/graphql/June2018/)).
```dart
import 'package:graphql_parser/graphql_parser.dart';
doSomething(String text) {
var tokens = scan(text);
var parser = Parser(tokens);
if (parser.errors.isNotEmpty) {
// Handle errors...
}
// Parse the GraphQL document using recursive descent
var doc = parser.parseDocument();
// Do something with the parsed GraphQL document...
}
```

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,26 @@
import 'package:graphql_parser/graphql_parser.dart';
final String text = '''
{
project(name: "GraphQL") {
tagline
}
}
'''
.trim();
main() {
var tokens = scan(text);
var parser = Parser(tokens);
var doc = parser.parseDocument();
var operation = doc.definitions.first as OperationDefinitionContext;
var projectField = operation.selectionSet.selections.first.field;
print(projectField.fieldName.name); // project
print(projectField.arguments.first.name); // name
print(projectField.arguments.first.value); // GraphQL
var taglineField = projectField.selectionSet.selections.first.field;
print(taglineField.fieldName.name); // tagline
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,2 @@
export 'src/language/ast/ast.dart';
export 'src/language/language.dart';

View file

@ -0,0 +1,33 @@
import 'package:source_span/source_span.dart';
import '../token.dart';
import 'node.dart';
/// An alternate name for a field within a [SelectionSet].
class AliasContext extends Node {
/// The source tokens.
final Token nameToken1, colonToken, nameToken2;
AliasContext(this.nameToken1, this.colonToken, this.nameToken2);
/// Use [nameToken1] instead.
@deprecated
Token get NAME1 => nameToken1;
/// Use [colonToken] instead.
@deprecated
Token get COLON => colonToken;
/// Use [nameToken2] instead.
@deprecated
Token get NAME2 => nameToken2;
/// The aliased name of the value.
String get alias => nameToken1.text;
/// The actual name of the value.
String get name => nameToken2.text;
@override
FileSpan get span =>
nameToken1.span.expand(colonToken.span).expand(nameToken2.span);
}

View file

@ -0,0 +1,34 @@
import 'package:source_span/source_span.dart';
import '../token.dart';
import 'node.dart';
import 'input_value.dart';
/// An argument passed to a [FieldContext].
class ArgumentContext extends Node {
/// The source tokens.
final Token nameToken, colonToken;
/// The value of the argument.
final InputValueContext value;
ArgumentContext(this.nameToken, this.colonToken, this.value);
/// Use [value] instead.
@deprecated
InputValueContext get valueOrVariable => value;
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// Use [colonToken] instead.
@deprecated
Token get COLON => colonToken;
/// The name of the argument, as a [String].
String get name => nameToken.text;
@override
FileSpan get span =>
nameToken.span.expand(colonToken.span).expand(value.span);
}

View file

@ -0,0 +1,34 @@
import 'package:source_span/source_span.dart';
import '../token.dart';
import 'input_value.dart';
/// A GraphQL list value literal.
class ListValueContext extends InputValueContext {
/// The source tokens.
final Token lBracketToken, rBracketToken;
/// The child values.
final List<InputValueContext> values = [];
ListValueContext(this.lBracketToken, this.rBracketToken);
/// Use [lBracketToken] instead.
@deprecated
Token get LBRACKET => lBracketToken;
/// Use [rBracketToken] instead.
@deprecated
Token get RBRACKET => rBracketToken;
@override
FileSpan get span {
var out =
values.fold<FileSpan>(lBracketToken.span, (o, v) => o.expand(v.span));
return out.expand(rBracketToken.span);
}
@override
computeValue(Map<String, dynamic> variables) {
return values.map((v) => v.computeValue(variables)).toList();
}
}

View file

@ -0,0 +1,31 @@
library graphql_parser.language.ast;
export 'alias.dart';
export 'array_value.dart';
export 'argument.dart';
export 'boolean_value.dart';
export 'default_value.dart';
export 'definition.dart';
export 'deprecated_value.dart';
export 'directive.dart';
export 'document.dart';
export 'field.dart';
export 'field_name.dart';
export 'fragment_definition.dart';
export 'fragment_spread.dart';
export 'inline_fragment.dart';
export 'input_value.dart';
export 'list_type.dart';
export 'misc_value.dart';
export 'node.dart';
export 'number_value.dart';
export 'operation_definition.dart';
export 'selection.dart';
export 'selection_set.dart';
export 'string_value.dart';
export 'type.dart';
export 'type_condition.dart';
export 'type_name.dart';
export 'variable.dart';
export 'variable_definition.dart';
export 'variable_definitions.dart';

View file

@ -0,0 +1,28 @@
import 'package:source_span/source_span.dart';
import 'input_value.dart';
import '../token.dart';
/// A GraphQL boolean value literal.
class BooleanValueContext extends InputValueContext<bool> {
bool _valueCache;
/// The source token.
final Token booleanToken;
BooleanValueContext(this.booleanToken) {
assert(booleanToken?.text == 'true' || booleanToken?.text == 'false');
}
/// The [bool] value of this literal.
bool get booleanValue => _valueCache ??= booleanToken.text == 'true';
/// Use [booleanToken] instead.
@deprecated
Token get BOOLEAN => booleanToken;
@override
FileSpan get span => booleanToken.span;
@override
bool computeValue(Map<String, dynamic> variables) => booleanValue;
}

View file

@ -0,0 +1,22 @@
import 'package:source_span/source_span.dart';
import '../token.dart';
import 'input_value.dart';
import 'node.dart';
/// The default value to be passed to an [ArgumentContext].
class DefaultValueContext extends Node {
/// The source token.
final Token equalsToken;
/// The default value for the argument.
final InputValueContext value;
DefaultValueContext(this.equalsToken, this.value);
/// Use [equalsToken] instead.
@deprecated
Token get EQUALS => equalsToken;
@override
FileSpan get span => equalsToken.span.expand(value.span);
}

View file

@ -0,0 +1,13 @@
import 'node.dart';
/// The base class for top-level GraphQL definitions.
abstract class DefinitionContext extends Node {}
/// An executable definition.
abstract class ExecutableDefinitionContext extends DefinitionContext {}
/// An ad-hoc type system declared in GraphQL.
abstract class TypeSystemDefinitionContext extends DefinitionContext {}
/// An extension to an existing ad-hoc type system.
abstract class TypeSystemExtensionContext extends DefinitionContext {}

View file

@ -0,0 +1,11 @@
import 'input_value.dart';
/// Use [ConstantContext] instead. This class remains solely for backwards compatibility.
@deprecated
abstract class ValueContext<T> extends InputValueContext<T> {
/// Return a constant value.
T get value;
@override
T computeValue(Map<String, dynamic> variables) => value;
}

View file

@ -0,0 +1,62 @@
import 'package:source_span/source_span.dart';
import '../token.dart';
import 'argument.dart';
import 'input_value.dart';
import 'node.dart';
/// A GraphQL directive, which may or may not have runtime semantics.
class DirectiveContext extends Node {
/// The source tokens.
final Token arrobaToken, nameToken, colonToken, lParenToken, rParenToken;
/// The argument being passed as the directive.
final ArgumentContext argument;
/// The (optional) value being passed with the directive.
final InputValueContext value;
DirectiveContext(this.arrobaToken, this.nameToken, this.colonToken,
this.lParenToken, this.rParenToken, this.argument, this.value) {
assert(nameToken != null);
}
/// Use [value] instead.
@deprecated
InputValueContext get valueOrVariable => value;
/// Use [arrobaToken] instead.
@deprecated
Token get ARROBA => arrobaToken;
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// Use [colonToken] instead.
@deprecated
Token get COLON => colonToken;
/// Use [lParenToken] instead.
@deprecated
Token get LPAREN => lParenToken;
/// Use [rParenToken] instead.
@deprecated
Token get RPAREN => rParenToken;
@override
FileSpan get span {
var out = arrobaToken.span.expand(nameToken.span);
if (colonToken != null) {
out = out.expand(colonToken.span).expand(value.span);
} else if (lParenToken != null) {
out = out
.expand(lParenToken.span)
.expand(argument.span)
.expand(rParenToken.span);
}
return out;
}
}

View file

@ -0,0 +1,17 @@
import 'package:source_span/source_span.dart';
import 'definition.dart';
import 'node.dart';
/// A GraphQL document.
class DocumentContext extends Node {
/// The top-level definitions in the document.
final List<DefinitionContext> definitions = [];
@override
FileSpan get span {
if (definitions.isEmpty) return null;
return definitions
.map<FileSpan>((d) => d.span)
.reduce((a, b) => a.expand(b));
}
}

View file

@ -0,0 +1,39 @@
import 'package:source_span/source_span.dart';
import 'argument.dart';
import 'directive.dart';
import 'field_name.dart';
import 'node.dart';
import 'selection_set.dart';
/// A field in a GraphQL [SelectionSet].
class FieldContext extends Node {
/// The name of this field.
final FieldNameContext fieldName;
/// Any arguments this field expects.
final List<ArgumentContext> arguments = [];
/// Any directives affixed to this field.
final List<DirectiveContext> directives = [];
/// The list of selections to resolve on an object.
final SelectionSetContext selectionSet;
FieldContext(this.fieldName, [this.selectionSet]);
@override
FileSpan get span {
if (selectionSet != null) {
return fieldName.span.expand(selectionSet.span);
} else if (directives.isNotEmpty) {
return directives.fold<FileSpan>(
fieldName.span, (out, d) => out.expand(d.span));
}
if (arguments.isNotEmpty) {
return arguments.fold<FileSpan>(
fieldName.span, (out, a) => out.expand(a.span));
} else {
return fieldName.span;
}
}
}

View file

@ -0,0 +1,27 @@
import 'package:source_span/source_span.dart';
import '../token.dart';
import 'alias.dart';
import 'node.dart';
/// The name of a GraphQL [FieldContext], which may or may not be [alias]ed.
class FieldNameContext extends Node {
/// The source token.
final Token nameToken;
/// An (optional) alias for the field.
final AliasContext alias;
FieldNameContext(this.nameToken, [this.alias]) {
assert(nameToken != null || alias != null);
}
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// The [String] value of the [nameToken], if any.
String get name => nameToken?.text;
@override
FileSpan get span => alias?.span ?? nameToken.span;
}

View file

@ -0,0 +1,49 @@
import '../token.dart';
import 'definition.dart';
import 'directive.dart';
import 'package:source_span/source_span.dart';
import 'selection_set.dart';
import 'type_condition.dart';
/// A GraphQL query fragment definition.
class FragmentDefinitionContext extends ExecutableDefinitionContext {
/// The source tokens.
final Token fragmentToken, nameToken, onToken;
/// The type to which this fragment applies.
final TypeConditionContext typeCondition;
/// Any directives on the fragment.
final List<DirectiveContext> directives = [];
/// The selections to apply when the [typeCondition] is met.
final SelectionSetContext selectionSet;
/// The [String] value of the [nameToken].
String get name => nameToken.text;
FragmentDefinitionContext(this.fragmentToken, this.nameToken, this.onToken,
this.typeCondition, this.selectionSet);
/// Use [fragmentToken] instead.
@deprecated
Token get FRAGMENT => fragmentToken;
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// Use [onToken] instead.
@deprecated
Token get ON => onToken;
@override
FileSpan get span {
var out = fragmentToken.span
.expand(nameToken.span)
.expand(onToken.span)
.expand(typeCondition.span);
out = directives.fold<FileSpan>(out, (o, d) => o.expand(d.span));
return out.expand(selectionSet.span);
}
}

View file

@ -0,0 +1,33 @@
import '../token.dart';
import 'directive.dart';
import 'node.dart';
import 'package:source_span/source_span.dart';
/// A GraphQL fragment spread.
class FragmentSpreadContext extends Node {
/// The source tokens.
final Token ellipsisToken, nameToken;
/// Any directives affixed to this fragment spread.
final List<DirectiveContext> directives = [];
FragmentSpreadContext(this.ellipsisToken, this.nameToken);
/// The [String] value of the [nameToken].
String get name => nameToken.text;
/// Use [ellipsisToken] instead.
@deprecated
Token get ELLIPSIS => ellipsisToken;
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
@override
FileSpan get span {
var out = ellipsisToken.span.expand(nameToken.span);
if (directives.isEmpty) return out;
return directives.fold<FileSpan>(out, (o, d) => o.expand(d.span));
}
}

View file

@ -0,0 +1,40 @@
import '../token.dart';
import 'directive.dart';
import 'node.dart';
import 'package:source_span/source_span.dart';
import 'selection_set.dart';
import 'type_condition.dart';
/// An inline fragment, which typically appears in a [SelectionSetContext].
class InlineFragmentContext extends Node {
/// The source tokens.
final Token ellipsisToken, onToken;
/// The type which this fragment matches.
final TypeConditionContext typeCondition;
/// Any directives affixed to this inline fragment.
final List<DirectiveContext> directives = [];
/// The selections applied when the [typeCondition] is met.
final SelectionSetContext selectionSet;
InlineFragmentContext(
this.ellipsisToken, this.onToken, this.typeCondition, this.selectionSet);
/// Use [ellipsisToken] instead.
@deprecated
Token get ELLIPSIS => ellipsisToken;
/// Use [onToken] instead.
@deprecated
Token get ON => onToken;
@override
FileSpan get span {
var out =
ellipsisToken.span.expand(onToken.span).expand(typeCondition.span);
out = directives.fold<FileSpan>(out, (o, d) => o.expand(d.span));
return out.expand(selectionSet.span);
}
}

View file

@ -0,0 +1,7 @@
import 'node.dart';
/// Represents a value in GraphQL.
abstract class InputValueContext<T> extends Node {
/// Computes the value, relative to some set of [variables].
T computeValue(Map<String, dynamic> variables);
}

View file

@ -0,0 +1,31 @@
import '../token.dart';
import 'node.dart';
import 'package:source_span/source_span.dart';
import 'type.dart';
/// Represents a type that holds a list of another type.
class ListTypeContext extends Node {
/// The source tokens.
final Token lBracketToken, rBracketToken;
/// The inner type.
final TypeContext innerType;
ListTypeContext(this.lBracketToken, this.innerType, this.rBracketToken);
/// Use [innerType] instead.
@deprecated
TypeContext get type => innerType;
/// Use [lBracketToken] instead.
@deprecated
Token get LBRACKET => lBracketToken;
/// Use [rBracketToken] instead.
@deprecated
Token get RBRACKET => rBracketToken;
@override
FileSpan get span =>
lBracketToken.span.expand(innerType.span).expand(rBracketToken.span);
}

View file

@ -0,0 +1,105 @@
import 'package:source_span/source_span.dart';
import '../token.dart';
import 'input_value.dart';
import 'node.dart';
/// A GraphQL `null` literal.
class NullValueContext extends InputValueContext<Null> {
/// The source token.
final Token nullToken;
NullValueContext(this.nullToken);
/// Use [nullToken] instead.
@deprecated
Token get NULL => nullToken;
@override
FileSpan get span => nullToken.span;
@override
Null computeValue(Map<String, dynamic> variables) => null;
}
/// A GraphQL enumeration literal.
class EnumValueContext extends InputValueContext<String> {
/// The source token.
final Token nameToken;
EnumValueContext(this.nameToken);
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
@override
FileSpan get span => nameToken.span;
@override
String computeValue(Map<String, dynamic> variables) => nameToken.span.text;
}
/// A GraphQL object literal.
class ObjectValueContext extends InputValueContext<Map<String, dynamic>> {
/// The source tokens.
final Token lBraceToken, rBraceToken;
/// The fields in the object.
final List<ObjectFieldContext> fields;
ObjectValueContext(this.lBraceToken, this.fields, this.rBraceToken);
/// Use [lBraceToken] instead.
Token get LBRACE => lBraceToken;
/// Use [rBraceToken] instead.
@deprecated
Token get RBRACE => rBraceToken;
@override
FileSpan get span {
var left = lBraceToken.span;
for (var field in fields) {
left = left.expand(field.span);
}
return left.expand(rBraceToken.span);
}
@override
Map<String, dynamic> computeValue(Map<String, dynamic> variables) {
if (fields.isEmpty) {
return <String, dynamic>{};
} else {
return fields.fold<Map<String, dynamic>>(<String, dynamic>{},
(map, field) {
return map
..[field.nameToken.text] = field.value.computeValue(variables);
});
}
}
}
/// A field within an [ObjectValueContext].
class ObjectFieldContext extends Node {
/// The source tokens.
final Token nameToken, colonToken;
/// The associated value.
final InputValueContext value;
ObjectFieldContext(this.nameToken, this.colonToken, this.value);
/// Use [nameToken] instead.
@deprecated
Token get NAME => nameToken;
/// Use [colonToken] instead.
@deprecated
Token get COLON => colonToken;
@override
FileSpan get span =>
nameToken.span.expand(colonToken.span).expand(value.span);
}

Some files were not shown because too many files have changed in this diff Show more