├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── project.clj ├── resources ├── bgg-schema.edn ├── graphiql │ ├── graphiql.css │ ├── graphiql.js │ └── index.html └── logback.xml └── src └── bgg_graphql_proxy ├── cache.clj ├── client.clj ├── main.clj ├── schema.clj └── server.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .idea/ 13 | *.iml 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## vNEXT - UNRELEASED 2 | 3 | Initial version. 4 | -------------------------------------------------------------------------------- /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 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bgg-graphql-proxy 2 | 3 | A GraphQL frontend to the BoardGameGeek XML web api. 4 | 5 | ## Usage 6 | 7 | Coming soon! 8 | 9 | ## License 10 | 11 | Copyright © 2017 Howard M. Lewis Ship 12 | 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.howardlewisship/bgg-graphql-proxy "0.0.1" 2 | :description "GraphQL interface to BoardGameGeek" 3 | :dependencies [[org.clojure/clojure "1.8.0"] 4 | [io.aviso/logging "0.2.0"] 5 | [com.walmartlabs/lacinia "0.13.0"] 6 | [clj-http "2.3.0"] 7 | [org.clojure/data.json "0.2.6"] 8 | [org.clojure/data.xml "0.0.8"] 9 | [io.pedestal/pedestal.service "0.5.2"] 10 | [io.pedestal/pedestal.jetty "0.5.2"]] 11 | :codox {:source-uri "https://github.com/hlship/boardgamegeek-graphql-proxy/blob/master/{filepath}#L{line}" 12 | :metadata {:doc/format :markdown}}) 13 | -------------------------------------------------------------------------------- /resources/bgg-schema.edn: -------------------------------------------------------------------------------- 1 | {:interfaces { 2 | :BGGEntity {:description "A Standard BGG entity with id, name, and optional description." 3 | :fields {:id {:type (non-null ID)} 4 | :name {:type (non-null String)} 5 | :description {:type String}}}} 6 | :objects { 7 | :Company {:description "A company that may publish a Board Game." 8 | :implements [:BGGEntity] 9 | :fields {:id {:type (non-null ID)} 10 | :name {:type (non-null String)} 11 | :description {:type String}}} 12 | 13 | :Designer {:description "Person who designs a game." 14 | :implements [:BGGEntity] 15 | :fields {:id {:type (non-null ID)} 16 | :name {:type (non-null String)} 17 | :description {:type String}}} 18 | 19 | :BoardGame {:description "Details about a Board Game including description, number of players, creators, and publishers." 20 | :implements [:BGGEntity] 21 | :fields {:id {:type (non-null ID)} 22 | :name {:type (non-null String) 23 | :description "Common name or title for the game."} 24 | :publish_year {:type Int} 25 | :min_players {:type Int} 26 | :max_players {:type Int} 27 | :playing_time {:type Int 28 | :description "Approximiate playing time, in minutes."} 29 | :min_player_age {:type Int 30 | :description "Minimum age, in years, for a player. A rough guide to complexity of the game."} 31 | :description {:type String 32 | :description "Short text description of the game."} 33 | :thumbnail {:type String 34 | :description "URL for a small image representing the game, used in various lists."} 35 | :image {:type String 36 | :description "URL for a larger image for the game."} 37 | :publishers {:type (list :Company) 38 | :args {:limit {:type Int 39 | :description "Maximum number of results to include. 40 | Default is no limit."}} 41 | :description "Company that has published the game." 42 | :resolve :resolve-game-publishers} 43 | :designers {:type (list :Designer) 44 | :args {:limit {:type Int 45 | :description "Maximum number of results to include. Default is no limit."}} 46 | :description "Person who contributed to the design of the game." 47 | :resolve :resolve-game-designers}}}} 48 | 49 | :queries { 50 | :search {:type (list :BoardGame) 51 | :description "Searches for board games matching a search term." 52 | :args {:term {:type (non-null String) 53 | :description "Search term used to locate Games."}} 54 | :resolve :resolve-search} 55 | :game {:type :BoardGame 56 | :description "Retrieve a single BoardGame by its unique id." 57 | :args {:id {:type (non-null ID) 58 | :description "Unique identifier for the game."}} 59 | :resolve :resolve-game}}} 60 | -------------------------------------------------------------------------------- /resources/graphiql/graphiql.css: -------------------------------------------------------------------------------- 1 | .graphiql-container { 2 | color: #141823; 3 | display: -webkit-box; 4 | display: -ms-flexbox; 5 | display: flex; 6 | -webkit-box-orient: horizontal; 7 | -webkit-box-direction: normal; 8 | -ms-flex-direction: row; 9 | flex-direction: row; 10 | font-family: 11 | system, 12 | -apple-system, 13 | 'San Francisco', 14 | '.SFNSDisplay-Regular', 15 | 'Segoe UI', 16 | Segoe, 17 | 'Segoe WP', 18 | 'Helvetica Neue', 19 | helvetica, 20 | 'Lucida Grande', 21 | arial, 22 | sans-serif; 23 | font-size: 14px; 24 | height: 100%; 25 | margin: 0; 26 | overflow: hidden; 27 | width: 100%; 28 | } 29 | 30 | .graphiql-container .editorWrap { 31 | display: -webkit-box; 32 | display: -ms-flexbox; 33 | display: flex; 34 | -webkit-box-orient: vertical; 35 | -webkit-box-direction: normal; 36 | -ms-flex-direction: column; 37 | flex-direction: column; 38 | -webkit-box-flex: 1; 39 | -ms-flex: 1; 40 | flex: 1; 41 | } 42 | 43 | .graphiql-container .title { 44 | font-size: 18px; 45 | } 46 | 47 | .graphiql-container .title em { 48 | font-family: georgia; 49 | font-size: 19px; 50 | } 51 | 52 | .graphiql-container .topBarWrap { 53 | display: -webkit-box; 54 | display: -ms-flexbox; 55 | display: flex; 56 | -webkit-box-orient: horizontal; 57 | -webkit-box-direction: normal; 58 | -ms-flex-direction: row; 59 | flex-direction: row; 60 | } 61 | 62 | .graphiql-container .topBar { 63 | -webkit-box-align: center; 64 | -ms-flex-align: center; 65 | align-items: center; 66 | background: -webkit-linear-gradient(#f7f7f7, #e2e2e2); 67 | background: linear-gradient(#f7f7f7, #e2e2e2); 68 | border-bottom: 1px solid #d0d0d0; 69 | cursor: default; 70 | display: -webkit-box; 71 | display: -ms-flexbox; 72 | display: flex; 73 | -webkit-box-orient: horizontal; 74 | -webkit-box-direction: normal; 75 | -ms-flex-direction: row; 76 | flex-direction: row; 77 | -webkit-box-flex: 1; 78 | -ms-flex: 1; 79 | flex: 1; 80 | height: 34px; 81 | padding: 7px 14px 6px; 82 | -webkit-user-select: none; 83 | -moz-user-select: none; 84 | -ms-user-select: none; 85 | user-select: none; 86 | } 87 | 88 | .graphiql-container .toolbar { 89 | overflow-x: auto; 90 | } 91 | 92 | .graphiql-container .docExplorerShow { 93 | background: -webkit-linear-gradient(#f7f7f7, #e2e2e2); 94 | background: linear-gradient(#f7f7f7, #e2e2e2); 95 | border-bottom: 1px solid #d0d0d0; 96 | border-left: 1px solid rgba(0, 0, 0, 0.2); 97 | border-right: none; 98 | border-top: none; 99 | color: #3B5998; 100 | cursor: pointer; 101 | font-size: 14px; 102 | margin: 0; 103 | outline: 0; 104 | padding: 2px 20px 0 18px; 105 | } 106 | 107 | .graphiql-container .docExplorerShow:before { 108 | border-left: 2px solid #3B5998; 109 | border-top: 2px solid #3B5998; 110 | content: ''; 111 | display: inline-block; 112 | height: 9px; 113 | margin: 0 3px -1px 0; 114 | position: relative; 115 | -webkit-transform: rotate(-45deg); 116 | transform: rotate(-45deg); 117 | width: 9px; 118 | } 119 | 120 | .graphiql-container .editorBar { 121 | display: -webkit-box; 122 | display: -ms-flexbox; 123 | display: flex; 124 | -webkit-box-orient: horizontal; 125 | -webkit-box-direction: normal; 126 | -ms-flex-direction: row; 127 | flex-direction: row; 128 | -webkit-box-flex: 1; 129 | -ms-flex: 1; 130 | flex: 1; 131 | } 132 | 133 | .graphiql-container .queryWrap { 134 | display: -webkit-box; 135 | display: -ms-flexbox; 136 | display: flex; 137 | -webkit-box-orient: vertical; 138 | -webkit-box-direction: normal; 139 | -ms-flex-direction: column; 140 | flex-direction: column; 141 | -webkit-box-flex: 1; 142 | -ms-flex: 1; 143 | flex: 1; 144 | } 145 | 146 | .graphiql-container .resultWrap { 147 | border-left: solid 1px #e0e0e0; 148 | display: -webkit-box; 149 | display: -ms-flexbox; 150 | display: flex; 151 | -webkit-box-orient: vertical; 152 | -webkit-box-direction: normal; 153 | -ms-flex-direction: column; 154 | flex-direction: column; 155 | -webkit-box-flex: 1; 156 | -ms-flex: 1; 157 | flex: 1; 158 | position: relative; 159 | } 160 | 161 | .graphiql-container .docExplorerWrap { 162 | background: white; 163 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 164 | position: relative; 165 | z-index: 3; 166 | } 167 | 168 | .graphiql-container .docExplorerResizer { 169 | cursor: col-resize; 170 | height: 100%; 171 | left: -5px; 172 | position: absolute; 173 | top: 0; 174 | width: 10px; 175 | z-index: 10; 176 | } 177 | 178 | .graphiql-container .docExplorerHide { 179 | cursor: pointer; 180 | font-size: 18px; 181 | margin: -7px -8px -6px 0; 182 | padding: 18px 16px 15px 12px; 183 | } 184 | 185 | .graphiql-container .query-editor { 186 | -webkit-box-flex: 1; 187 | -ms-flex: 1; 188 | flex: 1; 189 | position: relative; 190 | } 191 | 192 | .graphiql-container .variable-editor { 193 | display: -webkit-box; 194 | display: -ms-flexbox; 195 | display: flex; 196 | -webkit-box-orient: vertical; 197 | -webkit-box-direction: normal; 198 | -ms-flex-direction: column; 199 | flex-direction: column; 200 | height: 29px; 201 | position: relative; 202 | } 203 | 204 | .graphiql-container .variable-editor-title { 205 | background: #eeeeee; 206 | border-bottom: 1px solid #d6d6d6; 207 | border-top: 1px solid #e0e0e0; 208 | color: #777; 209 | font-variant: small-caps; 210 | font-weight: bold; 211 | letter-spacing: 1px; 212 | line-height: 14px; 213 | padding: 6px 0 8px 43px; 214 | text-transform: lowercase; 215 | -webkit-user-select: none; 216 | -moz-user-select: none; 217 | -ms-user-select: none; 218 | user-select: none; 219 | } 220 | 221 | .graphiql-container .codemirrorWrap { 222 | -webkit-box-flex: 1; 223 | -ms-flex: 1; 224 | flex: 1; 225 | height: 100%; 226 | position: relative; 227 | } 228 | 229 | .graphiql-container .result-window { 230 | -webkit-box-flex: 1; 231 | -ms-flex: 1; 232 | flex: 1; 233 | height: 100%; 234 | position: relative; 235 | } 236 | 237 | .graphiql-container .footer { 238 | background: #f6f7f8; 239 | border-left: 1px solid #e0e0e0; 240 | border-top: 1px solid #e0e0e0; 241 | margin-left: 12px; 242 | position: relative; 243 | } 244 | 245 | .graphiql-container .footer:before { 246 | background: #eeeeee; 247 | bottom: 0; 248 | content: " "; 249 | left: -13px; 250 | position: absolute; 251 | top: -1px; 252 | width: 12px; 253 | } 254 | 255 | .graphiql-container .result-window .CodeMirror { 256 | background: #f6f7f8; 257 | } 258 | 259 | .graphiql-container .result-window .CodeMirror-gutters { 260 | background-color: #eeeeee; 261 | border-color: #e0e0e0; 262 | cursor: col-resize; 263 | } 264 | 265 | .graphiql-container .result-window .CodeMirror-foldgutter, 266 | .graphiql-container .result-window .CodeMirror-foldgutter-open:after, 267 | .graphiql-container .result-window .CodeMirror-foldgutter-folded:after { 268 | padding-left: 3px; 269 | } 270 | 271 | .graphiql-container .toolbar-button { 272 | background: #fdfdfd; 273 | background: -webkit-linear-gradient(#fbfbfb, #f8f8f8); 274 | background: linear-gradient(#fbfbfb, #f8f8f8); 275 | border-color: #d3d3d3 #d0d0d0 #bababa; 276 | border-radius: 4px; 277 | border-style: solid; 278 | border-width: 0.5px; 279 | box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.13), inset 0 1px #fff; 280 | color: #444; 281 | cursor: pointer; 282 | display: inline-block; 283 | margin: 0 5px 0; 284 | padding: 2px 8px 4px; 285 | text-decoration: none; 286 | } 287 | 288 | .graphiql-container .toolbar-button:active { 289 | background: -webkit-linear-gradient(#ececec, #d8d8d8); 290 | background: linear-gradient(#ececec, #d8d8d8); 291 | border-color: #cacaca #c9c9c9 #b0b0b0; 292 | box-shadow: 293 | 0 1px 0 #fff, 294 | inset 0 1px rgba(255, 255, 255, 0.2), 295 | inset 0 1px 1px rgba(0, 0, 0, 0.08); 296 | } 297 | 298 | .graphiql-container .toolbar-button.error { 299 | background: -webkit-linear-gradient(#fdf3f3, #e6d6d7); 300 | background: linear-gradient(#fdf3f3, #e6d6d7); 301 | color: #b00; 302 | } 303 | 304 | .graphiql-container .execute-button-wrap { 305 | height: 34px; 306 | margin: 0 14px 0 28px; 307 | position: relative; 308 | } 309 | 310 | .graphiql-container .execute-button { 311 | background: -webkit-linear-gradient(#fdfdfd, #d2d3d6); 312 | background: linear-gradient(#fdfdfd, #d2d3d6); 313 | border-radius: 17px; 314 | border: 1px solid rgba(0,0,0,0.25); 315 | box-shadow: 0 1px 0 #fff; 316 | cursor: pointer; 317 | fill: #444; 318 | height: 34px; 319 | margin: 0; 320 | padding: 0; 321 | width: 34px; 322 | } 323 | 324 | .graphiql-container .execute-button svg { 325 | pointer-events: none; 326 | } 327 | 328 | .graphiql-container .execute-button:active { 329 | background: -webkit-linear-gradient(#e6e6e6, #c0c0c0); 330 | background: linear-gradient(#e6e6e6, #c0c0c0); 331 | box-shadow: 332 | 0 1px 0 #fff, 333 | inset 0 0 2px rgba(0, 0, 0, 0.3), 334 | inset 0 0 6px rgba(0, 0, 0, 0.2); 335 | } 336 | 337 | .graphiql-container .execute-button:focus { 338 | outline: 0; 339 | } 340 | 341 | .graphiql-container .execute-options { 342 | background: #fff; 343 | box-shadow: 344 | 0 0 0 1px rgba(0,0,0,0.1), 345 | 0 2px 4px rgba(0,0,0,0.25); 346 | left: -1px; 347 | margin: 0; 348 | padding: 8px 0; 349 | position: absolute; 350 | top: 37px; 351 | z-index: 100; 352 | } 353 | 354 | .graphiql-container .execute-options li { 355 | cursor: pointer; 356 | list-style: none; 357 | min-width: 100px; 358 | padding: 2px 30px 4px 10px; 359 | } 360 | 361 | .graphiql-container .execute-options li.selected { 362 | background: #e10098; 363 | color: white; 364 | } 365 | 366 | .graphiql-container .CodeMirror-scroll { 367 | overflow-scrolling: touch; 368 | } 369 | 370 | .graphiql-container .CodeMirror { 371 | color: #141823; 372 | font-family: 373 | 'Consolas', 374 | 'Inconsolata', 375 | 'Droid Sans Mono', 376 | 'Monaco', 377 | monospace; 378 | font-size: 13px; 379 | height: 100%; 380 | left: 0; 381 | position: absolute; 382 | top: 0; 383 | width: 100%; 384 | } 385 | 386 | .graphiql-container .CodeMirror-lines { 387 | padding: 20px 0; 388 | } 389 | 390 | .CodeMirror-hint-information .content { 391 | box-orient: vertical; 392 | color: #141823; 393 | display: -webkit-box; 394 | display: -ms-flexbox; 395 | display: flex; 396 | font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular', 'Segoe UI', Segoe, 'Segoe WP', 'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif; 397 | font-size: 13px; 398 | line-clamp: 3; 399 | line-height: 16px; 400 | max-height: 48px; 401 | overflow: hidden; 402 | text-overflow: -o-ellipsis-lastline; 403 | } 404 | 405 | .CodeMirror-hint-information .content p:first-child { 406 | margin-top: 0; 407 | } 408 | 409 | .CodeMirror-hint-information .content p:last-child { 410 | margin-bottom: 0; 411 | } 412 | 413 | .CodeMirror-hint-information .infoType { 414 | color: #30a; 415 | cursor: pointer; 416 | display: inline; 417 | margin-right: 0.5em; 418 | } 419 | 420 | .autoInsertedLeaf.cm-property { 421 | -webkit-animation-duration: 6s; 422 | animation-duration: 6s; 423 | -webkit-animation-name: insertionFade; 424 | animation-name: insertionFade; 425 | border-bottom: 2px solid rgba(255, 255, 255, 0); 426 | border-radius: 2px; 427 | margin: -2px -4px -1px; 428 | padding: 2px 4px 1px; 429 | } 430 | 431 | @-webkit-keyframes insertionFade { 432 | from, to { 433 | background: rgba(255, 255, 255, 0); 434 | border-color: rgba(255, 255, 255, 0); 435 | } 436 | 437 | 15%, 85% { 438 | background: #fbffc9; 439 | border-color: #f0f3c0; 440 | } 441 | } 442 | 443 | @keyframes insertionFade { 444 | from, to { 445 | background: rgba(255, 255, 255, 0); 446 | border-color: rgba(255, 255, 255, 0); 447 | } 448 | 449 | 15%, 85% { 450 | background: #fbffc9; 451 | border-color: #f0f3c0; 452 | } 453 | } 454 | 455 | div.CodeMirror-lint-tooltip { 456 | background-color: white; 457 | border-radius: 2px; 458 | border: 0; 459 | color: #141823; 460 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 461 | font-family: 462 | system, 463 | -apple-system, 464 | 'San Francisco', 465 | '.SFNSDisplay-Regular', 466 | 'Segoe UI', 467 | Segoe, 468 | 'Segoe WP', 469 | 'Helvetica Neue', 470 | helvetica, 471 | 'Lucida Grande', 472 | arial, 473 | sans-serif; 474 | font-size: 13px; 475 | line-height: 16px; 476 | opacity: 0; 477 | padding: 6px 10px; 478 | -webkit-transition: opacity 0.15s; 479 | transition: opacity 0.15s; 480 | } 481 | 482 | div.CodeMirror-lint-message-error, div.CodeMirror-lint-message-warning { 483 | padding-left: 23px; 484 | } 485 | 486 | /* COLORS */ 487 | 488 | .graphiql-container .CodeMirror-foldmarker { 489 | border-radius: 4px; 490 | background: #08f; 491 | background: -webkit-linear-gradient(#43A8FF, #0F83E8); 492 | background: linear-gradient(#43A8FF, #0F83E8); 493 | box-shadow: 494 | 0 1px 1px rgba(0, 0, 0, 0.2), 495 | inset 0 0 0 1px rgba(0, 0, 0, 0.1); 496 | color: white; 497 | font-family: arial; 498 | font-size: 12px; 499 | line-height: 0; 500 | margin: 0 3px; 501 | padding: 0px 4px 1px; 502 | text-shadow: 0 -1px rgba(0, 0, 0, 0.1); 503 | } 504 | 505 | .graphiql-container div.CodeMirror span.CodeMirror-matchingbracket { 506 | color: #555; 507 | text-decoration: underline; 508 | } 509 | 510 | .graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket { 511 | color: #f00; 512 | } 513 | 514 | /* Comment */ 515 | .cm-comment { 516 | color: #999; 517 | } 518 | 519 | /* Punctuation */ 520 | .cm-punctuation { 521 | color: #555; 522 | } 523 | 524 | /* Keyword */ 525 | .cm-keyword { 526 | color: #B11A04; 527 | } 528 | 529 | /* OperationName, FragmentName */ 530 | .cm-def { 531 | color: #D2054E; 532 | } 533 | 534 | /* FieldName */ 535 | .cm-property { 536 | color: #1F61A0; 537 | } 538 | 539 | /* FieldAlias */ 540 | .cm-qualifier { 541 | color: #1C92A9; 542 | } 543 | 544 | /* ArgumentName and ObjectFieldName */ 545 | .cm-attribute { 546 | color: #8B2BB9; 547 | } 548 | 549 | /* Number */ 550 | .cm-number { 551 | color: #2882F9; 552 | } 553 | 554 | /* String */ 555 | .cm-string { 556 | color: #D64292; 557 | } 558 | 559 | /* Boolean */ 560 | .cm-builtin { 561 | color: #D47509; 562 | } 563 | 564 | /* EnumValue */ 565 | .cm-string-2 { 566 | color: #0B7FC7; 567 | } 568 | 569 | /* Variable */ 570 | .cm-variable { 571 | color: #397D13; 572 | } 573 | 574 | /* Directive */ 575 | .cm-meta { 576 | color: #B33086; 577 | } 578 | 579 | /* Type */ 580 | .cm-atom { 581 | color: #CA9800; 582 | } 583 | /* BASICS */ 584 | 585 | .CodeMirror { 586 | /* Set height, width, borders, and global font properties here */ 587 | color: black; 588 | font-family: monospace; 589 | height: 300px; 590 | } 591 | 592 | /* PADDING */ 593 | 594 | .CodeMirror-lines { 595 | padding: 4px 0; /* Vertical padding around content */ 596 | } 597 | .CodeMirror pre { 598 | padding: 0 4px; /* Horizontal padding of content */ 599 | } 600 | 601 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 602 | background-color: white; /* The little square between H and V scrollbars */ 603 | } 604 | 605 | /* GUTTER */ 606 | 607 | .CodeMirror-gutters { 608 | border-right: 1px solid #ddd; 609 | background-color: #f7f7f7; 610 | white-space: nowrap; 611 | } 612 | .CodeMirror-linenumbers {} 613 | .CodeMirror-linenumber { 614 | color: #999; 615 | min-width: 20px; 616 | padding: 0 3px 0 5px; 617 | text-align: right; 618 | white-space: nowrap; 619 | } 620 | 621 | .CodeMirror-guttermarker { color: black; } 622 | .CodeMirror-guttermarker-subtle { color: #999; } 623 | 624 | /* CURSOR */ 625 | 626 | .CodeMirror div.CodeMirror-cursor { 627 | border-left: 1px solid black; 628 | } 629 | /* Shown when moving in bi-directional text */ 630 | .CodeMirror div.CodeMirror-secondarycursor { 631 | border-left: 1px solid silver; 632 | } 633 | .CodeMirror.cm-fat-cursor div.CodeMirror-cursor { 634 | background: #7e7; 635 | border: 0; 636 | width: auto; 637 | } 638 | .CodeMirror.cm-fat-cursor div.CodeMirror-cursors { 639 | z-index: 1; 640 | } 641 | 642 | .cm-animate-fat-cursor { 643 | -webkit-animation: blink 1.06s steps(1) infinite; 644 | animation: blink 1.06s steps(1) infinite; 645 | border: 0; 646 | width: auto; 647 | } 648 | @-webkit-keyframes blink { 649 | 0% { background: #7e7; } 650 | 50% { background: none; } 651 | 100% { background: #7e7; } 652 | } 653 | @keyframes blink { 654 | 0% { background: #7e7; } 655 | 50% { background: none; } 656 | 100% { background: #7e7; } 657 | } 658 | 659 | /* Can style cursor different in overwrite (non-insert) mode */ 660 | div.CodeMirror-overwrite div.CodeMirror-cursor {} 661 | 662 | .cm-tab { display: inline-block; text-decoration: inherit; } 663 | 664 | .CodeMirror-ruler { 665 | border-left: 1px solid #ccc; 666 | position: absolute; 667 | } 668 | 669 | /* DEFAULT THEME */ 670 | 671 | .cm-s-default .cm-keyword {color: #708;} 672 | .cm-s-default .cm-atom {color: #219;} 673 | .cm-s-default .cm-number {color: #164;} 674 | .cm-s-default .cm-def {color: #00f;} 675 | .cm-s-default .cm-variable, 676 | .cm-s-default .cm-punctuation, 677 | .cm-s-default .cm-property, 678 | .cm-s-default .cm-operator {} 679 | .cm-s-default .cm-variable-2 {color: #05a;} 680 | .cm-s-default .cm-variable-3 {color: #085;} 681 | .cm-s-default .cm-comment {color: #a50;} 682 | .cm-s-default .cm-string {color: #a11;} 683 | .cm-s-default .cm-string-2 {color: #f50;} 684 | .cm-s-default .cm-meta {color: #555;} 685 | .cm-s-default .cm-qualifier {color: #555;} 686 | .cm-s-default .cm-builtin {color: #30a;} 687 | .cm-s-default .cm-bracket {color: #997;} 688 | .cm-s-default .cm-tag {color: #170;} 689 | .cm-s-default .cm-attribute {color: #00c;} 690 | .cm-s-default .cm-header {color: blue;} 691 | .cm-s-default .cm-quote {color: #090;} 692 | .cm-s-default .cm-hr {color: #999;} 693 | .cm-s-default .cm-link {color: #00c;} 694 | 695 | .cm-negative {color: #d44;} 696 | .cm-positive {color: #292;} 697 | .cm-header, .cm-strong {font-weight: bold;} 698 | .cm-em {font-style: italic;} 699 | .cm-link {text-decoration: underline;} 700 | .cm-strikethrough {text-decoration: line-through;} 701 | 702 | .cm-s-default .cm-error {color: #f00;} 703 | .cm-invalidchar {color: #f00;} 704 | 705 | .CodeMirror-composing { border-bottom: 2px solid; } 706 | 707 | /* Default styles for common addons */ 708 | 709 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 710 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 711 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 712 | .CodeMirror-activeline-background {background: #e8f2ff;} 713 | 714 | /* STOP */ 715 | 716 | /* The rest of this file contains styles related to the mechanics of 717 | the editor. You probably shouldn't touch them. */ 718 | 719 | .CodeMirror { 720 | background: white; 721 | overflow: hidden; 722 | position: relative; 723 | } 724 | 725 | .CodeMirror-scroll { 726 | height: 100%; 727 | /* 30px is the magic margin used to hide the element's real scrollbars */ 728 | /* See overflow: hidden in .CodeMirror */ 729 | margin-bottom: -30px; margin-right: -30px; 730 | outline: none; /* Prevent dragging from highlighting the element */ 731 | overflow: scroll !important; /* Things will break if this is overridden */ 732 | padding-bottom: 30px; 733 | position: relative; 734 | } 735 | .CodeMirror-sizer { 736 | border-right: 30px solid transparent; 737 | position: relative; 738 | } 739 | 740 | /* The fake, visible scrollbars. Used to force redraw during scrolling 741 | before actual scrolling happens, thus preventing shaking and 742 | flickering artifacts. */ 743 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 744 | display: none; 745 | position: absolute; 746 | z-index: 6; 747 | } 748 | .CodeMirror-vscrollbar { 749 | overflow-x: hidden; 750 | overflow-y: scroll; 751 | right: 0; top: 0; 752 | } 753 | .CodeMirror-hscrollbar { 754 | bottom: 0; left: 0; 755 | overflow-x: scroll; 756 | overflow-y: hidden; 757 | } 758 | .CodeMirror-scrollbar-filler { 759 | right: 0; bottom: 0; 760 | } 761 | .CodeMirror-gutter-filler { 762 | left: 0; bottom: 0; 763 | } 764 | 765 | .CodeMirror-gutters { 766 | min-height: 100%; 767 | position: absolute; left: 0; top: 0; 768 | z-index: 3; 769 | } 770 | .CodeMirror-gutter { 771 | display: inline-block; 772 | height: 100%; 773 | margin-bottom: -30px; 774 | vertical-align: top; 775 | white-space: normal; 776 | /* Hack to make IE7 behave */ 777 | *zoom:1; 778 | *display:inline; 779 | } 780 | .CodeMirror-gutter-wrapper { 781 | background: none !important; 782 | border: none !important; 783 | position: absolute; 784 | z-index: 4; 785 | } 786 | .CodeMirror-gutter-background { 787 | position: absolute; 788 | top: 0; bottom: 0; 789 | z-index: 4; 790 | } 791 | .CodeMirror-gutter-elt { 792 | cursor: default; 793 | position: absolute; 794 | z-index: 4; 795 | } 796 | .CodeMirror-gutter-wrapper { 797 | -webkit-user-select: none; 798 | -moz-user-select: none; 799 | -ms-user-select: none; 800 | user-select: none; 801 | } 802 | 803 | .CodeMirror-lines { 804 | cursor: text; 805 | min-height: 1px; /* prevents collapsing before first draw */ 806 | } 807 | .CodeMirror pre { 808 | -webkit-tap-highlight-color: transparent; 809 | /* Reset some styles that the rest of the page might have set */ 810 | background: transparent; 811 | border-radius: 0; 812 | border-width: 0; 813 | color: inherit; 814 | font-family: inherit; 815 | font-size: inherit; 816 | -webkit-font-variant-ligatures: none; 817 | font-variant-ligatures: none; 818 | line-height: inherit; 819 | margin: 0; 820 | overflow: visible; 821 | position: relative; 822 | white-space: pre; 823 | word-wrap: normal; 824 | z-index: 2; 825 | } 826 | .CodeMirror-wrap pre { 827 | word-wrap: break-word; 828 | white-space: pre-wrap; 829 | word-break: normal; 830 | } 831 | 832 | .CodeMirror-linebackground { 833 | position: absolute; 834 | left: 0; right: 0; top: 0; bottom: 0; 835 | z-index: 0; 836 | } 837 | 838 | .CodeMirror-linewidget { 839 | overflow: auto; 840 | position: relative; 841 | z-index: 2; 842 | } 843 | 844 | .CodeMirror-widget {} 845 | 846 | .CodeMirror-code { 847 | outline: none; 848 | } 849 | 850 | /* Force content-box sizing for the elements where we expect it */ 851 | .CodeMirror-scroll, 852 | .CodeMirror-sizer, 853 | .CodeMirror-gutter, 854 | .CodeMirror-gutters, 855 | .CodeMirror-linenumber { 856 | box-sizing: content-box; 857 | } 858 | 859 | .CodeMirror-measure { 860 | height: 0; 861 | overflow: hidden; 862 | position: absolute; 863 | visibility: hidden; 864 | width: 100%; 865 | } 866 | 867 | .CodeMirror-cursor { position: absolute; } 868 | .CodeMirror-measure pre { position: static; } 869 | 870 | div.CodeMirror-cursors { 871 | position: relative; 872 | visibility: hidden; 873 | z-index: 3; 874 | } 875 | div.CodeMirror-dragcursors { 876 | visibility: visible; 877 | } 878 | 879 | .CodeMirror-focused div.CodeMirror-cursors { 880 | visibility: visible; 881 | } 882 | 883 | .CodeMirror-selected { background: #d9d9d9; } 884 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 885 | .CodeMirror-crosshair { cursor: crosshair; } 886 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 887 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 888 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 889 | 890 | .cm-searching { 891 | background: #ffa; 892 | background: rgba(255, 255, 0, .4); 893 | } 894 | 895 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 896 | .CodeMirror span { *vertical-align: text-bottom; } 897 | 898 | /* Used to force a border model for a node */ 899 | .cm-force-border { padding-right: .1px; } 900 | 901 | @media print { 902 | /* Hide the cursor when printing */ 903 | .CodeMirror div.CodeMirror-cursors { 904 | visibility: hidden; 905 | } 906 | } 907 | 908 | /* See issue #2901 */ 909 | .cm-tab-wrap-hack:after { content: ''; } 910 | 911 | /* Help users use markselection to safely style text background */ 912 | span.CodeMirror-selectedtext { background: none; } 913 | 914 | .CodeMirror-dialog { 915 | background: inherit; 916 | color: inherit; 917 | left: 0; right: 0; 918 | overflow: hidden; 919 | padding: .1em .8em; 920 | position: absolute; 921 | z-index: 15; 922 | } 923 | 924 | .CodeMirror-dialog-top { 925 | border-bottom: 1px solid #eee; 926 | top: 0; 927 | } 928 | 929 | .CodeMirror-dialog-bottom { 930 | border-top: 1px solid #eee; 931 | bottom: 0; 932 | } 933 | 934 | .CodeMirror-dialog input { 935 | background: transparent; 936 | border: 1px solid #d3d6db; 937 | color: inherit; 938 | font-family: monospace; 939 | outline: none; 940 | width: 20em; 941 | } 942 | 943 | .CodeMirror-dialog button { 944 | font-size: 70%; 945 | } 946 | .graphiql-container .doc-explorer { 947 | background: white; 948 | } 949 | 950 | .graphiql-container .doc-explorer-title-bar { 951 | cursor: default; 952 | display: -webkit-box; 953 | display: -ms-flexbox; 954 | display: flex; 955 | height: 34px; 956 | line-height: 14px; 957 | padding: 8px 8px 5px; 958 | position: relative; 959 | -webkit-user-select: none; 960 | -moz-user-select: none; 961 | -ms-user-select: none; 962 | user-select: none; 963 | } 964 | 965 | .graphiql-container .doc-explorer-title { 966 | -webkit-box-flex: 1; 967 | -ms-flex: 1; 968 | flex: 1; 969 | font-weight: bold; 970 | overflow-x: hidden; 971 | padding: 10px 0 10px 10px; 972 | text-align: center; 973 | text-overflow: ellipsis; 974 | white-space: nowrap; 975 | } 976 | 977 | .graphiql-container .doc-explorer-back { 978 | color: #3B5998; 979 | cursor: pointer; 980 | margin: -7px 0 -6px -8px; 981 | overflow-x: hidden; 982 | padding: 17px 12px 16px 16px; 983 | text-overflow: ellipsis; 984 | white-space: nowrap; 985 | } 986 | 987 | .doc-explorer-narrow .doc-explorer-back { 988 | width: 0; 989 | } 990 | 991 | .graphiql-container .doc-explorer-back:before { 992 | border-left: 2px solid #3B5998; 993 | border-top: 2px solid #3B5998; 994 | content: ''; 995 | display: inline-block; 996 | height: 9px; 997 | margin: 0 3px -1px 0; 998 | position: relative; 999 | -webkit-transform: rotate(-45deg); 1000 | transform: rotate(-45deg); 1001 | width: 9px; 1002 | } 1003 | 1004 | .graphiql-container .doc-explorer-rhs { 1005 | position: relative; 1006 | } 1007 | 1008 | .graphiql-container .doc-explorer-contents { 1009 | background-color: #ffffff; 1010 | border-top: 1px solid #d6d6d6; 1011 | bottom: 0; 1012 | left: 0; 1013 | min-width: 300px; 1014 | overflow-y: auto; 1015 | padding: 20px 15px; 1016 | position: absolute; 1017 | right: 0; 1018 | top: 47px; 1019 | } 1020 | 1021 | .graphiql-container .doc-type-description p:first-child , 1022 | .graphiql-container .doc-type-description blockquote:first-child { 1023 | margin-top: 0; 1024 | } 1025 | 1026 | .graphiql-container .doc-explorer-contents a { 1027 | cursor: pointer; 1028 | text-decoration: none; 1029 | } 1030 | 1031 | .graphiql-container .doc-explorer-contents a:hover { 1032 | text-decoration: underline; 1033 | } 1034 | 1035 | .graphiql-container .doc-value-description { 1036 | padding: 4px 0 8px 12px; 1037 | } 1038 | 1039 | .graphiql-container .doc-category { 1040 | margin: 20px 0; 1041 | } 1042 | 1043 | .graphiql-container .doc-category-title { 1044 | border-bottom: 1px solid #e0e0e0; 1045 | color: #777; 1046 | cursor: default; 1047 | font-size: 14px; 1048 | font-variant: small-caps; 1049 | font-weight: bold; 1050 | letter-spacing: 1px; 1051 | margin: 0 -15px 10px 0; 1052 | padding: 10px 0; 1053 | -webkit-user-select: none; 1054 | -moz-user-select: none; 1055 | -ms-user-select: none; 1056 | user-select: none; 1057 | } 1058 | 1059 | .graphiql-container .doc-category-item { 1060 | margin: 12px 0; 1061 | color: #555; 1062 | } 1063 | 1064 | .graphiql-container .keyword { 1065 | color: #B11A04; 1066 | } 1067 | 1068 | .graphiql-container .type-name { 1069 | color: #CA9800; 1070 | } 1071 | 1072 | .graphiql-container .field-name { 1073 | color: #1F61A0; 1074 | } 1075 | 1076 | .graphiql-container .value-name { 1077 | color: #0B7FC7; 1078 | } 1079 | 1080 | .graphiql-container .arg-name { 1081 | color: #8B2BB9; 1082 | } 1083 | 1084 | .graphiql-container .arg:after { 1085 | content: ', '; 1086 | } 1087 | 1088 | .graphiql-container .arg:last-child:after { 1089 | content: ''; 1090 | } 1091 | 1092 | .graphiql-container .doc-alert-text { 1093 | color: #F00F00; 1094 | font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; 1095 | font-size: 13px; 1096 | } 1097 | 1098 | .graphiql-container .search-box-outer { 1099 | border: 1px solid #d3d6db; 1100 | box-sizing: border-box; 1101 | display: inline-block; 1102 | font-size: 12px; 1103 | height: 24px; 1104 | margin-bottom: 12px; 1105 | padding: 3px 8px 5px; 1106 | vertical-align: middle; 1107 | width: 100%; 1108 | } 1109 | 1110 | .graphiql-container .search-box-input { 1111 | border: 0; 1112 | font-size: 12px; 1113 | margin: 0; 1114 | outline: 0; 1115 | padding: 0; 1116 | width: 100%; 1117 | } 1118 | .CodeMirror-foldmarker { 1119 | color: blue; 1120 | cursor: pointer; 1121 | font-family: arial; 1122 | line-height: .3; 1123 | text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; 1124 | } 1125 | .CodeMirror-foldgutter { 1126 | width: .7em; 1127 | } 1128 | .CodeMirror-foldgutter-open, 1129 | .CodeMirror-foldgutter-folded { 1130 | cursor: pointer; 1131 | } 1132 | .CodeMirror-foldgutter-open:after { 1133 | content: "\25BE"; 1134 | } 1135 | .CodeMirror-foldgutter-folded:after { 1136 | content: "\25B8"; 1137 | } 1138 | /* The lint marker gutter */ 1139 | .CodeMirror-lint-markers { 1140 | width: 16px; 1141 | } 1142 | 1143 | .CodeMirror-lint-tooltip { 1144 | background-color: infobackground; 1145 | border-radius: 4px 4px 4px 4px; 1146 | border: 1px solid black; 1147 | color: infotext; 1148 | font-family: monospace; 1149 | font-size: 10pt; 1150 | max-width: 600px; 1151 | opacity: 0; 1152 | overflow: hidden; 1153 | padding: 2px 5px; 1154 | position: fixed; 1155 | -webkit-transition: opacity .4s; 1156 | transition: opacity .4s; 1157 | white-space: pre-wrap; 1158 | white-space: pre; 1159 | z-index: 100; 1160 | } 1161 | 1162 | .CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { 1163 | background-position: left bottom; 1164 | background-repeat: repeat-x; 1165 | } 1166 | 1167 | .CodeMirror-lint-mark-error { 1168 | background-image: 1169 | url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") 1170 | ; 1171 | } 1172 | 1173 | .CodeMirror-lint-mark-warning { 1174 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); 1175 | } 1176 | 1177 | .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { 1178 | background-position: center center; 1179 | background-repeat: no-repeat; 1180 | cursor: pointer; 1181 | display: inline-block; 1182 | height: 16px; 1183 | position: relative; 1184 | vertical-align: middle; 1185 | width: 16px; 1186 | } 1187 | 1188 | .CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { 1189 | background-position: top left; 1190 | background-repeat: no-repeat; 1191 | padding-left: 18px; 1192 | } 1193 | 1194 | .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { 1195 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); 1196 | } 1197 | 1198 | .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { 1199 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); 1200 | } 1201 | 1202 | .CodeMirror-lint-marker-multiple { 1203 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); 1204 | background-position: right bottom; 1205 | background-repeat: no-repeat; 1206 | width: 100%; height: 100%; 1207 | } 1208 | .graphiql-container .spinner-container { 1209 | height: 36px; 1210 | left: 50%; 1211 | position: absolute; 1212 | top: 50%; 1213 | -webkit-transform: translate(-50%, -50%); 1214 | transform: translate(-50%, -50%); 1215 | width: 36px; 1216 | z-index: 10; 1217 | } 1218 | 1219 | .graphiql-container .spinner { 1220 | -webkit-animation: rotation .6s infinite linear; 1221 | animation: rotation .6s infinite linear; 1222 | border-bottom: 6px solid rgba(150, 150, 150, .15); 1223 | border-left: 6px solid rgba(150, 150, 150, .15); 1224 | border-radius: 100%; 1225 | border-right: 6px solid rgba(150, 150, 150, .15); 1226 | border-top: 6px solid rgba(150, 150, 150, .8); 1227 | display: inline-block; 1228 | height: 24px; 1229 | position: absolute; 1230 | vertical-align: middle; 1231 | width: 24px; 1232 | } 1233 | 1234 | @-webkit-keyframes rotation { 1235 | from { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 1236 | to { -webkit-transform: rotate(359deg); transform: rotate(359deg); } 1237 | } 1238 | 1239 | @keyframes rotation { 1240 | from { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 1241 | to { -webkit-transform: rotate(359deg); transform: rotate(359deg); } 1242 | } 1243 | .CodeMirror-hints { 1244 | background: white; 1245 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 1246 | font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; 1247 | font-size: 13px; 1248 | list-style: none; 1249 | margin-left: -6px; 1250 | margin: 0; 1251 | max-height: 14.5em; 1252 | overflow-y: auto; 1253 | overflow: hidden; 1254 | padding: 0; 1255 | position: absolute; 1256 | z-index: 10; 1257 | } 1258 | 1259 | .CodeMirror-hints-wrapper { 1260 | background: white; 1261 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); 1262 | margin-left: -6px; 1263 | position: absolute; 1264 | z-index: 10; 1265 | } 1266 | 1267 | .CodeMirror-hints-wrapper .CodeMirror-hints { 1268 | box-shadow: none; 1269 | margin-left: 0; 1270 | position: relative; 1271 | z-index: 0; 1272 | } 1273 | 1274 | .CodeMirror-hint { 1275 | border-top: solid 1px #f7f7f7; 1276 | color: #141823; 1277 | cursor: pointer; 1278 | margin: 0; 1279 | max-width: 300px; 1280 | overflow: hidden; 1281 | padding: 2px 6px; 1282 | white-space: pre; 1283 | } 1284 | 1285 | li.CodeMirror-hint-active { 1286 | background-color: #08f; 1287 | border-top-color: white; 1288 | color: white; 1289 | } 1290 | 1291 | .CodeMirror-hint-information { 1292 | border-top: solid 1px #c0c0c0; 1293 | max-width: 300px; 1294 | padding: 4px 6px; 1295 | position: relative; 1296 | z-index: 1; 1297 | } 1298 | 1299 | .CodeMirror-hint-information:first-child { 1300 | border-bottom: solid 1px #c0c0c0; 1301 | border-top: none; 1302 | margin-bottom: -1px; 1303 | } 1304 | -------------------------------------------------------------------------------- /resources/graphiql/index.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Loading... 28 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/bgg_graphql_proxy/cache.clj: -------------------------------------------------------------------------------- 1 | (ns bgg-graphql-proxy.cache 2 | "A little bit of in-memory caching so we don't overload BGG. 3 | 4 | THe cache is simply a standard atom." 5 | (:require [clojure.set :as set])) 6 | 7 | (defn resolve-by-id 8 | [cache category id] 9 | (get-in @cache [category id])) 10 | 11 | (defn resolve-by-ids 12 | [cache category ids] 13 | (let [category-cache (get @cache category {}) 14 | cached-values (keep category-cache ids) 15 | cached-ids (->> cached-values 16 | (map :id) 17 | set) 18 | uncached-ids (set/difference (set ids) cached-ids)] 19 | [cached-values (seq uncached-ids)])) 20 | 21 | (defn ^:private map-by-id 22 | [values] 23 | (persistent! (reduce (fn [m v] 24 | (assoc! m (:id v) v)) 25 | (transient {}) 26 | values))) 27 | 28 | (defn fill 29 | [cache category values] 30 | (swap! cache update category merge (map-by-id values))) 31 | -------------------------------------------------------------------------------- /src/bgg_graphql_proxy/client.clj: -------------------------------------------------------------------------------- 1 | (ns bgg-graphql-proxy.client 2 | "Client code for accessing BoardGameGeek, and converting XML responses into Clojure data." 3 | (:require 4 | [bgg-graphql-proxy.cache :as cache] 5 | [clj-http.client :as client] 6 | [clojure.data.xml :as xml] 7 | [clojure.string :as str] 8 | [clojure.tools.logging :as log]) 9 | (:import (java.io StringReader))) 10 | 11 | (def ^:private base-url "https://www.boardgamegeek.com/xmlapi") 12 | 13 | (defn ^:private expect-tag [tag element] 14 | (when-not (= tag (:tag element)) 15 | (throw (ex-info (format "Wrong tag. Expected `%s', not `%s'." 16 | (name tag) 17 | (-> element :tag name)) 18 | {:expected-tag tag 19 | :actual-tag (:tag element) 20 | :element element}))) 21 | element) 22 | 23 | (defn ^:private parse-int [s] (Integer/parseInt s)) 24 | 25 | (defn ^:private prefix-with-https [s] (str "https:" s)) 26 | 27 | (def ^:private boardgame-content-renames 28 | {:yearpublished [:publish-year parse-int] 29 | :minplayers [:min-players parse-int] 30 | :maxplayers [:max-players parse-int] 31 | :playingtime [:playing-time parse-int] 32 | :age [:min-player-age parse-int] 33 | :description [:description str] 34 | :thumbnail [:thumbnail prefix-with-https] 35 | :image [:image prefix-with-https]}) 36 | 37 | (defmulti process-bg-content 38 | (fn [_bg element] 39 | (:tag element))) 40 | 41 | (defmethod process-bg-content :default 42 | [bg element] 43 | (let [tag (:tag element) 44 | [output-key parser] (get boardgame-content-renames tag)] 45 | (if output-key 46 | (assoc bg output-key (-> element :content first parser)) 47 | bg))) 48 | 49 | (defmethod process-bg-content :name 50 | [bg element] 51 | ;; non-primary names are usually translations to other languages 52 | (if (-> element :attrs :primary) 53 | ;; TODO: Trim/reformat loose HTML 54 | (assoc bg :name (-> element :content first)) 55 | bg)) 56 | 57 | (defmethod process-bg-content :boardgamepublisher 58 | [bg element] 59 | (update bg :publisher-ids conj (-> element :attrs :objectid))) 60 | 61 | (defmethod process-bg-content :boardgamedesigner 62 | [bg element] 63 | (update bg :designer-ids conj (-> element :attrs :objectid))) 64 | 65 | (defn ^:private xml->board-game 66 | [element] 67 | (reduce process-bg-content 68 | {:id (-> element :attrs :objectid) 69 | :publisher-ids [] 70 | :designer-ids []} 71 | (:content element))) 72 | 73 | (defn ^:private get-xml 74 | [url query-params] 75 | (log/info (str "BGG Query: " url (when query-params 76 | (str " " (pr-str query-params))))) 77 | (->> (client/get url 78 | {:accept "text/xml" 79 | :query-params query-params 80 | :throw-exceptions false}) 81 | :body 82 | StringReader. 83 | xml/parse)) 84 | 85 | (defn get-board-game 86 | [cache id] 87 | (or (cache/resolve-by-id cache :games id) 88 | (let [game (->> (get-xml (str base-url "/boardgame/" id) nil) 89 | (expect-tag :boardgames) 90 | :content 91 | first 92 | xml->board-game)] 93 | (cache/fill cache :games [game]) 94 | game))) 95 | 96 | (defn search 97 | "Performs a search of matching games by name." 98 | [cache text] 99 | (let [game-ids (->> (get-xml (str base-url "/search") {:search text}) 100 | (expect-tag :boardgames) 101 | :content 102 | (map (comp :objectid :attrs))) 103 | [cached more-ids] (cache/resolve-by-ids cache :games game-ids) 104 | games (when more-ids 105 | (->> (get-xml (str base-url "/boardgame/" (str/join "," more-ids)) nil) 106 | (expect-tag :boardgames) 107 | :content 108 | (map xml->board-game)))] 109 | (cache/fill cache :games games) 110 | (concat cached games))) 111 | 112 | (defn ^:private xml->map 113 | [element keys] 114 | (-> (into {} 115 | (map #(vector (:tag %) 116 | ;; Assumes :content is a single element, a string containing the value. 117 | (-> % :content first))) 118 | (:content element)) 119 | (select-keys keys))) 120 | 121 | (defn ^:private xml->company 122 | [id company-element] 123 | (expect-tag :company company-element) 124 | (-> company-element 125 | (xml->map [:name :description]) 126 | (assoc :id (str id)))) 127 | 128 | (defn ^:private xml->person 129 | [id person-element] 130 | (expect-tag :person person-element) 131 | (-> person-element 132 | (xml->map [:name :description]) 133 | (assoc :id (str id)))) 134 | 135 | (defn publishers 136 | [cache ids] 137 | (let [[cached more-ids] (cache/resolve-by-ids cache :publishers ids) 138 | publishers (when more-ids 139 | (->> (get-xml (str base-url "/boardgamepublisher/" (str/join "," more-ids)) nil) 140 | (expect-tag :companies) 141 | :content 142 | ;; BGG doesn't return the company id in the XML, so we have to 143 | ;; hope it all lines up. Demoware. 144 | (map xml->company more-ids)))] 145 | (cache/fill cache :publishers publishers) 146 | (concat cached publishers))) 147 | 148 | (defn designers 149 | [cache ids] 150 | (let [[cached more-ids] (cache/resolve-by-ids cache :designers ids) 151 | designers (when more-ids 152 | (->> (get-xml (str base-url "/boardgamedesigner/" (str/join "," more-ids)) nil) 153 | (expect-tag :people) 154 | :content 155 | (map xml->person more-ids)))] 156 | (cache/fill cache :designers designers) 157 | (concat cached designers))) 158 | -------------------------------------------------------------------------------- /src/bgg_graphql_proxy/main.clj: -------------------------------------------------------------------------------- 1 | (ns bgg-graphql-proxy.main 2 | (:require 3 | [io.pedestal.http :as http] 4 | [com.walmartlabs.lacinia :refer [execute]] 5 | [bgg-graphql-proxy.schema :refer [bgg-schema]] 6 | [bgg-graphql-proxy.server :refer [pedestal-server]])) 7 | 8 | (defn stop-server 9 | [server] 10 | (http/stop server) 11 | nil) 12 | 13 | (defn start-server 14 | "Creates and starts Pedestal server, ready to handle Graphql (and Graphiql) requests." 15 | [] 16 | (-> (bgg-schema) 17 | pedestal-server 18 | http/start)) 19 | -------------------------------------------------------------------------------- /src/bgg_graphql_proxy/schema.clj: -------------------------------------------------------------------------------- 1 | (ns bgg-graphql-proxy.schema 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.edn :as edn] 5 | [com.walmartlabs.lacinia.schema :as schema] 6 | [com.walmartlabs.lacinia.util :refer [attach-resolvers]] 7 | [bgg-graphql-proxy.client :as client])) 8 | 9 | (defn ^:private resolve-board-game 10 | [context args _value] 11 | ;; TODO: Error handling, including not found 12 | (client/get-board-game (:cache context) (:id args))) 13 | 14 | (defn ^:private resolve-search 15 | [context args _value] 16 | (client/search (:cache context) (:term args))) 17 | 18 | (defn ^:private extract-ids 19 | [board-game key args] 20 | (let [{:keys [limit]} args] 21 | (cond->> (get board-game key) 22 | limit (take limit)))) 23 | 24 | (defn ^:private resolve-game-publishers 25 | [context args board-game] 26 | (client/publishers (:cache context) (extract-ids board-game :publisher-ids args))) 27 | 28 | (defn ^:private resolve-game-designers 29 | [context args board-game] 30 | (client/designers (:cache context) (extract-ids board-game :designer-ids args))) 31 | 32 | (defn bgg-schema 33 | [] 34 | (-> (io/resource "bgg-schema.edn") 35 | slurp 36 | edn/read-string 37 | (attach-resolvers {:resolve-game resolve-board-game 38 | :resolve-search resolve-search 39 | :resolve-game-publishers resolve-game-publishers 40 | :resolve-game-designers resolve-game-designers}) 41 | schema/compile)) 42 | -------------------------------------------------------------------------------- /src/bgg_graphql_proxy/server.clj: -------------------------------------------------------------------------------- 1 | (ns bgg-graphql-proxy.server 2 | (:require 3 | [io.pedestal.http :as http] 4 | [io.pedestal.http.route :as route] 5 | [io.pedestal.interceptor :refer [interceptor]] 6 | [clojure.java.io :as io] 7 | [clojure.data.json :as json] 8 | [com.walmartlabs.lacinia :refer [execute]] 9 | [ring.util.response :as response] 10 | [clojure.string :as str])) 11 | 12 | 13 | (defn ^:private index-handler 14 | "Handles the index request as if it were /graphiql/index.html." 15 | [request] 16 | (response/redirect "/index.html")) 17 | 18 | (defn variable-map 19 | "Reads the `variables` query parameter, which contains a JSON string 20 | for any and all GraphQL variables to be associated with this request. 21 | 22 | Returns a map of the variables (using keyword keys)." 23 | [request] 24 | (let [vars (get-in request [:query-params :variables])] 25 | (if-not (str/blank? vars) 26 | (json/read-str vars :key-fn keyword) 27 | {}))) 28 | 29 | (defn extract-query 30 | [request] 31 | (case (:request-method request) 32 | :get (get-in request [:query-params :query]) 33 | :post (slurp (:body request)) 34 | :else "")) 35 | 36 | (defn ^:private graphql-handler 37 | "Accepts a GraphQL query via GET or POST, and executes the query. 38 | Returns the result as text/json." 39 | [compiled-schema] 40 | (let [context {:cache (atom {})}] 41 | (fn [request] 42 | (let [vars (variable-map request) 43 | query (extract-query request) 44 | result (execute compiled-schema query vars context) 45 | status (if (-> result :errors seq) 46 | 400 47 | 200)] 48 | {:status status 49 | :headers {"Content-Type" "application/json"} 50 | :body (json/write-str result)})))) 51 | 52 | (defn ^:private routes 53 | [compiled-schema] 54 | (let [query-handler (graphql-handler compiled-schema)] 55 | (route/expand-routes 56 | #{["/" :get index-handler :route-name :graphiql-ide-index] 57 | ["/graphql" :post query-handler :route-name :graphql-post] 58 | ["/graphql" :get query-handler :route-name :graphql-get]}))) 59 | 60 | (defn pedestal-server 61 | "Creates and returns server instance, ready to be started." 62 | [compiled-schema] 63 | (http/create-server {:env :dev 64 | ::http/routes (routes compiled-schema) 65 | ::http/resource-path "graphiql" 66 | ::http/port 8888 67 | ::http/type :jetty 68 | ::http/join? false})) 69 | --------------------------------------------------------------------------------