├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build.sbt
├── packaging.sbt
├── project
├── build.properties
├── coverage.sbt
├── publishing.sbt
└── revolver.sbt
├── src
└── main
│ ├── resources
│ ├── application.conf
│ ├── assets
│ │ ├── graphiql.html
│ │ └── playground.html
│ ├── logback.xml
│ └── reference.conf
│ └── scala
│ └── sangria
│ └── gateway
│ ├── AppConfig.scala
│ ├── Main.scala
│ ├── file
│ ├── FileMonitor.scala
│ ├── FileMonitorActor.scala
│ ├── FileUtil.scala
│ └── FileWatcher.scala
│ ├── http
│ ├── GatewayServer.scala
│ ├── GraphQLRequestUnmarshaller.scala
│ ├── GraphQLRouting.scala
│ └── client
│ │ ├── AkkaHttpClient.scala
│ │ ├── HttpClient.scala
│ │ └── PlayHttpClient.scala
│ ├── json
│ ├── CirceJsonPath.scala
│ ├── CirceJsonProvider.scala
│ └── CirceMappingProvider.scala
│ ├── schema
│ ├── CustomScalars.scala
│ ├── ReloadableSchemaProvider.scala
│ ├── SchemaLoader.scala
│ ├── SchemaProvider.scala
│ ├── StaticSchemaProvider.scala
│ └── materializer
│ │ ├── GatewayContext.scala
│ │ ├── GatewayMaterializer.scala
│ │ └── directive
│ │ ├── BasicDirectiveProvider.scala
│ │ ├── DirectiveProvider.scala
│ │ ├── FakerDirectiveProvider.scala
│ │ ├── GraphQLDirectiveProvider.scala
│ │ └── HttpDirectiveProvider.scala
│ └── util
│ ├── LogColors.scala
│ └── Logging.scala
└── testSchema.graphql
/.gitignore:
--------------------------------------------------------------------------------
1 | # folders
2 | .idea
3 | target
4 | lib
5 | classes
6 |
7 | # files
8 | *.iml
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | scala:
3 | - 2.12.7
4 | jdk:
5 | - oraclejdk8
6 |
7 | script: |
8 | sbt ++$TRAVIS_SCALA_VERSION clean coverage test
9 |
10 | after_success: |
11 | sbt ++$TRAVIS_SCALA_VERSION coverageReport coveralls
12 |
13 | cache:
14 | directories:
15 | - $HOME/.ivy2/cache
16 | - $HOME/.sbt/boot/
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OlegIlyenko/graphql-gateway/693861e2461337584ebefe51fb781ce36ac309ae/CHANGELOG.md
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **graphql-gateway** - SDL-based GraphQL gateway for REST and GraphQL-based micro-services
2 |
3 | [](https://travis-ci.org/OlegIlyenko/graphql-gateway)
4 | [](https://microbadger.com/images/tenshi/graphql-gateway "Get your own image badge on microbadger.com")
5 | [](https://microbadger.com/images/tenshi/graphql-gateway "Get your own version badge on microbadger.com")
6 |
7 | ⚠️ **Project is a POC and at very early experimental/WIP stage!** ⚠️
8 |
9 | ### Getting Started
10 |
11 | You can use graphql-gateway as a docker container:
12 |
13 | ```bash
14 | docker run -it -p 8080:8080 -v $(pwd):/schema tenshi/graphql-gateway
15 | ```
16 |
17 | For you convenience, you can define an alias for it and put it in your `.bashrc`:
18 |
19 | ```bash
20 | alias graphql-gateway='docker run -it -p 8080:8080 -v $(pwd):/schema tenshi/graphql-gateway'
21 | ```
22 |
23 | After this is done, you can start the sever just by executing `graphql-gateway` -
24 | the server will automatically read the schema from all `*.graphql` files in a current
25 | working directory and watch them for changes.
26 |
27 | There is also a fat JAR available on release notes: [graphql-gateway.jar](https://github.com/OlegIlyenko/graphql-gateway/releases/download/v0.1/graphql-gateway.jar).
28 |
29 | ### Configuration
30 |
31 | Docker container accepts a number of environment variables:
32 |
33 | * **General**
34 | * `PORT` - Int - server port (by default 8080)
35 | * `BIND_HOST` - String - bind host (by default 0.0.0.0)
36 | * `GRAPHIQL` - Boolean - enable/disable GraphiQL (by default true)
37 | * `PLAYGROUND` - Boolean - enable/disable graphql-playground which replaces GraphiQL (by default true)
38 | * `INCLUDE_DIRECTIVES` - List of String - include specific directive sets (available directive sets are: http, graphql, faker, basic)
39 | * `EXCLUDE_DIRECTIVES` - List of String - exclude specific directive sets (available directive sets are: http, graphql, faker, basic)
40 | * **Query tracing and logging**
41 | * `SLOW_LOG_ENABLED` - Boolean - enable/disable logging of slow queries (by default true)
42 | * `SLOW_LOG_THRESHOLD` - FiniteDuration - SlowLog extension threshold (by default 10 seconds)
43 | * `SLOW_LOG_EXTENSION` - Boolean - enable/disable SlowLog GraphQL extension (by default false)
44 | * `SLOW_LOG_APOLLO_TRACING` - Boolean - enable/disable Apollo tracing GraphQL extension (by default true)
45 | * **Schema live reloading**
46 | * `WATCH_ENABLED` - Boolean - enable/disable schema file reloader (by default true)
47 | * `WATCH_THRESHOLD` - FiniteDuration - internal poll interval for file watcher (by default 50 millis)
48 | * `WATCH_PATHS` - List of String - which directories to watch for schema files (by default ".")
49 | * `WATCH_GLOB` - List of String - which files are the schema files (by default "\*\*/\*.graphql")
50 | * **Limits**
51 | * `LIMIT_COMPLEXITY` - Double - query complexity limit (by default 10000)
52 | * `LIMIT_MAX_DEPTH` - Int - max query depth (by default 15)
53 | * `ALLOW_INTROSPECTION` - Boolean - enable/disable GraphQL introspect API (by default true)
54 |
55 | The full configuration can be found in the [reference.conf](https://github.com/OlegIlyenko/graphql-gateway/blob/master/src/main/resources/reference.conf).
56 |
57 | ### Supported SDL Directives
58 |
59 | Schema definition is based on [GraphQL SDL](https://github.com/facebook/graphql/pull/90). SDL syntax allows you to define full GraphQL
60 | schema with interfaces, types, enums etc. In order to provide resolution logic for the fields, you can use directives described below.
61 | Directives will define how fields will behave. By default (if no directive is provided), field resolve function will treat a contextual
62 | value as a JSON object and will return its property with the same name. (check out an [example schema](https://github.com/OlegIlyenko/graphql-gateway/blob/master/testSchema.graphql))
63 |
64 | #### `@httpGet`
65 |
66 | ```graphql
67 | directive @httpGet(
68 | url: String!,
69 | headers: [Header!],
70 | delegateHeaders: [String!]
71 | query: [QueryParam!],
72 | forAll: String) on FIELD_DEFINITION
73 |
74 | input Header {
75 | name: String!
76 | value: String!
77 | }
78 |
79 | input QueryParam {
80 | name: String!
81 | value: String!
82 | }
83 | ```
84 |
85 | Provides a way to resolve the field with a result of a GET HTTP request.
86 |
87 | Supports following arguments:
88 |
89 | * `url` - the URL of an HTTP request
90 | * `headers` - headers that should be sent with the request (e.g. `[{name: "Authorization", value: "Bearer FOOBARBAZ"}]`)
91 | * `query` - query string parameters that should be sent with the request (e.g. `[{name: "page-number", value: "1"}]`)
92 | * `forAll` - A [JSON Path](http://goessner.net/articles/JsonPath/) expression. For every element, returned by this expression executed against current context value, a separate HTTP request would be sent. An `elem` placeholder scope may be used in combination with this argument.
93 |
94 | `url`, `headers` and `query` may contain the placeholders which are described below. `value` directive may be used in combination with `httpGet` - it will extract part of the relevant JSON out of the HTTP response.
95 |
96 | #### `@includeSchema`
97 |
98 | ```graphql
99 | directive @includeSchema(
100 | name: String!
101 | url: String!
102 | headers: [Header!],
103 | delegateHeaders: [String!]
104 | query: [QueryParam!]
105 | oauth: OAuthClientCredentials) repeatable on SCHEMA
106 |
107 | input OAuthClientCredentials {
108 | url: String!
109 | clientId: String!
110 | clientSecret: String!
111 | scopes: [String!]!
112 | }
113 | ```
114 |
115 | Includes external GraphQL schema (based on GraphQL endpoint URL)
116 |
117 | #### `@includeFields`
118 |
119 | ```graphql
120 | directive @includeFields(
121 | "the name of the schema included with @includeGraphQL"
122 | schema: String!
123 |
124 | "the name of the type from the external schema"
125 | type: String!
126 |
127 | "optional list of fields to include (of not provided, all fields are included)"
128 | fields: [String!]
129 |
130 | "optional list of fields to exclude"
131 | excludes: [String!]) repeatable on OBJECT
132 | ```
133 |
134 | Adds fields loaded from the external GraphQL schema.
135 |
136 | #### `@fake`
137 |
138 | ```graphql
139 | directive @fake(
140 | expr: String,
141 | type: FakeType,
142 | min: Int,
143 | max: Int,
144 | past: Boolean,
145 | future: Boolean) on FIELD_DEFINITION
146 | ```
147 |
148 | Provides fake data. If the field returns a list, it's size can be customized with `min`/`max`.
149 | If the field returns a `DateTime`, then `past`/`future` would be used.
150 |
151 | The fake data type can be customized with an expression `expr`. An expression has following structure:
152 |
153 | ```
154 | #{category.name 'arg1','arg2'}
155 | ```
156 |
157 | Here is a small example:
158 |
159 | ```graphql
160 | type Info {
161 | createAt: DateTime
162 | }
163 |
164 | type Query {
165 | name: String! @fake(expr: "name.fullName")
166 | address: String! @fake(expr: "#{address.fullAddress} - #{numerify 'SOME######'}")
167 | info: Info! @fake
168 | }
169 | ```
170 |
171 |
172 | FakeType Enum Definition
173 |
174 | ```graphql
175 | "The type of the content generated by the faker"
176 | enum FakeType {
177 | "Represents faker expression `address.buildingNumber`."
178 | AddressBuildingNumber
179 |
180 | "Represents faker expression `address.city`."
181 | AddressCity
182 |
183 | "Represents faker expression `address.cityName`."
184 | AddressCityName
185 |
186 | "Represents faker expression `address.cityPrefix`."
187 | AddressCityPrefix
188 |
189 | "Represents faker expression `address.citySuffix`."
190 | AddressCitySuffix
191 |
192 | "Represents faker expression `address.country`."
193 | AddressCountry
194 |
195 | "Represents faker expression `address.countryCode`."
196 | AddressCountryCode
197 |
198 | "Represents faker expression `address.firstName`."
199 | AddressFirstName
200 |
201 | "Represents faker expression `address.fullAddress`."
202 | AddressFullAddress
203 |
204 | "Represents faker expression `address.lastName`."
205 | AddressLastName
206 |
207 | "Represents faker expression `address.latitude`."
208 | AddressLatitude
209 |
210 | "Represents faker expression `address.longitude`."
211 | AddressLongitude
212 |
213 | "Represents faker expression `address.secondaryAddress`."
214 | AddressSecondaryAddress
215 |
216 | "Represents faker expression `address.state`."
217 | AddressState
218 |
219 | "Represents faker expression `address.stateAbbr`."
220 | AddressStateAbbr
221 |
222 | "Represents faker expression `address.streetAddress`."
223 | AddressStreetAddress
224 |
225 | "Represents faker expression `address.streetAddressNumber`."
226 | AddressStreetAddressNumber
227 |
228 | "Represents faker expression `address.streetName`."
229 | AddressStreetName
230 |
231 | "Represents faker expression `address.streetPrefix`."
232 | AddressStreetPrefix
233 |
234 | "Represents faker expression `address.streetSuffix`."
235 | AddressStreetSuffix
236 |
237 | "Represents faker expression `address.timeZone`."
238 | AddressTimeZone
239 |
240 | "Represents faker expression `address.zipCode`."
241 | AddressZipCode
242 |
243 | "Represents faker expression `ancient.god`."
244 | AncientGod
245 |
246 | "Represents faker expression `ancient.hero`."
247 | AncientHero
248 |
249 | "Represents faker expression `ancient.primordial`."
250 | AncientPrimordial
251 |
252 | "Represents faker expression `ancient.titan`."
253 | AncientTitan
254 |
255 | "Represents faker expression `app.author`."
256 | AppAuthor
257 |
258 | "Represents faker expression `app.name`."
259 | AppName
260 |
261 | "Represents faker expression `app.version`."
262 | AppVersion
263 |
264 | "Represents faker expression `artist.name`."
265 | ArtistName
266 |
267 | "Represents faker expression `avatar.image`."
268 | AvatarImage
269 |
270 | "Represents faker expression `beer.hop`."
271 | BeerHop
272 |
273 | "Represents faker expression `beer.malt`."
274 | BeerMalt
275 |
276 | "Represents faker expression `beer.name`."
277 | BeerName
278 |
279 | "Represents faker expression `beer.style`."
280 | BeerStyle
281 |
282 | "Represents faker expression `beer.yeast`."
283 | BeerYeast
284 |
285 | "Represents faker expression `book.author`."
286 | BookAuthor
287 |
288 | "Represents faker expression `book.genre`."
289 | BookGenre
290 |
291 | "Represents faker expression `book.publisher`."
292 | BookPublisher
293 |
294 | "Represents faker expression `book.title`."
295 | BookTitle
296 |
297 | "Represents faker expression `bool.bool`."
298 | BoolBool
299 |
300 | "Represents faker expression `business.creditCardExpiry`."
301 | BusinessCreditCardExpiry
302 |
303 | "Represents faker expression `business.creditCardNumber`."
304 | BusinessCreditCardNumber
305 |
306 | "Represents faker expression `business.creditCardType`."
307 | BusinessCreditCardType
308 |
309 | "Represents faker expression `cat.breed`."
310 | CatBreed
311 |
312 | "Represents faker expression `cat.name`."
313 | CatName
314 |
315 | "Represents faker expression `cat.registry`."
316 | CatRegistry
317 |
318 | "Represents faker expression `chuckNorris.fact`."
319 | ChuckNorrisFact
320 |
321 | "Represents faker expression `code.asin`."
322 | CodeAsin
323 |
324 | "Represents faker expression `code.ean13`."
325 | CodeEan13
326 |
327 | "Represents faker expression `code.ean8`."
328 | CodeEan8
329 |
330 | "Represents faker expression `code.gtin13`."
331 | CodeGtin13
332 |
333 | "Represents faker expression `code.gtin8`."
334 | CodeGtin8
335 |
336 | "Represents faker expression `code.imei`."
337 | CodeImei
338 |
339 | "Represents faker expression `code.isbn10`."
340 | CodeIsbn10
341 |
342 | "Represents faker expression `code.isbn13`."
343 | CodeIsbn13
344 |
345 | "Represents faker expression `code.isbnGroup`."
346 | CodeIsbnGroup
347 |
348 | "Represents faker expression `code.isbnGs1`."
349 | CodeIsbnGs1
350 |
351 | "Represents faker expression `code.isbnRegistrant`."
352 | CodeIsbnRegistrant
353 |
354 | "Represents faker expression `color.name`."
355 | ColorName
356 |
357 | "Represents faker expression `commerce.color`."
358 | CommerceColor
359 |
360 | "Represents faker expression `commerce.department`."
361 | CommerceDepartment
362 |
363 | "Represents faker expression `commerce.material`."
364 | CommerceMaterial
365 |
366 | "Represents faker expression `commerce.price`."
367 | CommercePrice
368 |
369 | "Represents faker expression `commerce.productName`."
370 | CommerceProductName
371 |
372 | "Represents faker expression `commerce.promotionCode`."
373 | CommercePromotionCode
374 |
375 | "Represents faker expression `company.bs`."
376 | CompanyBs
377 |
378 | "Represents faker expression `company.buzzword`."
379 | CompanyBuzzword
380 |
381 | "Represents faker expression `company.catchPhrase`."
382 | CompanyCatchPhrase
383 |
384 | "Represents faker expression `company.industry`."
385 | CompanyIndustry
386 |
387 | "Represents faker expression `company.logo`."
388 | CompanyLogo
389 |
390 | "Represents faker expression `company.name`."
391 | CompanyName
392 |
393 | "Represents faker expression `company.profession`."
394 | CompanyProfession
395 |
396 | "Represents faker expression `company.suffix`."
397 | CompanySuffix
398 |
399 | "Represents faker expression `company.url`."
400 | CompanyUrl
401 |
402 | "Represents faker expression `crypto.md5`."
403 | CryptoMd5
404 |
405 | "Represents faker expression `crypto.sha1`."
406 | CryptoSha1
407 |
408 | "Represents faker expression `crypto.sha256`."
409 | CryptoSha256
410 |
411 | "Represents faker expression `crypto.sha512`."
412 | CryptoSha512
413 |
414 | "Represents faker expression `currency.code`."
415 | CurrencyCode
416 |
417 | "Represents faker expression `currency.name`."
418 | CurrencyName
419 |
420 | "Represents faker expression `demographic.demonym`."
421 | DemographicDemonym
422 |
423 | "Represents faker expression `demographic.educationalAttainment`."
424 | DemographicEducationalAttainment
425 |
426 | "Represents faker expression `demographic.maritalStatus`."
427 | DemographicMaritalStatus
428 |
429 | "Represents faker expression `demographic.race`."
430 | DemographicRace
431 |
432 | "Represents faker expression `demographic.sex`."
433 | DemographicSex
434 |
435 | "Represents faker expression `dog.age`."
436 | DogAge
437 |
438 | "Represents faker expression `dog.breed`."
439 | DogBreed
440 |
441 | "Represents faker expression `dog.coatLength`."
442 | DogCoatLength
443 |
444 | "Represents faker expression `dog.gender`."
445 | DogGender
446 |
447 | "Represents faker expression `dog.memePhrase`."
448 | DogMemePhrase
449 |
450 | "Represents faker expression `dog.name`."
451 | DogName
452 |
453 | "Represents faker expression `dog.size`."
454 | DogSize
455 |
456 | "Represents faker expression `dog.sound`."
457 | DogSound
458 |
459 | "Represents faker expression `dragonBall.character`."
460 | DragonBallCharacter
461 |
462 | "Represents faker expression `educator.campus`."
463 | EducatorCampus
464 |
465 | "Represents faker expression `educator.course`."
466 | EducatorCourse
467 |
468 | "Represents faker expression `educator.secondarySchool`."
469 | EducatorSecondarySchool
470 |
471 | "Represents faker expression `educator.university`."
472 | EducatorUniversity
473 |
474 | "Represents faker expression `esports.event`."
475 | EsportsEvent
476 |
477 | "Represents faker expression `esports.game`."
478 | EsportsGame
479 |
480 | "Represents faker expression `esports.league`."
481 | EsportsLeague
482 |
483 | "Represents faker expression `esports.player`."
484 | EsportsPlayer
485 |
486 | "Represents faker expression `esports.team`."
487 | EsportsTeam
488 |
489 | "Represents faker expression `file.extension`."
490 | FileExtension
491 |
492 | "Represents faker expression `file.fileName`."
493 | FileFileName
494 |
495 | "Represents faker expression `file.mimeType`."
496 | FileMimeType
497 |
498 | "Represents faker expression `finance.bic`."
499 | FinanceBic
500 |
501 | "Represents faker expression `finance.creditCard`."
502 | FinanceCreditCard
503 |
504 | "Represents faker expression `finance.iban`."
505 | FinanceIban
506 |
507 | "Represents faker expression `food.ingredient`."
508 | FoodIngredient
509 |
510 | "Represents faker expression `food.measurement`."
511 | FoodMeasurement
512 |
513 | "Represents faker expression `food.spice`."
514 | FoodSpice
515 |
516 | "Represents faker expression `friends.character`."
517 | FriendsCharacter
518 |
519 | "Represents faker expression `friends.location`."
520 | FriendsLocation
521 |
522 | "Represents faker expression `friends.quote`."
523 | FriendsQuote
524 |
525 | "Represents faker expression `funnyName.name`."
526 | FunnyNameName
527 |
528 | "Represents faker expression `gameOfThrones.character`."
529 | GameOfThronesCharacter
530 |
531 | "Represents faker expression `gameOfThrones.city`."
532 | GameOfThronesCity
533 |
534 | "Represents faker expression `gameOfThrones.dragon`."
535 | GameOfThronesDragon
536 |
537 | "Represents faker expression `gameOfThrones.house`."
538 | GameOfThronesHouse
539 |
540 | "Represents faker expression `gameOfThrones.quote`."
541 | GameOfThronesQuote
542 |
543 | "Represents faker expression `hacker.abbreviation`."
544 | HackerAbbreviation
545 |
546 | "Represents faker expression `hacker.adjective`."
547 | HackerAdjective
548 |
549 | "Represents faker expression `hacker.ingverb`."
550 | HackerIngverb
551 |
552 | "Represents faker expression `hacker.noun`."
553 | HackerNoun
554 |
555 | "Represents faker expression `hacker.verb`."
556 | HackerVerb
557 |
558 | "Represents faker expression `harryPotter.book`."
559 | HarryPotterBook
560 |
561 | "Represents faker expression `harryPotter.character`."
562 | HarryPotterCharacter
563 |
564 | "Represents faker expression `harryPotter.location`."
565 | HarryPotterLocation
566 |
567 | "Represents faker expression `harryPotter.quote`."
568 | HarryPotterQuote
569 |
570 | "Represents faker expression `hipster.word`."
571 | HipsterWord
572 |
573 | "Represents faker expression `hitchhikersGuideToTheGalaxy.character`."
574 | HitchhikersGuideToTheGalaxyCharacter
575 |
576 | "Represents faker expression `hitchhikersGuideToTheGalaxy.location`."
577 | HitchhikersGuideToTheGalaxyLocation
578 |
579 | "Represents faker expression `hitchhikersGuideToTheGalaxy.marvinQuote`."
580 | HitchhikersGuideToTheGalaxyMarvinQuote
581 |
582 | "Represents faker expression `hitchhikersGuideToTheGalaxy.planet`."
583 | HitchhikersGuideToTheGalaxyPlanet
584 |
585 | "Represents faker expression `hitchhikersGuideToTheGalaxy.quote`."
586 | HitchhikersGuideToTheGalaxyQuote
587 |
588 | "Represents faker expression `hitchhikersGuideToTheGalaxy.specie`."
589 | HitchhikersGuideToTheGalaxySpecie
590 |
591 | "Represents faker expression `hitchhikersGuideToTheGalaxy.starship`."
592 | HitchhikersGuideToTheGalaxyStarship
593 |
594 | "Represents faker expression `hobbit.character`."
595 | HobbitCharacter
596 |
597 | "Represents faker expression `hobbit.location`."
598 | HobbitLocation
599 |
600 | "Represents faker expression `hobbit.quote`."
601 | HobbitQuote
602 |
603 | "Represents faker expression `hobbit.thorinsCompany`."
604 | HobbitThorinsCompany
605 |
606 | "Represents faker expression `howIMetYourMother.catchPhrase`."
607 | HowIMetYourMotherCatchPhrase
608 |
609 | "Represents faker expression `howIMetYourMother.character`."
610 | HowIMetYourMotherCharacter
611 |
612 | "Represents faker expression `howIMetYourMother.highFive`."
613 | HowIMetYourMotherHighFive
614 |
615 | "Represents faker expression `howIMetYourMother.quote`."
616 | HowIMetYourMotherQuote
617 |
618 | "Represents faker expression `idNumber.invalid`."
619 | IdNumberInvalid
620 |
621 | "Represents faker expression `idNumber.invalidSvSeSsn`."
622 | IdNumberInvalidSvSeSsn
623 |
624 | "Represents faker expression `idNumber.ssnValid`."
625 | IdNumberSsnValid
626 |
627 | "Represents faker expression `idNumber.valid`."
628 | IdNumberValid
629 |
630 | "Represents faker expression `idNumber.validSvSeSsn`."
631 | IdNumberValidSvSeSsn
632 |
633 | "Represents faker expression `internet.avatar`."
634 | InternetAvatar
635 |
636 | "Represents faker expression `internet.domainName`."
637 | InternetDomainName
638 |
639 | "Represents faker expression `internet.domainSuffix`."
640 | InternetDomainSuffix
641 |
642 | "Represents faker expression `internet.domainWord`."
643 | InternetDomainWord
644 |
645 | "Represents faker expression `internet.emailAddress`."
646 | InternetEmailAddress
647 |
648 | "Represents faker expression `internet.image`."
649 | InternetImage
650 |
651 | "Represents faker expression `internet.ipV4Address`."
652 | InternetIpV4Address
653 |
654 | "Represents faker expression `internet.ipV4Cidr`."
655 | InternetIpV4Cidr
656 |
657 | "Represents faker expression `internet.ipV6Address`."
658 | InternetIpV6Address
659 |
660 | "Represents faker expression `internet.ipV6Cidr`."
661 | InternetIpV6Cidr
662 |
663 | "Represents faker expression `internet.macAddress`."
664 | InternetMacAddress
665 |
666 | "Represents faker expression `internet.password`."
667 | InternetPassword
668 |
669 | "Represents faker expression `internet.privateIpV4Address`."
670 | InternetPrivateIpV4Address
671 |
672 | "Represents faker expression `internet.publicIpV4Address`."
673 | InternetPublicIpV4Address
674 |
675 | "Represents faker expression `internet.safeEmailAddress`."
676 | InternetSafeEmailAddress
677 |
678 | "Represents faker expression `internet.slug`."
679 | InternetSlug
680 |
681 | "Represents faker expression `internet.url`."
682 | InternetUrl
683 |
684 | "Represents faker expression `internet.uuid`."
685 | InternetUuid
686 |
687 | "Represents faker expression `job.field`."
688 | JobField
689 |
690 | "Represents faker expression `job.keySkills`."
691 | JobKeySkills
692 |
693 | "Represents faker expression `job.position`."
694 | JobPosition
695 |
696 | "Represents faker expression `job.seniority`."
697 | JobSeniority
698 |
699 | "Represents faker expression `job.title`."
700 | JobTitle
701 |
702 | "Represents faker expression `leagueOfLegends.champion`."
703 | LeagueOfLegendsChampion
704 |
705 | "Represents faker expression `leagueOfLegends.location`."
706 | LeagueOfLegendsLocation
707 |
708 | "Represents faker expression `leagueOfLegends.masteries`."
709 | LeagueOfLegendsMasteries
710 |
711 | "Represents faker expression `leagueOfLegends.quote`."
712 | LeagueOfLegendsQuote
713 |
714 | "Represents faker expression `leagueOfLegends.rank`."
715 | LeagueOfLegendsRank
716 |
717 | "Represents faker expression `leagueOfLegends.summonerSpell`."
718 | LeagueOfLegendsSummonerSpell
719 |
720 | "Represents faker expression `lebowski.actor`."
721 | LebowskiActor
722 |
723 | "Represents faker expression `lebowski.character`."
724 | LebowskiCharacter
725 |
726 | "Represents faker expression `lebowski.quote`."
727 | LebowskiQuote
728 |
729 | "Represents faker expression `lordOfTheRings.character`."
730 | LordOfTheRingsCharacter
731 |
732 | "Represents faker expression `lordOfTheRings.location`."
733 | LordOfTheRingsLocation
734 |
735 | "Represents faker expression `lorem.character`."
736 | LoremCharacter
737 |
738 | "Represents faker expression `lorem.characters`."
739 | LoremCharacters
740 |
741 | "Represents faker expression `lorem.paragraph`."
742 | LoremParagraph
743 |
744 | "Represents faker expression `lorem.sentence`."
745 | LoremSentence
746 |
747 | "Represents faker expression `lorem.word`."
748 | LoremWord
749 |
750 | "Represents faker expression `matz.quote`."
751 | MatzQuote
752 |
753 | "Represents faker expression `music.chord`."
754 | MusicChord
755 |
756 | "Represents faker expression `music.instrument`."
757 | MusicInstrument
758 |
759 | "Represents faker expression `music.key`."
760 | MusicKey
761 |
762 | "Represents faker expression `name.firstName`."
763 | NameFirstName
764 |
765 | "Represents faker expression `name.fullName`."
766 | NameFullName
767 |
768 | "Represents faker expression `name.lastName`."
769 | NameLastName
770 |
771 | "Represents faker expression `name.name`."
772 | NameName
773 |
774 | "Represents faker expression `name.nameWithMiddle`."
775 | NameNameWithMiddle
776 |
777 | "Represents faker expression `name.prefix`."
778 | NamePrefix
779 |
780 | "Represents faker expression `name.suffix`."
781 | NameSuffix
782 |
783 | "Represents faker expression `name.title`."
784 | NameTitle
785 |
786 | "Represents faker expression `name.username`."
787 | NameUsername
788 |
789 | "Represents faker expression `number.digit`."
790 | NumberDigit
791 |
792 | "Represents faker expression `number.randomDigit`."
793 | NumberRandomDigit
794 |
795 | "Represents faker expression `number.randomDigitNotZero`."
796 | NumberRandomDigitNotZero
797 |
798 | "Represents faker expression `number.randomNumber`."
799 | NumberRandomNumber
800 |
801 | "Represents faker expression `overwatch.hero`."
802 | OverwatchHero
803 |
804 | "Represents faker expression `overwatch.location`."
805 | OverwatchLocation
806 |
807 | "Represents faker expression `overwatch.quote`."
808 | OverwatchQuote
809 |
810 | "Represents faker expression `phoneNumber.cellPhone`."
811 | PhoneNumberCellPhone
812 |
813 | "Represents faker expression `phoneNumber.phoneNumber`."
814 | PhoneNumberPhoneNumber
815 |
816 | "Represents faker expression `pokemon.location`."
817 | PokemonLocation
818 |
819 | "Represents faker expression `pokemon.name`."
820 | PokemonName
821 |
822 | "Represents faker expression `random.nextDouble`."
823 | RandomNextDouble
824 |
825 | "Represents faker expression `random.nextLong`."
826 | RandomNextLong
827 |
828 | "Represents faker expression `rickAndMorty.character`."
829 | RickAndMortyCharacter
830 |
831 | "Represents faker expression `rickAndMorty.location`."
832 | RickAndMortyLocation
833 |
834 | "Represents faker expression `rickAndMorty.quote`."
835 | RickAndMortyQuote
836 |
837 | "Represents faker expression `robin.quote`."
838 | RobinQuote
839 |
840 | "Represents faker expression `rockBand.name`."
841 | RockBandName
842 |
843 | "Represents faker expression `shakespeare.asYouLikeItQuote`."
844 | ShakespeareAsYouLikeItQuote
845 |
846 | "Represents faker expression `shakespeare.hamletQuote`."
847 | ShakespeareHamletQuote
848 |
849 | "Represents faker expression `shakespeare.kingRichardIIIQuote`."
850 | ShakespeareKingRichardIIIQuote
851 |
852 | "Represents faker expression `shakespeare.romeoAndJulietQuote`."
853 | ShakespeareRomeoAndJulietQuote
854 |
855 | "Represents faker expression `slackEmoji.activity`."
856 | SlackEmojiActivity
857 |
858 | "Represents faker expression `slackEmoji.celebration`."
859 | SlackEmojiCelebration
860 |
861 | "Represents faker expression `slackEmoji.custom`."
862 | SlackEmojiCustom
863 |
864 | "Represents faker expression `slackEmoji.emoji`."
865 | SlackEmojiEmoji
866 |
867 | "Represents faker expression `slackEmoji.foodAndDrink`."
868 | SlackEmojiFoodAndDrink
869 |
870 | "Represents faker expression `slackEmoji.nature`."
871 | SlackEmojiNature
872 |
873 | "Represents faker expression `slackEmoji.objectsAndSymbols`."
874 | SlackEmojiObjectsAndSymbols
875 |
876 | "Represents faker expression `slackEmoji.people`."
877 | SlackEmojiPeople
878 |
879 | "Represents faker expression `slackEmoji.travelAndPlaces`."
880 | SlackEmojiTravelAndPlaces
881 |
882 | "Represents faker expression `space.agency`."
883 | SpaceAgency
884 |
885 | "Represents faker expression `space.agencyAbbreviation`."
886 | SpaceAgencyAbbreviation
887 |
888 | "Represents faker expression `space.company`."
889 | SpaceCompany
890 |
891 | "Represents faker expression `space.constellation`."
892 | SpaceConstellation
893 |
894 | "Represents faker expression `space.distanceMeasurement`."
895 | SpaceDistanceMeasurement
896 |
897 | "Represents faker expression `space.galaxy`."
898 | SpaceGalaxy
899 |
900 | "Represents faker expression `space.meteorite`."
901 | SpaceMeteorite
902 |
903 | "Represents faker expression `space.moon`."
904 | SpaceMoon
905 |
906 | "Represents faker expression `space.nasaSpaceCraft`."
907 | SpaceNasaSpaceCraft
908 |
909 | "Represents faker expression `space.nebula`."
910 | SpaceNebula
911 |
912 | "Represents faker expression `space.planet`."
913 | SpacePlanet
914 |
915 | "Represents faker expression `space.star`."
916 | SpaceStar
917 |
918 | "Represents faker expression `space.starCluster`."
919 | SpaceStarCluster
920 |
921 | "Represents faker expression `starTrek.character`."
922 | StarTrekCharacter
923 |
924 | "Represents faker expression `starTrek.location`."
925 | StarTrekLocation
926 |
927 | "Represents faker expression `starTrek.specie`."
928 | StarTrekSpecie
929 |
930 | "Represents faker expression `starTrek.villain`."
931 | StarTrekVillain
932 |
933 | "Represents faker expression `stock.nsdqSymbol`."
934 | StockNsdqSymbol
935 |
936 | "Represents faker expression `stock.nyseSymbol`."
937 | StockNyseSymbol
938 |
939 | "Represents faker expression `superhero.descriptor`."
940 | SuperheroDescriptor
941 |
942 | "Represents faker expression `superhero.name`."
943 | SuperheroName
944 |
945 | "Represents faker expression `superhero.power`."
946 | SuperheroPower
947 |
948 | "Represents faker expression `superhero.prefix`."
949 | SuperheroPrefix
950 |
951 | "Represents faker expression `superhero.suffix`."
952 | SuperheroSuffix
953 |
954 | "Represents faker expression `team.creature`."
955 | TeamCreature
956 |
957 | "Represents faker expression `team.name`."
958 | TeamName
959 |
960 | "Represents faker expression `team.sport`."
961 | TeamSport
962 |
963 | "Represents faker expression `team.state`."
964 | TeamState
965 |
966 | "Represents faker expression `twinPeaks.character`."
967 | TwinPeaksCharacter
968 |
969 | "Represents faker expression `twinPeaks.location`."
970 | TwinPeaksLocation
971 |
972 | "Represents faker expression `twinPeaks.quote`."
973 | TwinPeaksQuote
974 |
975 | "Represents faker expression `university.name`."
976 | UniversityName
977 |
978 | "Represents faker expression `university.prefix`."
979 | UniversityPrefix
980 |
981 | "Represents faker expression `university.suffix`."
982 | UniversitySuffix
983 |
984 | "Represents faker expression `weather.description`."
985 | WeatherDescription
986 |
987 | "Represents faker expression `weather.temperatureCelsius`."
988 | WeatherTemperatureCelsius
989 |
990 | "Represents faker expression `weather.temperatureFahrenheit`."
991 | WeatherTemperatureFahrenheit
992 |
993 | "Represents faker expression `witcher.character`."
994 | WitcherCharacter
995 |
996 | "Represents faker expression `witcher.location`."
997 | WitcherLocation
998 |
999 | "Represents faker expression `witcher.monster`."
1000 | WitcherMonster
1001 |
1002 | "Represents faker expression `witcher.quote`."
1003 | WitcherQuote
1004 |
1005 | "Represents faker expression `witcher.school`."
1006 | WitcherSchool
1007 |
1008 | "Represents faker expression `witcher.witcher`."
1009 | WitcherWitcher
1010 |
1011 | "Represents faker expression `yoda.quote`."
1012 | YodaQuote
1013 |
1014 | "Represents faker expression `zelda.character`."
1015 | ZeldaCharacter
1016 |
1017 | "Represents faker expression `zelda.game`."
1018 | ZeldaGame
1019 | }
1020 | ```
1021 |
1022 |
1023 |
1024 | Full list of all available faker expressions
1025 |
1026 | * `bothify(string: String)`
1027 |
1028 | Applies both a numerify(String) and a letterify(String) over the incoming string.
1029 | * `bothify(string: String, isUpper: Boolean)`
1030 |
1031 | Applies both a numerify(String) and a letterify(String) over the incoming string.
1032 | * `letterify(letterString: String, isUpper: Boolean)`
1033 |
1034 | Returns a string with the '?' characters in the parameter replaced with random alphabetic characters. For example, the string "12??34" could be replaced with a string like "12AB34".
1035 | * `letterify(letterString: String)`
1036 |
1037 | Returns a string with the '?' characters in the parameter replaced with random alphabetic characters. For example, the string "12??34" could be replaced with a string like "12AB34".
1038 | * `numerify(numberString: String)`
1039 |
1040 | Returns a string with the '#' characters in the parameter replaced with random digits between 0-9 inclusive. For example, the string "ABC##EFG" could be replaced with a string like "ABC99EFG".
1041 | * `regexify(regex: String)`
1042 |
1043 | Generates a String that matches the given regular expression.
1044 | * `resolve(key: String)`
1045 | * `address`
1046 | * `address.buildingNumber`
1047 | * `address.city`
1048 | * `address.cityName`
1049 | * `address.cityPrefix`
1050 | * `address.citySuffix`
1051 | * `address.country`
1052 | * `address.countryCode`
1053 | * `address.firstName`
1054 | * `address.fullAddress`
1055 | * `address.lastName`
1056 | * `address.latitude`
1057 | * `address.longitude`
1058 | * `address.secondaryAddress`
1059 | * `address.state`
1060 | * `address.stateAbbr`
1061 | * `address.streetAddress`
1062 | * `address.streetAddress(includeSecondary: Boolean)`
1063 | * `address.streetAddressNumber`
1064 | * `address.streetName`
1065 | * `address.streetPrefix`
1066 | * `address.streetSuffix`
1067 | * `address.timeZone`
1068 | * `address.zipCode`
1069 | * `address.zipCodeByState(stateAbbr: String)`
1070 | * `ancient`
1071 | * `ancient.god`
1072 | * `ancient.hero`
1073 | * `ancient.primordial`
1074 | * `ancient.titan`
1075 | * `app`
1076 | * `app.author`
1077 | * `app.name`
1078 | * `app.version`
1079 | * `artist`
1080 | * `artist.name`
1081 | * `avatar`
1082 | * `avatar.image`
1083 | * `beer`
1084 | * `beer.hop`
1085 | * `beer.malt`
1086 | * `beer.name`
1087 | * `beer.style`
1088 | * `beer.yeast`
1089 | * `book`
1090 | * `book.author`
1091 | * `book.genre`
1092 | * `book.publisher`
1093 | * `book.title`
1094 | * `bool`
1095 | * `bool.bool`
1096 | * `business`
1097 | * `business.creditCardExpiry`
1098 | * `business.creditCardNumber`
1099 | * `business.creditCardType`
1100 | * `cat`
1101 | * `cat.breed`
1102 | * `cat.name`
1103 | * `cat.registry`
1104 | * `chuckNorris`
1105 | * `chuckNorris.fact`
1106 | * `code`
1107 | * `code.asin`
1108 | * `code.ean13`
1109 | * `code.ean8`
1110 | * `code.gtin13`
1111 | * `code.gtin8`
1112 | * `code.imei`
1113 | * `code.isbn10`
1114 | * `code.isbn10(separator: Boolean)`
1115 | * `code.isbn13`
1116 | * `code.isbn13(separator: Boolean)`
1117 | * `code.isbnGroup`
1118 |
1119 | This can be overridden by specifying code: isbn_group: "some expression" in the appropriate yml file.
1120 | * `code.isbnGs1`
1121 |
1122 | This can be overridden by specifying code: isbn_gs1: "some expression" in the appropriate yml file.
1123 | * `code.isbnRegistrant`
1124 |
1125 | This can be overridden by specifying code: isbn_registrant: "some expression" in the appropriate yml file.
1126 | * `color`
1127 | * `color.name`
1128 | * `commerce`
1129 | * `commerce.color`
1130 | * `commerce.department`
1131 | * `commerce.material`
1132 | * `commerce.price(min: Double, max: Double)`
1133 | * `commerce.price`
1134 |
1135 | Generate a random price between 0.00 and 100.00
1136 | * `commerce.productName`
1137 | * `commerce.promotionCode`
1138 | * `commerce.promotionCode(digits: Int)`
1139 | * `company`
1140 | * `company.bs`
1141 |
1142 | When a straight answer won't do, BS to the rescue!
1143 | * `company.buzzword`
1144 | * `company.catchPhrase`
1145 |
1146 | Generate a buzzword-laden catch phrase.
1147 | * `company.industry`
1148 | * `company.logo`
1149 |
1150 | Generate a random company logo url in PNG format.
1151 | * `company.name`
1152 | * `company.profession`
1153 | * `company.suffix`
1154 | * `company.url`
1155 | * `crypto`
1156 | * `crypto.md5`
1157 | * `crypto.sha1`
1158 | * `crypto.sha256`
1159 | * `crypto.sha512`
1160 | * `currency`
1161 | * `currency.code`
1162 | * `currency.name`
1163 | * `date`
1164 | * `demographic`
1165 | * `demographic.demonym`
1166 | * `demographic.educationalAttainment`
1167 | * `demographic.maritalStatus`
1168 | * `demographic.race`
1169 | * `demographic.sex`
1170 | * `dog`
1171 | * `dog.age`
1172 | * `dog.breed`
1173 | * `dog.coatLength`
1174 | * `dog.gender`
1175 | * `dog.memePhrase`
1176 | * `dog.name`
1177 | * `dog.size`
1178 | * `dog.sound`
1179 | * `dragonBall`
1180 | * `dragonBall.character`
1181 | * `educator`
1182 | * `educator.campus`
1183 | * `educator.course`
1184 | * `educator.secondarySchool`
1185 | * `educator.university`
1186 | * `esports`
1187 | * `esports.event`
1188 | * `esports.game`
1189 | * `esports.league`
1190 | * `esports.player`
1191 | * `esports.team`
1192 | * `file`
1193 | * `file.extension`
1194 | * `file.fileName`
1195 | * `file.fileName(dirOrNull: String, nameOrNull: String, extensionOrNull: String, separatorOrNull: String)`
1196 | * `file.mimeType`
1197 | * `finance`
1198 | * `finance.bic`
1199 |
1200 | Generates a random Business Identifier Code
1201 | * `finance.creditCard`
1202 | * `finance.creditCard(creditCardType: CreditCardType)`
1203 | * `finance.iban`
1204 | * `finance.iban(countryCode: String)`
1205 | * `food`
1206 | * `food.ingredient`
1207 | * `food.measurement`
1208 | * `food.spice`
1209 | * `friends`
1210 | * `friends.character`
1211 | * `friends.location`
1212 | * `friends.quote`
1213 | * `funnyName`
1214 | * `funnyName.name`
1215 | * `gameOfThrones`
1216 | * `gameOfThrones.character`
1217 | * `gameOfThrones.city`
1218 | * `gameOfThrones.dragon`
1219 | * `gameOfThrones.house`
1220 | * `gameOfThrones.quote`
1221 | * `hacker`
1222 | * `hacker.abbreviation`
1223 | * `hacker.adjective`
1224 | * `hacker.ingverb`
1225 | * `hacker.noun`
1226 | * `hacker.verb`
1227 | * `harryPotter`
1228 | * `harryPotter.book`
1229 | * `harryPotter.character`
1230 | * `harryPotter.location`
1231 | * `harryPotter.quote`
1232 | * `hipster`
1233 | * `hipster.word`
1234 | * `hitchhikersGuideToTheGalaxy`
1235 | * `hitchhikersGuideToTheGalaxy.character`
1236 | * `hitchhikersGuideToTheGalaxy.location`
1237 | * `hitchhikersGuideToTheGalaxy.marvinQuote`
1238 | * `hitchhikersGuideToTheGalaxy.planet`
1239 | * `hitchhikersGuideToTheGalaxy.quote`
1240 | * `hitchhikersGuideToTheGalaxy.specie`
1241 | * `hitchhikersGuideToTheGalaxy.starship`
1242 | * `hobbit`
1243 | * `hobbit.character`
1244 | * `hobbit.location`
1245 | * `hobbit.quote`
1246 | * `hobbit.thorinsCompany`
1247 | * `howIMetYourMother`
1248 | * `howIMetYourMother.catchPhrase`
1249 | * `howIMetYourMother.character`
1250 | * `howIMetYourMother.highFive`
1251 | * `howIMetYourMother.quote`
1252 | * `idNumber`
1253 | * `idNumber.invalid`
1254 | * `idNumber.invalidSvSeSsn`
1255 |
1256 | Specified as #{IDNumber.invalid_sv_se_ssn} in sv-SE.yml
1257 | * `idNumber.ssnValid`
1258 | * `idNumber.valid`
1259 | * `idNumber.validSvSeSsn`
1260 |
1261 | Specified as #{IDNumber.valid_sv_se_ssn} in sv-SE.yml
1262 | * `internet`
1263 | * `internet.avatar`
1264 |
1265 | Generates a random avatar url based on a collection of profile pictures of real people. All this avatar have been authorized by its awesome users to be used on live websites (not just mockups). For more information, please visit: http://uifaces.com/authorized
1266 | * `internet.domainName`
1267 | * `internet.domainSuffix`
1268 | * `internet.domainWord`
1269 | * `internet.emailAddress`
1270 | * `internet.emailAddress(localPart: String)`
1271 | * `internet.image`
1272 |
1273 | Generates a random image url based on the lorempixel service. All the images provided by this service are released under the creative commons license (CC BY-SA). For more information, please visit: http://lorempixel.com/
1274 | * `internet.image(width: Int, height: Int, gray: java.lang.Boolean, text: String)`
1275 |
1276 | Same as image() but allows client code to choose a few image characteristics
1277 | * `internet.ipV4Address`
1278 |
1279 | returns an IPv4 address in dot separated octets.
1280 | * `internet.ipV4Cidr`
1281 | * `internet.ipV6Address`
1282 |
1283 | Returns an IPv6 address in hh:hh:hh:hh:hh:hh:hh:hh format.
1284 | * `internet.ipV6Cidr`
1285 | * `internet.macAddress(prefix: String)`
1286 |
1287 | Returns a MAC address in the following format: 6-bytes in MM:MM:MM:SS:SS:SS format.
1288 | * `internet.macAddress`
1289 | * `internet.password(minimumLength: Int, maximumLength: Int, includeUppercase: Boolean, includeSpecial: Boolean)`
1290 | * `internet.password(minimumLength: Int, maximumLength: Int, includeUppercase: Boolean)`
1291 | * `internet.password`
1292 | * `internet.password(minimumLength: Int, maximumLength: Int)`
1293 | * `internet.privateIpV4Address`
1294 | * `internet.publicIpV4Address`
1295 | * `internet.safeEmailAddress(localPart: String)`
1296 | * `internet.safeEmailAddress`
1297 | * `internet.slug(wordsOrNull: [String], glueOrNull: String)`
1298 | * `internet.slug`
1299 | * `internet.url`
1300 | * `internet.uuid`
1301 |
1302 | Returns a UUID (type 4) as String.
1303 | * `job`
1304 | * `job.field`
1305 | * `job.keySkills`
1306 | * `job.position`
1307 | * `job.seniority`
1308 | * `job.title`
1309 | * `leagueOfLegends`
1310 | * `leagueOfLegends.champion`
1311 | * `leagueOfLegends.location`
1312 | * `leagueOfLegends.masteries`
1313 | * `leagueOfLegends.quote`
1314 | * `leagueOfLegends.rank`
1315 | * `leagueOfLegends.summonerSpell`
1316 | * `lebowski`
1317 | * `lebowski.actor`
1318 | * `lebowski.character`
1319 | * `lebowski.quote`
1320 | * `lordOfTheRings`
1321 | * `lordOfTheRings.character`
1322 | * `lordOfTheRings.location`
1323 | * `lorem`
1324 | * `lorem.character(includeUppercase: Boolean)`
1325 | * `lorem.character`
1326 | * `lorem.characters(includeUppercase: Boolean)`
1327 | * `lorem.characters(minimumLength: Int, maximumLength: Int, includeUppercase: Boolean)`
1328 | * `lorem.characters(minimumLength: Int, maximumLength: Int)`
1329 | * `lorem.characters`
1330 | * `lorem.characters(includeUppercase: Boolean)`
1331 | * `lorem.characters(minimumLength: Int, maximumLength: Int)`
1332 | * `lorem.fixedString(numberOfLetters: Int)`
1333 |
1334 | Create a string with a fixed size. Can be useful for testing validator based on length string for example
1335 | * `lorem.paragraph(sentenceCount: Int)`
1336 | * `lorem.paragraph`
1337 | * `lorem.sentence(wordCount: Int)`
1338 |
1339 | Create a sentence with a random number of words within the range (wordCount+1)..(wordCount+6).
1340 | * `lorem.sentence`
1341 |
1342 | Create a sentence with a random number of words within the range 4..10.
1343 | * `lorem.sentence(wordCount: Int, randomWordsToAdd: Int)`
1344 |
1345 | Create a sentence with a random number of words within the range (wordCount+1)..(wordCount+randomWordsToAdd). Set randomWordsToAdd to 0 to generate sentences with a fixed number of words.
1346 | * `lorem.word`
1347 | * `matz`
1348 | * `matz.quote`
1349 | * `music`
1350 | * `music.chord`
1351 | * `music.instrument`
1352 | * `music.key`
1353 | * `name`
1354 | * `name.firstName`
1355 |
1356 | Returns a random 'given' name such as Aaliyah, Aaron, Abagail or Abbey
1357 | * `name.fullName`
1358 |
1359 | Returns the same value as name()
1360 | * `name.lastName`
1361 |
1362 | Returns a random last name such as Smith, Jones or Baldwin
1363 | * `name.name`
1364 |
1365 | A multipart name composed of an optional prefix, a firstname and a lastname or other possible variances based on locale. Examples: James Jones Jr. Julie Johnson
1366 | * `name.nameWithMiddle`
1367 |
1368 | A multipart name composed of an optional prefix, a given and family name, another 'firstname' for the middle name and an optional suffix such as Jr. Examples: Mrs. Ella Geraldine Fitzgerald Jason Tom Sawyer Jr. Helen Jessica Troy
1369 | * `name.prefix`
1370 |
1371 | Returns a name prefix such as Mr., Mrs., Ms., Miss, or Dr.
1372 | * `name.suffix`
1373 |
1374 | Returns a name suffix such as Jr., Sr., I, II, III, IV, V, MD, DDS, PhD or DVM
1375 | * `name.title`
1376 |
1377 | A three part title composed of a descriptor level and job. Some examples are : (template) {descriptor} {level} {job} Lead Solutions Specialist National Marketing Manager Central Response Liaison
1378 | * `name.username`
1379 |
1380 | A lowercase username composed of the first_name and last_name joined with a '.'. Some examples are: (template) firstName().lastName() jim.jones jason.leigh tracy.jordan
1381 | * `number`
1382 | * `number.digit`
1383 | * `number.digits(count: Int)`
1384 | * `number.numberBetween(min: Int, max: Int)`
1385 | * `number.numberBetween(min: Int, max: Int)`
1386 | * `number.randomDigit`
1387 |
1388 | Returns a random number from 0-9 (both inclusive)
1389 | * `number.randomDigitNotZero`
1390 |
1391 | Returns a random number from 1-9 (both inclusive)
1392 | * `number.randomDouble(maxNumberOfDecimals: Int, min: Int, max: Int)`
1393 | * `number.randomDouble(maxNumberOfDecimals: Int, min: Int, max: Int)`
1394 | * `number.randomNumber`
1395 |
1396 | Returns a random number
1397 | * `number.randomNumber(numberOfDigits: Int, strict: Boolean)`
1398 | * `options`
1399 | * `overwatch`
1400 | * `overwatch.hero`
1401 | * `overwatch.location`
1402 | * `overwatch.quote`
1403 | * `phoneNumber`
1404 | * `phoneNumber.cellPhone`
1405 | * `phoneNumber.phoneNumber`
1406 | * `pokemon`
1407 | * `pokemon.location`
1408 | * `pokemon.name`
1409 | * `random`
1410 | * `random.nextDouble`
1411 | * `random.nextInt(min: Int, max: Int)`
1412 | * `random.nextInt(n: Int)`
1413 | * `random.nextLong(n: long)`
1414 | * `random.nextLong`
1415 | * `rickAndMorty`
1416 | * `rickAndMorty.character`
1417 | * `rickAndMorty.location`
1418 | * `rickAndMorty.quote`
1419 | * `robin`
1420 | * `robin.quote`
1421 | * `rockBand`
1422 | * `rockBand.name`
1423 | * `shakespeare`
1424 | * `shakespeare.asYouLikeItQuote`
1425 | * `shakespeare.hamletQuote`
1426 | * `shakespeare.kingRichardIIIQuote`
1427 | * `shakespeare.romeoAndJulietQuote`
1428 | * `slackEmoji`
1429 | * `slackEmoji.activity`
1430 | * `slackEmoji.celebration`
1431 | * `slackEmoji.custom`
1432 | * `slackEmoji.emoji`
1433 | * `slackEmoji.foodAndDrink`
1434 | * `slackEmoji.nature`
1435 | * `slackEmoji.objectsAndSymbols`
1436 | * `slackEmoji.people`
1437 | * `slackEmoji.travelAndPlaces`
1438 | * `space`
1439 | * `space.agency`
1440 | * `space.agencyAbbreviation`
1441 | * `space.company`
1442 | * `space.constellation`
1443 | * `space.distanceMeasurement`
1444 | * `space.galaxy`
1445 | * `space.meteorite`
1446 | * `space.moon`
1447 | * `space.nasaSpaceCraft`
1448 | * `space.nebula`
1449 | * `space.planet`
1450 | * `space.star`
1451 | * `space.starCluster`
1452 | * `starTrek`
1453 | * `starTrek.character`
1454 | * `starTrek.location`
1455 | * `starTrek.specie`
1456 | * `starTrek.villain`
1457 | * `stock`
1458 | * `stock.nsdqSymbol`
1459 | * `stock.nyseSymbol`
1460 | * `superhero`
1461 | * `superhero.descriptor`
1462 | * `superhero.name`
1463 | * `superhero.power`
1464 | * `superhero.prefix`
1465 | * `superhero.suffix`
1466 | * `team`
1467 | * `team.creature`
1468 | * `team.name`
1469 | * `team.sport`
1470 | * `team.state`
1471 | * `twinPeaks`
1472 | * `twinPeaks.character`
1473 | * `twinPeaks.location`
1474 | * `twinPeaks.quote`
1475 | * `university`
1476 | * `university.name`
1477 | * `university.prefix`
1478 | * `university.suffix`
1479 | * `weather`
1480 | * `weather.description`
1481 |
1482 | Generates a short weather description.
1483 | * `weather.temperatureCelsius(minTemperature: Int, maxTemperature: Int)`
1484 |
1485 | Generates a random temperature celsius between two temperatures.
1486 | * `weather.temperatureCelsius`
1487 |
1488 | Generates a random temperature celsius between -30 and 38 degrees.
1489 | * `weather.temperatureFahrenheit`
1490 |
1491 | Generates a random temperature fahrenheit between -22 and 100 degrees.
1492 | * `weather.temperatureFahrenheit(minTemperature: Int, maxTemperature: Int)`
1493 |
1494 | Generates a random temperature fahrenheit between two temperatures.
1495 | * `witcher`
1496 | * `witcher.character`
1497 | * `witcher.location`
1498 | * `witcher.monster`
1499 | * `witcher.quote`
1500 | * `witcher.school`
1501 | * `witcher.witcher`
1502 | * `yoda`
1503 | * `yoda.quote`
1504 | * `zelda`
1505 | * `zelda.character`
1506 | * `zelda.game`
1507 |
1508 |
1509 | #### `@fakeConfig`
1510 |
1511 | ```graphql
1512 | directive @fakeConfig(locale: String, seed: Int) on SCHEMA
1513 | ```
1514 |
1515 | Customize fake data generation
1516 |
1517 | #### `@const`
1518 |
1519 | ```graphql
1520 | directive @const(value: Any!) on FIELD_DEFINITION | SCHEMA
1521 | ```
1522 |
1523 | Provides a way to resolve a field with a constant value. `value` can be any valid GraphQL input value. It would be treated as a JSON value.
1524 |
1525 | #### `@jsonConst`
1526 |
1527 | ```graphql
1528 | directive @jsonConst(value: String!) on FIELD_DEFINITION | SCHEMA
1529 | ```
1530 |
1531 | Provides a way to resolve a field with a constant value. `value` should be a valid JSON value.
1532 |
1533 | #### `@arg`
1534 |
1535 | ```graphql
1536 | directive @arg(name: String!) on FIELD_DEFINITION
1537 | ```
1538 |
1539 | Provides a way to resolve a field with value of one of its arguments.
1540 |
1541 | #### `@value`
1542 |
1543 | ```graphql
1544 | directive @value(name: String, path: String) on FIELD_DEFINITION
1545 | ```
1546 |
1547 | Extracts a value(s) from the context object. It supports following extractors via arguments (only one can be used):
1548 |
1549 | * `name` - Extracts a named property value from a context JSON object
1550 | * `path` - A [JSON Path](http://goessner.net/articles/JsonPath/) expression. It would be executed against current context JSON value.
1551 |
1552 | #### `@context`
1553 |
1554 | ```graphql
1555 | directive @context(name: String, path: String) on FIELD_DEFINITION
1556 | ```
1557 |
1558 | Extracts a value(s) from the context object defined on the schema level. It supports following extractors via arguments (only one can be used):
1559 |
1560 | * `name` - Extracts a named property value from a JSON object
1561 | * `path` - A [JSON Path](http://goessner.net/articles/JsonPath/) expression. It would be executed against current context JSON value, which is defined at the schema level.
1562 |
1563 | ### Placeholders
1564 |
1565 | Placeholders may be used in some the directive arguments (inside of the strings) and the syntax looks like this:
1566 |
1567 | ```
1568 | ${value.$.results[0].film}
1569 | ```
1570 |
1571 | The placeholder consists of two parts separated by dot (`.`): the scope (`value` in this case) and the extractor (`$.results[0].film`) - a JSON Path extractor in this example). The scope defines a place/value from which you would like extract a value. Following scopes are supported:
1572 |
1573 | * `arg` - field argument
1574 | * `value` - a context value
1575 | * `ctx` - a context value which is defined on a schema level
1576 | * `elem` - an extracted element that comes from the `forAll` argument
1577 | * `env` - an environment variable
1578 |
1579 | The extractor can be either a string (the name of the property) or a [JSON Path](http://goessner.net/articles/JsonPath/) expression.
1580 |
1581 | ### Setup
1582 |
1583 | **Prerequisites**
1584 |
1585 | * [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)
1586 | * [SBT](http://www.scala-sbt.org/download.html)
1587 |
1588 | **Running**
1589 |
1590 | After starting the server with
1591 |
1592 | ```bash
1593 | sbt run
1594 |
1595 | # or, if you want to watch the source code changes
1596 |
1597 | sbt ~reStart
1598 | ```
1599 |
1600 | you can run queries interactively using [GraphiQL](https://github.com/graphql/graphiql) by opening [http://localhost:8080](http://localhost:8080) in a browser or query the `/graphql` endpoint directly.
1601 |
1602 | **Publishing**
1603 |
1604 | The docker publishing is done with sbt-native-packager plugin:
1605 |
1606 | ```bash
1607 | sbt docker:publishLocal
1608 |
1609 | # or
1610 |
1611 | sbt docker:publish
1612 | ```
1613 |
1614 | Fat JAR is created with sbt-assembly plugin:
1615 |
1616 | ```bash
1617 | sbt assembly
1618 | ```
1619 |
1620 | ## License
1621 |
1622 | **graphql-gateway** is licensed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
1623 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | name := "graphql-gateway"
2 | organization := "org.sangria-graphql"
3 | version := "0.1.0-SNAPSHOT"
4 |
5 | description := "GraphQL Gateway - SDL-based GraphQL gateway for REST and GraphQL-based microservices"
6 | homepage := Some(url("http://sangria-graphql.org"))
7 | licenses := Seq("Apache License, ASL Version 2.0" → url("http://www.apache.org/licenses/LICENSE-2.0"))
8 |
9 | scalaVersion := "2.12.7"
10 |
11 | scalacOptions ++= Seq("-deprecation", "-feature")
12 |
13 | mainClass in Compile := Some("sangria.gateway.Main")
14 |
15 | val sangriaVersion = "1.4.3-SNAPSHOT"
16 | val circeVersion = "0.10.0"
17 |
18 | libraryDependencies ++= Seq(
19 | "org.sangria-graphql" %% "sangria" % sangriaVersion,
20 | "org.sangria-graphql" %% "sangria-slowlog" % "0.1.8",
21 | "org.sangria-graphql" %% "sangria-circe" % "1.2.1",
22 |
23 | "com.typesafe.akka" %% "akka-http" % "10.1.5",
24 | "com.typesafe.akka" %% "akka-slf4j" % "2.5.17",
25 | "de.heikoseeberger" %% "akka-http-circe" % "1.22.0",
26 | "de.heikoseeberger" %% "akka-sse" % "3.0.0",
27 |
28 | "com.github.pathikrit" %% "better-files-akka" % "3.6.0",
29 |
30 | "io.circe" %% "circe-core" % circeVersion,
31 | "io.circe" %% "circe-generic" % circeVersion,
32 | "io.circe" %% "circe-parser" % circeVersion,
33 | "io.circe" %% "circe-optics" % circeVersion,
34 |
35 | "com.jayway.jsonpath" % "json-path" % "2.4.0",
36 |
37 | "com.typesafe.play" %% "play-ahc-ws-standalone" % "1.1.10",
38 |
39 | "com.iheart" %% "ficus" % "1.4.3",
40 | "com.github.javafaker" % "javafaker" % "0.16",
41 | "info.henix" %% "ssoup" % "0.5",
42 |
43 | "org.slf4j" % "slf4j-api" % "1.7.25",
44 | "ch.qos.logback" % "logback-classic" % "1.2.3",
45 | "com.typesafe.scala-logging" %% "scala-logging" % "3.9.0",
46 |
47 | "org.scalatest" %% "scalatest" % "3.0.5" % Test
48 | )
49 |
50 | resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"
51 |
52 | // nice *magenta* prompt!
53 |
54 | shellPrompt in ThisBuild := { state ⇒
55 | scala.Console.MAGENTA + Project.extract(state).currentRef.project + "> " + scala.Console.RESET
56 | }
57 |
--------------------------------------------------------------------------------
/packaging.sbt:
--------------------------------------------------------------------------------
1 | import com.typesafe.sbt.packager.docker.Cmd
2 |
3 | import scala.sys.process._
4 |
5 | dockerBaseImage := "frolvlad/alpine-oraclejdk8:8.161.12-slim"
6 | version in Docker := ("git rev-parse HEAD" !!).trim
7 | dockerRepository := Some("tenshi")
8 | dockerUpdateLatest := true
9 | dockerExposedVolumes := Seq(s"/schema")
10 | dockerExposedPorts := Seq(8080)
11 |
12 | dockerCommands := Seq(
13 | dockerCommands.value.head,
14 | // Install bash to be able to start the application
15 | Cmd("RUN apk add --update bash && rm -rf /var/cache/apk/*")
16 | ) ++ dockerCommands.value.tail
17 |
18 | dockerCommands += Cmd("ENV", "WATCH_PATHS=/schema")
19 |
20 | assemblyJarName := "graphql-gateway.jar"
21 |
22 | enablePlugins(JavaServerAppPackaging, DockerPlugin)
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.2.3
2 |
--------------------------------------------------------------------------------
/project/coverage.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")
2 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.2")
3 |
--------------------------------------------------------------------------------
/project/publishing.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.3")
2 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
--------------------------------------------------------------------------------
/project/revolver.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1")
--------------------------------------------------------------------------------
/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | akka {
2 | loggers = ["akka.event.slf4j.Slf4jLogger"]
3 | loglevel = "DEBUG"
4 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
5 | }
--------------------------------------------------------------------------------
/src/main/resources/assets/graphiql.html:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Loading...
58 |
59 |
199 |
200 |
201 |
--------------------------------------------------------------------------------
/src/main/resources/assets/playground.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | GraphQL Playground
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
58 |
59 |
443 |
444 |
477 |
Loading
478 | GraphQL Playground
479 |
480 |
481 |
482 |
540 |
541 |
--------------------------------------------------------------------------------
/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 | %cyan(%d{HH:mm:ss.SSS}) %highlight(%-5level) %cyan(%logger{5}) - %msg%n
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/main/resources/reference.conf:
--------------------------------------------------------------------------------
1 | port = 8080
2 | port = ${?PORT}
3 |
4 | bindHost = 0.0.0.0
5 | bindHost = ${?BIND_HOST}
6 |
7 | graphiql = true
8 | graphiql = ${?GRAPHIQL}
9 |
10 | playground = true
11 | playground = ${?PLAYGROUND}
12 |
13 | // Available directives: http, graphql, faker, basic
14 | includeDirectivesStr = ${?INCLUDE_DIRECTIVES}
15 | excludeDirectivesStr = ${?EXCLUDE_DIRECTIVES}
16 |
17 | slowLog {
18 | enabled = true
19 | enabled = ${?SLOW_LOG_ENABLED}
20 |
21 | threshold = 10 seconds
22 | threshold = ${?SLOW_LOG_THRESHOLD}
23 |
24 | extension = false
25 | extension = ${?SLOW_LOG_EXTENSION}
26 |
27 | // Allows apollo-tracing when X-Apollo-Tracing header is provided
28 | apolloTracing = true
29 | apolloTracing = ${?SLOW_LOG_APOLLO_TRACING}
30 | }
31 |
32 | watch {
33 | enabled = true
34 | enabled = ${?WATCH_ENABLED}
35 |
36 | threshold = 50 millis
37 | threshold = ${?WATCH_THRESHOLD}
38 |
39 | paths = ["."]
40 | pathsStr = ${?WATCH_PATHS}
41 |
42 | glob = ["**/*.graphql"]
43 | globStr = ${?WATCH_GLOB}
44 | }
45 |
46 | limit {
47 | complexity = 10000
48 | complexity = ${?LIMIT_COMPLEXITY}
49 |
50 | maxDepth = 15
51 | maxDepth = ${?LIMIT_MAX_DEPTH}
52 |
53 | allowIntrospection = true
54 | allowIntrospection = ${?ALLOW_INTROSPECTION}
55 | }
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/AppConfig.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway
2 |
3 | import better.files.File
4 | import com.typesafe.config.Config
5 | import net.ceedubs.ficus.Ficus._
6 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._
7 |
8 | import scala.concurrent.duration.FiniteDuration
9 |
10 | case class WatchConfig(
11 | enabled: Boolean,
12 | paths: Seq[String],
13 | pathsStr: Option[String],
14 | threshold: FiniteDuration,
15 | glob: Seq[String],
16 | globStr: Option[String]
17 | ) {
18 | lazy val allPaths = pathsStr.map(_.split("\\s*,\\s*").toSeq) getOrElse paths
19 | lazy val allFiles = allPaths.map(File(_))
20 | lazy val allGlobs = globStr.map(_.split("\\s*,\\s*").toSeq) getOrElse glob
21 | }
22 |
23 | case class LimitConfig(
24 | complexity: Double,
25 | maxDepth: Int,
26 | allowIntrospection: Boolean)
27 |
28 | case class SlowLogConfig(
29 | enabled: Boolean,
30 | threshold: FiniteDuration,
31 | extension: Boolean,
32 | apolloTracing: Boolean)
33 |
34 | case class AppConfig(
35 | port: Int,
36 | bindHost: String,
37 | graphiql: Boolean,
38 | playground: Boolean,
39 | slowLog: SlowLogConfig,
40 | watch: WatchConfig,
41 | limit: LimitConfig,
42 | includeDirectives: Option[Seq[String]],
43 | includeDirectivesStr: Option[String],
44 | excludeDirectives: Option[Seq[String]],
45 | excludeDirectivesStr: Option[String]
46 | ) {
47 | lazy val allIncludeDirectives = includeDirectivesStr.map(_.split("\\s*,\\s*").toSeq) orElse includeDirectives
48 | lazy val allExcludeDirectives = excludeDirectivesStr.map(_.split("\\s*,\\s*").toSeq) orElse excludeDirectives
49 |
50 | def isEnabled(directivesName: String) =
51 | !allExcludeDirectives.exists(_.contains(directivesName)) && (
52 | allIncludeDirectives.isEmpty ||
53 | allIncludeDirectives.exists(_.contains(directivesName)))
54 | }
55 |
56 | object AppConfig {
57 | def load(config: Config): AppConfig = config.as[AppConfig]
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/Main.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway
2 |
3 | import com.typesafe.config.ConfigFactory
4 | import sangria.gateway.http.GatewayServer
5 |
6 | object Main extends App {
7 | val config = AppConfig.load(ConfigFactory.load())
8 | val server = new GatewayServer
9 |
10 | server.startup(config)
11 |
12 | Runtime.getRuntime.addShutdownHook(new Thread {
13 | override def run(): Unit = server.shutdown()
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/file/FileMonitor.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.file
2 |
3 | import java.nio.file._
4 |
5 | import better.files._
6 | import com.sun.nio.file.SensitivityWatchEventModifier
7 |
8 | import scala.concurrent.ExecutionContext
9 | import scala.util.Try
10 | import scala.util.control.NonFatal
11 |
12 | class FileMonitor(val root: File, maxDepth: Int) extends File.Monitor {
13 | protected[this] val service = root.newWatchService
14 |
15 | def this(root: File, recursive: Boolean = true) = this(root, if (recursive) Int.MaxValue else 0)
16 |
17 | /**
18 | * If watching non-directory, don't react to siblings
19 | * @param target
20 | * @return
21 | */
22 | protected[this] def reactTo(target: File) = root.isDirectory || root.isSamePathAs(target)
23 |
24 | protected[this] def process(key: WatchKey) = {
25 | val path = key.watchable().asInstanceOf[Path]
26 |
27 | import scala.collection.JavaConverters._
28 | key.pollEvents().asScala foreach {
29 | case event: WatchEvent[Path] @unchecked ⇒
30 | val target: File = path.resolve(event.context())
31 | if (reactTo(target)) {
32 | if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
33 | val depth = root.relativize(target).getNameCount
34 | watch(target, (maxDepth - depth) max 0) // auto-watch new files in a directory
35 | }
36 | onEvent(event.kind(), target, event.count())
37 | }
38 | case event ⇒ if (reactTo(path)) onUnknownEvent(event)
39 | }
40 | key.reset()
41 | }
42 |
43 | protected[this] def watch(file: File, depth: Int): Unit = {
44 | def toWatch: Iterator[File] = if (file.isDirectory) {
45 | file.walk(depth).filter(f ⇒ f.isDirectory && f.exists)
46 | } else {
47 | when(file.exists)(file.parent).iterator // There is no way to watch a regular file; so watch its parent instead
48 | }
49 |
50 | try
51 | toWatch
52 | .foreach(f ⇒
53 | Try[Unit](f.path.register(service, File.Events.all.toArray,
54 | // this is com.sun internal, but the service is useless on OSX without it
55 | SensitivityWatchEventModifier.HIGH))
56 | .recover {case error ⇒ onException(error)}.get)
57 | catch {
58 | case NonFatal(e) ⇒ onException(e)
59 | }
60 | }
61 |
62 | @inline private def when[A](condition: Boolean)(f: => A): Option[A] = if (condition) Some(f) else None
63 |
64 | def start()(implicit executionContext: ExecutionContext) = {
65 | watch(root, maxDepth)
66 | executionContext.execute(new Runnable {
67 | override def run() = {
68 | try Iterator.continually(service.take()).foreach(process) catch {
69 | case _: ClosedWatchServiceException ⇒ // just ignore, the service is already closed!
70 | }
71 | }
72 | })
73 | }
74 |
75 | def close() = service.close()
76 |
77 | // Although this class is abstract, we give provide implementations so user can choose to implement a subset of these
78 | def onCreate(file: File, count: Int) = {}
79 | def onModify(file: File, count: Int) = {}
80 | def onDelete(file: File, count: Int) = {}
81 | def onUnknownEvent(event: WatchEvent[_]) = {}
82 | def onException(exception: Throwable) = {}
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/file/FileMonitorActor.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.file
2 |
3 | import java.nio.file.{NoSuchFileException, StandardWatchEventKinds}
4 |
5 | import akka.actor.{Actor, ActorRef, Cancellable, PoisonPill, Props}
6 | import akka.event.Logging
7 | import better.files._
8 | import sangria.gateway.file.FileWatcher._
9 |
10 | import scala.collection.mutable
11 | import scala.concurrent.duration.FiniteDuration
12 |
13 | class FileMonitorActor(paths: Seq[File], threshold: FiniteDuration, globs: Seq[String], cb: Vector[File] ⇒ Unit) extends Actor {
14 | import FileMonitorActor._
15 |
16 | import context.dispatcher
17 |
18 | val log = Logging(context.system, this)
19 | var watchers: Seq[ActorRef] = _
20 | val pendingFiles: mutable.HashSet[File] = mutable.HashSet[File]()
21 | var scheduled: Option[Cancellable] = None
22 |
23 | override def preStart(): Unit = {
24 | watchers = paths.map(_.newWatcher(recursive = true))
25 |
26 | watchers.foreach { watcher ⇒
27 | watcher ! when(events = StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE) {
28 | case (_, file) ⇒ self ! FileChange(file)
29 | }
30 | }
31 | }
32 |
33 | def receive = {
34 | case FileChange(file) ⇒
35 | try {
36 | if (file.exists && !file.isDirectory && globs.exists(file.glob(_, includePath = false).nonEmpty)) {
37 | pendingFiles += file
38 |
39 | if (scheduled.isEmpty)
40 | scheduled = Some(context.system.scheduler.scheduleOnce(threshold, self, Threshold))
41 | }
42 | } catch {
43 | case _: NoSuchFileException ⇒ // ignore, it's ok
44 | }
45 |
46 | case Threshold ⇒
47 | val files = pendingFiles.toVector.sortBy(_.name)
48 |
49 | if (files.nonEmpty)
50 | cb(files)
51 |
52 | pendingFiles.clear()
53 | scheduled = None
54 | }
55 | }
56 |
57 | object FileMonitorActor {
58 | case class FileChange(file: File)
59 | case object Threshold
60 |
61 | def props(paths: Seq[File], threshold: FiniteDuration, globs: Seq[String], cb: Vector[File] ⇒ Unit) =
62 | Props(new FileMonitorActor(paths, threshold, globs, cb))
63 | }
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/file/FileUtil.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.file
2 |
3 | import better.files.File
4 |
5 | object FileUtil {
6 | def loadFiles(files: Seq[File], globs: Seq[String]) = {
7 | val foundFiles = files.flatMap(file ⇒ globs.flatMap(glob ⇒ file.glob(glob, includePath = false))).toSet.toVector.sortBy((file: File) ⇒ file.name)
8 |
9 | foundFiles.filterNot(_.isDirectory).map(f ⇒ f → f.lines.mkString("\n"))
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/file/FileWatcher.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.file
2 |
3 | import akka.actor._
4 | import better.files.{Disposable, File, newMultiMap, repeat}
5 | import better.files._
6 |
7 | import scala.collection.mutable
8 |
9 | /**
10 | * An actor that can watch a file or a directory
11 | * Instead of directly calling the constructor of this, call file.newWatcher to create the actor
12 | *
13 | * @param file watch this file (or directory)
14 | * @param maxDepth In case of directories, how much depth should we watch
15 | */
16 | class FileWatcher(file: File, maxDepth: Int) extends Actor {
17 | import FileWatcher._
18 |
19 | def this(file: File, recursive: Boolean = true) = this(file, if (recursive) Int.MaxValue else 0)
20 |
21 | protected[this] val callbacks = newMultiMap[Event, Callback]
22 |
23 | protected[this] val monitor: File.Monitor = new FileMonitor(file, maxDepth) {
24 | override def onEvent(event: Event, file: File, count: Int) = self ! Message.NewEvent(event, file, count)
25 | override def onException(exception: Throwable) = self ! Status.Failure(exception)
26 | }
27 |
28 | override def preStart() = monitor.start()(executionContext = context.dispatcher)
29 |
30 | override def receive = {
31 | case Message.NewEvent(event, target, count) if callbacks.contains(event) => callbacks(event).foreach(f => repeat(count)(f(event -> target)))
32 | case Message.RegisterCallback(events, callback) => events.foreach(event => callbacks.addBinding(event, callback))
33 | case Message.RemoveCallback(event, callback) => callbacks.removeBinding(event, callback)
34 | }
35 |
36 | override def postStop() = monitor.stop()
37 | }
38 |
39 | object FileWatcher {
40 | import java.nio.file.{Path, WatchEvent}
41 |
42 | type Event = WatchEvent.Kind[Path]
43 | type Callback = PartialFunction[(Event, File), Unit]
44 |
45 | sealed trait Message
46 | object Message {
47 | case class NewEvent(event: Event, file: File, count: Int) extends Message
48 | case class RegisterCallback(events: Traversable[Event], callback: Callback) extends Message
49 | case class RemoveCallback(event: Event, callback: Callback) extends Message
50 | }
51 |
52 | implicit val disposeActorSystem: Disposable[ActorSystem] =
53 | Disposable(_.terminate())
54 |
55 | implicit class FileWatcherOps(file: File) {
56 | def watcherProps(recursive: Boolean): Props =
57 | Props(new FileWatcher(file, recursive))
58 |
59 | def newWatcher(recursive: Boolean = true)(implicit ctx: ActorRefFactory): ActorRef =
60 | ctx.actorOf(watcherProps(recursive))
61 | }
62 |
63 | def when(events: Event*)(callback: Callback): Message =
64 | Message.RegisterCallback(events, callback)
65 |
66 | def on(event: Event)(callback: File => Unit): Message =
67 | when(event) { case (`event`, file) => callback(file) }
68 |
69 | def stop(event: Event, callback: Callback): Message =
70 | Message.RemoveCallback(event, callback)
71 |
72 | private def newMultiMap[A, B]: mutable.MultiMap[A, B] = new mutable.HashMap[A, mutable.Set[B]] with mutable.MultiMap[A, B]
73 | @inline private def repeat[U](n: Int)(f: => U): Unit = (1 to n).foreach(_ ⇒ f)
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/http/GatewayServer.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.http
2 |
3 | import language.postfixOps
4 | import akka.actor.ActorSystem
5 | import akka.http.scaladsl.Http
6 | import akka.stream.ActorMaterializer
7 | import play.api.libs.ws.ahc.StandaloneAhcWSClient
8 | import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClient
9 | import sangria.gateway.AppConfig
10 | import sangria.gateway.http.client.PlayHttpClient
11 | import sangria.gateway.schema.materializer.GatewayMaterializer
12 | import sangria.gateway.schema.materializer.directive._
13 | import sangria.gateway.schema.{ReloadableSchemaProvider, StaticSchemaProvider}
14 | import sangria.gateway.util.Logging
15 |
16 | import scala.util.{Failure, Success}
17 | import scala.util.control.NonFatal
18 |
19 | class GatewayServer extends Logging {
20 | implicit val system: ActorSystem = ActorSystem("sangria-server")
21 | implicit val materializer: ActorMaterializer = ActorMaterializer()
22 |
23 | import system.dispatcher
24 |
25 | //val client = new AkkaHttpClient
26 | val client = new PlayHttpClient(new StandaloneAhcWSClient(new DefaultAsyncHttpClient))
27 |
28 | val directiveProviders = Map(
29 | "http" → new HttpDirectiveProvider(client),
30 | "graphql" → new GraphQLDirectiveProvider,
31 | "faker" → new FakerDirectiveProvider,
32 | "basic" → new BasicDirectiveProvider)
33 |
34 | def startup(config: AppConfig) =
35 | try {
36 | val gatewayMaterializer = new GatewayMaterializer(filterDirectives(config, directiveProviders))
37 |
38 | val schemaProvider =
39 | if (config.watch.enabled)
40 | new ReloadableSchemaProvider(config, client, gatewayMaterializer)
41 | else
42 | new StaticSchemaProvider(config, client, gatewayMaterializer)
43 |
44 | schemaProvider.schemaInfo // trigger initial schema load at startup
45 |
46 | val routing = new GraphQLRouting(config, schemaProvider)
47 |
48 | Http().bindAndHandle(routing.route, config.bindHost, config.port).andThen {
49 | case Success(_) ⇒
50 | logger.info(s"Server started on ${config.bindHost}:${config.port}")
51 |
52 | if (config.watch.enabled)
53 | logger.info(s"Watching files at following path: ${config.watch.allFiles.mkString(", ")}. Looking for files: ${config.watch.allGlobs.mkString(", ")}.")
54 |
55 | case Failure(_) ⇒
56 | shutdown()
57 | }
58 | } catch {
59 | case NonFatal(error) ⇒
60 | logger.error("Error during server startup", error)
61 |
62 | shutdown()
63 | }
64 |
65 | def shutdown(): Unit = {
66 | logger.info("Shutting down server")
67 | system.terminate()
68 | }
69 |
70 | private def filterDirectives(config: AppConfig, providers: Map[String, DirectiveProvider]) = {
71 | val includes = config.allIncludeDirectives.fold(Set.empty[String])(_.toSet)
72 | val excludes = config.allExcludeDirectives.fold(Set.empty[String])(_.toSet)
73 | val initial = providers.toVector
74 |
75 | val withIncludes =
76 | if (config.allIncludeDirectives.nonEmpty)
77 | initial.filter(dp ⇒ includes contains dp._1)
78 | else
79 | initial
80 |
81 | val withExcludes =
82 | if (config.allExcludeDirectives.nonEmpty)
83 | withIncludes.filterNot(dp ⇒ excludes contains dp._1)
84 | else
85 | withIncludes
86 |
87 | withExcludes.map(_._2)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/http/GraphQLRequestUnmarshaller.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.http
2 |
3 | import java.nio.charset.Charset
4 |
5 | import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller}
6 | import akka.http.scaladsl.model._
7 | import akka.http.scaladsl.model.headers.Accept
8 | import akka.http.scaladsl.server.Directive0
9 | import akka.http.scaladsl.server.Directives._
10 | import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}
11 | import akka.util.ByteString
12 | import sangria.ast.Document
13 | import sangria.parser.QueryParser
14 | import sangria.renderer.{QueryRenderer, QueryRendererConfig}
15 |
16 | import scala.collection.immutable.Seq
17 |
18 | object GraphQLRequestUnmarshaller {
19 | val `application/graphql` = MediaType.applicationWithFixedCharset("graphql", HttpCharsets.`UTF-8`, "graphql")
20 |
21 | def explicitlyAccepts(mediaType: MediaType): Directive0 =
22 | headerValuePF {
23 | case Accept(ranges) if ranges.exists(range ⇒ !range.isWildcard && range.matches(mediaType)) ⇒ ranges
24 | }.flatMap(_ ⇒ pass)
25 |
26 | def includeIf(include: Boolean): Directive0 =
27 | if (include) pass
28 | else reject
29 |
30 | def unmarshallerContentTypes: Seq[ContentTypeRange] =
31 | mediaTypes.map(ContentTypeRange.apply)
32 |
33 | def mediaTypes: Seq[MediaType.WithFixedCharset] =
34 | List(`application/graphql`)
35 |
36 | implicit final def documentMarshaller(implicit config: QueryRendererConfig = QueryRenderer.Compact): ToEntityMarshaller[Document] =
37 | Marshaller.oneOf(mediaTypes: _*) { mediaType ⇒
38 | Marshaller.withFixedContentType(ContentType(mediaType)) { json ⇒
39 | HttpEntity(mediaType, QueryRenderer.render(json, config))
40 | }
41 | }
42 |
43 | /**
44 | * HTTP entity => `Json`
45 | *
46 | * @return unmarshaller for `Json`
47 | */
48 | implicit final val documentUnmarshaller: FromEntityUnmarshaller[Document] =
49 | Unmarshaller.byteStringUnmarshaller
50 | .forContentTypes(unmarshallerContentTypes: _*)
51 | .map {
52 | case ByteString.empty ⇒ throw Unmarshaller.NoContentException
53 | case data ⇒
54 | import sangria.parser.DeliveryScheme.Throw
55 |
56 | QueryParser.parse(data.decodeString(Charset.forName("UTF-8")))
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/http/GraphQLRouting.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.http
2 |
3 | import akka.http.scaladsl.model.StatusCodes._
4 | import akka.http.scaladsl.server.Directives._
5 | import akka.http.scaladsl.model.headers._
6 | import akka.http.scaladsl.server.{ExceptionHandler => _, _}
7 | import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport.{jsonMarshaller, jsonUnmarshaller}
8 | import sangria.gateway.http.GraphQLRequestUnmarshaller.{explicitlyAccepts, _}
9 | import akka.http.scaladsl.marshalling.{ToResponseMarshallable => TRM}
10 | import akka.http.scaladsl.model.HttpEntity
11 | import akka.http.scaladsl.model.MediaTypes.`text/html`
12 | import de.heikoseeberger.akkasse.scaladsl.model._
13 | import de.heikoseeberger.akkasse.scaladsl.marshalling.EventStreamMarshalling._
14 | import io.circe._
15 | import io.circe.optics.JsonPath._
16 | import io.circe.parser._
17 | import sangria.ast.Document
18 | import sangria.execution._
19 | import sangria.gateway.AppConfig
20 | import sangria.gateway.schema.SchemaProvider
21 | import sangria.gateway.schema.materializer.GatewayContext
22 | import sangria.gateway.util.Logging
23 | import sangria.parser.{QueryParser, SyntaxError}
24 | import sangria.marshalling.circe._
25 | import sangria.slowlog.SlowLog
26 |
27 | import scala.concurrent.{ExecutionContext, Future}
28 | import scala.util.{Failure, Success}
29 | import scala.util.control.NonFatal
30 |
31 | class GraphQLRouting[Val](config: AppConfig, schemaProvider: SchemaProvider[GatewayContext, Val])(implicit ec: ExecutionContext) extends Logging {
32 | val route: Route =
33 | optionalHeaderValueByName("X-Apollo-Tracing") { tracing ⇒
34 | path("graphql") {
35 | get {
36 | (includeIf(config.graphiql || config.playground) & explicitlyAccepts(`text/html`)) {
37 | if (config.playground)
38 | getFromResource("assets/playground.html")
39 | else
40 | getFromResource("assets/graphiql.html")
41 | } ~
42 | (extractRequestContext & parameters('query, 'operationName.?, 'variables.?)) { (request, query, operationName, variables) ⇒
43 | QueryParser.parse(query) match {
44 | case Success(ast) ⇒
45 | variables.map(parse) match {
46 | case Some(Left(error)) ⇒ complete(BadRequest, formatError(error))
47 | case Some(Right(json)) ⇒ executeGraphQL(ast, operationName, json, tracing.isDefined, extractHeaders(request))
48 | case None ⇒ executeGraphQL(ast, operationName, Json.obj(), tracing.isDefined, extractHeaders(request))
49 | }
50 | case Failure(error) ⇒ complete(BadRequest, formatError(error))
51 | }
52 | }
53 | } ~
54 | post {
55 | (extractRequestContext & parameters('query.?, 'operationName.?, 'variables.?)) { (request, queryParam, operationNameParam, variablesParam) ⇒
56 | entity(as[Json]) { body ⇒
57 | val query = queryParam orElse root.query.string.getOption(body)
58 | val operationName = operationNameParam orElse root.operationName.string.getOption(body)
59 | val variablesStr = variablesParam orElse root.variables.string.getOption(body)
60 |
61 | query.map(QueryParser.parse(_)) match {
62 | case Some(Success(ast)) ⇒
63 | variablesStr.map(parse) match {
64 | case Some(Left(error)) ⇒ complete(BadRequest, formatError(error))
65 | case Some(Right(json)) ⇒ executeGraphQL(ast, operationName, json, tracing.isDefined, extractHeaders(request))
66 | case None ⇒ executeGraphQL(ast, operationName, root.variables.json.getOption(body) getOrElse Json.obj(), tracing.isDefined, extractHeaders(request))
67 | }
68 | case Some(Failure(error)) ⇒ complete(BadRequest, formatError(error))
69 | case None ⇒ complete(BadRequest, formatError("No query to execute"))
70 | }
71 | } ~
72 | entity(as[Document]) { document ⇒
73 | variablesParam.map(parse) match {
74 | case Some(Left(error)) ⇒ complete(BadRequest, formatError(error))
75 | case Some(Right(json)) ⇒ executeGraphQL(document, operationNameParam, json, tracing.isDefined, extractHeaders(request))
76 | case None ⇒ executeGraphQL(document, operationNameParam, Json.obj(), tracing.isDefined, extractHeaders(request))
77 | }
78 | }
79 | }
80 | }
81 | }
82 | } ~
83 | (get & path("schema-updates")) {
84 | schemaProvider.schemaChanges match {
85 | case Some(source) ⇒
86 | complete(source.filter(identity).map(_ ⇒ ServerSentEvent("changed")))
87 | case None ⇒
88 | complete(200 → "Schema reloading is disabled")
89 | }
90 | } ~
91 | (get & path("schema.json")) {
92 | complete(schemaProvider.schemaInfo.map {
93 | case Some(info) ⇒ TRM(OK → info.schemaIntrospection)
94 | case None ⇒ noSchema
95 | })
96 | } ~
97 | (get & path("schema.graphql")) {
98 | complete(schemaProvider.schemaInfo.map {
99 | case Some(info) ⇒ TRM(OK → HttpEntity(`application/graphql`, info.schemaRendered))
100 | case None ⇒ noSchema
101 | })
102 | } ~
103 | (get & pathEndOrSingleSlash) {
104 | redirect("/graphql", PermanentRedirect)
105 | }
106 |
107 | private val reducers = {
108 | val complexityRejector = QueryReducer.rejectComplexQueries(config.limit.complexity, (complexity: Double, _: Any) ⇒
109 | TooComplexQueryError(s"Query complexity is $complexity but max allowed complexity is ${config.limit.complexity}. Please reduce the number of the fields in the query."))
110 |
111 | val depthRejector = QueryReducer.rejectMaxDepth(config.limit.maxDepth)
112 |
113 | val baseReducers =
114 | if (config.limit.allowIntrospection)
115 | Nil
116 | else
117 | QueryReducer.rejectIntrospection(includeTypeName = false) :: Nil
118 |
119 | baseReducers ++ List(complexityRejector, depthRejector)
120 | }
121 |
122 | private val middleware =
123 | if (config.slowLog.enabled)
124 | SlowLog(logger.underlying, config.slowLog.threshold, config.slowLog.extension) :: Nil
125 | else
126 | Nil
127 |
128 | def extractHeaders(request: RequestContext) =
129 | request.request.headers.map(h ⇒ h.name() → h.value())
130 |
131 | def executeGraphQL(query: Document, operationName: Option[String], variables: Json, tracing: Boolean, originalHeaders: Seq[(String, String)]) =
132 | complete(schemaProvider.schemaInfo.flatMap {
133 | case Some(schemaInfo) ⇒
134 | val actualVariables = if (variables.isNull) Json.obj() else variables
135 |
136 | Executor.execute(schemaInfo.schema, query, schemaInfo.ctx.copy(operationName = operationName, queryVars = actualVariables, originalHeaders = originalHeaders),
137 | root = schemaInfo.value,
138 | variables = actualVariables,
139 | operationName = operationName,
140 | queryReducers = reducers.asInstanceOf[List[QueryReducer[GatewayContext, _]]],
141 | middleware = middleware ++ schemaInfo.middleware ++ (if (tracing && config.slowLog.apolloTracing) SlowLog.apolloTracing :: Nil else Nil),
142 | exceptionHandler = exceptionHandler,
143 | deferredResolver = schemaInfo.deferredResolver)
144 | .map(res ⇒ TRM(OK → res))
145 | .recover {
146 | case error: QueryAnalysisError ⇒ TRM(BadRequest → error.resolveError)
147 | case error: ErrorWithResolver ⇒ TRM(InternalServerError → error.resolveError)
148 | }
149 | case None ⇒ Future.successful(noSchema)
150 | })
151 |
152 | def formatError(error: Throwable): Json = error match {
153 | case syntaxError: SyntaxError ⇒
154 | Json.obj("errors" → Json.arr(
155 | Json.obj(
156 | "message" → Json.fromString(syntaxError.getMessage),
157 | "locations" → Json.arr(Json.obj(
158 | "line" → Json.fromBigInt(syntaxError.originalError.position.line),
159 | "column" → Json.fromBigInt(syntaxError.originalError.position.column))))))
160 | case NonFatal(e) ⇒
161 | formatError(e.getMessage)
162 | case e ⇒
163 | throw e
164 | }
165 |
166 | def formatError(message: String): Json =
167 | Json.obj("errors" → Json.arr(Json.obj("message" → Json.fromString(message))))
168 |
169 | def noSchema = TRM(InternalServerError → "No schema defined")
170 |
171 | val exceptionHandler = ExceptionHandler {
172 | case (m, error: TooComplexQueryError) ⇒ HandledException(error.getMessage)
173 | case (m, QueryReducingError(error: MaxQueryDepthReachedError, _)) ⇒ HandledException(error.getMessage)
174 | case (m, NonFatal(error)) ⇒
175 | logger.error("Error during GraphQL query execution", error)
176 | HandledException(error.getMessage)
177 | }
178 |
179 | case class TooComplexQueryError(message: String) extends Exception(message)
180 | }
181 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/http/client/AkkaHttpClient.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.http.client
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.model.HttpHeader.ParsingResult
6 | import akka.http.scaladsl.model._
7 | import akka.http.scaladsl.model.Uri.Query
8 | import akka.http.scaladsl.model.headers.Location
9 | import akka.http.scaladsl.unmarshalling.Unmarshal
10 | import akka.stream.Materializer
11 | import akka.util.ByteString
12 | import sangria.gateway.util.Logging
13 |
14 | import scala.concurrent.{ExecutionContext, Future}
15 |
16 | class AkkaHttpClient(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext) extends HttpClient with Logging {
17 | import AkkaHttpClient._
18 | import HttpClient._
19 |
20 | override def request(method: Method.Value, url: String, queryParams: Seq[(String, String)] = Seq.empty, headers: Seq[(String, String)] = Seq.empty, body: Option[(String, String)] = None) = {
21 | val m = mapMethod(method)
22 | val query = Query(queryParams: _*)
23 | val hs = headers.map(header)
24 | val uri = Uri(url).withQuery(query)
25 | val entity = body.fold(HttpEntity.Empty){case (tpe, content) ⇒ HttpEntity(contentType(tpe), ByteString(content))}
26 | val request = HttpRequest(m, uri, hs.toVector, entity)
27 | val client = Http().singleRequest(_: HttpRequest)
28 | val richClient = RichHttpClient.httpClientWithRedirect(client)
29 |
30 | logger.debug(s"Http request: ${m.value} $url")
31 |
32 | richClient(request).map(AkkaHttpResponse(m, url, _))
33 | }
34 |
35 | override def oauthClientCredentials(url: String, clientId: String, clientSecret: String, scopes: Seq[String]): Future[HttpResponse] =
36 | throw new IllegalStateException("Not yet implemented, please use play implementation.")
37 |
38 | private def contentType(str: String) = ContentType.parse(str).fold(
39 | errors ⇒ throw ClientError(s"Invalid content type '$str'", errors.map(_.detail)),
40 | identity)
41 |
42 | private def header(nameValue: (String, String)) = HttpHeader.parse(nameValue._1, nameValue._2) match {
43 | case ParsingResult.Ok(_, errors) if errors.nonEmpty ⇒ throw ClientError(s"Invalid header '${nameValue._1}'", errors.map(_.detail))
44 | case ParsingResult.Error(error) ⇒ throw ClientError(s"Invalid header '${nameValue._1}'", Seq(error.detail))
45 | case ParsingResult.Ok(h, _) ⇒ h
46 | }
47 |
48 | def mapMethod(method: Method.Value) = method match {
49 | case Method.Get ⇒ HttpMethods.GET
50 | case Method.Post ⇒ HttpMethods.POST
51 | }
52 |
53 | object RichHttpClient {
54 | import akka.http.scaladsl.model.HttpResponse
55 | type HttpClient = HttpRequest ⇒ Future[HttpResponse]
56 |
57 | def redirectOrResult(client: HttpClient)(response: HttpResponse): Future[HttpResponse] =
58 | response.status match {
59 | case StatusCodes.Found | StatusCodes.MovedPermanently | StatusCodes.SeeOther ⇒
60 | val newUri = response.header[Location].get.uri
61 | // Always make sure you consume the response entity streams (of type Source[ByteString,Unit]) by for example connecting it to a Sink (for example response.discardEntityBytes() if you don’t care about the response entity), since otherwise Akka HTTP (and the underlying Streams infrastructure) will understand the lack of entity consumption as a back-pressure signal and stop reading from the underlying TCP connection!
62 | response.discardEntityBytes()
63 |
64 | logger.debug(s"Http redirect: ${HttpMethods.GET.value} $newUri")
65 |
66 | client(HttpRequest(method = HttpMethods.GET, uri = newUri))
67 |
68 | case _ ⇒ Future.successful(response)
69 | }
70 |
71 | def httpClientWithRedirect(client: HttpClient): HttpClient = {
72 | lazy val redirectingClient: HttpClient =
73 | req ⇒ client(req).flatMap(redirectOrResult(redirectingClient)) // recurse to support multiple redirects
74 |
75 | redirectingClient
76 | }
77 | }
78 |
79 | case class ClientError(message: String, errors: Seq[String]) extends Exception(message + ":\n" + errors.map(" * " + _).mkString("\n"))
80 | }
81 |
82 | object AkkaHttpClient {
83 | case class AkkaHttpResponse(method: HttpMethod, url: String, response: HttpResponse)(implicit mat: Materializer) extends HttpClient.HttpResponse {
84 | def asString = Unmarshal(response).to[String]
85 | def statusCode = response.status.intValue()
86 | def isSuccessful = response.status.isSuccess()
87 | def debugInfo = s"${method.value} $url"
88 | }
89 | }
90 |
91 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/http/client/HttpClient.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.http.client
2 |
3 | import scala.concurrent.Future
4 |
5 | trait HttpClient {
6 | import HttpClient._
7 |
8 | def request(
9 | method: Method.Value,
10 | url: String,
11 | queryParams: Seq[(String, String)] = Seq.empty,
12 | headers: Seq[(String, String)] = Seq.empty,
13 | body: Option[(String, String)] = None): Future[HttpResponse]
14 |
15 | def oauthClientCredentials(
16 | url: String,
17 | clientId: String,
18 | clientSecret: String,
19 | scopes: Seq[String]): Future[HttpResponse]
20 | }
21 |
22 | object HttpClient {
23 | object Method extends Enumeration {
24 | val Get, Post = Value
25 | }
26 |
27 | trait HttpResponse {
28 | def statusCode: Int
29 | def isSuccessful: Boolean
30 | def asString: Future[String]
31 | def debugInfo: String
32 | }
33 | }
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/http/client/PlayHttpClient.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.http.client
2 |
3 | import akka.util.ByteString
4 | import io.circe.parser._
5 | import play.api.libs.ws.{BodyWritable, InMemoryBody, StandaloneWSClient, WSAuthScheme}
6 | import sangria.gateway.http.client.HttpClient.Method
7 | import sangria.gateway.util.Logging
8 |
9 | import scala.concurrent.duration._
10 | import scala.concurrent.{ExecutionContext, Future}
11 |
12 | class PlayHttpClient(ws: StandaloneWSClient)(implicit ec: ExecutionContext) extends play.api.libs.ws.DefaultBodyWritables with HttpClient with Logging {
13 | private[this] val json = "application/json"
14 |
15 | override def request(
16 | method: Method.Value, url: String,
17 | queryParams: Seq[(String, String)], headers: Seq[(String, String)],
18 | body: Option[(String, String)]
19 | ) = {
20 | val m = mapMethod(method)
21 | val baseRequest = ws.url(url).withRequestTimeout(10.minutes).withMethod(m)
22 | val withParams = baseRequest.withQueryStringParameters(queryParams: _*).withHttpHeaders(headers: _*)
23 | val withBody = body match {
24 | case Some((t, content)) if t == json ⇒ withParams.withBody(parse(content).right.get)(BodyWritable(s ⇒ InMemoryBody(ByteString(s.spaces2)), t))
25 | case Some((t, _)) ⇒ throw new IllegalStateException(s"Unhandled body type [$t].")
26 | case None ⇒ withParams
27 | }
28 | val finalRequest = withBody
29 |
30 | logger.debug(s"Http request: $m $url")
31 |
32 | finalRequest.execute().map(rsp ⇒ new HttpClient.HttpResponse {
33 | override def statusCode = rsp.status
34 | override def isSuccessful = rsp.status >= 200 && rsp.status < 300
35 | override def asString = Future.successful(rsp.body)
36 | override def debugInfo = s"$method $url"
37 | })
38 | }
39 |
40 | override def oauthClientCredentials(url: String, clientId: String, clientSecret: String, scopes: Seq[String]): Future[HttpClient.HttpResponse] = {
41 | val request =
42 | ws.url(url)
43 | .withMethod("POST")
44 | .withAuth(clientId, clientSecret, WSAuthScheme.BASIC)
45 | .withBody(Map("grant_type" → Seq("client_credentials"), "scope" → scopes))
46 |
47 | logger.debug(s"HTTP OAuth client credentials request: $url")
48 |
49 | request.execute().map(rsp ⇒ new HttpClient.HttpResponse {
50 | override def statusCode = rsp.status
51 | override def isSuccessful = rsp.status >= 200 && rsp.status < 300
52 | override def asString = Future.successful(rsp.body)
53 | override def debugInfo = s"POST $url"
54 | })
55 | }
56 |
57 | def mapMethod(method: Method.Value) = method match {
58 | case Method.Get ⇒ "GET"
59 | case Method.Post ⇒ "POST"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/json/CirceJsonPath.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.json
2 |
3 | import com.jayway.jsonpath.{Configuration, JsonPath}
4 | import io.circe.Json
5 |
6 | object CirceJsonPath {
7 | private lazy val config =
8 | Configuration.builder()
9 | .jsonProvider(new CirceJsonProvider)
10 | .mappingProvider(new CirceMappingProvider)
11 | .build()
12 |
13 | def query(json: Json, jsonPath: String): Json =
14 | JsonPathValueWrapper.toJson(JsonPath.using(config).parse(json).read(jsonPath))
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/json/CirceJsonProvider.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.json
2 |
3 | import java.io.InputStream
4 |
5 | import com.jayway.jsonpath.{InvalidJsonException, JsonPathException}
6 | import com.jayway.jsonpath.spi.json.JsonProvider
7 | import io.circe.{Json, JsonObject}
8 |
9 | import scala.collection.JavaConverters._
10 |
11 | import scala.io.Source
12 |
13 | class CirceJsonProvider extends JsonProvider {
14 | override def createArray(): AnyRef = JsonPathValueWrapper.emptyArray
15 |
16 | override def setArrayIndex(array: Any, idx: Int, newValue: Any): Unit =
17 | array match {
18 | case arr: java.util.ArrayList[Any] @unchecked ⇒ if (idx == arr.size) arr.add(newValue) else arr.set(idx, newValue)
19 | case _ ⇒ error("setArrayIndex is only available on new objects")
20 | }
21 |
22 | override def length(obj: Any): Int = {
23 | obj match {
24 | case json: Json if json.isArray ⇒
25 | json.asArray.get.size
26 | case json: Json if json.isObject ⇒
27 | json.asObject.get.size
28 | case json: Json if json.isString ⇒
29 | json.asString.get.length
30 | case arr: java.util.ArrayList[_] ⇒
31 | arr.size
32 | case obj: java.util.LinkedHashMap[_, _] ⇒
33 | obj.size
34 | case s: String ⇒
35 | s.length
36 | case _ ⇒ throw new JsonPathException(s"Length operation cannot be applied to ${if (obj != null) obj.getClass.getName else "null"}")
37 | }
38 | }
39 |
40 | override def getArrayIndex(obj: Any, idx: Int): AnyRef = obj match {
41 | case arr: java.util.ArrayList[AnyRef @unchecked] ⇒ arr.get(idx)
42 | case json: Json if json.isArray ⇒ json.asArray.get(idx)
43 | case o ⇒ notJson(o)
44 | }
45 |
46 | override def getArrayIndex(obj: Any, idx: Int, unwrap: Boolean): AnyRef =
47 | getArrayIndex(obj, idx)
48 |
49 | override def createMap(): AnyRef = JsonPathValueWrapper.emptyMap
50 | override def setProperty(obj: Any, key: Any, value: Any): Unit = obj match {
51 | case obj: java.util.LinkedHashMap[String @unchecked, Any @unchecked] ⇒ obj.put(key.asInstanceOf[String], value)
52 | case _ ⇒ error("setProperty is only available on new objects")
53 | }
54 | override def getPropertyKeys(obj: Any) =
55 | obj match {
56 | case obj: java.util.LinkedHashMap[String, Any] @unchecked ⇒ obj.keySet
57 | case obj: Json if obj.isObject ⇒ obj.asObject.get.keys.asJavaCollection
58 | case o ⇒ Vector.empty.asJavaCollection
59 | }
60 |
61 | override def removeProperty(obj: Any, key: Any): Unit =
62 | obj match {
63 | case obj: java.util.LinkedHashMap[_, _] ⇒ obj.remove(key)
64 | case _ ⇒ error("removeProperty is only available on new objects")
65 | }
66 |
67 | override def getMapValue(obj: Any, key: String): AnyRef =
68 | obj match {
69 | case obj: java.util.LinkedHashMap[String, AnyRef] @unchecked if obj.containsKey(key) ⇒ obj.get(key)
70 | case json: Json if json.isObject && json.asObject.get.contains(key) ⇒ json.asObject.get(key).get
71 | case _ ⇒ JsonProvider.UNDEFINED
72 | }
73 |
74 | override def toIterable(obj: Any) =
75 | if (isArray(obj)) obj.asInstanceOf[Json].asArray.get.asJava
76 | else error(s"Cannot iterate over ${if (obj != null) obj.getClass.getName else "null"}")
77 |
78 | override def unwrap(obj: Any): AnyRef =
79 | obj match {
80 | case json: Json ⇒
81 | json.fold(
82 | jsonNull = null,
83 | jsonBoolean = b ⇒ b: java.lang.Boolean,
84 | jsonNumber = _.toBigDecimal.get,
85 | jsonString = identity,
86 | jsonArray = JsonPathValueWrapper.array(_),
87 | jsonObject = JsonPathValueWrapper.map(_))
88 |
89 | case obj ⇒ obj.asInstanceOf[AnyRef]
90 | }
91 |
92 | override def isMap(obj: Any): Boolean = obj match {
93 | case obj: java.util.HashMap[_, _] ⇒ true
94 | case obj: Json if obj.isObject ⇒ true
95 | case _ ⇒ false
96 | }
97 |
98 | override def isArray(obj: Any): Boolean = obj match {
99 | case obj: java.util.ArrayList[_] ⇒ true
100 | case obj: Json if obj.isArray ⇒ true
101 | case _ ⇒ false
102 | }
103 |
104 | override def toJson(obj: Any): String =
105 | obj match {
106 | case json: Json ⇒ json.spaces2
107 | case obj ⇒ JsonPathValueWrapper.toJson(obj).spaces2
108 | }
109 |
110 | override def parse(json: String): AnyRef =
111 | io.circe.parser.parse(json).fold(e ⇒ throw new InvalidJsonException(e, json), identity)
112 |
113 | override def parse(jsonStream: InputStream, charset: String): AnyRef = {
114 | val json = Source.fromInputStream(jsonStream, charset).getLines.mkString("\n")
115 |
116 | io.circe.parser.parse(json).fold(e ⇒ throw new InvalidJsonException(e, json), identity)
117 | }
118 |
119 | private def notJson(obj: Any) = error("Not a JSON value")
120 | private def error(message: String) = throw new JsonPathException(message)
121 | }
122 |
123 | object JsonPathValueWrapper {
124 | def emptyMap: java.util.LinkedHashMap[String, Any] =
125 | new java.util.LinkedHashMap[String, Any]
126 |
127 | def map(obj: JsonObject): java.util.LinkedHashMap[String, Any] = {
128 | val values = new java.util.LinkedHashMap[String, Any](obj.size)
129 |
130 | obj.keys.foreach { key ⇒
131 | values.put(key, obj(key).get)
132 | }
133 |
134 | values
135 | }
136 |
137 | def emptyArray = new java.util.ArrayList[Any]
138 |
139 | def array(obj: Vector[Any]): java.util.ArrayList[Any] = {
140 | val values = new java.util.ArrayList[Any](obj.size)
141 |
142 | obj.foreach { key ⇒
143 | values.add(key)
144 | }
145 |
146 | values
147 | }
148 |
149 | def toJson(obj: Any): Json = obj match {
150 | case arr: java.util.ArrayList[_] ⇒
151 | Json.arr(arr.asScala.map(toJson): _*)
152 | case obj: java.util.LinkedHashMap[String, Any] @unchecked ⇒
153 | Json.obj(obj.asScala.toSeq.map{case (k, v) ⇒ k → toJson(v)}: _*)
154 | case json: Json ⇒
155 | json
156 | case v: String ⇒
157 | Json.fromString(v)
158 | case v: Boolean ⇒
159 | Json.fromBoolean(v)
160 | case v: Int ⇒
161 | Json.fromInt(v)
162 | case v: Float ⇒
163 | Json.fromFloat(v).get
164 | case v: Double ⇒
165 | Json.fromDouble(v).get
166 | case v: BigInt ⇒
167 | Json.fromBigInt(v)
168 | case v: BigDecimal ⇒
169 | Json.fromBigDecimal(v)
170 | case null ⇒
171 | Json.Null
172 | case v ⇒
173 | throw new JsonPathException("Unsupported value: " + v)
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/json/CirceMappingProvider.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.json
2 |
3 | import com.jayway.jsonpath.{Configuration, TypeRef}
4 | import com.jayway.jsonpath.spi.mapper.MappingProvider
5 |
6 | class CirceMappingProvider extends MappingProvider {
7 | override def map[T](source: Any, targetType: Class[T], configuration: Configuration): T =
8 | throw new UnsupportedOperationException("Circe JSON provider does not support mapping!")
9 |
10 | override def map[T](source: Any, targetType: TypeRef[T], configuration: Configuration): T =
11 | throw new UnsupportedOperationException("Circe JSON provider does not support TypeRef mapping!")
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/CustomScalars.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema
2 |
3 | import java.time.format.DateTimeFormatter
4 | import java.time.{Instant, OffsetDateTime, ZoneOffset, ZonedDateTime}
5 |
6 | import sangria.schema._
7 | import sangria.ast
8 | import sangria.validation.ValueCoercionViolation
9 |
10 | import scala.util.{Failure, Success, Try}
11 |
12 | object CustomScalars {
13 | implicit val DateTimeType = ScalarType[ZonedDateTime]("DateTime",
14 | description = Some("DateTime is a scalar value that represents an ISO8601 formatted date and time."),
15 | coerceOutput = (date, _) ⇒ DateTimeFormatter.ISO_INSTANT.format(date),
16 | coerceUserInput = {
17 | case s: String ⇒ parseDateTime(s) match {
18 | case Success(date) ⇒ Right(date)
19 | case Failure(_) ⇒ Left(DateCoercionViolation)
20 | }
21 | case _ ⇒ Left(DateCoercionViolation)
22 | },
23 | coerceInput = {
24 | case ast.StringValue(s, _, _, _, _) ⇒ parseDateTime(s) match {
25 | case Success(date) ⇒ Right(date)
26 | case Failure(_) ⇒ Left(DateCoercionViolation)
27 | }
28 | case _ ⇒ Left(DateCoercionViolation)
29 | })
30 |
31 | def parseDateTime(s: String) = Try(DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(s).asInstanceOf[ZonedDateTime])
32 |
33 | case object DateCoercionViolation extends ValueCoercionViolation("Date value expected")
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/ReloadableSchemaProvider.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema
2 |
3 | import java.util.concurrent.atomic.AtomicReference
4 |
5 | import akka.actor.ActorSystem
6 | import akka.stream.{Materializer, OverflowStrategy}
7 | import akka.stream.scaladsl.{BroadcastHub, Keep, RunnableGraph, Source}
8 | import better.files.File
9 | import sangria.gateway.AppConfig
10 | import sangria.gateway.file.FileMonitorActor
11 | import sangria.gateway.http.client.HttpClient
12 | import sangria.gateway.schema.materializer.{GatewayContext, GatewayMaterializer}
13 | import sangria.gateway.util.Logging
14 |
15 | import scala.concurrent.{ExecutionContext, Future}
16 | import scala.util.{Failure, Success}
17 |
18 | // TODO: on a timer reload all external schemas and check for changes
19 | class ReloadableSchemaProvider(config: AppConfig, client: HttpClient, mat: GatewayMaterializer)(implicit system: ActorSystem, ec: ExecutionContext, amat: Materializer) extends SchemaProvider[GatewayContext, Any] with Logging {
20 | val loader = new SchemaLoader(config, client, mat)
21 | val schemaRef = new AtomicReference[Option[SchemaInfo[GatewayContext, Any]]](None)
22 |
23 | system.actorOf(FileMonitorActor.props(config.watch.allFiles, config.watch.threshold, config.watch.allGlobs, reloadSchema))
24 |
25 | private val producer = Source.actorRef[Boolean](100, OverflowStrategy.dropTail)
26 | private val runnableGraph = producer.toMat(BroadcastHub.sink(bufferSize = 256))(Keep.both)
27 | private val (changesPublisher, changesSource) = runnableGraph.run()
28 |
29 | val schemaChanges = Some(changesSource)
30 |
31 | def schemaInfo =
32 | schemaRef.get() match {
33 | case v @ Some(_) ⇒ Future.successful(v)
34 | case None ⇒ reloadSchema
35 | }
36 |
37 | def reloadSchema(files: Vector[File]): Unit = {
38 | logger.info(s"Schema files are changed: ${files mkString ", "}. Reloading schema")
39 |
40 | reloadSchema
41 | }
42 |
43 | def reloadSchema: Future[Option[SchemaInfo[GatewayContext, Any]]] =
44 | loader.loadSchema.andThen {
45 | case Success(Some(newSchema)) ⇒
46 | schemaRef.get() match {
47 | case Some(currentSchema) ⇒
48 | val changes = newSchema.schema.compare(currentSchema.schema)
49 | val renderedChanges =
50 | if (changes.nonEmpty)
51 | " with following changes:\n" + changes.map(c ⇒ " * " + c.description + (if (c.breakingChange) " (breaking)" else "")).mkString("\n")
52 | else
53 | " without any changes."
54 |
55 | changesPublisher ! true
56 | logger.info(s"Schema successfully reloaded$renderedChanges")
57 | case None ⇒
58 | logger.info(s"Schema successfully loaded from files:\n${newSchema.files.map(f ⇒ " * " + f).mkString("\n")}")
59 | }
60 |
61 | schemaRef.set(Some(newSchema))
62 | case Failure(error) ⇒
63 | logger.error("Failed to load the schema", error)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/SchemaLoader.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema
2 |
3 | import better.files.File
4 | import sangria.ast.Document
5 | import sangria.execution.Executor
6 | import sangria.execution.deferred.DeferredResolver
7 | import sangria.gateway.AppConfig
8 | import sangria.gateway.file.FileUtil
9 | import sangria.gateway.http.client.HttpClient
10 | import sangria.gateway.schema.materializer.{GatewayContext, GatewayMaterializer}
11 | import sangria.gateway.util.Logging
12 | import sangria.parser.QueryParser
13 | import sangria.schema.Schema
14 | import sangria.marshalling.circe._
15 |
16 | import scala.concurrent.{ExecutionContext, Future}
17 | import scala.util.control.NonFatal
18 | import scala.util.{Failure, Success}
19 |
20 | class SchemaLoader(config: AppConfig, client: HttpClient, mat: GatewayMaterializer)(implicit ec: ExecutionContext) extends Logging {
21 | def loadSchema: Future[Option[SchemaInfo[GatewayContext, Any]]] = {
22 | val files = FileUtil.loadFiles(config.watch.allFiles, config.watch.allGlobs)
23 |
24 | if (files.nonEmpty) {
25 | val parsed =
26 | files.map {
27 | case (path, content) ⇒ path → QueryParser.parse(content)
28 | }
29 |
30 | val failed = parsed.collect {case (path, Failure(e)) ⇒ path → e}
31 |
32 | if (failed.nonEmpty) {
33 | failed.foreach { case (path, error) ⇒
34 | logger.error(s"Can't parse file '$path':\n${error.getMessage}")
35 | }
36 |
37 | Future.successful(None)
38 | } else {
39 | val successful = parsed.collect {case (path, Success(doc)) ⇒ path → doc}
40 | val document = Document.merge(successful.map(_._2))
41 |
42 | try {
43 | val info =
44 | for {
45 | ctx ← GatewayContext.loadContext(config, client, document)
46 | schema = Schema.buildFromAst(document, mat.schemaBuilder(ctx).validateSchemaWithException(document))
47 | intro ← executeIntrospection(schema, ctx)
48 | } yield Some(SchemaInfo(
49 | schema,
50 | ctx,
51 | (),
52 | Nil,
53 | DeferredResolver.empty,
54 | schema.renderPretty,
55 | intro,
56 | files.map(_._1)))
57 |
58 | info.recover(handleError(files))
59 | } catch {
60 | case e if handleError(files).isDefinedAt(e) ⇒
61 | Future.successful(handleError(files)(e))
62 | }
63 | }
64 | } else {
65 | logger.error("No schema files found!")
66 | Future.successful(None)
67 | }
68 | }
69 |
70 | private def handleError(files: Vector[(File, String)]): PartialFunction[Throwable, Option[SchemaInfo[GatewayContext, Any]]] = {
71 | case NonFatal(e) ⇒
72 | logger.error(s"Can't create the schema from files: ${files.map(_._1).mkString(", ")}. " + e.getMessage)
73 | None
74 | }
75 |
76 | private def executeIntrospection(schema: Schema[GatewayContext, Any], ctx: GatewayContext) =
77 | Executor.execute(schema, sangria.introspection.introspectionQuery(schemaDescription = false, directiveRepeatableFlag = false), ctx)
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/SchemaProvider.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema
2 |
3 | import akka.NotUsed
4 | import akka.stream.scaladsl.Source
5 | import better.files.File
6 | import io.circe.Json
7 | import sangria.execution.{Executor, Middleware}
8 | import sangria.execution.deferred.DeferredResolver
9 | import sangria.schema.Schema
10 |
11 | import scala.concurrent.{Await, Future}
12 |
13 | trait SchemaProvider[Ctx, Val] {
14 | def schemaInfo: Future[Option[SchemaInfo[Ctx, Val]]]
15 | def schemaChanges: Option[Source[Boolean, NotUsed]]
16 | }
17 |
18 | case class SchemaInfo[Ctx, Val](
19 | schema: Schema[Ctx, Val],
20 | ctx: Ctx,
21 | value: Val,
22 | middleware: List[Middleware[Ctx]],
23 | deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
24 | schemaRendered: String,
25 | schemaIntrospection: Json,
26 | files: Vector[File])
27 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/StaticSchemaProvider.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema
2 |
3 | import sangria.gateway.AppConfig
4 | import sangria.gateway.http.client.HttpClient
5 | import sangria.gateway.schema.materializer.{GatewayContext, GatewayMaterializer}
6 |
7 | import scala.concurrent.ExecutionContext
8 |
9 | class StaticSchemaProvider(config: AppConfig, client: HttpClient, mat: GatewayMaterializer)(implicit ec: ExecutionContext) extends SchemaProvider[GatewayContext, Any] {
10 | val loader = new SchemaLoader(config, client, mat)
11 |
12 | val schemaInfo = loader.loadSchema
13 | val schemaChanges = None
14 | }
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/materializer/GatewayContext.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema.materializer
2 |
3 | import java.util.{Locale, Random}
4 |
5 | import com.github.javafaker.Faker
6 |
7 | import language.existentials
8 | import io.circe.Json
9 | import io.circe.optics.JsonPath.root
10 | import sangria.ast
11 | import sangria.gateway.AppConfig
12 | import sangria.gateway.http.client.HttpClient
13 | import sangria.gateway.json.CirceJsonPath
14 | import sangria.gateway.schema.materializer.directive.{BasicDirectiveProvider, FakerDirectiveProvider, GraphQLDirectiveProvider, OAuthClientCredentials}
15 | import sangria.gateway.util.Logging
16 | import sangria.schema._
17 | import sangria.schema.ResolverBasedAstSchemaBuilder.resolveDirectives
18 | import sangria.marshalling.circe._
19 | import sangria.marshalling.queryAst._
20 | import sangria.marshalling.MarshallingUtil._
21 | import sangria.gateway.schema.materializer.directive.HttpDirectiveProvider.{extractDelegatedHeaders, extractMap}
22 |
23 | import scala.concurrent.{ExecutionContext, Future}
24 | import scala.collection.JavaConverters._
25 |
26 | case class GatewayContext(client: HttpClient, rnd: Option[Random], faker: Option[Faker], vars: Json, graphqlIncludes: Vector[GraphQLIncludedSchema], operationName: Option[String] = None, queryVars: Json = Json.obj(), originalHeaders: Seq[(String, String)] = Seq.empty) {
27 | import GatewayContext._
28 |
29 | val allTypes = graphqlIncludes.flatMap(_.types)
30 |
31 | def request(c: Context[GatewayContext, _], schema: GraphQLIncludedSchema, query: ast.Document, variables: Json, extractName: String)(implicit ec: ExecutionContext): Future[Json] = {
32 | val fields = Vector("query" → Json.fromString(query.renderPretty))
33 | val withVars =
34 | if (variables != Json.obj()) fields :+ ("variables" → variables)
35 | else fields
36 |
37 | val body = Json.fromFields(withVars)
38 |
39 | val url = GatewayContext.fillPlaceholders(Some(c), schema.include.url)
40 | val queryParams = schema.include.queryParams
41 | val oauthHead = oauthHeaders(schema.oauthToken)
42 | val headers = oauthHead ++ schema.include.headers ++ extractDelegatedHeaders(c, Some(schema.include.delegateHeaders))
43 |
44 | client.request(HttpClient.Method.Post, url, queryParams, headers, Some("application/json" → body.noSpaces)).flatMap(GatewayContext.parseJson).map(resp ⇒
45 | root.data.at(extractName).getOption(resp).flatten.get)
46 | }
47 |
48 | def findFields(name: String, typeName: String, includeFields: Option[Seq[String]], excludeFields: Option[Seq[String]]): List[MaterializedField[GatewayContext, _]] =
49 | graphqlIncludes.find(_.include.name == name).toList.flatMap { s ⇒
50 | val tpe = s.schema.getOutputType(ast.NamedType(typeName), topLevel = true)
51 | val fields = tpe.toList
52 | .collect {case obj: ObjectLikeType[_, _] ⇒ obj}
53 | .flatMap { t ⇒
54 | val withIncludes = includeFields match {
55 | case Some(inc) ⇒ t.uniqueFields.filter(f ⇒ includeFields.forall(_.contains(f.name)))
56 | case _ ⇒ t.uniqueFields
57 | }
58 |
59 | val withExcludes = excludeFields match {
60 | case Some(excl) ⇒ withIncludes.filterNot(f ⇒ excl.contains(f.name))
61 | case _ ⇒ withIncludes
62 | }
63 |
64 | withExcludes.asInstanceOf[Vector[Field[GatewayContext, Any]]]
65 | }
66 |
67 | fields.map(f ⇒ MaterializedField(s, f.copy(astDirectives = Vector(ast.Directive("delegate", Vector.empty)))))
68 | }
69 | }
70 |
71 | object GatewayContext extends Logging {
72 | val envJson = Json.obj(System.getenv().asScala.mapValues(Json.fromString).toSeq: _*)
73 |
74 | def fillPlaceholders(ctx: Option[Context[GatewayContext, _]], value: String, cachedArgs: Option[Json] = None, elem: Json = Json.Null): String = {
75 | lazy val args = cachedArgs orElse ctx.map(c ⇒ convertArgs(c.args, c.astFields.head)) getOrElse Json.obj()
76 |
77 | PlaceholderRegExp.findAllMatchIn(value).toVector.foldLeft(value) { case (acc, p) ⇒
78 | val placeholder = p.group(0)
79 |
80 | val idx = p.group(1).indexOf(".")
81 |
82 | if (idx < 0) throw new IllegalStateException(s"Invalid placeholder '$placeholder'. It should contain two parts: scope (like value or ctx) and extractor (name of the field or JSON path) separated byt dot (.).")
83 |
84 | val (scope, selectorWithDot) = p.group(1).splitAt(idx)
85 | val selector = selectorWithDot.substring(1)
86 |
87 | def unsupported(s: String) = throw new IllegalStateException(s"Unsupported placeholder scope '$s'. Supported scopes are: value, ctx, arg, elem.")
88 |
89 | val source = scope match {
90 | case "value" ⇒ ctx.getOrElse(unsupported("value")).value.asInstanceOf[Json]
91 | case "ctx" ⇒ ctx.getOrElse(unsupported("ctx")).ctx.vars
92 | case "arg" ⇒
93 | ctx.getOrElse(unsupported("arg"))
94 | args
95 | case "elem" ⇒ elem
96 | case "env" ⇒ envJson
97 | case s ⇒ unsupported(s)
98 | }
99 |
100 | val value =
101 | if (selector.startsWith("$"))
102 | render(CirceJsonPath.query(source, selector))
103 | else
104 | source.get(selector).map(render).getOrElse("")
105 |
106 | acc.replace(placeholder, value)
107 | }
108 | }
109 |
110 | def parseJson(resp: HttpClient.HttpResponse)(implicit ec: ExecutionContext) =
111 | if (resp.isSuccessful)
112 | resp.asString.map(s ⇒ {
113 | io.circe.parser.parse(s).fold({ e ⇒
114 | logger.error(s"Failed to parse JSON response for successful request (${resp.statusCode} ${resp.debugInfo}). Body: $s", e)
115 |
116 | throw e
117 | }, identity)
118 | })
119 | else
120 | Future.failed(new IllegalStateException(s"Failed HTTP request with status code ${resp.statusCode}. ${resp.debugInfo}"))
121 |
122 | val rootValueLoc = Set(DirectiveLocation.Schema)
123 |
124 | def rootValue(schemaAst: ast.Document) = {
125 | val values = resolveDirectives(schemaAst,
126 | GenericDirectiveResolver(BasicDirectiveProvider.Dirs.JsonConst, rootValueLoc,
127 | c ⇒ Some(io.circe.parser.parse(c arg BasicDirectiveProvider.Args.JsonValue).fold(throw _, identity))),
128 | GenericDynamicDirectiveResolver[Json, Json]("const", rootValueLoc,
129 | c ⇒ c.args.get("value")))
130 |
131 | Json.fromFields(values.collect {
132 | case json: Json if json.isObject ⇒ json.asObject.get.toIterable
133 | }.flatten)
134 | }
135 |
136 | def graphqlIncludes(schemaAst: ast.Document) = {
137 | import GraphQLDirectiveProvider.{Args, Dirs}
138 |
139 | resolveDirectives(schemaAst,
140 | GenericDirectiveResolver(Dirs.IncludeSchema, resolve = c ⇒
141 | c.withArgs(Args.Name, Args.Url, Args.Headers, Args.DelegateHeaders, Args.QueryParams, Args.OAuth)((name, url, headers, delegateHeaders, queryParams, oauth) ⇒
142 | Some(GraphQLInclude(url, name, extractMap(None, headers), extractMap(None, queryParams), delegateHeaders getOrElse Seq.empty, oauth)))))
143 | }
144 |
145 | def fakerConfig(schemaAst: ast.Document) = {
146 | import sangria.gateway.schema.materializer.directive.FakerDirectiveProvider._
147 |
148 | resolveDirectives(schemaAst,
149 | GenericDirectiveResolver(Dirs.FakeConfig,
150 | resolve = c ⇒ Some(c.arg(Args.Locale).map(Locale.forLanguageTag) → c.arg(Args.Seed)))).headOption.getOrElse(None → None)
151 | }
152 |
153 | def loadIncludedSchemas(client: HttpClient, includes: Vector[GraphQLInclude])(implicit ec: ExecutionContext): Future[Vector[GraphQLIncludedSchema]] = {
154 | val loaded =
155 | includes.map { include ⇒
156 | include.oauth match {
157 | case Some(oauth) ⇒
158 | for {
159 | token ← loadOAuthToken(client, oauth)
160 | resp ← loadSchemaIntorospection(client, include, token)
161 | } yield GraphQLIncludedSchema(include, Schema.buildFromIntrospection(resp), token)
162 |
163 | case None ⇒
164 | loadSchemaIntorospection(client, include, None)
165 | .map(resp ⇒ GraphQLIncludedSchema(include, Schema.buildFromIntrospection(resp), None))
166 | }
167 | }
168 |
169 | Future.sequence(loaded)
170 | }
171 |
172 | def loadSchemaIntorospection(client: HttpClient, include: GraphQLInclude, oauthToken: Option[String])(implicit ec: ExecutionContext) = {
173 | val introspectionBody = Json.obj("query" → Json.fromString(sangria.introspection.introspectionQuery(schemaDescription = false, directiveRepeatableFlag = false).renderPretty))
174 | val url = GatewayContext.fillPlaceholders(None, include.url)
175 | val oauthHead = oauthHeaders(oauthToken)
176 |
177 | client.request(HttpClient.Method.Post, url, include.queryParams, include.headers ++ oauthHead, Some("application/json" → introspectionBody.noSpaces)).flatMap(parseJson)
178 | }
179 |
180 | def oauthHeaders(oauthToken: Option[String]) = oauthToken match {
181 | case Some(token) ⇒ Seq("Authorization" → s"Bearer $token")
182 | case _ ⇒ Seq.empty
183 | }
184 |
185 | def loadOAuthToken(client: HttpClient, credentials: OAuthClientCredentials)(implicit ec: ExecutionContext) = {
186 | val url = GatewayContext.fillPlaceholders(None, credentials.url)
187 | val clientId = GatewayContext.fillPlaceholders(None, credentials.clientId)
188 | val clientSecret = GatewayContext.fillPlaceholders(None, credentials.clientSecret)
189 | val scopes = credentials.scopes.map(GatewayContext.fillPlaceholders(None, _))
190 |
191 | client.oauthClientCredentials(url, clientId, clientSecret, scopes).flatMap(parseJson)
192 | .map(json ⇒ json.asObject.get.apply("access_token").flatMap(_.asString))
193 | }
194 |
195 | def loadContext(config: AppConfig, client: HttpClient, schemaAst: ast.Document)(implicit ec: ExecutionContext): Future[GatewayContext] = {
196 | val includes = graphqlIncludes(schemaAst)
197 | val vars = rootValue(schemaAst)
198 | val (fakerLocale, fakerSeed) = fakerConfig(schemaAst)
199 |
200 | val (rnd, faker) =
201 | if (config isEnabled "faker") {
202 | val rnd = fakerSeed.fold(new Random(System.currentTimeMillis()))(s ⇒ new Random(s.toLong))
203 |
204 | Some(rnd) → Some(fakerLocale.fold(new Faker(rnd))(l ⇒ new Faker(l, rnd)))
205 | } else None → None
206 |
207 | loadIncludedSchemas(client, includes).map(GatewayContext(client, rnd, faker, vars, _))
208 | }
209 |
210 | def namedType(tpe: OutputType[_]): OutputType[_] = tpe match {
211 | case ListType(of) ⇒ namedType(of)
212 | case OptionType(of) ⇒ namedType(of)
213 | case t ⇒ t
214 | }
215 |
216 | def convertArgs(args: Args, field: ast.Field): Json =
217 | Json.fromFields(args.raw.keys.flatMap(name ⇒ field.arguments.find(_.name == name).map(a ⇒ a.name → a.value.convertMarshaled[Json])))
218 |
219 | private val PlaceholderRegExp = """\$\{([^}]+)\}""".r
220 |
221 | // TODO: improve :)
222 | def render(value: Json) = value.fold(
223 | jsonNull = "null",
224 | jsonBoolean = "" + _,
225 | jsonNumber = "" + _.toBigDecimal.get,
226 | jsonString = identity,
227 | jsonArray = "" + _,
228 | jsonObject = "" + _)
229 |
230 | implicit class JsonOps(value: Json) {
231 | def get(key: String) = value match {
232 | case json: Json if json.isObject ⇒ json.asObject.get(key)
233 | case _ ⇒ None
234 | }
235 | }
236 | }
237 |
238 | case class GraphQLInclude(url: String, name: String, headers: Seq[(String, String)], queryParams: Seq[(String, String)], delegateHeaders: Seq[String], oauth: Option[OAuthClientCredentials])
239 | case class GraphQLIncludedSchema(include: GraphQLInclude, schema: Schema[_, _], oauthToken: Option[String]) extends MatOrigin {
240 | private val rootTypeNames = Set(schema.query.name) ++ schema.mutation.map(_.name).toSet ++ schema.subscription.map(_.name).toSet
241 |
242 | val types = schema.allTypes.values
243 | .filterNot(t ⇒ Schema.isBuiltInType(t.name) || rootTypeNames.contains(t.name)).toVector
244 | .map {
245 | case t: ObjectType[_, _] ⇒
246 | t.withInstanceCheck((value, _, tpe) ⇒ value match {
247 | case json: Json ⇒ root.__typename.string.getOption(json).contains(tpe.name)
248 | case _ ⇒ false
249 | })
250 |
251 | case t ⇒ t
252 | }
253 | .map(MaterializedType(this, _))
254 |
255 | def description = s"included schema '${include.name}'"
256 | }
257 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/materializer/GatewayMaterializer.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema.materializer
2 |
3 | import sangria.schema._
4 | import sangria.gateway.schema.materializer.directive.DirectiveProvider
5 |
6 | import scala.concurrent.ExecutionContext
7 |
8 | class GatewayMaterializer(directiveProviders: Seq[DirectiveProvider])(implicit ec: ExecutionContext) {
9 | def commonResolvers(ctx: GatewayContext, ar: Seq[PartialFunction[Context[GatewayContext, _], Action[GatewayContext, Any]]]) = Seq[AstSchemaResolver[GatewayContext]](
10 | AnyFieldResolver {
11 | case origin if !origin.isInstanceOf[ExistingSchemaOrigin[_, _]] ⇒ c ⇒ {
12 | val fn = ar.find(_.isDefinedAt(c)).getOrElse(throw new SchemaMaterializationException(s"Field resolver is not defined. Unable to handle value `${c.value}`."))
13 |
14 | fn(c)
15 | }
16 | })
17 |
18 | def schemaBuilder(ctx: GatewayContext) = {
19 | val anyResolvers = directiveProviders.flatMap(_.anyResolver)
20 | val resolvers = directiveProviders.flatMap(_.resolvers(ctx)) ++ commonResolvers(ctx, anyResolvers)
21 |
22 | AstSchemaBuilder.resolverBased[GatewayContext](resolvers: _*)
23 | }
24 | }
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/materializer/directive/BasicDirectiveProvider.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema.materializer.directive
2 |
3 | import io.circe.Json
4 | import sangria.gateway.json.CirceJsonPath
5 | import sangria.gateway.schema.CustomScalars
6 | import sangria.gateway.schema.materializer.GatewayContext
7 | import sangria.gateway.schema.materializer.GatewayContext._
8 | import sangria.schema.ResolverBasedAstSchemaBuilder.{extractFieldValue, extractValue}
9 | import sangria.schema._
10 | import sangria.marshalling.circe._
11 |
12 | import scala.concurrent.ExecutionContext
13 |
14 | class BasicDirectiveProvider(implicit ec: ExecutionContext) extends DirectiveProvider {
15 | import BasicDirectiveProvider._
16 |
17 | def resolvers(ctx: GatewayContext) = Seq(
18 | AdditionalTypes(CustomScalars.DateTimeType),
19 |
20 | DirectiveResolver(Dirs.Context, c ⇒ c.withArgs(Args.NameOpt, Args.Path) { (name, path) ⇒
21 | name
22 | .map(n ⇒ extractValue(c.ctx.field.fieldType, c.ctx.ctx.vars.get(n)))
23 | .orElse(path.map(p ⇒ extractValue(c.ctx.field.fieldType, Some(CirceJsonPath.query(c.ctx.ctx.vars, p)))))
24 | .getOrElse(throw SchemaMaterializationException(s"Can't find a directive argument 'path' or 'name'."))
25 | }),
26 |
27 | DirectiveResolver(Dirs.Value, c ⇒ c.withArgs(Args.NameOpt, Args.Path) { (name, path) ⇒
28 | def extract(value: Any) =
29 | name
30 | .map(n ⇒ extractValue(c.ctx.field.fieldType, value.asInstanceOf[Json].get(n)))
31 | .orElse(path.map(p ⇒ extractValue(c.ctx.field.fieldType, Some(CirceJsonPath.query(value.asInstanceOf[Json], p)))))
32 | .getOrElse(throw SchemaMaterializationException(s"Can't find a directive argument 'path' or 'name'."))
33 |
34 | c.lastValue map (_.map(extract)) getOrElse extract(c.ctx.value)
35 | }),
36 |
37 | DirectiveResolver(Dirs.Arg, c ⇒
38 | extractValue(c.ctx.field.fieldType,
39 | convertArgs(c.ctx.args, c.ctx.astFields.head).get(c arg Args.NameReq))),
40 |
41 | DynamicDirectiveResolver[GatewayContext, Json]("const", c ⇒
42 | extractValue(c.ctx.field.fieldType, Some(c.args.get("value") match {
43 | case Some(json: Json) if json.isString ⇒ Json.fromString(GatewayContext.fillPlaceholders(Some(c.ctx), json.asString.get))
44 | case Some(jv) ⇒ jv
45 | case _ ⇒ Json.Null
46 | }))),
47 |
48 | DirectiveResolver(Dirs.JsonConst, c ⇒
49 | extractValue(c.ctx.field.fieldType,
50 | Some(io.circe.parser.parse(GatewayContext.fillPlaceholders(Some(c.ctx), c arg Args.JsonValue)).fold(throw _, identity)))))
51 |
52 | override val anyResolver = Some({
53 | case c if c.value.isInstanceOf[Json] ⇒ ResolverBasedAstSchemaBuilder.extractFieldValue[GatewayContext, Json](c)
54 | })
55 | }
56 |
57 | object BasicDirectiveProvider {
58 | object Args {
59 | val NameOpt = Argument("name", OptionInputType(StringType))
60 | val NameReq = Argument("name", StringType)
61 | val Path = Argument("path", OptionInputType(StringType))
62 | val JsonValue = Argument("value", StringType)
63 | }
64 |
65 | object Dirs {
66 | val Context = Directive("context",
67 | arguments = Args.NameOpt :: Args.Path :: Nil,
68 | locations = Set(DirectiveLocation.FieldDefinition))
69 |
70 | val Value = Directive("value",
71 | arguments = Args.NameOpt :: Args.Path :: Nil,
72 | locations = Set(DirectiveLocation.FieldDefinition))
73 |
74 | val Arg = Directive("arg",
75 | arguments = Args.NameReq :: Nil,
76 | locations = Set(DirectiveLocation.FieldDefinition))
77 |
78 | val JsonConst = Directive("jsonValue",
79 | arguments = Args.JsonValue :: Nil,
80 | locations = Set(DirectiveLocation.FieldDefinition, DirectiveLocation.Schema))
81 | }
82 | }
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/materializer/directive/DirectiveProvider.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema.materializer.directive
2 |
3 | import sangria.gateway.schema.materializer.GatewayContext
4 | import sangria.schema.{Action, AstSchemaResolver, Context}
5 |
6 | trait DirectiveProvider {
7 | def resolvers(ctx: GatewayContext): Seq[AstSchemaResolver[GatewayContext]]
8 | def anyResolver: Option[PartialFunction[Context[GatewayContext, _], Action[GatewayContext, Any]]] = None
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/materializer/directive/FakerDirectiveProvider.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema.materializer.directive
2 |
3 | import java.time.{ZoneId, ZonedDateTime}
4 | import java.util.concurrent.TimeUnit
5 | import java.util.Random
6 | import java.util.regex.Pattern
7 |
8 | import com.github.javafaker.Faker
9 | import sangria.gateway.schema.CustomScalars
10 | import sangria.gateway.schema.materializer.GatewayContext
11 | import sangria.renderer.SchemaRenderer
12 | import sangria.schema._
13 |
14 | class FakerDirectiveProvider extends DirectiveProvider {
15 | import FakerDirectiveProvider._
16 |
17 | def resolvers(ctx: GatewayContext) = Seq(
18 | AdditionalDirectives(Seq(Dirs.FakeConfig)),
19 | DirectiveResolver(Dirs.Fake, c ⇒ c.withArgs(Args.Expr, Args.Type, Args.MinElems, Args.MaxElems, Args.Past, Args.Future) { (expr, tpe, min, max, past, future) ⇒
20 | FakeValue(expr orElse tpe, min, max, past, future, c.ctx.ctx.rnd.get, c.ctx.ctx.faker.get).coerce(c.ctx)
21 | })
22 | )
23 |
24 | override def anyResolver = Some({
25 | case c if c.value.isInstanceOf[FakeValue] ⇒ c.value.asInstanceOf[FakeValue].coerce(c)
26 | })
27 | }
28 |
29 | object FakerDirectiveProvider {
30 | val FakeType =
31 | EnumType[String]("FakeType", Some("The type of the content generated by the faker"), List(
32 | EnumValue("AddressBuildingNumber", Some("Represents faker expression `address.buildingNumber`."), "address.buildingNumber"),
33 | EnumValue("AddressCity", Some("Represents faker expression `address.city`."), "address.city"),
34 | EnumValue("AddressCityName", Some("Represents faker expression `address.cityName`."), "address.cityName"),
35 | EnumValue("AddressCityPrefix", Some("Represents faker expression `address.cityPrefix`."), "address.cityPrefix"),
36 | EnumValue("AddressCitySuffix", Some("Represents faker expression `address.citySuffix`."), "address.citySuffix"),
37 | EnumValue("AddressCountry", Some("Represents faker expression `address.country`."), "address.country"),
38 | EnumValue("AddressCountryCode", Some("Represents faker expression `address.countryCode`."), "address.countryCode"),
39 | EnumValue("AddressFirstName", Some("Represents faker expression `address.firstName`."), "address.firstName"),
40 | EnumValue("AddressFullAddress", Some("Represents faker expression `address.fullAddress`."), "address.fullAddress"),
41 | EnumValue("AddressLastName", Some("Represents faker expression `address.lastName`."), "address.lastName"),
42 | EnumValue("AddressLatitude", Some("Represents faker expression `address.latitude`."), "address.latitude"),
43 | EnumValue("AddressLongitude", Some("Represents faker expression `address.longitude`."), "address.longitude"),
44 | EnumValue("AddressSecondaryAddress", Some("Represents faker expression `address.secondaryAddress`."), "address.secondaryAddress"),
45 | EnumValue("AddressState", Some("Represents faker expression `address.state`."), "address.state"),
46 | EnumValue("AddressStateAbbr", Some("Represents faker expression `address.stateAbbr`."), "address.stateAbbr"),
47 | EnumValue("AddressStreetAddress", Some("Represents faker expression `address.streetAddress`."), "address.streetAddress"),
48 | EnumValue("AddressStreetAddressNumber", Some("Represents faker expression `address.streetAddressNumber`."), "address.streetAddressNumber"),
49 | EnumValue("AddressStreetName", Some("Represents faker expression `address.streetName`."), "address.streetName"),
50 | EnumValue("AddressStreetPrefix", Some("Represents faker expression `address.streetPrefix`."), "address.streetPrefix"),
51 | EnumValue("AddressStreetSuffix", Some("Represents faker expression `address.streetSuffix`."), "address.streetSuffix"),
52 | EnumValue("AddressTimeZone", Some("Represents faker expression `address.timeZone`."), "address.timeZone"),
53 | EnumValue("AddressZipCode", Some("Represents faker expression `address.zipCode`."), "address.zipCode"),
54 | EnumValue("AncientGod", Some("Represents faker expression `ancient.god`."), "ancient.god"),
55 | EnumValue("AncientHero", Some("Represents faker expression `ancient.hero`."), "ancient.hero"),
56 | EnumValue("AncientPrimordial", Some("Represents faker expression `ancient.primordial`."), "ancient.primordial"),
57 | EnumValue("AncientTitan", Some("Represents faker expression `ancient.titan`."), "ancient.titan"),
58 | EnumValue("AppAuthor", Some("Represents faker expression `app.author`."), "app.author"),
59 | EnumValue("AppName", Some("Represents faker expression `app.name`."), "app.name"),
60 | EnumValue("AppVersion", Some("Represents faker expression `app.version`."), "app.version"),
61 | EnumValue("ArtistName", Some("Represents faker expression `artist.name`."), "artist.name"),
62 | EnumValue("AvatarImage", Some("Represents faker expression `avatar.image`."), "avatar.image"),
63 | EnumValue("BeerHop", Some("Represents faker expression `beer.hop`."), "beer.hop"),
64 | EnumValue("BeerMalt", Some("Represents faker expression `beer.malt`."), "beer.malt"),
65 | EnumValue("BeerName", Some("Represents faker expression `beer.name`."), "beer.name"),
66 | EnumValue("BeerStyle", Some("Represents faker expression `beer.style`."), "beer.style"),
67 | EnumValue("BeerYeast", Some("Represents faker expression `beer.yeast`."), "beer.yeast"),
68 | EnumValue("BookAuthor", Some("Represents faker expression `book.author`."), "book.author"),
69 | EnumValue("BookGenre", Some("Represents faker expression `book.genre`."), "book.genre"),
70 | EnumValue("BookPublisher", Some("Represents faker expression `book.publisher`."), "book.publisher"),
71 | EnumValue("BookTitle", Some("Represents faker expression `book.title`."), "book.title"),
72 | EnumValue("BoolBool", Some("Represents faker expression `bool.bool`."), "bool.bool"),
73 | EnumValue("BusinessCreditCardExpiry", Some("Represents faker expression `business.creditCardExpiry`."), "business.creditCardExpiry"),
74 | EnumValue("BusinessCreditCardNumber", Some("Represents faker expression `business.creditCardNumber`."), "business.creditCardNumber"),
75 | EnumValue("BusinessCreditCardType", Some("Represents faker expression `business.creditCardType`."), "business.creditCardType"),
76 | EnumValue("CatBreed", Some("Represents faker expression `cat.breed`."), "cat.breed"),
77 | EnumValue("CatName", Some("Represents faker expression `cat.name`."), "cat.name"),
78 | EnumValue("CatRegistry", Some("Represents faker expression `cat.registry`."), "cat.registry"),
79 | EnumValue("ChuckNorrisFact", Some("Represents faker expression `chuckNorris.fact`."), "chuckNorris.fact"),
80 | EnumValue("CodeAsin", Some("Represents faker expression `code.asin`."), "code.asin"),
81 | EnumValue("CodeEan13", Some("Represents faker expression `code.ean13`."), "code.ean13"),
82 | EnumValue("CodeEan8", Some("Represents faker expression `code.ean8`."), "code.ean8"),
83 | EnumValue("CodeGtin13", Some("Represents faker expression `code.gtin13`."), "code.gtin13"),
84 | EnumValue("CodeGtin8", Some("Represents faker expression `code.gtin8`."), "code.gtin8"),
85 | EnumValue("CodeImei", Some("Represents faker expression `code.imei`."), "code.imei"),
86 | EnumValue("CodeIsbn10", Some("Represents faker expression `code.isbn10`."), "code.isbn10"),
87 | EnumValue("CodeIsbn13", Some("Represents faker expression `code.isbn13`."), "code.isbn13"),
88 | EnumValue("CodeIsbnGroup", Some("Represents faker expression `code.isbnGroup`."), "code.isbnGroup"),
89 | EnumValue("CodeIsbnGs1", Some("Represents faker expression `code.isbnGs1`."), "code.isbnGs1"),
90 | EnumValue("CodeIsbnRegistrant", Some("Represents faker expression `code.isbnRegistrant`."), "code.isbnRegistrant"),
91 | EnumValue("ColorName", Some("Represents faker expression `color.name`."), "color.name"),
92 | EnumValue("CommerceColor", Some("Represents faker expression `commerce.color`."), "commerce.color"),
93 | EnumValue("CommerceDepartment", Some("Represents faker expression `commerce.department`."), "commerce.department"),
94 | EnumValue("CommerceMaterial", Some("Represents faker expression `commerce.material`."), "commerce.material"),
95 | EnumValue("CommercePrice", Some("Represents faker expression `commerce.price`."), "commerce.price"),
96 | EnumValue("CommerceProductName", Some("Represents faker expression `commerce.productName`."), "commerce.productName"),
97 | EnumValue("CommercePromotionCode", Some("Represents faker expression `commerce.promotionCode`."), "commerce.promotionCode"),
98 | EnumValue("CompanyBs", Some("Represents faker expression `company.bs`."), "company.bs"),
99 | EnumValue("CompanyBuzzword", Some("Represents faker expression `company.buzzword`."), "company.buzzword"),
100 | EnumValue("CompanyCatchPhrase", Some("Represents faker expression `company.catchPhrase`."), "company.catchPhrase"),
101 | EnumValue("CompanyIndustry", Some("Represents faker expression `company.industry`."), "company.industry"),
102 | EnumValue("CompanyLogo", Some("Represents faker expression `company.logo`."), "company.logo"),
103 | EnumValue("CompanyName", Some("Represents faker expression `company.name`."), "company.name"),
104 | EnumValue("CompanyProfession", Some("Represents faker expression `company.profession`."), "company.profession"),
105 | EnumValue("CompanySuffix", Some("Represents faker expression `company.suffix`."), "company.suffix"),
106 | EnumValue("CompanyUrl", Some("Represents faker expression `company.url`."), "company.url"),
107 | EnumValue("CryptoMd5", Some("Represents faker expression `crypto.md5`."), "crypto.md5"),
108 | EnumValue("CryptoSha1", Some("Represents faker expression `crypto.sha1`."), "crypto.sha1"),
109 | EnumValue("CryptoSha256", Some("Represents faker expression `crypto.sha256`."), "crypto.sha256"),
110 | EnumValue("CryptoSha512", Some("Represents faker expression `crypto.sha512`."), "crypto.sha512"),
111 | EnumValue("CurrencyCode", Some("Represents faker expression `currency.code`."), "currency.code"),
112 | EnumValue("CurrencyName", Some("Represents faker expression `currency.name`."), "currency.name"),
113 | EnumValue("DemographicDemonym", Some("Represents faker expression `demographic.demonym`."), "demographic.demonym"),
114 | EnumValue("DemographicEducationalAttainment", Some("Represents faker expression `demographic.educationalAttainment`."), "demographic.educationalAttainment"),
115 | EnumValue("DemographicMaritalStatus", Some("Represents faker expression `demographic.maritalStatus`."), "demographic.maritalStatus"),
116 | EnumValue("DemographicRace", Some("Represents faker expression `demographic.race`."), "demographic.race"),
117 | EnumValue("DemographicSex", Some("Represents faker expression `demographic.sex`."), "demographic.sex"),
118 | EnumValue("DogAge", Some("Represents faker expression `dog.age`."), "dog.age"),
119 | EnumValue("DogBreed", Some("Represents faker expression `dog.breed`."), "dog.breed"),
120 | EnumValue("DogCoatLength", Some("Represents faker expression `dog.coatLength`."), "dog.coatLength"),
121 | EnumValue("DogGender", Some("Represents faker expression `dog.gender`."), "dog.gender"),
122 | EnumValue("DogMemePhrase", Some("Represents faker expression `dog.memePhrase`."), "dog.memePhrase"),
123 | EnumValue("DogName", Some("Represents faker expression `dog.name`."), "dog.name"),
124 | EnumValue("DogSize", Some("Represents faker expression `dog.size`."), "dog.size"),
125 | EnumValue("DogSound", Some("Represents faker expression `dog.sound`."), "dog.sound"),
126 | EnumValue("DragonBallCharacter", Some("Represents faker expression `dragonBall.character`."), "dragonBall.character"),
127 | EnumValue("EducatorCampus", Some("Represents faker expression `educator.campus`."), "educator.campus"),
128 | EnumValue("EducatorCourse", Some("Represents faker expression `educator.course`."), "educator.course"),
129 | EnumValue("EducatorSecondarySchool", Some("Represents faker expression `educator.secondarySchool`."), "educator.secondarySchool"),
130 | EnumValue("EducatorUniversity", Some("Represents faker expression `educator.university`."), "educator.university"),
131 | EnumValue("EsportsEvent", Some("Represents faker expression `esports.event`."), "esports.event"),
132 | EnumValue("EsportsGame", Some("Represents faker expression `esports.game`."), "esports.game"),
133 | EnumValue("EsportsLeague", Some("Represents faker expression `esports.league`."), "esports.league"),
134 | EnumValue("EsportsPlayer", Some("Represents faker expression `esports.player`."), "esports.player"),
135 | EnumValue("EsportsTeam", Some("Represents faker expression `esports.team`."), "esports.team"),
136 | EnumValue("FileExtension", Some("Represents faker expression `file.extension`."), "file.extension"),
137 | EnumValue("FileFileName", Some("Represents faker expression `file.fileName`."), "file.fileName"),
138 | EnumValue("FileMimeType", Some("Represents faker expression `file.mimeType`."), "file.mimeType"),
139 | EnumValue("FinanceBic", Some("Represents faker expression `finance.bic`."), "finance.bic"),
140 | EnumValue("FinanceCreditCard", Some("Represents faker expression `finance.creditCard`."), "finance.creditCard"),
141 | EnumValue("FinanceIban", Some("Represents faker expression `finance.iban`."), "finance.iban"),
142 | EnumValue("FoodIngredient", Some("Represents faker expression `food.ingredient`."), "food.ingredient"),
143 | EnumValue("FoodMeasurement", Some("Represents faker expression `food.measurement`."), "food.measurement"),
144 | EnumValue("FoodSpice", Some("Represents faker expression `food.spice`."), "food.spice"),
145 | EnumValue("FriendsCharacter", Some("Represents faker expression `friends.character`."), "friends.character"),
146 | EnumValue("FriendsLocation", Some("Represents faker expression `friends.location`."), "friends.location"),
147 | EnumValue("FriendsQuote", Some("Represents faker expression `friends.quote`."), "friends.quote"),
148 | EnumValue("FunnyNameName", Some("Represents faker expression `funnyName.name`."), "funnyName.name"),
149 | EnumValue("GameOfThronesCharacter", Some("Represents faker expression `gameOfThrones.character`."), "gameOfThrones.character"),
150 | EnumValue("GameOfThronesCity", Some("Represents faker expression `gameOfThrones.city`."), "gameOfThrones.city"),
151 | EnumValue("GameOfThronesDragon", Some("Represents faker expression `gameOfThrones.dragon`."), "gameOfThrones.dragon"),
152 | EnumValue("GameOfThronesHouse", Some("Represents faker expression `gameOfThrones.house`."), "gameOfThrones.house"),
153 | EnumValue("GameOfThronesQuote", Some("Represents faker expression `gameOfThrones.quote`."), "gameOfThrones.quote"),
154 | EnumValue("HackerAbbreviation", Some("Represents faker expression `hacker.abbreviation`."), "hacker.abbreviation"),
155 | EnumValue("HackerAdjective", Some("Represents faker expression `hacker.adjective`."), "hacker.adjective"),
156 | EnumValue("HackerIngverb", Some("Represents faker expression `hacker.ingverb`."), "hacker.ingverb"),
157 | EnumValue("HackerNoun", Some("Represents faker expression `hacker.noun`."), "hacker.noun"),
158 | EnumValue("HackerVerb", Some("Represents faker expression `hacker.verb`."), "hacker.verb"),
159 | EnumValue("HarryPotterBook", Some("Represents faker expression `harryPotter.book`."), "harryPotter.book"),
160 | EnumValue("HarryPotterCharacter", Some("Represents faker expression `harryPotter.character`."), "harryPotter.character"),
161 | EnumValue("HarryPotterLocation", Some("Represents faker expression `harryPotter.location`."), "harryPotter.location"),
162 | EnumValue("HarryPotterQuote", Some("Represents faker expression `harryPotter.quote`."), "harryPotter.quote"),
163 | EnumValue("HipsterWord", Some("Represents faker expression `hipster.word`."), "hipster.word"),
164 | EnumValue("HitchhikersGuideToTheGalaxyCharacter", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.character`."), "hitchhikersGuideToTheGalaxy.character"),
165 | EnumValue("HitchhikersGuideToTheGalaxyLocation", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.location`."), "hitchhikersGuideToTheGalaxy.location"),
166 | EnumValue("HitchhikersGuideToTheGalaxyMarvinQuote", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.marvinQuote`."), "hitchhikersGuideToTheGalaxy.marvinQuote"),
167 | EnumValue("HitchhikersGuideToTheGalaxyPlanet", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.planet`."), "hitchhikersGuideToTheGalaxy.planet"),
168 | EnumValue("HitchhikersGuideToTheGalaxyQuote", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.quote`."), "hitchhikersGuideToTheGalaxy.quote"),
169 | EnumValue("HitchhikersGuideToTheGalaxySpecie", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.specie`."), "hitchhikersGuideToTheGalaxy.specie"),
170 | EnumValue("HitchhikersGuideToTheGalaxyStarship", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.starship`."), "hitchhikersGuideToTheGalaxy.starship"),
171 | EnumValue("HobbitCharacter", Some("Represents faker expression `hobbit.character`."), "hobbit.character"),
172 | EnumValue("HobbitLocation", Some("Represents faker expression `hobbit.location`."), "hobbit.location"),
173 | EnumValue("HobbitQuote", Some("Represents faker expression `hobbit.quote`."), "hobbit.quote"),
174 | EnumValue("HobbitThorinsCompany", Some("Represents faker expression `hobbit.thorinsCompany`."), "hobbit.thorinsCompany"),
175 | EnumValue("HowIMetYourMotherCatchPhrase", Some("Represents faker expression `howIMetYourMother.catchPhrase`."), "howIMetYourMother.catchPhrase"),
176 | EnumValue("HowIMetYourMotherCharacter", Some("Represents faker expression `howIMetYourMother.character`."), "howIMetYourMother.character"),
177 | EnumValue("HowIMetYourMotherHighFive", Some("Represents faker expression `howIMetYourMother.highFive`."), "howIMetYourMother.highFive"),
178 | EnumValue("HowIMetYourMotherQuote", Some("Represents faker expression `howIMetYourMother.quote`."), "howIMetYourMother.quote"),
179 | EnumValue("IdNumberInvalid", Some("Represents faker expression `idNumber.invalid`."), "idNumber.invalid"),
180 | EnumValue("IdNumberInvalidSvSeSsn", Some("Represents faker expression `idNumber.invalidSvSeSsn`."), "idNumber.invalidSvSeSsn"),
181 | EnumValue("IdNumberSsnValid", Some("Represents faker expression `idNumber.ssnValid`."), "idNumber.ssnValid"),
182 | EnumValue("IdNumberValid", Some("Represents faker expression `idNumber.valid`."), "idNumber.valid"),
183 | EnumValue("IdNumberValidSvSeSsn", Some("Represents faker expression `idNumber.validSvSeSsn`."), "idNumber.validSvSeSsn"),
184 | EnumValue("InternetAvatar", Some("Represents faker expression `internet.avatar`."), "internet.avatar"),
185 | EnumValue("InternetDomainName", Some("Represents faker expression `internet.domainName`."), "internet.domainName"),
186 | EnumValue("InternetDomainSuffix", Some("Represents faker expression `internet.domainSuffix`."), "internet.domainSuffix"),
187 | EnumValue("InternetDomainWord", Some("Represents faker expression `internet.domainWord`."), "internet.domainWord"),
188 | EnumValue("InternetEmailAddress", Some("Represents faker expression `internet.emailAddress`."), "internet.emailAddress"),
189 | EnumValue("InternetImage", Some("Represents faker expression `internet.image`."), "internet.image"),
190 | EnumValue("InternetIpV4Address", Some("Represents faker expression `internet.ipV4Address`."), "internet.ipV4Address"),
191 | EnumValue("InternetIpV4Cidr", Some("Represents faker expression `internet.ipV4Cidr`."), "internet.ipV4Cidr"),
192 | EnumValue("InternetIpV6Address", Some("Represents faker expression `internet.ipV6Address`."), "internet.ipV6Address"),
193 | EnumValue("InternetIpV6Cidr", Some("Represents faker expression `internet.ipV6Cidr`."), "internet.ipV6Cidr"),
194 | EnumValue("InternetMacAddress", Some("Represents faker expression `internet.macAddress`."), "internet.macAddress"),
195 | EnumValue("InternetPassword", Some("Represents faker expression `internet.password`."), "internet.password"),
196 | EnumValue("InternetPrivateIpV4Address", Some("Represents faker expression `internet.privateIpV4Address`."), "internet.privateIpV4Address"),
197 | EnumValue("InternetPublicIpV4Address", Some("Represents faker expression `internet.publicIpV4Address`."), "internet.publicIpV4Address"),
198 | EnumValue("InternetSafeEmailAddress", Some("Represents faker expression `internet.safeEmailAddress`."), "internet.safeEmailAddress"),
199 | EnumValue("InternetSlug", Some("Represents faker expression `internet.slug`."), "internet.slug"),
200 | EnumValue("InternetUrl", Some("Represents faker expression `internet.url`."), "internet.url"),
201 | EnumValue("InternetUuid", Some("Represents faker expression `internet.uuid`."), "internet.uuid"),
202 | EnumValue("JobField", Some("Represents faker expression `job.field`."), "job.field"),
203 | EnumValue("JobKeySkills", Some("Represents faker expression `job.keySkills`."), "job.keySkills"),
204 | EnumValue("JobPosition", Some("Represents faker expression `job.position`."), "job.position"),
205 | EnumValue("JobSeniority", Some("Represents faker expression `job.seniority`."), "job.seniority"),
206 | EnumValue("JobTitle", Some("Represents faker expression `job.title`."), "job.title"),
207 | EnumValue("LeagueOfLegendsChampion", Some("Represents faker expression `leagueOfLegends.champion`."), "leagueOfLegends.champion"),
208 | EnumValue("LeagueOfLegendsLocation", Some("Represents faker expression `leagueOfLegends.location`."), "leagueOfLegends.location"),
209 | EnumValue("LeagueOfLegendsMasteries", Some("Represents faker expression `leagueOfLegends.masteries`."), "leagueOfLegends.masteries"),
210 | EnumValue("LeagueOfLegendsQuote", Some("Represents faker expression `leagueOfLegends.quote`."), "leagueOfLegends.quote"),
211 | EnumValue("LeagueOfLegendsRank", Some("Represents faker expression `leagueOfLegends.rank`."), "leagueOfLegends.rank"),
212 | EnumValue("LeagueOfLegendsSummonerSpell", Some("Represents faker expression `leagueOfLegends.summonerSpell`."), "leagueOfLegends.summonerSpell"),
213 | EnumValue("LebowskiActor", Some("Represents faker expression `lebowski.actor`."), "lebowski.actor"),
214 | EnumValue("LebowskiCharacter", Some("Represents faker expression `lebowski.character`."), "lebowski.character"),
215 | EnumValue("LebowskiQuote", Some("Represents faker expression `lebowski.quote`."), "lebowski.quote"),
216 | EnumValue("LordOfTheRingsCharacter", Some("Represents faker expression `lordOfTheRings.character`."), "lordOfTheRings.character"),
217 | EnumValue("LordOfTheRingsLocation", Some("Represents faker expression `lordOfTheRings.location`."), "lordOfTheRings.location"),
218 | EnumValue("LoremCharacter", Some("Represents faker expression `lorem.character`."), "lorem.character"),
219 | EnumValue("LoremCharacters", Some("Represents faker expression `lorem.characters`."), "lorem.characters"),
220 | EnumValue("LoremParagraph", Some("Represents faker expression `lorem.paragraph`."), "lorem.paragraph"),
221 | EnumValue("LoremSentence", Some("Represents faker expression `lorem.sentence`."), "lorem.sentence"),
222 | EnumValue("LoremWord", Some("Represents faker expression `lorem.word`."), "lorem.word"),
223 | EnumValue("MatzQuote", Some("Represents faker expression `matz.quote`."), "matz.quote"),
224 | EnumValue("MusicChord", Some("Represents faker expression `music.chord`."), "music.chord"),
225 | EnumValue("MusicInstrument", Some("Represents faker expression `music.instrument`."), "music.instrument"),
226 | EnumValue("MusicKey", Some("Represents faker expression `music.key`."), "music.key"),
227 | EnumValue("NameFirstName", Some("Represents faker expression `name.firstName`."), "name.firstName"),
228 | EnumValue("NameFullName", Some("Represents faker expression `name.fullName`."), "name.fullName"),
229 | EnumValue("NameLastName", Some("Represents faker expression `name.lastName`."), "name.lastName"),
230 | EnumValue("NameName", Some("Represents faker expression `name.name`."), "name.name"),
231 | EnumValue("NameNameWithMiddle", Some("Represents faker expression `name.nameWithMiddle`."), "name.nameWithMiddle"),
232 | EnumValue("NamePrefix", Some("Represents faker expression `name.prefix`."), "name.prefix"),
233 | EnumValue("NameSuffix", Some("Represents faker expression `name.suffix`."), "name.suffix"),
234 | EnumValue("NameTitle", Some("Represents faker expression `name.title`."), "name.title"),
235 | EnumValue("NameUsername", Some("Represents faker expression `name.username`."), "name.username"),
236 | EnumValue("NumberDigit", Some("Represents faker expression `number.digit`."), "number.digit"),
237 | EnumValue("NumberRandomDigit", Some("Represents faker expression `number.randomDigit`."), "number.randomDigit"),
238 | EnumValue("NumberRandomDigitNotZero", Some("Represents faker expression `number.randomDigitNotZero`."), "number.randomDigitNotZero"),
239 | EnumValue("NumberRandomNumber", Some("Represents faker expression `number.randomNumber`."), "number.randomNumber"),
240 | EnumValue("OverwatchHero", Some("Represents faker expression `overwatch.hero`."), "overwatch.hero"),
241 | EnumValue("OverwatchLocation", Some("Represents faker expression `overwatch.location`."), "overwatch.location"),
242 | EnumValue("OverwatchQuote", Some("Represents faker expression `overwatch.quote`."), "overwatch.quote"),
243 | EnumValue("PhoneNumberCellPhone", Some("Represents faker expression `phoneNumber.cellPhone`."), "phoneNumber.cellPhone"),
244 | EnumValue("PhoneNumberPhoneNumber", Some("Represents faker expression `phoneNumber.phoneNumber`."), "phoneNumber.phoneNumber"),
245 | EnumValue("PokemonLocation", Some("Represents faker expression `pokemon.location`."), "pokemon.location"),
246 | EnumValue("PokemonName", Some("Represents faker expression `pokemon.name`."), "pokemon.name"),
247 | EnumValue("RandomNextDouble", Some("Represents faker expression `random.nextDouble`."), "random.nextDouble"),
248 | EnumValue("RandomNextLong", Some("Represents faker expression `random.nextLong`."), "random.nextLong"),
249 | EnumValue("RickAndMortyCharacter", Some("Represents faker expression `rickAndMorty.character`."), "rickAndMorty.character"),
250 | EnumValue("RickAndMortyLocation", Some("Represents faker expression `rickAndMorty.location`."), "rickAndMorty.location"),
251 | EnumValue("RickAndMortyQuote", Some("Represents faker expression `rickAndMorty.quote`."), "rickAndMorty.quote"),
252 | EnumValue("RobinQuote", Some("Represents faker expression `robin.quote`."), "robin.quote"),
253 | EnumValue("RockBandName", Some("Represents faker expression `rockBand.name`."), "rockBand.name"),
254 | EnumValue("ShakespeareAsYouLikeItQuote", Some("Represents faker expression `shakespeare.asYouLikeItQuote`."), "shakespeare.asYouLikeItQuote"),
255 | EnumValue("ShakespeareHamletQuote", Some("Represents faker expression `shakespeare.hamletQuote`."), "shakespeare.hamletQuote"),
256 | EnumValue("ShakespeareKingRichardIIIQuote", Some("Represents faker expression `shakespeare.kingRichardIIIQuote`."), "shakespeare.kingRichardIIIQuote"),
257 | EnumValue("ShakespeareRomeoAndJulietQuote", Some("Represents faker expression `shakespeare.romeoAndJulietQuote`."), "shakespeare.romeoAndJulietQuote"),
258 | EnumValue("SlackEmojiActivity", Some("Represents faker expression `slackEmoji.activity`."), "slackEmoji.activity"),
259 | EnumValue("SlackEmojiCelebration", Some("Represents faker expression `slackEmoji.celebration`."), "slackEmoji.celebration"),
260 | EnumValue("SlackEmojiCustom", Some("Represents faker expression `slackEmoji.custom`."), "slackEmoji.custom"),
261 | EnumValue("SlackEmojiEmoji", Some("Represents faker expression `slackEmoji.emoji`."), "slackEmoji.emoji"),
262 | EnumValue("SlackEmojiFoodAndDrink", Some("Represents faker expression `slackEmoji.foodAndDrink`."), "slackEmoji.foodAndDrink"),
263 | EnumValue("SlackEmojiNature", Some("Represents faker expression `slackEmoji.nature`."), "slackEmoji.nature"),
264 | EnumValue("SlackEmojiObjectsAndSymbols", Some("Represents faker expression `slackEmoji.objectsAndSymbols`."), "slackEmoji.objectsAndSymbols"),
265 | EnumValue("SlackEmojiPeople", Some("Represents faker expression `slackEmoji.people`."), "slackEmoji.people"),
266 | EnumValue("SlackEmojiTravelAndPlaces", Some("Represents faker expression `slackEmoji.travelAndPlaces`."), "slackEmoji.travelAndPlaces"),
267 | EnumValue("SpaceAgency", Some("Represents faker expression `space.agency`."), "space.agency"),
268 | EnumValue("SpaceAgencyAbbreviation", Some("Represents faker expression `space.agencyAbbreviation`."), "space.agencyAbbreviation"),
269 | EnumValue("SpaceCompany", Some("Represents faker expression `space.company`."), "space.company"),
270 | EnumValue("SpaceConstellation", Some("Represents faker expression `space.constellation`."), "space.constellation"),
271 | EnumValue("SpaceDistanceMeasurement", Some("Represents faker expression `space.distanceMeasurement`."), "space.distanceMeasurement"),
272 | EnumValue("SpaceGalaxy", Some("Represents faker expression `space.galaxy`."), "space.galaxy"),
273 | EnumValue("SpaceMeteorite", Some("Represents faker expression `space.meteorite`."), "space.meteorite"),
274 | EnumValue("SpaceMoon", Some("Represents faker expression `space.moon`."), "space.moon"),
275 | EnumValue("SpaceNasaSpaceCraft", Some("Represents faker expression `space.nasaSpaceCraft`."), "space.nasaSpaceCraft"),
276 | EnumValue("SpaceNebula", Some("Represents faker expression `space.nebula`."), "space.nebula"),
277 | EnumValue("SpacePlanet", Some("Represents faker expression `space.planet`."), "space.planet"),
278 | EnumValue("SpaceStar", Some("Represents faker expression `space.star`."), "space.star"),
279 | EnumValue("SpaceStarCluster", Some("Represents faker expression `space.starCluster`."), "space.starCluster"),
280 | EnumValue("StarTrekCharacter", Some("Represents faker expression `starTrek.character`."), "starTrek.character"),
281 | EnumValue("StarTrekLocation", Some("Represents faker expression `starTrek.location`."), "starTrek.location"),
282 | EnumValue("StarTrekSpecie", Some("Represents faker expression `starTrek.specie`."), "starTrek.specie"),
283 | EnumValue("StarTrekVillain", Some("Represents faker expression `starTrek.villain`."), "starTrek.villain"),
284 | EnumValue("StockNsdqSymbol", Some("Represents faker expression `stock.nsdqSymbol`."), "stock.nsdqSymbol"),
285 | EnumValue("StockNyseSymbol", Some("Represents faker expression `stock.nyseSymbol`."), "stock.nyseSymbol"),
286 | EnumValue("SuperheroDescriptor", Some("Represents faker expression `superhero.descriptor`."), "superhero.descriptor"),
287 | EnumValue("SuperheroName", Some("Represents faker expression `superhero.name`."), "superhero.name"),
288 | EnumValue("SuperheroPower", Some("Represents faker expression `superhero.power`."), "superhero.power"),
289 | EnumValue("SuperheroPrefix", Some("Represents faker expression `superhero.prefix`."), "superhero.prefix"),
290 | EnumValue("SuperheroSuffix", Some("Represents faker expression `superhero.suffix`."), "superhero.suffix"),
291 | EnumValue("TeamCreature", Some("Represents faker expression `team.creature`."), "team.creature"),
292 | EnumValue("TeamName", Some("Represents faker expression `team.name`."), "team.name"),
293 | EnumValue("TeamSport", Some("Represents faker expression `team.sport`."), "team.sport"),
294 | EnumValue("TeamState", Some("Represents faker expression `team.state`."), "team.state"),
295 | EnumValue("TwinPeaksCharacter", Some("Represents faker expression `twinPeaks.character`."), "twinPeaks.character"),
296 | EnumValue("TwinPeaksLocation", Some("Represents faker expression `twinPeaks.location`."), "twinPeaks.location"),
297 | EnumValue("TwinPeaksQuote", Some("Represents faker expression `twinPeaks.quote`."), "twinPeaks.quote"),
298 | EnumValue("UniversityName", Some("Represents faker expression `university.name`."), "university.name"),
299 | EnumValue("UniversityPrefix", Some("Represents faker expression `university.prefix`."), "university.prefix"),
300 | EnumValue("UniversitySuffix", Some("Represents faker expression `university.suffix`."), "university.suffix"),
301 | EnumValue("WeatherDescription", Some("Represents faker expression `weather.description`."), "weather.description"),
302 | EnumValue("WeatherTemperatureCelsius", Some("Represents faker expression `weather.temperatureCelsius`."), "weather.temperatureCelsius"),
303 | EnumValue("WeatherTemperatureFahrenheit", Some("Represents faker expression `weather.temperatureFahrenheit`."), "weather.temperatureFahrenheit"),
304 | EnumValue("WitcherCharacter", Some("Represents faker expression `witcher.character`."), "witcher.character"),
305 | EnumValue("WitcherLocation", Some("Represents faker expression `witcher.location`."), "witcher.location"),
306 | EnumValue("WitcherMonster", Some("Represents faker expression `witcher.monster`."), "witcher.monster"),
307 | EnumValue("WitcherQuote", Some("Represents faker expression `witcher.quote`."), "witcher.quote"),
308 | EnumValue("WitcherSchool", Some("Represents faker expression `witcher.school`."), "witcher.school"),
309 | EnumValue("WitcherWitcher", Some("Represents faker expression `witcher.witcher`."), "witcher.witcher"),
310 | EnumValue("YodaQuote", Some("Represents faker expression `yoda.quote`."), "yoda.quote"),
311 | EnumValue("ZeldaCharacter", Some("Represents faker expression `zelda.character`."), "zelda.character"),
312 | EnumValue("ZeldaGame", Some("Represents faker expression `zelda.game`."), "zelda.game")
313 | ))
314 |
315 | object Args {
316 | val Expr = Argument("expr", OptionInputType(StringType))
317 | val Type = Argument("type", OptionInputType(FakeType))
318 | val Locale = Argument("locale", OptionInputType(StringType))
319 | val Seed = Argument("seed", OptionInputType(IntType))
320 | val MinElems = Argument("min", OptionInputType(IntType))
321 | val MaxElems = Argument("max", OptionInputType(IntType))
322 | val Past = Argument("past", OptionInputType(BooleanType))
323 | val Future = Argument("future", OptionInputType(BooleanType))
324 | }
325 |
326 | object Dirs {
327 | val Fake = Directive("fake",
328 | arguments = Args.Expr :: Args.Type :: Args.MinElems :: Args.MaxElems :: Args.Past :: Args.Future :: Nil,
329 | locations = Set(DirectiveLocation.FieldDefinition))
330 |
331 | val FakeConfig = Directive("fakeConfig",
332 | arguments = Args.Locale :: Args.Seed :: Nil,
333 | locations = Set(DirectiveLocation.Schema))
334 | }
335 | }
336 |
337 | case class FakeValue(expr: Option[String], min: Option[Int], max: Option[Int], past: Option[Boolean], future: Option[Boolean], rnd: Random, faker: Faker) {
338 | private lazy val withoutExpr = copy(expr = None, min = None, max = None)
339 |
340 | def coerce(ctx: Context[GatewayContext, _]): Action[GatewayContext, Any] = {
341 | def coerceType(tpe: OutputType[_]): Any = tpe match {
342 | case OptionType(ofType) ⇒
343 | if (rnd.nextBoolean()) None
344 | else coerceType(ofType)
345 |
346 | case ListType(ofType) ⇒
347 | val size = min.getOrElse(0) + rnd.nextInt(max.fold(20)(max ⇒ max - min.getOrElse(0) + 1))
348 |
349 | (1 to size).toVector.map(_ ⇒ coerceType(ofType))
350 |
351 | case s: ScalarAlias[_, _] ⇒
352 | coerceType(s.aliasFor)
353 |
354 | case s: ScalarType[_] if s.name == StringType.name ⇒
355 | expr match {
356 | case Some(e) ⇒ resolveExpr(e)
357 | case None ⇒ faker.lorem().paragraph()
358 | }
359 |
360 | case s: ScalarType[_] if s.name == IntType.name ⇒
361 | expr match {
362 | case Some(e) ⇒ resolveExpr(e).toInt
363 | case None ⇒ rnd.nextInt()
364 | }
365 |
366 | case s: ScalarType[_] if s.name == LongType.name ⇒
367 | expr match {
368 | case Some(e) ⇒ resolveExpr(e).toLong
369 | case None ⇒ rnd.nextLong()
370 | }
371 |
372 | case s: ScalarType[_] if s.name == BigIntType.name ⇒
373 | expr match {
374 | case Some(e) ⇒ BigInt(resolveExpr(e))
375 | case None ⇒ BigInt(rnd.nextLong())
376 | }
377 |
378 | case s: ScalarType[_] if s.name == BigDecimalType.name ⇒
379 | expr match {
380 | case Some(e) ⇒ BigDecimal(resolveExpr(e))
381 | case None ⇒ BigDecimal(rnd.nextDouble())
382 | }
383 |
384 | case s: ScalarType[_] if s.name == IDType.name ⇒
385 | expr match {
386 | case Some(e) ⇒ resolveExpr(e)
387 | case None ⇒ "" + rnd.nextLong()
388 | }
389 |
390 | case s: ScalarType[_] if s.name == FloatType.name ⇒
391 | expr match {
392 | case Some(e) ⇒ resolveExpr(e).toDouble
393 | case None ⇒ rnd.nextDouble()
394 | }
395 |
396 | case s: ScalarType[_] if s.name == BooleanType.name ⇒
397 | expr match {
398 | case Some(e) ⇒ resolveExpr(e).toBoolean
399 | case None ⇒ rnd.nextBoolean()
400 | }
401 |
402 | case s: ScalarType[_] if s.name == CustomScalars.DateTimeType.name ⇒
403 | resolveDate
404 |
405 | case e: EnumType[_] ⇒
406 | e.values(rnd.nextInt(e.values.size)).value
407 |
408 | case s: CompositeType[_] ⇒
409 | withoutExpr
410 |
411 | case t ⇒
412 | throw new IllegalStateException(s"Can't fake type ${SchemaRenderer.renderTypeName(t)} just yet.")
413 | }
414 |
415 | coerceType(ctx.field.fieldType)
416 | }
417 |
418 | def resolveExpr(e: String) = {
419 | if (FakeValue.ExpressionPattern.matcher(e).find())
420 | faker.expression(e)
421 | else
422 | faker.expression("#{" + e + "}")
423 | }
424 |
425 | def resolveDate = {
426 | val isPast =
427 | (past, future) match {
428 | case (Some(p), Some(f)) ⇒ p
429 | case (Some(p), None) ⇒ p
430 | case (None, Some(f)) ⇒ !f
431 | case (None, None) ⇒ rnd.nextBoolean()
432 | }
433 |
434 | val date =
435 | if (isPast)
436 | faker.date().past(700, TimeUnit.DAYS)
437 | else
438 | faker.date().future(700, TimeUnit.DAYS)
439 |
440 | ZonedDateTime.ofInstant(date.toInstant, ZoneId.of("UTC"))
441 | }
442 | }
443 |
444 | object FakeValue {
445 | val ExpressionPattern = Pattern.compile("#\\{([a-z0-9A-Z_.]+)\\s?(?:'([^']+)')?(?:,'([^']+)')*\\}")
446 | }
447 |
448 |
449 |
450 |
451 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/materializer/directive/GraphQLDirectiveProvider.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema.materializer.directive
2 |
3 | import io.circe.Json
4 | import sangria.gateway.schema.materializer.{GatewayContext, GraphQLIncludedSchema}
5 | import sangria.schema._
6 | import sangria.macros.derive._
7 | import sangria.ast
8 | import sangria.ast.AstVisitor
9 | import sangria.gateway.schema.CustomScalars
10 | import sangria.gateway.schema.materializer.directive.HttpDirectiveProvider.Args.{HeaderType, QueryParamType}
11 | import sangria.marshalling.queryAst.queryAstInputUnmarshaller
12 | import sangria.validation.TypeInfo
13 | import sangria.visitor.VisitorCommand
14 | import sangria.introspection.TypeNameMetaField
15 | import sangria.marshalling.circe._
16 | import io.circe.generic.auto._
17 |
18 | import scala.collection.mutable
19 | import scala.concurrent.ExecutionContext
20 |
21 | class GraphQLDirectiveProvider(implicit ec: ExecutionContext) extends DirectiveProvider {
22 | import GraphQLDirectiveProvider._
23 |
24 | private val TypeNameField = ast.Field(None, TypeNameMetaField.name, Vector.empty, Vector.empty, Vector.empty)
25 |
26 | def resolvers(ctx: GatewayContext) = Seq(
27 | AdditionalDirectives(Seq(Dirs.IncludeSchema)),
28 | AdditionalTypes(ctx.allTypes.toList),
29 |
30 | ExistingFieldResolver {
31 | case (o: GraphQLIncludedSchema, _, f) if ctx.graphqlIncludes.exists(_.include.name == o.include.name) && f.astDirectives.exists(_.name == "delegate") ⇒
32 | val schema = ctx.graphqlIncludes.find(_.include.name == o.include.name).get
33 |
34 | c ⇒ {
35 | val (updatedFields, fragments, vars) = prepareOriginFields(o, c.query, c.schema, c.astFields, c.parentType)
36 | val varDefs = vars.toVector.flatMap(v ⇒ c.query.operation(c.ctx.operationName).get.variables.find(_.name == v))
37 | val queryOp = ast.OperationDefinition(ast.OperationType.Query,
38 | name = Some("DelegatedQuery"),
39 | variables = varDefs,
40 | selections = updatedFields)
41 | val query = ast.Document(queryOp +: fragments)
42 |
43 | ctx.request(c, schema, query, c.ctx.queryVars, c.astFields.head.outputName).map(value ⇒
44 | ResolverBasedAstSchemaBuilder.extractValue[Json](c.field.fieldType, Some(value)))
45 | }
46 | },
47 |
48 | ExistingScalarResolver {
49 | case ctx if
50 | ctx.origin.isInstanceOf[GraphQLIncludedSchema] ||
51 | ctx.existing.name != CustomScalars.DateTimeType.name ⇒
52 |
53 | ctx.existing.copy(
54 | coerceUserInput = Right(_),
55 | coerceOutput = (v, _) ⇒ v,
56 | coerceInput = v ⇒ Right(queryAstInputUnmarshaller.getScalaScalarValue(v)))
57 | },
58 |
59 | // Current behaviour: for all conflicting types, only one would be picked -
60 | // we assume that their structure and semantics are the same.
61 | // Potential future improvement: provide more flexible, directive-based approach to resolve the name conflicts
62 | ConflictResolver((origin, conflictingTypes) ⇒
63 | conflictingTypes
64 | .collectFirst{case opt: BuiltMaterializedTypeInst ⇒ opt}
65 | .getOrElse(conflictingTypes.head)),
66 |
67 | DirectiveFieldProvider(Dirs.IncludeFields, _.withArgs(Args.Schema, Args.Type, Args.Fields, Args.Excludes)((schema, tpe, fields, excludes) ⇒
68 | ctx.findFields(schema, tpe, fields, excludes))))
69 |
70 | private def prepareOriginFields(origin: GraphQLIncludedSchema, query: ast.Document, schema: Schema[GatewayContext, _], queryFields: Vector[ast.Field], parentType: OutputType[_]) =
71 | queryFields.foldLeft((Vector.empty[ast.Field], Vector.empty[ast.FragmentDefinition], Set.empty[String])) {
72 | case ((fieldAcc, fragAcc, varAcc), f) ⇒
73 | val (ufield, ufrag, uvars) = prepareAst(f, origin, query, schema, Some(parentType), fragAcc.map(_.name).toSet)
74 |
75 | (fieldAcc :+ ufield, fragAcc ++ ufrag, varAcc ++ uvars)
76 | }
77 |
78 | private def prepareAst[N <: ast.AstNode](
79 | node: N,
80 | origin: GraphQLIncludedSchema,
81 | query: ast.Document,
82 | schema: Schema[GatewayContext, _],
83 | parentType: Option[OutputType[_]],
84 | seenFragments: Set[String]
85 | ): (N, Vector[ast.FragmentDefinition], Set[String]) = {
86 | val fragments = mutable.HashSet[String]()
87 | val vars = mutable.HashSet[String]()
88 |
89 | val updatedNode =
90 | AstVisitor.visitAstWithTypeInfo(schema, node) { typeInfo ⇒
91 | parentType foreach typeInfo.forcePushType
92 |
93 | AstVisitor(
94 | onEnter = {
95 | case field: ast.Field ⇒
96 | typeInfo.previousParentType match {
97 | case Some(parent) ⇒
98 | origin.schema.outputTypes.get(parent.name) match {
99 | case Some(originType: ObjectLikeType[_, _]) if !originType.fieldsByName.contains(field.name) ⇒
100 | // Field does not belong to an origin - no delegation
101 | VisitorCommand.Delete
102 |
103 | case _ ⇒ VisitorCommand.Continue
104 | }
105 |
106 | case None ⇒ VisitorCommand.Continue
107 | }
108 |
109 | case v: ast.VariableValue ⇒
110 | vars += v.name
111 | VisitorCommand.Continue
112 |
113 | case f: ast.FragmentSpread ⇒
114 | fragments += f.name
115 | VisitorCommand.Continue
116 | },
117 | onLeave = {
118 | case field: ast.Field ⇒
119 | typeInfo.parentType match {
120 | case Some(parent) ⇒
121 | origin.schema.outputTypes.get(parent.name) match {
122 | case Some(originType: ObjectLikeType[_, _]) if originType.fieldsByName contains field.name ⇒
123 | injectTypeName(origin, field, parent, typeInfo)
124 |
125 | case _ ⇒ VisitorCommand.Continue
126 | }
127 |
128 | case None ⇒ VisitorCommand.Continue
129 | }
130 | })
131 | }
132 |
133 | val fragmentsToUpdate = fragments.toVector.filterNot(name ⇒ seenFragments.contains(name))
134 | val allSeenFragments = seenFragments ++ fragmentsToUpdate
135 | val updatedFragments = fragmentsToUpdate.map(fn ⇒ prepareAst(query.fragments(fn), origin, query, schema, None, allSeenFragments))
136 | val allVars = vars.toSet ++ updatedFragments.flatMap(_._3)
137 | val finalFragments = updatedFragments.flatMap(u ⇒ u._1 +: u._2)
138 |
139 | (updatedNode, finalFragments, allVars)
140 | }
141 |
142 | def injectTypeName(origin: GraphQLIncludedSchema, field: ast.Field, parent: CompositeType[_], typeInfo: TypeInfo): VisitorCommand = {
143 | val fieldType =
144 | origin.schema.outputTypes.get(parent.name) match {
145 | case Some(originType: ObjectLikeType[_, _]) ⇒ originType.fieldsByName.get(field.name).map(_.head.fieldType)
146 | case _ ⇒ None
147 | }
148 |
149 | fieldType.map(_.namedType).fold(VisitorCommand.Continue: VisitorCommand) {
150 | case _: AbstractType ⇒
151 | // Field has abstract result type, so we need to inject `__typename` to make proper instance checks at the Gateway level
152 | VisitorCommand.Transform(field.copy(selections = TypeNameField +: field.selections))
153 | case _ ⇒ VisitorCommand.Continue
154 | }
155 | }
156 | }
157 |
158 | object GraphQLDirectiveProvider {
159 | object Args {
160 | val OAuthClientCredentialsType = deriveInputObjectType[OAuthClientCredentials]()
161 |
162 | val Name = Argument("name", StringType)
163 | val Url = Argument("url", StringType)
164 | val Headers = Argument("headers", OptionInputType(ListInputType(HeaderType)))
165 | val DelegateHeaders = Argument("delegateHeaders", OptionInputType(ListInputType(StringType)),
166 | "Delegate headers from original gateway request to the downstream services.")
167 | val QueryParams = Argument("query", OptionInputType(ListInputType(QueryParamType)))
168 | val OAuth = Argument("oauth", OptionInputType(OAuthClientCredentialsType))
169 |
170 | val Schema = Argument("schema", StringType, description = "the name of the schema included with @includeGraphQL")
171 | val Type = Argument("type", StringType, description = "name of the type from the external schema")
172 | val Fields = Argument("fields", OptionInputType(ListInputType(StringType)))
173 | val Excludes = Argument("excludes", OptionInputType(ListInputType(StringType)))
174 | }
175 |
176 | object Dirs {
177 | val IncludeSchema = Directive("includeSchema",
178 | repeatable = true,
179 | arguments = Args.Name :: Args.Url :: Args.Headers :: Args.DelegateHeaders :: Args.QueryParams :: Args.OAuth :: Nil,
180 | locations = Set(DirectiveLocation.Schema))
181 |
182 | val IncludeFields = Directive("includeFields",
183 | repeatable = true,
184 | arguments = Args.Schema :: Args.Type :: Args.Fields :: Args.Excludes :: Nil,
185 | locations = Set(DirectiveLocation.Object))
186 | }
187 | }
188 |
189 | case class OAuthClientCredentials(url: String, clientId: String, clientSecret: String, scopes: Seq[String])
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/schema/materializer/directive/HttpDirectiveProvider.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.schema.materializer.directive
2 |
3 | import language.existentials
4 |
5 | import io.circe.Json
6 | import sangria.gateway.http.client.HttpClient
7 | import sangria.gateway.json.CirceJsonPath
8 | import sangria.gateway.schema.materializer.GatewayContext
9 | import sangria.gateway.schema.materializer.GatewayContext.{convertArgs, namedType}
10 | import sangria.schema.ResolverBasedAstSchemaBuilder.extractValue
11 | import sangria.schema._
12 | import sangria.marshalling.circe._
13 |
14 | import scala.concurrent.{ExecutionContext, Future}
15 |
16 | class HttpDirectiveProvider(client: HttpClient)(implicit ec: ExecutionContext) extends DirectiveProvider {
17 | import HttpDirectiveProvider._
18 |
19 | def resolvers(ctx: GatewayContext) = Seq(
20 | DirectiveResolver(Dirs.HttpGet,
21 | complexity = Some(_ ⇒ (_, _, _) ⇒ 1000.0),
22 | resolve = c ⇒ c.withArgs(Args.Url, Args.Headers, Args.DelegateHeaders, Args.QueryParams, Args.ForAll) { (rawUrl, rawHeaders, delegateHeaders, rawQueryParams, forAll) ⇒
23 | val args = Some(convertArgs(c.ctx.args, c.ctx.astFields.head))
24 |
25 | def makeRequest(tpe: OutputType[_], c: Context[GatewayContext, _], args: Option[Json], elem: Json = Json.Null) = {
26 | val somec = Some(c)
27 | val url = GatewayContext.fillPlaceholders(somec, rawUrl, args, elem)
28 | val headers = extractMap(somec, rawHeaders, args, elem)
29 | val query = extractMap(somec, rawQueryParams, args, elem)
30 | val extraHeaders = extractDelegatedHeaders(c, delegateHeaders)
31 |
32 | client.request(HttpClient.Method.Get, url, query, headers ++ extraHeaders).flatMap(GatewayContext.parseJson)
33 | }
34 |
35 | forAll match {
36 | case Some(elem) ⇒
37 | CirceJsonPath.query(c.ctx.value.asInstanceOf[Json], elem) match {
38 | case json: Json if json.isArray ⇒
39 | Future.sequence(json.asArray.get.map(e ⇒ makeRequest(namedType(c.ctx.field.fieldType), c.ctx, args, e))) map { v ⇒
40 | extractValue(c.ctx.field.fieldType, Some(Json.arr(v.asInstanceOf[Seq[Json]]: _*)))
41 | }
42 | case e ⇒
43 | makeRequest(c.ctx.field.fieldType, c.ctx, args, e)
44 | }
45 | case None ⇒
46 | makeRequest(c.ctx.field.fieldType, c.ctx, args)
47 | }
48 | }))
49 | }
50 |
51 | object HttpDirectiveProvider {
52 | def extractMap(ctx: Option[Context[GatewayContext, _]], in: Option[Seq[InputObjectType.DefaultInput]], args: Option[Json] = None, elem: Json = Json.Null) = {
53 | in.map(_.map { h ⇒
54 | val name = h("name").asInstanceOf[String]
55 | val value = h("value").asInstanceOf[String]
56 |
57 | name → GatewayContext.fillPlaceholders(ctx, value, args, elem)
58 | }).getOrElse(Nil)
59 | }
60 |
61 | def extractDelegatedHeaders(ctx: Context[GatewayContext, _], delegateHeaders: Option[Seq[String]]) =
62 | delegateHeaders match {
63 | case Some(hs) ⇒ ctx.ctx.originalHeaders.filter(orig ⇒ hs.contains(orig._1))
64 | case None ⇒ Seq.empty
65 | }
66 |
67 | object Args {
68 | val HeaderType = InputObjectType("Header", fields = List(
69 | InputField("name", StringType),
70 | InputField("value", StringType)))
71 |
72 | val QueryParamType = InputObjectType("QueryParam", fields = List(
73 | InputField("name", StringType),
74 | InputField("value", StringType)))
75 |
76 | val Url = Argument("url", StringType)
77 | val Headers = Argument("headers", OptionInputType(ListInputType(HeaderType)))
78 | val DelegateHeaders = Argument("delegateHeaders", OptionInputType(ListInputType(StringType)),
79 | "Delegate headers from original gateway request to the downstream services.")
80 | val QueryParams = Argument("query", OptionInputType(ListInputType(QueryParamType)))
81 | val ForAll = Argument("forAll", OptionInputType(StringType))
82 | }
83 |
84 | object Dirs {
85 | val HttpGet = Directive("httpGet",
86 | arguments = Args.Url :: Args.Headers :: Args.DelegateHeaders :: Args.QueryParams :: Args.ForAll :: Nil,
87 | locations = Set(DirectiveLocation.FieldDefinition))
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/util/LogColors.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.util
2 |
3 | import ch.qos.logback.classic.Level
4 | import ch.qos.logback.classic.spi.ILoggingEvent
5 | import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase
6 | import ch.qos.logback.core.pattern.color.ANSIConstants._
7 |
8 | class LogColors extends ForegroundCompositeConverterBase[ILoggingEvent] {
9 | override def getForegroundColorCode(event: ILoggingEvent): String =
10 | event.getLevel.toInt match {
11 | case Level.ERROR_INT ⇒
12 | BOLD + RED_FG
13 | case Level.WARN_INT ⇒
14 | YELLOW_FG
15 | case Level.INFO_INT ⇒
16 | GREEN_FG
17 | case Level.DEBUG_INT ⇒
18 | CYAN_FG
19 | case _ ⇒
20 | DEFAULT_FG
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/scala/sangria/gateway/util/Logging.scala:
--------------------------------------------------------------------------------
1 | package sangria.gateway.util
2 |
3 | import com.typesafe.scalalogging.Logger
4 |
5 | trait Logging {
6 | val logger = Logger(this.getClass)
7 | }
8 |
--------------------------------------------------------------------------------
/testSchema.graphql:
--------------------------------------------------------------------------------
1 | # It's an example schema:
2 | #
3 | # * Proxies some endpoints from the http://swapi.co
4 | # * Merges and delegates execution for 2 GraphQL external GraphQL API
5 |
6 | extend schema
7 | @includeSchema(name: "starWars", url: "http://try.sangria-graphql.org/graphql")
8 | @includeSchema(name: "db", url: "https://developer.deutschebahn.com/free1bahnql/graphql")
9 |
10 | """
11 | The root query type.
12 | """
13 | type Query
14 | @includeFields(schema: "starWars", type: "Query")
15 | @includeFields(schema: "db", type: "Query") {
16 |
17 | "A character from the StarWars (REST API)"
18 | person(id: Int!): Person
19 | @httpGet(url: "https://swapi.co/api/people/${arg.id}/")
20 |
21 | "A list of characters from the StarWars (REST API)"
22 | people(page: Int): [Person]
23 | @httpGet(
24 | url: "http://swapi.co/api/people"
25 | query: {name: "page", value: "${arg.page}"})
26 | @value(name: "results")
27 |
28 | fruits: [Fruit!]! @fake(min: 5)
29 | }
30 |
31 | type Film {
32 | title: String
33 | }
34 |
35 | type Person {
36 | name: String
37 | address: String! @fake(expr: "#{address.fullAddress}")
38 | birthday: DateTime! @fake(past: true)
39 | size: Int @value(name: "height")
40 | homeworld: Planet @httpGet(url: "${value.homeworld}")
41 | films: [Film] @httpGet(forAll: "$.films", url: "${elem.$}")
42 | }
43 |
44 | "A planet from the StarWars universe"
45 | type Planet {
46 | name: String
47 | }
48 |
49 | type Fruit {
50 | weight: Int!
51 | description: String! @fake(expr: "#{hipster.word}")
52 | color: Color!
53 | }
54 |
55 | enum Color {
56 | RED
57 | BLUE
58 | MAGENTA
59 | }
--------------------------------------------------------------------------------