├── .eslintrc ├── .gitignore ├── .npmrc ├── README.md ├── nodemon.json ├── package-lock.json ├── package.json └── src └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "indent": ["warn", 2], 5 | "no-console": "off", 6 | "new-cap": "off", 7 | "class-methods-use-this": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎸 graphql-music 2 | 3 | For this introductory GraphQL workshop, we'll be building an API with music data to support clients that display information about artists, songs, lyrics, tabs (sheet music), and concerts 🎵 4 | 5 | This workshop assumes you already have a basic knowledge of [what GraphQL is](https://www.howtographql.com/basics/0-introduction/) and [how to write code in Node.js](https://codeburst.io/the-only-nodejs-introduction-youll-ever-need-d969a47ef219). 6 | 7 | This workshop typically takes about 2 to 2.5 hours to complete. I've broken it down into sections to make it easier to take breaks and jump back in whenever you're ready. To start at the beginning of any given section, just `git checkout` the branch with that name (i.e. `part1`, `part2`, etc.) 8 | 9 | ## Contents 10 | 11 | ### Setup 12 | 13 | > Starting branch: [master](https://github.com/nathanchapman/graphql-music/tree/master) 14 | 15 | * [Setup](#setup) 16 | * [Organize](#organize) 17 | 18 | ### Part 1 19 | 20 | > Starting branch: [part1](https://github.com/nathanchapman/graphql-music/tree/part1) 21 | 22 | * [Creating your first Query](#creating-your-first-query) 23 | * [Creating your first Resolver](#creating-your-first-resolver) 24 | * [Let's get some Context](#lets-get-some-context) 25 | * [Creating your first Connector](#creating-your-first-connector) 26 | 27 | ### Part 2 28 | 29 | > Starting branch: [part2](https://github.com/nathanchapman/graphql-music/tree/part2) 30 | 31 | * [Song Data](#song-data) 32 | * [Limits](#limits) 33 | * [Graph Relationships](#graph-relationships) 34 | * [Lyrics and Tabs](#lyrics-and-tabs) 35 | 36 | ### Part 3 37 | 38 | > Starting branch: [part3](https://github.com/nathanchapman/graphql-music/tree/part3) 39 | 40 | * [Events](#events) 41 | * [Weather](#weather) 42 | 43 | ### Part 4 44 | 45 | > Starting branch: [part4](https://github.com/nathanchapman/graphql-music/tree/part4) 46 | 47 | * [More Graph Relationships](#more-graph-relationships) 48 | * [N+1 Queries](#n1-queries) 49 | * [DataLoader (Batching & Caching)](#dataloader-batching--caching) 50 | 51 | ### Conclusion 52 | 53 | * [Conclusion](#%F0%9F%9A%80-conclusion) 54 | 55 | ## Setup 56 | 57 | Clone the project & `cd` into it 58 | 59 | ```bash 60 | $ git clone git@github.com:nathanchapman/graphql-music.git 61 | $ cd graphql-music 62 | ``` 63 | 64 | Install the dev dependencies 65 | 66 | ```bash 67 | $ npm install 68 | ``` 69 | 70 | Install [apollo-server](https://github.com/apollographql/apollo-server), [graphql-js](https://github.com/graphql/graphql-js), and [graphql-import](https://github.com/prisma/graphql-import) 🚀 71 | 72 | ```bash 73 | $ npm install apollo-server graphql graphql-import 74 | ``` 75 | 76 | Take a look at the boilerplate code in `src/index.js` 77 | 78 | ```js 79 | const { ApolloServer, gql } = require('apollo-server'); 80 | 81 | const typeDefs = gql` 82 | type Query { 83 | greet(name: String): String! 84 | } 85 | `; 86 | 87 | const resolvers = { 88 | Query: { 89 | greet: (_, { name }) => `Hello ${name || 'World'}`, 90 | }, 91 | }; 92 | 93 | const server = new ApolloServer({ 94 | typeDefs, 95 | resolvers, 96 | }); 97 | 98 | server.listen().then(({ url }) => { 99 | console.log(`🚀 Server ready at ${url}`); 100 | }); 101 | ``` 102 | 103 | At this point, you should be able to run `npm start` to start the server. Your server will automatically restart each time we make changes. Navigate to to see the demo server's Playground. [GraphQL Playground](https://www.apollographql.com/docs/apollo-server/features/graphql-playground.html) is a graphical, interactive, in-browser GraphQL IDE where you can explore the schema, craft queries, and view performance information like tracing. 104 | 105 | At any point during this workshop, you can view the current schema by clicking the `SCHEMA` or `DOCS` buttons on the right side of the Playground. The development server will restart when you make changes to files in the project and Playground will automatically pick those up, so there's no need to refresh the page. 106 | 107 | We can test our demo server by sending our first query in the Playground. 108 | 109 | ```graphql 110 | { 111 | greet 112 | } 113 | ``` 114 | 115 | The response from a GraphQL server will be [JSON](https://www.w3schools.com/whatis/whatis_json.asp) in the same shape as the query you sent. 116 | 117 | ```json 118 | { 119 | "data": { 120 | "greet": "Hello World" 121 | } 122 | } 123 | ``` 124 | 125 | ## Organize 126 | 127 | Let's organize things a little better! 128 | 129 | Go ahead and delete the example code for `typeDefs` and `resolvers` from `src/index.js`. 130 | 131 | Create a folder `src/resolvers` and add an `index.js` to it. 132 | 133 | Create another folder `src/schema` and add a file named `schema.graphql` to it. 134 | 135 | Now we need to import these into our `src/index.js`. 136 | 137 | Your `src/index.js` should look like this: 138 | 139 | ```js 140 | const { ApolloServer } = require('apollo-server'); 141 | const { importSchema } = require('graphql-import'); 142 | const resolvers = require('./resolvers'); 143 | 144 | const typeDefs = importSchema('src/schema/schema.graphql'); 145 | 146 | const server = new ApolloServer({ 147 | typeDefs, 148 | resolvers, 149 | }); 150 | 151 | server.listen().then(({ url }) => { 152 | console.log(`🚀 Server ready at ${url}`); 153 | }); 154 | ``` 155 | 156 | At this point, your changes should be in line with the starting branch for [part1](https://github.com/nathanchapman/graphql-music/tree/part1). 157 | 158 | ## Creating your first Query 159 | 160 | We know our clients will need information about `artist`s. Let's define what an artist is by adding the `Artist` type in a new file `src/schema/artist.graphql`. 161 | 162 | ```graphql 163 | type Artist { 164 | id: ID! 165 | name: String! 166 | url: String 167 | genre: String 168 | } 169 | ``` 170 | 171 | ***Note:*** These fields should be determined by **both** the needs of the clients and capabilities of our backend APIs. 172 | 173 | Now let's add our first Query to `src/schema/schema.graphql` 174 | 175 | ```graphql 176 | # import Artist from 'artist.graphql' 177 | 178 | type Query { 179 | artists(name: String!): [Artist]! 180 | } 181 | ``` 182 | 183 | This query will allow our clients to search for artists and get an array of results! 184 | 185 | Notice the `import` statement in our `schema.graphql` and how we're using `importSchema` in our `src/index.js` file? Both of those come from the [graphql-import](https://github.com/prisma/graphql-import) module we installed earlier. 186 | 187 | There are [several ways to represent a GraphQL schema](https://blog.apollographql.com/three-ways-to-represent-your-graphql-schema-a41f4175100d), including: using the [GraphQL.js](https://github.com/graphql/graphql-js#using-graphqljs) `GraphQLSchema` and `GraphQLObjectType` classes or [GraphQL Schema Definition Language](https://www.prisma.io/blog/graphql-sdl-schema-definition-language-6755bcb9ce51) (SDL). We'll be using GraphQL SDL in this workshop because it's the most popular and arguably easier to read and understand. 188 | 189 | Further, you can represent GraphQL SDL in a number of ways, including: [strings](https://www.apollographql.com/docs/graphql-tools/generate-schema#example), [graphql-tag](https://github.com/apollographql/graphql-tag#gql) (gql), and directly in `.graphql` or `.gql` files. Any of these approaches work just fine, but we'll be using `.graphql` files to keep things simple and reduce the amount of boilerplate code. 190 | 191 | [graphql-import](https://github.com/prisma/graphql-import) gives us the ability to import our schema into JavaScript files using `importSchema` as well as break up our schema into different files by letting us import `.graphql` files into other `.graphql` files. Pretty cool! 192 | 193 | Now we have our type definitions for what an `Artist` is and how to `Query` for one. Awesome! But how do we actually fetch and return data? 194 | 195 | ## Creating your first Resolver 196 | 197 | `Resolvers` are functions that are executed by our server to resolve the data for our schema. The object we create containing these functions will have the same shape as our schema. We can define a resolver for any field on any type, but often times we're able to rely on the [default resolver](https://www.apollographql.com/docs/graphql-tools/resolvers/#default-resolver) for trivial resolutions like returning a named property on an object. 198 | 199 | Add a `Query.artists` resolver to `src/resolvers/index.js` 200 | 201 | ```js 202 | const resolvers = { 203 | Query: { 204 | artists: (_, { name }) => [{ name }], 205 | }, 206 | }; 207 | 208 | module.exports = resolvers; 209 | ``` 210 | 211 | Open the Playground at and send a query for `artists` 212 | 213 | ```graphql 214 | { 215 | artists(name: "Fake") { 216 | name 217 | } 218 | } 219 | ``` 220 | 221 | You'll receive fake data because we're just mocking an array with one object as the return value of the resolver, but now we have something executing! 222 | 223 | *Notice* that if you ask for any non-nullable fields (denoted with a `!` in the schema) like `id`, you'll get an error `Cannot return null for non-nullable field Artist.id`. This is because we aren't returning a value for `id` from our resolver, only the `name`. Asking for normal, nullable fields like `url` and `genre` won't cause an error because they aren't guaranteed by our GraphQL server based on the type definition of `Artist` in the schema. For those nullable fields, you'll just receive `null` when no value is returned from the resolver. 224 | 225 | ## Let's get some Context 226 | 227 | Resolvers take in 4 parameters: `root`, `args`, `context`, and `info`, respectively. 228 | 229 | * `root` the value of the previous execution level (more on `execution levels` later) 230 | * `args` any arguments passed directly into the given field 231 | * `context` an object containing any data that should be made available to all resolvers (think logging functions, session information, data sources used to fetch information, etc.) 232 | * `info` an object containing information about the query such as the selection set, the AST of the query, parent information, etc. This parameter isn't used as often, and I'd consider it as intended for more advanced cases. 233 | 234 | ## Creating your first Connector 235 | 236 | Most GraphQL services follow some sort of `connector` pattern for data access. The idea here is to have a layer on top of a database/backend driver that has GraphQL-specific error handling, logging, batching, and caching. We'll touch more on these topics later. For now, let's just think of it as our sources for fetching data. 237 | 238 | You guessed it! The connector will go on the `context` object passed into all resolvers. 239 | 240 | Let's create a new folder `src/connectors` with an `index.js` 241 | 242 | ```js 243 | const createConnectors = () => ({}); 244 | 245 | module.exports = createConnectors; 246 | ``` 247 | 248 | In our `src/index.js`, let's import that file and update our server to include a new `context` object 249 | 250 | ```js 251 | ... 252 | const createConnectors = require('./connectors'); 253 | 254 | const typeDefs = importSchema('src/schema/schema.graphql'); 255 | const context = { connectors: createConnectors() }; 256 | 257 | const server = new ApolloServer({ 258 | typeDefs, 259 | resolvers, 260 | context, 261 | }); 262 | ... 263 | ``` 264 | 265 | Let's add a new file, `connectors/iTunes.js` 266 | 267 | ```js 268 | class iTunes {} 269 | 270 | module.exports = iTunes; 271 | ``` 272 | 273 | and import it into `connectors/index.js` 274 | 275 | ```js 276 | const iTunes = require('./iTunes'); 277 | 278 | const createConnectors = () => ({ 279 | iTunes: new iTunes(), 280 | }); 281 | 282 | module.exports = createConnectors; 283 | ``` 284 | 285 | We'll need to make an [HTTP request](https://www.codecademy.com/articles/http-requests) to the iTunes API in our `iTunes` connector so we'll be using [got](https://github.com/sindresorhus/got), a simplified HTTP request library with support for [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). 286 | 287 | Let's kill our server with `ctrl+c`, install [got](https://github.com/sindresorhus/got), and start the server back up. 288 | 289 | ```bash 290 | $ npm install got 291 | $ npm start 292 | ``` 293 | 294 | Now we can make asynchronous HTTP requests! 295 | 296 | At the top of `connectors/iTunes.js`, let's `require` the new dependency 297 | 298 | ```js 299 | const { get } = require('got'); 300 | ``` 301 | 302 | And let's add our first method inside the `iTunes` class 303 | 304 | ```js 305 | async artists({ name }) { 306 | const options = { 307 | query: { 308 | term: name, 309 | country: 'us', 310 | entity: 'allArtist', 311 | }, 312 | json: true, 313 | }; 314 | 315 | const { body } = await get('https://itunes.apple.com/search', options); 316 | const { results } = body; 317 | return results.map(artist => ({ 318 | name: artist.artistName, 319 | url: artist.artistLinkUrl, 320 | id: artist.artistId, 321 | genre: artist.primaryGenreName, 322 | })); 323 | } 324 | ``` 325 | 326 | *Notice* that once we get the results, we're remapping the iTunes API results into objects that match our GraphQL type for `Artist`. 327 | 328 | Now we can go back to `resolvers/index.js` and consume this connector from our `context` 329 | 330 | ```js 331 | Query: { 332 | artists: (_, args, ctx) => ctx.connectors.iTunes.artists(args), 333 | }, 334 | ``` 335 | 336 | And that's it! 337 | 338 | You can open the [Playground](http://localhost:4000) again and send a query for `artists`: 339 | 340 | ```graphql 341 | { 342 | artists(name: "The Beatles") { 343 | id 344 | name 345 | url 346 | genre 347 | } 348 | } 349 | ``` 350 | 351 | It works! 😎 352 | 353 | At this point, your changes should be in line with the starting branch for [part2](https://github.com/nathanchapman/graphql-music/tree/part2). 354 | 355 | ## Song Data 356 | 357 | Create a `Song` type in a new file `src/schema/song.graphql`. 358 | 359 | ```graphql 360 | type Song { 361 | id: ID! 362 | name: String! 363 | artistName: String 364 | album: String 365 | url: String 366 | } 367 | ``` 368 | 369 | and add a new `Query` for `songs` in `src/schema/schema.graphql` 370 | 371 | ```graphql 372 | # import Artist from 'artist.graphql' 373 | # import Song from 'song.graphql' 374 | 375 | type Query { 376 | artists(name: String!): [Artist]! 377 | songs(name: String!): [Song]! 378 | } 379 | ``` 380 | 381 | Let's add another method to the `iTunes` connector 382 | 383 | ```js 384 | async songs({ name }) { 385 | const options = { 386 | query: { 387 | term: name, 388 | country: 'us', 389 | entity: 'song', 390 | }, 391 | json: true, 392 | }; 393 | 394 | const { body } = await get('https://itunes.apple.com/search', options); 395 | const { results } = body; 396 | return results.map(song => ({ 397 | name: song.trackName, 398 | artistName: song.artistName, 399 | album: song.collectionName, 400 | url: song.trackViewUrl, 401 | id: song.trackId, 402 | })); 403 | } 404 | ``` 405 | 406 | *Notice* we're remapping the results again since the iTunes API definition of a song isn't exactly the same as the `Song` type definition we're using in our GraphQL API. 407 | 408 | Now we just have to add a resolver for the `songs` query 409 | 410 | ```js 411 | Query: { 412 | artists: (_, args, ctx) => ctx.connectors.iTunes.artists(args), 413 | songs: (_, args, ctx) => ctx.connectors.iTunes.songs(args), 414 | }, 415 | ``` 416 | 417 | Open the [Playground](http://localhost:4000) again and send a query for `songs` 418 | 419 | ```graphql 420 | { 421 | songs(name: "Abbey Road") { 422 | id 423 | name 424 | artistName 425 | album 426 | url 427 | } 428 | } 429 | ``` 430 | 431 | Wow.. there are a lot more results than our clients need to display! This large payload will have to be downloaded and parsed by the client whether they use all of the results or not. Let's fix that! 432 | 433 | ## Limits 434 | 435 | Let's add some limiting to our queries so the clients can specify how many results they need. 436 | 437 | In your `schema`, add limit query parameters with some reasonable defaults 438 | 439 | ```graphql 440 | type Query { 441 | artists(name: String!, limit: Int = 5): [Artist]! 442 | songs(name: String!, limit: Int = 10): [Song]! 443 | } 444 | ``` 445 | 446 | In your `iTunes` connector, add `limit` in **both** the `artists` and `songs` method signatures and to their `options.qs` objects 447 | 448 | ```js 449 | async artists({ name, limit }) { 450 | const options = { 451 | query: { 452 | term: name, 453 | country: 'us', 454 | entity: 'allArtist', 455 | limit, 456 | }, 457 | json: true, 458 | }; 459 | ... 460 | } 461 | ``` 462 | 463 | Now the clients can specify a limit or rely on the defaults we set in our schema 464 | 465 | ```graphql 466 | { 467 | songs(name: "Abbey Road", limit: 1) { 468 | id 469 | name 470 | artistName 471 | album 472 | url 473 | } 474 | } 475 | ``` 476 | 477 | ## Graph Relationships 478 | 479 | Now we can request information about artists and songs, but they're separate. 480 | 481 | Our clients would have to send queries like this for artist info and their songs: 482 | 483 | ```graphql 484 | { 485 | artists(name: "The Beatles", limit: 1) { 486 | id 487 | name 488 | url 489 | genre 490 | } 491 | songs(name: "The Beatles") { 492 | id 493 | name 494 | artistName 495 | album 496 | url 497 | } 498 | } 499 | ``` 500 | 501 | We could improve this slightly by using `query variables` 502 | 503 | ```graphql 504 | query artistsWithSongs($name: String!) { 505 | artists(name: $name, limit: 1) { 506 | id 507 | name 508 | url 509 | genre 510 | } 511 | songs(name: $name) { 512 | id 513 | name 514 | artistName 515 | album 516 | url 517 | } 518 | } 519 | ``` 520 | 521 | ```json 522 | { 523 | "name": "The Beatles" 524 | } 525 | ``` 526 | 527 | But there's still no direct relationship between an `Artist` and their `songs`. 528 | 529 | Shouldn't we be able to query for `songs` under an `artist` and vice versa? 530 | 531 | In your `schema`, add a `songs` field under the `Artist` type 532 | 533 | ```graphql 534 | type Artist { 535 | id: ID! 536 | name: String! 537 | url: String 538 | genre: String 539 | songs(limit: Int = 10): [Song]! 540 | } 541 | ``` 542 | 543 | and in your `resolvers` add a new type resolver object for `Artist` with a resolver for `songs` 544 | 545 | ```js 546 | Query: { 547 | ... 548 | }, 549 | Artist: { 550 | songs: ({ name }, { limit }, ctx) => ( 551 | ctx.connectors.iTunes.songs({ name, limit }) 552 | ), 553 | }, 554 | ``` 555 | 556 | Our `Query.artists` resolver doesn't return the necessary data for `songs`. That's okay! In the next `execution level`, the `Artist.songs` resolver is called on the `Artist` object to fetch this data. 557 | 558 | This new field resolver is almost identical to the `Query.songs` resolver, but the `name` comes from the root object `Artist` (once it's resolved) instead of a field argument. 559 | 560 | Now our clients can send more concise queries for artist info and their songs 561 | 562 | ```graphql 563 | { 564 | artists(name: "The Beatles") { 565 | id 566 | name 567 | url 568 | genre 569 | songs { 570 | id 571 | name 572 | artistName 573 | album 574 | url 575 | } 576 | } 577 | } 578 | ``` 579 | 580 | ## Lyrics and Tabs 581 | 582 | There's a new feature coming out soon and the clients need to get data for lyrics and tabs (sheet music), but neither of those are supported by the iTunes API. 583 | 584 | Go ahead and add these fields to the `Song` type in your `schema` 585 | 586 | ```graphql 587 | type Song { 588 | id: ID! 589 | name: String! 590 | artistName: String 591 | album: String 592 | url: String 593 | lyrics: String 594 | tabs: String 595 | } 596 | ``` 597 | 598 | Add a new file `src/connectors/Lyrics.js` 599 | 600 | ```js 601 | const { get } = require('got'); 602 | 603 | class Lyrics { 604 | async bySong({ name, artistName }) { 605 | const options = { 606 | json: true, 607 | }; 608 | 609 | const url = `https://api.lyrics.ovh/v1/${artistName}/${name}`; 610 | try { 611 | const { body } = await get(url, options); 612 | return body.lyrics; 613 | } catch (error) { 614 | return null; 615 | } 616 | } 617 | } 618 | 619 | module.exports = Lyrics; 620 | ``` 621 | 622 | Let's import it in `connectors/index.js` 623 | 624 | ```js 625 | ... 626 | const Lyrics = require('./Lyrics'); 627 | 628 | const createConnectors = () => ({ 629 | ... 630 | Lyrics: new Lyrics(), 631 | }); 632 | 633 | module.exports = createConnectors; 634 | ``` 635 | 636 | and in your `resolvers`, add a new root type resolver object for `Song` with a field resolver for `lyrics` 637 | 638 | ```js 639 | Query: { 640 | ... 641 | }, 642 | Artist: { 643 | ... 644 | }, 645 | Song: { 646 | lyrics: (song, _, ctx) => ctx.connectors.Lyrics.bySong(song), 647 | }, 648 | ``` 649 | 650 | We have lyrics! 🎤 651 | 652 | What about tabs? 653 | 654 | [Songterr](https://www.songsterr.com/) provides tabs and an API, but they also have direct URLs we can use for loading sheet music by artist name and song name. That's all our clients needed! In this case, we don't even need a connector or an API call. 655 | 656 | Just add a field resolver for `tabs` under the `Song` object 657 | 658 | ```js 659 | ... 660 | Song: { 661 | lyrics: (song, _, ctx) => ctx.connectors.Lyrics.bySong(song), 662 | tabs: ({ name, artistName }) => ( 663 | `http://www.songsterr.com/a/wa/bestMatchForQueryString?s=${name}&a=${artistName}` 664 | ), 665 | }, 666 | ``` 667 | 668 | Open the [Playground](http://localhost:4000) again and send a query for `songs` with lyrics and tabs 669 | 670 | ```graphql 671 | { 672 | songs(name: "Here Comes The Sun", limit: 1) { 673 | id 674 | name 675 | artistName 676 | album 677 | url 678 | lyrics 679 | tabs 680 | } 681 | } 682 | ``` 683 | 684 | 🎼🎼🎼🎼 685 | 686 | At this point, your changes should be in line with the starting branch for [part3](https://github.com/nathanchapman/graphql-music/tree/part3). 687 | 688 | ## Events 689 | 690 | Let's add some `Event`-related types to our `schema` (think concerts, festivals, etc.) 691 | 692 | > src/schema/event.graphql 693 | 694 | ```graphql 695 | # import Ticket from 'ticket.graphql' 696 | # import Venue from 'venue.graphql' 697 | 698 | type Event { 699 | date: String! 700 | time: String! 701 | venue: Venue 702 | tickets: Ticket 703 | lineup: [String] 704 | } 705 | ``` 706 | 707 | > src/schema/ticket.graphql 708 | 709 | ```graphql 710 | type Ticket { 711 | status: String 712 | url: String 713 | } 714 | ``` 715 | 716 | > src/schema/venue.graphql 717 | 718 | ```graphql 719 | type Venue { 720 | name: String 721 | latitude: String 722 | longitude: String 723 | city: String 724 | region: String 725 | country: String 726 | } 727 | ``` 728 | 729 | and add an `events` field under the `Artist` type 730 | 731 | ```graphql 732 | # import Event from 'event.graphql' 733 | 734 | type Artist { 735 | id: ID! 736 | name: String! 737 | url: String 738 | genre: String 739 | songs(limit: Int = 10): [Song]! 740 | events(limit: Int = 10): [Event] 741 | } 742 | ``` 743 | 744 | We'll need a connector for event data. For this we'll be using the [BandsInTown API](https://app.swaggerhub.com/apis/Bandsintown/PublicAPI/3.0.0#/). 745 | 746 | Add a new file `connectors/BandsInTown.js` 747 | 748 | ```js 749 | const { get } = require('got'); 750 | 751 | class BandsInTown { 752 | async events({ name, limit }) { 753 | const options = { 754 | json: true, 755 | }; 756 | 757 | const url = `https://rest.bandsintown.com/artists/${name}/events?app_id=qfasdfasdf`; 758 | const { body } = await get(url, options); 759 | return body.slice(0, limit); 760 | } 761 | } 762 | 763 | module.exports = BandsInTown; 764 | ``` 765 | 766 | and import it in `connectors/index.js` 767 | 768 | ```js 769 | ... 770 | const BandsInTown = require('./BandsInTown'); 771 | 772 | const createConnectors = () => ({ 773 | ... 774 | BandsInTown: new BandsInTown(), 775 | }); 776 | 777 | module.exports = createConnectors; 778 | ``` 779 | 780 | Add the `events` field resolver under the `Artist` root object 781 | 782 | ```js 783 | ... 784 | Artist: { 785 | ... 786 | events: ({ name }, { limit }, ctx) => ( 787 | ctx.connectors.BandsInTown.events({ name, limit }) 788 | ), 789 | }, 790 | ... 791 | ``` 792 | 793 | Open the [Playground](http://localhost:4000) again and send a query for `artists` with events 794 | 795 | ```graphql 796 | { 797 | artists(name: "Blink-182", limit: 1) { 798 | name 799 | events { 800 | date 801 | time 802 | venue { 803 | name 804 | latitude 805 | longitude 806 | city 807 | region 808 | country 809 | } 810 | tickets { 811 | status 812 | url 813 | } 814 | lineup 815 | } 816 | } 817 | } 818 | ``` 819 | 820 | Uh oh! We got a `Cannot return null for non-nullable field Event.date` error from our GraphQL server. 821 | 822 | It looks like both `date` and `time` are `null` even though we're guaranteeing them as non-nullable in our schema (denoted with a `!`) 823 | 824 | What caused the `null` values? 825 | 826 | The BandsInTown API returned an object with a field named `datetime` instead of two different fields for `date` and `time`. Let's resolve the time and date from `datetime` using field resolvers on the `Event` type so our clients have the data they need. 827 | 828 | ```js 829 | ... 830 | Event: { 831 | time: event => new Date(event.datetime).toLocaleTimeString(), 832 | date: event => new Date(event.datetime).toLocaleDateString(), 833 | }, 834 | ``` 835 | 836 | That's fixed, but `tickets` is `null`! 837 | 838 | The response from `BandsInTown` has an `offers` array instead. 839 | 840 | ```js 841 | Event: { 842 | ... 843 | tickets: event => event.offers.find(offer => offer.type === 'Tickets'), 844 | }, 845 | ``` 846 | 847 | No more errors or `null` — awesome! 848 | 849 | ## Weather 850 | 851 | Now let's add some `Weather` types to our `schema` so we can fetch the weather conditions on the day of an `Event` 852 | 853 | > src/schema/weather.graphql 854 | 855 | ```graphql 856 | type Weather { 857 | condition: String 858 | temperature(unit: TemperatureUnit = F): Temperature 859 | } 860 | 861 | type Temperature { 862 | high: Int 863 | low: Int 864 | unit: TemperatureUnit 865 | } 866 | 867 | enum TemperatureUnit { 868 | C 869 | F 870 | } 871 | ``` 872 | 873 | Under the `Event` type, add a `weather` field 874 | 875 | ```graphql 876 | # import Ticket from 'ticket.graphql' 877 | # import Venue from 'venue.graphql' 878 | # import * from 'weather.graphql' 879 | 880 | type Event { 881 | date: String! 882 | time: String! 883 | venue: Venue 884 | tickets: Ticket 885 | lineup: [String] 886 | weather: Weather 887 | } 888 | ``` 889 | 890 | This workshop used to consume the Yahoo Weather API until the public version was removed in January 2019. It was here that I'd point out that Yahoo went as far as to create their own custom query language for interacting with their APIs called `yql`. 891 | 892 | You'd pass the `yql` as part of the URL query string like so 893 | 894 | ```js 895 | const url = 'https://query.yahooapis.com/v1/public/yql?q=' 896 | .concat('select * from weather.forecast where woeid in ') 897 | .concat(`(select woeid from geo.places(1) where text="${city}, ${region}") `) 898 | .concat(`and u='${unit.toLowerCase()}'&format=json`) 899 | .concat('&env=store://datatables.org/alltableswithkeys'), 900 | ``` 901 | 902 | 🤦‍ Not ideal... 903 | 904 | Any ideas for a technology that would greatly simplify their API? 💡 905 | 906 | All we're asking for is forecast data for a `city` and `region`. 907 | 908 | What if we could send them a GraphQL query instead? 909 | 910 | ```graphql 911 | { 912 | forecast(city: $city, region: $region, unit: $unit) { 913 | high 914 | low 915 | condition 916 | } 917 | } 918 | ``` 919 | 920 | Maybe some day.. 🤞 921 | 922 | We'll use [MetaWeather](https://www.metaweather.com/api/) instead. It's a bit easier to work with anyways. 923 | 924 | Add a new connector `connectors/Weather.js` 925 | 926 | ```js 927 | const { get } = require('got'); 928 | 929 | const format = weather => ({ 930 | condition: weather.weather_state_name, 931 | high: weather.max_temp, 932 | low: weather.min_temp, 933 | }); 934 | 935 | class Weather { 936 | async forecast({ datetime, venue }) { 937 | const date = new Date(datetime); 938 | const [year, month, day] = [ 939 | date.getFullYear(), date.getMonth() + 1, date.getDate(), 940 | ]; 941 | 942 | const { latitude, longitude } = venue; 943 | 944 | const location = { 945 | query: { 946 | lattlong: `${latitude},${longitude}`, 947 | }, 948 | json: true, 949 | }; 950 | 951 | const { 952 | body: [{ woeid }], // use the first city woeid returned from the search 953 | } = await get('https://www.metaweather.com/api/location/search/', location); 954 | 955 | const options = { json: true }; 956 | const weather = y => m => d => `https://www.metaweather.com/api/location/${woeid}/${y}/${m}/${d}/`; 957 | 958 | // Forecasts only work 5-10 days in the future 959 | const { body: [forecasted] } = await get( 960 | weather(year)(month)(day), 961 | options, 962 | ); 963 | 964 | if (forecasted) return format(forecasted); 965 | 966 | // Fallback to last year's weather report 967 | const { body: [historical] } = await get( 968 | weather(year-1)(month)(day), 969 | options, 970 | ); 971 | 972 | if (historical) return format(historical); 973 | 974 | throw new Error('Unable to retrieve weather data for event'); 975 | } 976 | } 977 | 978 | module.exports = Weather; 979 | ``` 980 | 981 | Initialize your `Weather` connector in `connectors/index.js` like you've done with the other connectors 982 | 983 | ```js 984 | ... 985 | const Weather = require('./Weather'); 986 | 987 | const createConnectors = () => ({ 988 | ... 989 | Weather: new Weather(), 990 | }); 991 | 992 | module.exports = createConnectors; 993 | ``` 994 | 995 | Add a field resolver for `weather` under the `Event` object 996 | 997 | ```js 998 | Event: { 999 | ... 1000 | weather: ({ datetime, venue }, _, ctx) => ( 1001 | ctx.connectors.Weather.forecast({ datetime, venue }) 1002 | ), 1003 | }, 1004 | ``` 1005 | 1006 | *Notice* we're using the `datetime` from the root object (`Event` returned by BandsInTown) even though we didn't publicly expose that field in our GraphQL API. GraphQL gives us the entire object returned from the previous execution level as `root` (the first argument). This is a great way to pass data from a root object to the next execution level without exposing the implementation details of your API! 1007 | 1008 | Open the [Playground](http://localhost:4000) again and send a query for `artists` with `events` and `weather` 1009 | 1010 | ```graphql 1011 | { 1012 | artists(name: "Blink-182", limit: 1) { 1013 | name 1014 | events(limit: 1) { 1015 | weather { 1016 | temperature { 1017 | high 1018 | low 1019 | unit 1020 | } 1021 | condition 1022 | } 1023 | date 1024 | time 1025 | venue { 1026 | name 1027 | latitude 1028 | longitude 1029 | city 1030 | region 1031 | country 1032 | } 1033 | tickets { 1034 | status 1035 | url 1036 | } 1037 | lineup 1038 | } 1039 | } 1040 | } 1041 | ``` 1042 | 1043 | `temperature` is `null` 😧 1044 | 1045 | We're returning the temperatures in our Weather connector as `Weather.high` and `Weather.low` (both in Celsius from the MetaWeather API). This doesn't quite line up with our schema! They should be under the `Weather.temperature` object and return the correct values for both Celsius and Farenheit. Let's fix it by adding a resolver for `Weather.temperature` that uses the `high` and `low` fields from the `Weather` object and handles conversion. 1046 | 1047 | ```js 1048 | ... 1049 | Weather: { 1050 | temperature: ({ high, low }, { unit }) => { 1051 | const fahrenheit = c => c * 9 / 5 + 32 1052 | const h = unit === 'C' ? high : fahrenheit(high); 1053 | const l = unit === 'C' ? low : fahrenheit(low); 1054 | 1055 | return { 1056 | unit, 1057 | high: Math.round(h), 1058 | low: Math.round(l), 1059 | }; 1060 | }, 1061 | }, 1062 | ``` 1063 | 1064 | Try the query again and make sure to prepare for the weather! ☔️⛅️😎 1065 | 1066 | Our clients can still get `null` for `weather`, but that's only if the MetaWeather API fails to return both the forecast and historical data. This is a lot less likely, but if it does fail, the clients will also get an error telling them about the issue. 1067 | 1068 | ```json 1069 | { 1070 | "data": { 1071 | "artists": [ 1072 | { 1073 | "name": "Blink-182", 1074 | "events": [ 1075 | { 1076 | "weather": null, 1077 | "date": "2019-6-12" 1078 | } 1079 | ] 1080 | } 1081 | ] 1082 | }, 1083 | "errors": [ 1084 | { 1085 | "message": "Unable to retrieve weather data for event", 1086 | "locations": [ 1087 | { 1088 | "line": 5, 1089 | "column": 7 1090 | } 1091 | ], 1092 | "path": ["artists", 0, "events", 0, "weather"], 1093 | "extensions": { 1094 | "code": "INTERNAL_SERVER_ERROR", 1095 | "exception": { 1096 | "stacktrace": [ 1097 | "Error: Unable to retrieve weather data for event" 1098 | ] 1099 | } 1100 | } 1101 | } 1102 | ], 1103 | } 1104 | ``` 1105 | 1106 | Errors are very useful! We can use errors to intelligently inform our clients about issues with provided inputs, API degredations, and many other types of issues. Remember to not expose sensitive information like unexpected errors, stacktraces, etc. in production! See [Apollo's Error handling guide](https://www.apollographql.com/docs/apollo-server/features/errors/) for more information. 1107 | 1108 | At this point, your changes should be in line with the starting branch for [part4](https://github.com/nathanchapman/graphql-music/tree/part4). 1109 | 1110 | ## More Graph Relationships 1111 | 1112 | Our clients want to be able to search for songs and get more artist information back than just `artistName`. 1113 | 1114 | Remember earlier when we set up a graph relationship between artists and their songs? 1115 | 1116 | We should create a similar relationship between a `Song` and its `Artist`. 1117 | 1118 | In your `schema`, add a field for `artist` under the `Song` type 1119 | 1120 | ```graphql 1121 | type Song { 1122 | ... 1123 | artist: Artist 1124 | } 1125 | ``` 1126 | 1127 | We'll need to modify our `songs` method in our `iTunes` connector to return the artist's ID 1128 | 1129 | ```js 1130 | return { 1131 | ... 1132 | artistId: song.artistId, 1133 | }; 1134 | ``` 1135 | 1136 | Let's add another method to our `iTunes` connector to lookup an artist by ID 1137 | 1138 | ```js 1139 | async artist({ id }) { 1140 | const options = { 1141 | query: { id }, 1142 | json: true, 1143 | }; 1144 | 1145 | console.log(`looking up artist ${id}`); 1146 | 1147 | const { body } = await get('https://itunes.apple.com/lookup', options); 1148 | const artist = body.results[0]; 1149 | 1150 | return { 1151 | name: artist.artistName, 1152 | url: artist.artistLinkUrl, 1153 | id: artist.artistId, 1154 | genre: artist.primaryGenreName, 1155 | }; 1156 | } 1157 | ``` 1158 | 1159 | and add a field `resolver` for `artist` under the `Song` root object 1160 | 1161 | ```js 1162 | Song: { 1163 | ... 1164 | artist: ({ artistId }, _, ctx) => ( 1165 | ctx.connectors.iTunes.artist({ id: artistId }) 1166 | ), 1167 | }, 1168 | ``` 1169 | 1170 | Lastly, let's deprecate the old `artistName` field in our `schema` so that new clients won't know about that field. It will still work as expected for older clients that may still be requesting it and you should keep it around until you can confirm it's not being called anymore (think mobile apps that haven't been updated yet!) 1171 | 1172 | ```graphql 1173 | type Song { 1174 | ... 1175 | artist: Artist 1176 | artistName: String @deprecated(reason: "Use `artist.name`.") 1177 | ... 1178 | } 1179 | ``` 1180 | 1181 | Open the [Playground](http://localhost:4000) again and send a query for `songs` with artist details 1182 | 1183 | ```graphql 1184 | { 1185 | songs(name: "Sun") { 1186 | id 1187 | name 1188 | artistName 1189 | album 1190 | url 1191 | artist { 1192 | id 1193 | name 1194 | url 1195 | genre 1196 | } 1197 | } 1198 | } 1199 | ``` 1200 | 1201 | You should notice a new deprecation warning on the `artistName` field. 1202 | 1203 | We also have a working graph relationship between songs and artists — awesome! 1204 | 1205 | ...But look at your `console`. Notice any duplicates? 1206 | 1207 | This means we're fetching the same data multiple times from the iTunes API. This can overload your backend APIs and will cause your clients to spend additional time waiting for a response. 1208 | 1209 | Let's turn on `tracing` to keep an eye on the performance as we try to fix this. 1210 | 1211 | In `src/index.js`, let's set `tracing: true` in the server configuration 1212 | 1213 | ```js 1214 | ... 1215 | const server = new ApolloServer({ 1216 | tracing: true, 1217 | typeDefs, 1218 | resolvers, 1219 | context, 1220 | }); 1221 | ``` 1222 | 1223 | Now back in `Playground`, click the `TRACING` tab in the bottom right corner and run your query again. Here we can see exactly how long it took for each resolver to run. 1224 | 1225 | ## N+1 Queries 1226 | 1227 | Each `artist` call might take about `170ms` depending on your Internet speed. For these 10 results, we're making 7 unnecessary calls. These will likely fire off concurrently since they're in the same execution level, but it's possible they could add an additional `1.2s` or more to the response time if we overload the API we're calling. Yikes! 1228 | 1229 | Imagine how much worse this becomes when we change the `songs` search limit to `100`, `1000`, etc. 1230 | 1231 | What if we query for all songs on The Beatles' album `Abbey Road`? The `artist` resolver for `Song` will be calling the iTunes API **17** times for the exact same artist ID. 1232 | 1233 | This could definitely cause performance issues for both our clients and our backend services, databases, etc. How can we fix this? 1234 | 1235 | ## DataLoader (Batching & Caching) 1236 | 1237 | [DataLoader](https://github.com/facebook/dataloader#using-with-graphql) will coalesce all individual `load`s which occur within a single frame of execution (a single tick of the event loop) and then call your batch function with all requested keys. The result is cached on the request, so additional calls to `load` for the same key on the same request will return the cached value. 1238 | 1239 | Let's kill our server to install `dataloader` and start it back up 1240 | 1241 | ```bash 1242 | $ npm install dataloader 1243 | $ npm start 1244 | ``` 1245 | 1246 | In `src/index.js`, we'll want to `require` dataloader at the top 1247 | 1248 | ```js 1249 | const DataLoader = require('dataloader'); 1250 | ``` 1251 | 1252 | We'll also want to change our `context` to include a `loaders` field so they can be used in all `resolvers`. 1253 | 1254 | Our `context` is just a static object, but we'll need a new context to be generated for each request so our cache isn't [held across requests](https://github.com/facebook/dataloader#creating-a-new-dataloader-per-request). This is generally a good idea whether you're using DataLoaders or not. You might want to have a cache in your connectors themselves, but those caches generally shouldn't be shared across requests or between different users. So let's make `context` a function! 1255 | 1256 | ```js 1257 | const context = () => { 1258 | const connectors = createConnectors(); 1259 | const loaders = {}; 1260 | 1261 | return { connectors, loaders }; 1262 | }; 1263 | 1264 | const server = new ApolloServer({ 1265 | tracing: true, 1266 | typeDefs, 1267 | resolvers, 1268 | context, 1269 | }); 1270 | ``` 1271 | 1272 | and we'll create our first loader for `artist` 1273 | 1274 | ```js 1275 | const context = () => { 1276 | const connectors = createConnectors(); 1277 | const loaders = { 1278 | artist: new DataLoader(IDs => Promise.resolve( 1279 | IDs.map(id => connectors.iTunes.artist({ id })), 1280 | )), 1281 | }; 1282 | 1283 | return { connectors, loaders }; 1284 | }; 1285 | ``` 1286 | 1287 | Now let's modify our `artist` field resolver under the `Song` root object to use the loader 1288 | 1289 | ```js 1290 | Song: { 1291 | ... 1292 | artist: ({ artistId }, _, ctx) => ( 1293 | ctx.loaders.artist.load(artistId) 1294 | ), 1295 | }, 1296 | ``` 1297 | 1298 | Open the [Playground](http://localhost:4000) again and send the same query for `songs` with artist details 1299 | 1300 | ```graphql 1301 | { 1302 | songs(name: "Sun") { 1303 | id 1304 | name 1305 | album 1306 | url 1307 | artist { 1308 | id 1309 | name 1310 | url 1311 | genre 1312 | } 1313 | } 1314 | } 1315 | ``` 1316 | 1317 | Each artist ID should only be looked up once! 🎉 1318 | 1319 | We can also solve this problem using a memoization cache instead of a DataLoader. [Apollo](https://www.apollographql.com/) built the [RESTDataSource](https://www.apollographql.com/docs/apollo-server/features/data-sources) to use a memoization cache on `GET` requests in order to solve this problem and I think it's more straightforward than using a DataLoader. We would just need to rewrite our connectors to `extend RESTDataSource`. I'll leave that exercise up to you! 1320 | 1321 | Even with a RESTDataSource, a DataLoader is still useful for [batching requests](https://www.apollographql.com/docs/apollo-server/features/data-sources/#batching) to APIs that support a batch endpoint (something like `getArtistsByIDs`). 1322 | 1323 | ## 🚀 Conclusion 1324 | 1325 | You've reached the end of this introductory GraphQL workshop. 1326 | 1327 | How do you feel? Heck, I'm proud of you! 1328 | 1329 | Today you learned about: 1330 | 1331 | * GraphQL servers (Apollo) 1332 | * GraphQL tools (Playground, tracing, graphql-import) 1333 | * Organizing GraphQL projects 1334 | * Queries 1335 | * Schema / Types 1336 | * Resolvers 1337 | * Context 1338 | * Connectors (and making HTTP requests) 1339 | * Execution levels (and passing data down) 1340 | * Field Arguments 1341 | * Query Variables 1342 | * Nullability 1343 | * Throwing Errors 1344 | * Deprecating Fields 1345 | * Graph Relationships 1346 | * Naive Pitfalls (N+1 Queries) 1347 | * ... and how to solve those with DataLoaders or RESTDataSource 1348 | 1349 | You can see the entire completed API project in the [complete branch](https://github.com/nathanchapman/graphql-music/tree/complete). 1350 | 1351 | If you liked this workshop, please give it a ⭐️ and [follow me](https://github.com/nathanchapman) for more content like this! 1352 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "js,json,graphql,gql" 3 | } 4 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-music", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.0.0", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", 10 | "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.0.0" 14 | } 15 | }, 16 | "@babel/highlight": { 17 | "version": "7.0.0", 18 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", 19 | "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", 20 | "dev": true, 21 | "requires": { 22 | "chalk": "^2.0.0", 23 | "esutils": "^2.0.2", 24 | "js-tokens": "^4.0.0" 25 | } 26 | }, 27 | "acorn": { 28 | "version": "6.1.1", 29 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", 30 | "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", 31 | "dev": true 32 | }, 33 | "acorn-jsx": { 34 | "version": "5.0.1", 35 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", 36 | "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", 37 | "dev": true 38 | }, 39 | "ajv": { 40 | "version": "6.10.0", 41 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", 42 | "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", 43 | "dev": true, 44 | "requires": { 45 | "fast-deep-equal": "^2.0.1", 46 | "fast-json-stable-stringify": "^2.0.0", 47 | "json-schema-traverse": "^0.4.1", 48 | "uri-js": "^4.2.2" 49 | } 50 | }, 51 | "ansi-escapes": { 52 | "version": "3.2.0", 53 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", 54 | "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", 55 | "dev": true 56 | }, 57 | "ansi-regex": { 58 | "version": "3.0.1", 59 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", 60 | "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", 61 | "dev": true 62 | }, 63 | "ansi-styles": { 64 | "version": "3.2.1", 65 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 66 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 67 | "dev": true, 68 | "requires": { 69 | "color-convert": "^1.9.0" 70 | } 71 | }, 72 | "anymatch": { 73 | "version": "3.1.3", 74 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 75 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 76 | "dev": true, 77 | "requires": { 78 | "normalize-path": "^3.0.0", 79 | "picomatch": "^2.0.4" 80 | } 81 | }, 82 | "argparse": { 83 | "version": "1.0.10", 84 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 85 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 86 | "dev": true, 87 | "requires": { 88 | "sprintf-js": "~1.0.2" 89 | } 90 | }, 91 | "aria-query": { 92 | "version": "3.0.0", 93 | "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", 94 | "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", 95 | "dev": true, 96 | "requires": { 97 | "ast-types-flow": "0.0.7", 98 | "commander": "^2.11.0" 99 | } 100 | }, 101 | "array-includes": { 102 | "version": "3.0.3", 103 | "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", 104 | "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", 105 | "dev": true, 106 | "requires": { 107 | "define-properties": "^1.1.2", 108 | "es-abstract": "^1.7.0" 109 | } 110 | }, 111 | "ast-types-flow": { 112 | "version": "0.0.7", 113 | "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", 114 | "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", 115 | "dev": true 116 | }, 117 | "astral-regex": { 118 | "version": "1.0.0", 119 | "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", 120 | "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", 121 | "dev": true 122 | }, 123 | "axobject-query": { 124 | "version": "2.0.2", 125 | "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", 126 | "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", 127 | "dev": true, 128 | "requires": { 129 | "ast-types-flow": "0.0.7" 130 | } 131 | }, 132 | "balanced-match": { 133 | "version": "1.0.0", 134 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 135 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 136 | "dev": true 137 | }, 138 | "binary-extensions": { 139 | "version": "2.3.0", 140 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 141 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 142 | "dev": true 143 | }, 144 | "brace-expansion": { 145 | "version": "1.1.11", 146 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 147 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 148 | "dev": true, 149 | "requires": { 150 | "balanced-match": "^1.0.0", 151 | "concat-map": "0.0.1" 152 | } 153 | }, 154 | "braces": { 155 | "version": "3.0.3", 156 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 157 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 158 | "dev": true, 159 | "requires": { 160 | "fill-range": "^7.1.1" 161 | } 162 | }, 163 | "callsites": { 164 | "version": "3.1.0", 165 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 166 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 167 | "dev": true 168 | }, 169 | "chalk": { 170 | "version": "2.4.2", 171 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 172 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 173 | "dev": true, 174 | "requires": { 175 | "ansi-styles": "^3.2.1", 176 | "escape-string-regexp": "^1.0.5", 177 | "supports-color": "^5.3.0" 178 | } 179 | }, 180 | "chardet": { 181 | "version": "0.7.0", 182 | "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", 183 | "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", 184 | "dev": true 185 | }, 186 | "chokidar": { 187 | "version": "3.6.0", 188 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 189 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 190 | "dev": true, 191 | "requires": { 192 | "anymatch": "~3.1.2", 193 | "braces": "~3.0.2", 194 | "fsevents": "~2.3.2", 195 | "glob-parent": "~5.1.2", 196 | "is-binary-path": "~2.1.0", 197 | "is-glob": "~4.0.1", 198 | "normalize-path": "~3.0.0", 199 | "readdirp": "~3.6.0" 200 | } 201 | }, 202 | "cli-cursor": { 203 | "version": "2.1.0", 204 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", 205 | "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", 206 | "dev": true, 207 | "requires": { 208 | "restore-cursor": "^2.0.0" 209 | } 210 | }, 211 | "cli-width": { 212 | "version": "2.2.0", 213 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", 214 | "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", 215 | "dev": true 216 | }, 217 | "color-convert": { 218 | "version": "1.9.3", 219 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 220 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 221 | "dev": true, 222 | "requires": { 223 | "color-name": "1.1.3" 224 | } 225 | }, 226 | "color-name": { 227 | "version": "1.1.3", 228 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 229 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 230 | "dev": true 231 | }, 232 | "commander": { 233 | "version": "2.20.0", 234 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", 235 | "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", 236 | "dev": true 237 | }, 238 | "concat-map": { 239 | "version": "0.0.1", 240 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 241 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 242 | "dev": true 243 | }, 244 | "contains-path": { 245 | "version": "0.1.0", 246 | "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", 247 | "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", 248 | "dev": true 249 | }, 250 | "cross-spawn": { 251 | "version": "6.0.5", 252 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 253 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 254 | "dev": true, 255 | "requires": { 256 | "nice-try": "^1.0.4", 257 | "path-key": "^2.0.1", 258 | "semver": "^5.5.0", 259 | "shebang-command": "^1.2.0", 260 | "which": "^1.2.9" 261 | } 262 | }, 263 | "damerau-levenshtein": { 264 | "version": "1.0.4", 265 | "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", 266 | "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=", 267 | "dev": true 268 | }, 269 | "debug": { 270 | "version": "4.1.1", 271 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 272 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 273 | "dev": true, 274 | "requires": { 275 | "ms": "^2.1.1" 276 | } 277 | }, 278 | "deep-is": { 279 | "version": "0.1.3", 280 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 281 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", 282 | "dev": true 283 | }, 284 | "define-properties": { 285 | "version": "1.1.3", 286 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 287 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 288 | "dev": true, 289 | "requires": { 290 | "object-keys": "^1.0.12" 291 | } 292 | }, 293 | "doctrine": { 294 | "version": "1.5.0", 295 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", 296 | "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", 297 | "dev": true, 298 | "requires": { 299 | "esutils": "^2.0.2", 300 | "isarray": "^1.0.0" 301 | } 302 | }, 303 | "emoji-regex": { 304 | "version": "7.0.3", 305 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 306 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 307 | "dev": true 308 | }, 309 | "error-ex": { 310 | "version": "1.3.2", 311 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", 312 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", 313 | "dev": true, 314 | "requires": { 315 | "is-arrayish": "^0.2.1" 316 | } 317 | }, 318 | "es-abstract": { 319 | "version": "1.13.0", 320 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", 321 | "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", 322 | "dev": true, 323 | "requires": { 324 | "es-to-primitive": "^1.2.0", 325 | "function-bind": "^1.1.1", 326 | "has": "^1.0.3", 327 | "is-callable": "^1.1.4", 328 | "is-regex": "^1.0.4", 329 | "object-keys": "^1.0.12" 330 | } 331 | }, 332 | "es-to-primitive": { 333 | "version": "1.2.0", 334 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", 335 | "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", 336 | "dev": true, 337 | "requires": { 338 | "is-callable": "^1.1.4", 339 | "is-date-object": "^1.0.1", 340 | "is-symbol": "^1.0.2" 341 | } 342 | }, 343 | "escape-string-regexp": { 344 | "version": "1.0.5", 345 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 346 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 347 | "dev": true 348 | }, 349 | "eslint": { 350 | "version": "5.16.0", 351 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", 352 | "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", 353 | "dev": true, 354 | "requires": { 355 | "@babel/code-frame": "^7.0.0", 356 | "ajv": "^6.9.1", 357 | "chalk": "^2.1.0", 358 | "cross-spawn": "^6.0.5", 359 | "debug": "^4.0.1", 360 | "doctrine": "^3.0.0", 361 | "eslint-scope": "^4.0.3", 362 | "eslint-utils": "^1.3.1", 363 | "eslint-visitor-keys": "^1.0.0", 364 | "espree": "^5.0.1", 365 | "esquery": "^1.0.1", 366 | "esutils": "^2.0.2", 367 | "file-entry-cache": "^5.0.1", 368 | "functional-red-black-tree": "^1.0.1", 369 | "glob": "^7.1.2", 370 | "globals": "^11.7.0", 371 | "ignore": "^4.0.6", 372 | "import-fresh": "^3.0.0", 373 | "imurmurhash": "^0.1.4", 374 | "inquirer": "^6.2.2", 375 | "js-yaml": "^3.13.0", 376 | "json-stable-stringify-without-jsonify": "^1.0.1", 377 | "levn": "^0.3.0", 378 | "lodash": "^4.17.11", 379 | "minimatch": "^3.0.4", 380 | "mkdirp": "^0.5.1", 381 | "natural-compare": "^1.4.0", 382 | "optionator": "^0.8.2", 383 | "path-is-inside": "^1.0.2", 384 | "progress": "^2.0.0", 385 | "regexpp": "^2.0.1", 386 | "semver": "^5.5.1", 387 | "strip-ansi": "^4.0.0", 388 | "strip-json-comments": "^2.0.1", 389 | "table": "^5.2.3", 390 | "text-table": "^0.2.0" 391 | }, 392 | "dependencies": { 393 | "doctrine": { 394 | "version": "3.0.0", 395 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 396 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 397 | "dev": true, 398 | "requires": { 399 | "esutils": "^2.0.2" 400 | } 401 | } 402 | } 403 | }, 404 | "eslint-config-airbnb": { 405 | "version": "17.1.0", 406 | "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-17.1.0.tgz", 407 | "integrity": "sha512-R9jw28hFfEQnpPau01NO5K/JWMGLi6aymiF6RsnMURjTk+MqZKllCqGK/0tOvHkPi/NWSSOU2Ced/GX++YxLnw==", 408 | "dev": true, 409 | "requires": { 410 | "eslint-config-airbnb-base": "^13.1.0", 411 | "object.assign": "^4.1.0", 412 | "object.entries": "^1.0.4" 413 | } 414 | }, 415 | "eslint-config-airbnb-base": { 416 | "version": "13.1.0", 417 | "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.1.0.tgz", 418 | "integrity": "sha512-XWwQtf3U3zIoKO1BbHh6aUhJZQweOwSt4c2JrPDg9FP3Ltv3+YfEv7jIDB8275tVnO/qOHbfuYg3kzw6Je7uWw==", 419 | "dev": true, 420 | "requires": { 421 | "eslint-restricted-globals": "^0.1.1", 422 | "object.assign": "^4.1.0", 423 | "object.entries": "^1.0.4" 424 | } 425 | }, 426 | "eslint-import-resolver-node": { 427 | "version": "0.3.2", 428 | "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", 429 | "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", 430 | "dev": true, 431 | "requires": { 432 | "debug": "^2.6.9", 433 | "resolve": "^1.5.0" 434 | }, 435 | "dependencies": { 436 | "debug": { 437 | "version": "2.6.9", 438 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 439 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 440 | "dev": true, 441 | "requires": { 442 | "ms": "2.0.0" 443 | } 444 | }, 445 | "ms": { 446 | "version": "2.0.0", 447 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 448 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 449 | "dev": true 450 | } 451 | } 452 | }, 453 | "eslint-module-utils": { 454 | "version": "2.4.0", 455 | "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz", 456 | "integrity": "sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw==", 457 | "dev": true, 458 | "requires": { 459 | "debug": "^2.6.8", 460 | "pkg-dir": "^2.0.0" 461 | }, 462 | "dependencies": { 463 | "debug": { 464 | "version": "2.6.9", 465 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 466 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 467 | "dev": true, 468 | "requires": { 469 | "ms": "2.0.0" 470 | } 471 | }, 472 | "ms": { 473 | "version": "2.0.0", 474 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 475 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 476 | "dev": true 477 | } 478 | } 479 | }, 480 | "eslint-plugin-import": { 481 | "version": "2.17.3", 482 | "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.17.3.tgz", 483 | "integrity": "sha512-qeVf/UwXFJbeyLbxuY8RgqDyEKCkqV7YC+E5S5uOjAp4tOc8zj01JP3ucoBM8JcEqd1qRasJSg6LLlisirfy0Q==", 484 | "dev": true, 485 | "requires": { 486 | "array-includes": "^3.0.3", 487 | "contains-path": "^0.1.0", 488 | "debug": "^2.6.9", 489 | "doctrine": "1.5.0", 490 | "eslint-import-resolver-node": "^0.3.2", 491 | "eslint-module-utils": "^2.4.0", 492 | "has": "^1.0.3", 493 | "lodash": "^4.17.11", 494 | "minimatch": "^3.0.4", 495 | "read-pkg-up": "^2.0.0", 496 | "resolve": "^1.11.0" 497 | }, 498 | "dependencies": { 499 | "debug": { 500 | "version": "2.6.9", 501 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 502 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 503 | "dev": true, 504 | "requires": { 505 | "ms": "2.0.0" 506 | } 507 | }, 508 | "ms": { 509 | "version": "2.0.0", 510 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 511 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 512 | "dev": true 513 | } 514 | } 515 | }, 516 | "eslint-plugin-jsx-a11y": { 517 | "version": "6.2.1", 518 | "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.1.tgz", 519 | "integrity": "sha512-cjN2ObWrRz0TTw7vEcGQrx+YltMvZoOEx4hWU8eEERDnBIU00OTq7Vr+jA7DFKxiwLNv4tTh5Pq2GUNEa8b6+w==", 520 | "dev": true, 521 | "requires": { 522 | "aria-query": "^3.0.0", 523 | "array-includes": "^3.0.3", 524 | "ast-types-flow": "^0.0.7", 525 | "axobject-query": "^2.0.2", 526 | "damerau-levenshtein": "^1.0.4", 527 | "emoji-regex": "^7.0.2", 528 | "has": "^1.0.3", 529 | "jsx-ast-utils": "^2.0.1" 530 | } 531 | }, 532 | "eslint-plugin-react": { 533 | "version": "7.13.0", 534 | "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.13.0.tgz", 535 | "integrity": "sha512-uA5LrHylu8lW/eAH3bEQe9YdzpPaFd9yAJTwTi/i/BKTD7j6aQMKVAdGM/ML72zD6womuSK7EiGtMKuK06lWjQ==", 536 | "dev": true, 537 | "requires": { 538 | "array-includes": "^3.0.3", 539 | "doctrine": "^2.1.0", 540 | "has": "^1.0.3", 541 | "jsx-ast-utils": "^2.1.0", 542 | "object.fromentries": "^2.0.0", 543 | "prop-types": "^15.7.2", 544 | "resolve": "^1.10.1" 545 | }, 546 | "dependencies": { 547 | "doctrine": { 548 | "version": "2.1.0", 549 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", 550 | "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", 551 | "dev": true, 552 | "requires": { 553 | "esutils": "^2.0.2" 554 | } 555 | }, 556 | "jsx-ast-utils": { 557 | "version": "2.1.0", 558 | "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.1.0.tgz", 559 | "integrity": "sha512-yDGDG2DS4JcqhA6blsuYbtsT09xL8AoLuUR2Gb5exrw7UEM19sBcOTq+YBBhrNbl0PUC4R4LnFu+dHg2HKeVvA==", 560 | "dev": true, 561 | "requires": { 562 | "array-includes": "^3.0.3" 563 | } 564 | } 565 | } 566 | }, 567 | "eslint-restricted-globals": { 568 | "version": "0.1.1", 569 | "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", 570 | "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=", 571 | "dev": true 572 | }, 573 | "eslint-scope": { 574 | "version": "4.0.3", 575 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", 576 | "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", 577 | "dev": true, 578 | "requires": { 579 | "esrecurse": "^4.1.0", 580 | "estraverse": "^4.1.1" 581 | } 582 | }, 583 | "eslint-utils": { 584 | "version": "1.3.1", 585 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", 586 | "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", 587 | "dev": true 588 | }, 589 | "eslint-visitor-keys": { 590 | "version": "1.0.0", 591 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", 592 | "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", 593 | "dev": true 594 | }, 595 | "espree": { 596 | "version": "5.0.1", 597 | "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", 598 | "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", 599 | "dev": true, 600 | "requires": { 601 | "acorn": "^6.0.7", 602 | "acorn-jsx": "^5.0.0", 603 | "eslint-visitor-keys": "^1.0.0" 604 | } 605 | }, 606 | "esprima": { 607 | "version": "4.0.1", 608 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 609 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 610 | "dev": true 611 | }, 612 | "esquery": { 613 | "version": "1.0.1", 614 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", 615 | "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", 616 | "dev": true, 617 | "requires": { 618 | "estraverse": "^4.0.0" 619 | } 620 | }, 621 | "esrecurse": { 622 | "version": "4.2.1", 623 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", 624 | "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", 625 | "dev": true, 626 | "requires": { 627 | "estraverse": "^4.1.0" 628 | } 629 | }, 630 | "estraverse": { 631 | "version": "4.2.0", 632 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", 633 | "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", 634 | "dev": true 635 | }, 636 | "esutils": { 637 | "version": "2.0.2", 638 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 639 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 640 | "dev": true 641 | }, 642 | "external-editor": { 643 | "version": "3.0.3", 644 | "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", 645 | "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", 646 | "dev": true, 647 | "requires": { 648 | "chardet": "^0.7.0", 649 | "iconv-lite": "^0.4.24", 650 | "tmp": "^0.0.33" 651 | } 652 | }, 653 | "fast-deep-equal": { 654 | "version": "2.0.1", 655 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 656 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", 657 | "dev": true 658 | }, 659 | "fast-json-stable-stringify": { 660 | "version": "2.0.0", 661 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 662 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", 663 | "dev": true 664 | }, 665 | "fast-levenshtein": { 666 | "version": "2.0.6", 667 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 668 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", 669 | "dev": true 670 | }, 671 | "figures": { 672 | "version": "2.0.0", 673 | "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", 674 | "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", 675 | "dev": true, 676 | "requires": { 677 | "escape-string-regexp": "^1.0.5" 678 | } 679 | }, 680 | "file-entry-cache": { 681 | "version": "5.0.1", 682 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", 683 | "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", 684 | "dev": true, 685 | "requires": { 686 | "flat-cache": "^2.0.1" 687 | } 688 | }, 689 | "fill-range": { 690 | "version": "7.1.1", 691 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 692 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 693 | "dev": true, 694 | "requires": { 695 | "to-regex-range": "^5.0.1" 696 | } 697 | }, 698 | "find-up": { 699 | "version": "2.1.0", 700 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", 701 | "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", 702 | "dev": true, 703 | "requires": { 704 | "locate-path": "^2.0.0" 705 | } 706 | }, 707 | "flat-cache": { 708 | "version": "2.0.1", 709 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", 710 | "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", 711 | "dev": true, 712 | "requires": { 713 | "flatted": "^2.0.0", 714 | "rimraf": "2.6.3", 715 | "write": "1.0.3" 716 | } 717 | }, 718 | "flatted": { 719 | "version": "2.0.0", 720 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", 721 | "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", 722 | "dev": true 723 | }, 724 | "fs.realpath": { 725 | "version": "1.0.0", 726 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 727 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 728 | "dev": true 729 | }, 730 | "fsevents": { 731 | "version": "2.3.3", 732 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 733 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 734 | "dev": true, 735 | "optional": true 736 | }, 737 | "function-bind": { 738 | "version": "1.1.1", 739 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 740 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 741 | "dev": true 742 | }, 743 | "functional-red-black-tree": { 744 | "version": "1.0.1", 745 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 746 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 747 | "dev": true 748 | }, 749 | "glob": { 750 | "version": "7.1.3", 751 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 752 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 753 | "dev": true, 754 | "requires": { 755 | "fs.realpath": "^1.0.0", 756 | "inflight": "^1.0.4", 757 | "inherits": "2", 758 | "minimatch": "^3.0.4", 759 | "once": "^1.3.0", 760 | "path-is-absolute": "^1.0.0" 761 | } 762 | }, 763 | "glob-parent": { 764 | "version": "5.1.2", 765 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 766 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 767 | "dev": true, 768 | "requires": { 769 | "is-glob": "^4.0.1" 770 | } 771 | }, 772 | "globals": { 773 | "version": "11.11.0", 774 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz", 775 | "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==", 776 | "dev": true 777 | }, 778 | "graceful-fs": { 779 | "version": "4.1.15", 780 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", 781 | "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", 782 | "dev": true 783 | }, 784 | "has": { 785 | "version": "1.0.3", 786 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 787 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 788 | "dev": true, 789 | "requires": { 790 | "function-bind": "^1.1.1" 791 | } 792 | }, 793 | "has-flag": { 794 | "version": "3.0.0", 795 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 796 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 797 | "dev": true 798 | }, 799 | "has-symbols": { 800 | "version": "1.0.0", 801 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", 802 | "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", 803 | "dev": true 804 | }, 805 | "hosted-git-info": { 806 | "version": "2.7.1", 807 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", 808 | "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", 809 | "dev": true 810 | }, 811 | "iconv-lite": { 812 | "version": "0.4.24", 813 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 814 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 815 | "dev": true, 816 | "requires": { 817 | "safer-buffer": ">= 2.1.2 < 3" 818 | } 819 | }, 820 | "ignore": { 821 | "version": "4.0.6", 822 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", 823 | "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", 824 | "dev": true 825 | }, 826 | "ignore-by-default": { 827 | "version": "1.0.1", 828 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 829 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 830 | "dev": true 831 | }, 832 | "import-fresh": { 833 | "version": "3.0.0", 834 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz", 835 | "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==", 836 | "dev": true, 837 | "requires": { 838 | "parent-module": "^1.0.0", 839 | "resolve-from": "^4.0.0" 840 | } 841 | }, 842 | "imurmurhash": { 843 | "version": "0.1.4", 844 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 845 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 846 | "dev": true 847 | }, 848 | "inflight": { 849 | "version": "1.0.6", 850 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 851 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 852 | "dev": true, 853 | "requires": { 854 | "once": "^1.3.0", 855 | "wrappy": "1" 856 | } 857 | }, 858 | "inherits": { 859 | "version": "2.0.3", 860 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 861 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 862 | "dev": true 863 | }, 864 | "inquirer": { 865 | "version": "6.2.2", 866 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz", 867 | "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==", 868 | "dev": true, 869 | "requires": { 870 | "ansi-escapes": "^3.2.0", 871 | "chalk": "^2.4.2", 872 | "cli-cursor": "^2.1.0", 873 | "cli-width": "^2.0.0", 874 | "external-editor": "^3.0.3", 875 | "figures": "^2.0.0", 876 | "lodash": "^4.17.11", 877 | "mute-stream": "0.0.7", 878 | "run-async": "^2.2.0", 879 | "rxjs": "^6.4.0", 880 | "string-width": "^2.1.0", 881 | "strip-ansi": "^5.0.0", 882 | "through": "^2.3.6" 883 | }, 884 | "dependencies": { 885 | "ansi-regex": { 886 | "version": "4.1.1", 887 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", 888 | "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", 889 | "dev": true 890 | }, 891 | "strip-ansi": { 892 | "version": "5.2.0", 893 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 894 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 895 | "dev": true, 896 | "requires": { 897 | "ansi-regex": "^4.1.0" 898 | } 899 | } 900 | } 901 | }, 902 | "is-arrayish": { 903 | "version": "0.2.1", 904 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 905 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", 906 | "dev": true 907 | }, 908 | "is-binary-path": { 909 | "version": "2.1.0", 910 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 911 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 912 | "dev": true, 913 | "requires": { 914 | "binary-extensions": "^2.0.0" 915 | } 916 | }, 917 | "is-callable": { 918 | "version": "1.1.4", 919 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", 920 | "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", 921 | "dev": true 922 | }, 923 | "is-date-object": { 924 | "version": "1.0.1", 925 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 926 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 927 | "dev": true 928 | }, 929 | "is-extglob": { 930 | "version": "2.1.1", 931 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 932 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 933 | "dev": true 934 | }, 935 | "is-fullwidth-code-point": { 936 | "version": "2.0.0", 937 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 938 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 939 | "dev": true 940 | }, 941 | "is-glob": { 942 | "version": "4.0.3", 943 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 944 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 945 | "dev": true, 946 | "requires": { 947 | "is-extglob": "^2.1.1" 948 | } 949 | }, 950 | "is-number": { 951 | "version": "7.0.0", 952 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 953 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 954 | "dev": true 955 | }, 956 | "is-promise": { 957 | "version": "2.1.0", 958 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 959 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", 960 | "dev": true 961 | }, 962 | "is-regex": { 963 | "version": "1.0.4", 964 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 965 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 966 | "dev": true, 967 | "requires": { 968 | "has": "^1.0.1" 969 | } 970 | }, 971 | "is-symbol": { 972 | "version": "1.0.2", 973 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", 974 | "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", 975 | "dev": true, 976 | "requires": { 977 | "has-symbols": "^1.0.0" 978 | } 979 | }, 980 | "isarray": { 981 | "version": "1.0.0", 982 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 983 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 984 | "dev": true 985 | }, 986 | "isexe": { 987 | "version": "2.0.0", 988 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 989 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 990 | "dev": true 991 | }, 992 | "js-tokens": { 993 | "version": "4.0.0", 994 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 995 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 996 | "dev": true 997 | }, 998 | "js-yaml": { 999 | "version": "3.13.1", 1000 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 1001 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 1002 | "dev": true, 1003 | "requires": { 1004 | "argparse": "^1.0.7", 1005 | "esprima": "^4.0.0" 1006 | } 1007 | }, 1008 | "json-schema-traverse": { 1009 | "version": "0.4.1", 1010 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1011 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 1012 | "dev": true 1013 | }, 1014 | "json-stable-stringify-without-jsonify": { 1015 | "version": "1.0.1", 1016 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 1017 | "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", 1018 | "dev": true 1019 | }, 1020 | "jsx-ast-utils": { 1021 | "version": "2.0.1", 1022 | "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz", 1023 | "integrity": "sha1-6AGxs5mF4g//yHtA43SAgOLcrH8=", 1024 | "dev": true, 1025 | "requires": { 1026 | "array-includes": "^3.0.3" 1027 | } 1028 | }, 1029 | "levn": { 1030 | "version": "0.3.0", 1031 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 1032 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 1033 | "dev": true, 1034 | "requires": { 1035 | "prelude-ls": "~1.1.2", 1036 | "type-check": "~0.3.2" 1037 | } 1038 | }, 1039 | "load-json-file": { 1040 | "version": "2.0.0", 1041 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", 1042 | "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", 1043 | "dev": true, 1044 | "requires": { 1045 | "graceful-fs": "^4.1.2", 1046 | "parse-json": "^2.2.0", 1047 | "pify": "^2.0.0", 1048 | "strip-bom": "^3.0.0" 1049 | } 1050 | }, 1051 | "locate-path": { 1052 | "version": "2.0.0", 1053 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", 1054 | "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", 1055 | "dev": true, 1056 | "requires": { 1057 | "p-locate": "^2.0.0", 1058 | "path-exists": "^3.0.0" 1059 | } 1060 | }, 1061 | "lodash": { 1062 | "version": "4.17.11", 1063 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 1064 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", 1065 | "dev": true 1066 | }, 1067 | "loose-envify": { 1068 | "version": "1.4.0", 1069 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 1070 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 1071 | "dev": true, 1072 | "requires": { 1073 | "js-tokens": "^3.0.0 || ^4.0.0" 1074 | } 1075 | }, 1076 | "mimic-fn": { 1077 | "version": "1.2.0", 1078 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", 1079 | "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", 1080 | "dev": true 1081 | }, 1082 | "minimatch": { 1083 | "version": "3.0.4", 1084 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1085 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1086 | "dev": true, 1087 | "requires": { 1088 | "brace-expansion": "^1.1.7" 1089 | } 1090 | }, 1091 | "minimist": { 1092 | "version": "0.0.8", 1093 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 1094 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 1095 | "dev": true 1096 | }, 1097 | "mkdirp": { 1098 | "version": "0.5.1", 1099 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 1100 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 1101 | "dev": true, 1102 | "requires": { 1103 | "minimist": "0.0.8" 1104 | } 1105 | }, 1106 | "ms": { 1107 | "version": "2.1.1", 1108 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 1109 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", 1110 | "dev": true 1111 | }, 1112 | "mute-stream": { 1113 | "version": "0.0.7", 1114 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", 1115 | "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", 1116 | "dev": true 1117 | }, 1118 | "natural-compare": { 1119 | "version": "1.4.0", 1120 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 1121 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 1122 | "dev": true 1123 | }, 1124 | "nice-try": { 1125 | "version": "1.0.5", 1126 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 1127 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", 1128 | "dev": true 1129 | }, 1130 | "nodemon": { 1131 | "version": "3.1.3", 1132 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.3.tgz", 1133 | "integrity": "sha512-m4Vqs+APdKzDFpuaL9F9EVOF85+h070FnkHVEoU4+rmT6Vw0bmNl7s61VEkY/cJkL7RCv1p4urnUDUMrS5rk2w==", 1134 | "dev": true, 1135 | "requires": { 1136 | "chokidar": "^3.5.2", 1137 | "debug": "^4", 1138 | "ignore-by-default": "^1.0.1", 1139 | "minimatch": "^3.1.2", 1140 | "pstree.remy": "^1.1.8", 1141 | "semver": "^7.5.3", 1142 | "simple-update-notifier": "^2.0.0", 1143 | "supports-color": "^5.5.0", 1144 | "touch": "^3.1.0", 1145 | "undefsafe": "^2.0.5" 1146 | }, 1147 | "dependencies": { 1148 | "minimatch": { 1149 | "version": "3.1.2", 1150 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1151 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1152 | "dev": true, 1153 | "requires": { 1154 | "brace-expansion": "^1.1.7" 1155 | } 1156 | }, 1157 | "semver": { 1158 | "version": "7.6.2", 1159 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", 1160 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", 1161 | "dev": true 1162 | } 1163 | } 1164 | }, 1165 | "normalize-package-data": { 1166 | "version": "2.5.0", 1167 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", 1168 | "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", 1169 | "dev": true, 1170 | "requires": { 1171 | "hosted-git-info": "^2.1.4", 1172 | "resolve": "^1.10.0", 1173 | "semver": "2 || 3 || 4 || 5", 1174 | "validate-npm-package-license": "^3.0.1" 1175 | } 1176 | }, 1177 | "normalize-path": { 1178 | "version": "3.0.0", 1179 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1180 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1181 | "dev": true 1182 | }, 1183 | "object-assign": { 1184 | "version": "4.1.1", 1185 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1186 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 1187 | "dev": true 1188 | }, 1189 | "object-keys": { 1190 | "version": "1.0.12", 1191 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", 1192 | "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", 1193 | "dev": true 1194 | }, 1195 | "object.assign": { 1196 | "version": "4.1.0", 1197 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", 1198 | "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", 1199 | "dev": true, 1200 | "requires": { 1201 | "define-properties": "^1.1.2", 1202 | "function-bind": "^1.1.1", 1203 | "has-symbols": "^1.0.0", 1204 | "object-keys": "^1.0.11" 1205 | } 1206 | }, 1207 | "object.entries": { 1208 | "version": "1.1.0", 1209 | "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", 1210 | "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", 1211 | "dev": true, 1212 | "requires": { 1213 | "define-properties": "^1.1.3", 1214 | "es-abstract": "^1.12.0", 1215 | "function-bind": "^1.1.1", 1216 | "has": "^1.0.3" 1217 | } 1218 | }, 1219 | "object.fromentries": { 1220 | "version": "2.0.0", 1221 | "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.0.tgz", 1222 | "integrity": "sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA==", 1223 | "dev": true, 1224 | "requires": { 1225 | "define-properties": "^1.1.2", 1226 | "es-abstract": "^1.11.0", 1227 | "function-bind": "^1.1.1", 1228 | "has": "^1.0.1" 1229 | } 1230 | }, 1231 | "once": { 1232 | "version": "1.4.0", 1233 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1234 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1235 | "dev": true, 1236 | "requires": { 1237 | "wrappy": "1" 1238 | } 1239 | }, 1240 | "onetime": { 1241 | "version": "2.0.1", 1242 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", 1243 | "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", 1244 | "dev": true, 1245 | "requires": { 1246 | "mimic-fn": "^1.0.0" 1247 | } 1248 | }, 1249 | "optionator": { 1250 | "version": "0.8.2", 1251 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", 1252 | "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", 1253 | "dev": true, 1254 | "requires": { 1255 | "deep-is": "~0.1.3", 1256 | "fast-levenshtein": "~2.0.4", 1257 | "levn": "~0.3.0", 1258 | "prelude-ls": "~1.1.2", 1259 | "type-check": "~0.3.2", 1260 | "wordwrap": "~1.0.0" 1261 | } 1262 | }, 1263 | "os-tmpdir": { 1264 | "version": "1.0.2", 1265 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 1266 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", 1267 | "dev": true 1268 | }, 1269 | "p-limit": { 1270 | "version": "1.3.0", 1271 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", 1272 | "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", 1273 | "dev": true, 1274 | "requires": { 1275 | "p-try": "^1.0.0" 1276 | } 1277 | }, 1278 | "p-locate": { 1279 | "version": "2.0.0", 1280 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", 1281 | "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", 1282 | "dev": true, 1283 | "requires": { 1284 | "p-limit": "^1.1.0" 1285 | } 1286 | }, 1287 | "p-try": { 1288 | "version": "1.0.0", 1289 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", 1290 | "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", 1291 | "dev": true 1292 | }, 1293 | "parent-module": { 1294 | "version": "1.0.1", 1295 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 1296 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 1297 | "dev": true, 1298 | "requires": { 1299 | "callsites": "^3.0.0" 1300 | } 1301 | }, 1302 | "parse-json": { 1303 | "version": "2.2.0", 1304 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", 1305 | "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", 1306 | "dev": true, 1307 | "requires": { 1308 | "error-ex": "^1.2.0" 1309 | } 1310 | }, 1311 | "path-exists": { 1312 | "version": "3.0.0", 1313 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 1314 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 1315 | "dev": true 1316 | }, 1317 | "path-is-absolute": { 1318 | "version": "1.0.1", 1319 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1320 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1321 | "dev": true 1322 | }, 1323 | "path-is-inside": { 1324 | "version": "1.0.2", 1325 | "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", 1326 | "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", 1327 | "dev": true 1328 | }, 1329 | "path-key": { 1330 | "version": "2.0.1", 1331 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 1332 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 1333 | "dev": true 1334 | }, 1335 | "path-parse": { 1336 | "version": "1.0.6", 1337 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 1338 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 1339 | "dev": true 1340 | }, 1341 | "path-type": { 1342 | "version": "2.0.0", 1343 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", 1344 | "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", 1345 | "dev": true, 1346 | "requires": { 1347 | "pify": "^2.0.0" 1348 | } 1349 | }, 1350 | "picomatch": { 1351 | "version": "2.3.1", 1352 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1353 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1354 | "dev": true 1355 | }, 1356 | "pify": { 1357 | "version": "2.3.0", 1358 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 1359 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 1360 | "dev": true 1361 | }, 1362 | "pkg-dir": { 1363 | "version": "2.0.0", 1364 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", 1365 | "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", 1366 | "dev": true, 1367 | "requires": { 1368 | "find-up": "^2.1.0" 1369 | } 1370 | }, 1371 | "prelude-ls": { 1372 | "version": "1.1.2", 1373 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 1374 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", 1375 | "dev": true 1376 | }, 1377 | "progress": { 1378 | "version": "2.0.3", 1379 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 1380 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", 1381 | "dev": true 1382 | }, 1383 | "prop-types": { 1384 | "version": "15.7.2", 1385 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", 1386 | "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", 1387 | "dev": true, 1388 | "requires": { 1389 | "loose-envify": "^1.4.0", 1390 | "object-assign": "^4.1.1", 1391 | "react-is": "^16.8.1" 1392 | } 1393 | }, 1394 | "pstree.remy": { 1395 | "version": "1.1.8", 1396 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 1397 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 1398 | "dev": true 1399 | }, 1400 | "punycode": { 1401 | "version": "2.1.1", 1402 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1403 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 1404 | "dev": true 1405 | }, 1406 | "react-is": { 1407 | "version": "16.8.6", 1408 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", 1409 | "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==", 1410 | "dev": true 1411 | }, 1412 | "read-pkg": { 1413 | "version": "2.0.0", 1414 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", 1415 | "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", 1416 | "dev": true, 1417 | "requires": { 1418 | "load-json-file": "^2.0.0", 1419 | "normalize-package-data": "^2.3.2", 1420 | "path-type": "^2.0.0" 1421 | } 1422 | }, 1423 | "read-pkg-up": { 1424 | "version": "2.0.0", 1425 | "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", 1426 | "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", 1427 | "dev": true, 1428 | "requires": { 1429 | "find-up": "^2.0.0", 1430 | "read-pkg": "^2.0.0" 1431 | } 1432 | }, 1433 | "readdirp": { 1434 | "version": "3.6.0", 1435 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1436 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1437 | "dev": true, 1438 | "requires": { 1439 | "picomatch": "^2.2.1" 1440 | } 1441 | }, 1442 | "regexpp": { 1443 | "version": "2.0.1", 1444 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", 1445 | "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", 1446 | "dev": true 1447 | }, 1448 | "resolve": { 1449 | "version": "1.11.1", 1450 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", 1451 | "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", 1452 | "dev": true, 1453 | "requires": { 1454 | "path-parse": "^1.0.6" 1455 | } 1456 | }, 1457 | "resolve-from": { 1458 | "version": "4.0.0", 1459 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 1460 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 1461 | "dev": true 1462 | }, 1463 | "restore-cursor": { 1464 | "version": "2.0.0", 1465 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", 1466 | "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", 1467 | "dev": true, 1468 | "requires": { 1469 | "onetime": "^2.0.0", 1470 | "signal-exit": "^3.0.2" 1471 | } 1472 | }, 1473 | "rimraf": { 1474 | "version": "2.6.3", 1475 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", 1476 | "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", 1477 | "dev": true, 1478 | "requires": { 1479 | "glob": "^7.1.3" 1480 | } 1481 | }, 1482 | "run-async": { 1483 | "version": "2.3.0", 1484 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", 1485 | "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", 1486 | "dev": true, 1487 | "requires": { 1488 | "is-promise": "^2.1.0" 1489 | } 1490 | }, 1491 | "rxjs": { 1492 | "version": "6.4.0", 1493 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", 1494 | "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", 1495 | "dev": true, 1496 | "requires": { 1497 | "tslib": "^1.9.0" 1498 | } 1499 | }, 1500 | "safer-buffer": { 1501 | "version": "2.1.2", 1502 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1503 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1504 | "dev": true 1505 | }, 1506 | "semver": { 1507 | "version": "5.7.0", 1508 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", 1509 | "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", 1510 | "dev": true 1511 | }, 1512 | "shebang-command": { 1513 | "version": "1.2.0", 1514 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 1515 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 1516 | "dev": true, 1517 | "requires": { 1518 | "shebang-regex": "^1.0.0" 1519 | } 1520 | }, 1521 | "shebang-regex": { 1522 | "version": "1.0.0", 1523 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 1524 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 1525 | "dev": true 1526 | }, 1527 | "signal-exit": { 1528 | "version": "3.0.2", 1529 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 1530 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 1531 | "dev": true 1532 | }, 1533 | "simple-update-notifier": { 1534 | "version": "2.0.0", 1535 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", 1536 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", 1537 | "dev": true, 1538 | "requires": { 1539 | "semver": "^7.5.3" 1540 | }, 1541 | "dependencies": { 1542 | "semver": { 1543 | "version": "7.6.2", 1544 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", 1545 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", 1546 | "dev": true 1547 | } 1548 | } 1549 | }, 1550 | "slice-ansi": { 1551 | "version": "2.1.0", 1552 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", 1553 | "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", 1554 | "dev": true, 1555 | "requires": { 1556 | "ansi-styles": "^3.2.0", 1557 | "astral-regex": "^1.0.0", 1558 | "is-fullwidth-code-point": "^2.0.0" 1559 | } 1560 | }, 1561 | "spdx-correct": { 1562 | "version": "3.1.0", 1563 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", 1564 | "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", 1565 | "dev": true, 1566 | "requires": { 1567 | "spdx-expression-parse": "^3.0.0", 1568 | "spdx-license-ids": "^3.0.0" 1569 | } 1570 | }, 1571 | "spdx-exceptions": { 1572 | "version": "2.2.0", 1573 | "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", 1574 | "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", 1575 | "dev": true 1576 | }, 1577 | "spdx-expression-parse": { 1578 | "version": "3.0.0", 1579 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", 1580 | "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", 1581 | "dev": true, 1582 | "requires": { 1583 | "spdx-exceptions": "^2.1.0", 1584 | "spdx-license-ids": "^3.0.0" 1585 | } 1586 | }, 1587 | "spdx-license-ids": { 1588 | "version": "3.0.4", 1589 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", 1590 | "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", 1591 | "dev": true 1592 | }, 1593 | "sprintf-js": { 1594 | "version": "1.0.3", 1595 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1596 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 1597 | "dev": true 1598 | }, 1599 | "string-width": { 1600 | "version": "2.1.1", 1601 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 1602 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 1603 | "dev": true, 1604 | "requires": { 1605 | "is-fullwidth-code-point": "^2.0.0", 1606 | "strip-ansi": "^4.0.0" 1607 | } 1608 | }, 1609 | "strip-ansi": { 1610 | "version": "4.0.0", 1611 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 1612 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 1613 | "dev": true, 1614 | "requires": { 1615 | "ansi-regex": "^3.0.0" 1616 | } 1617 | }, 1618 | "strip-bom": { 1619 | "version": "3.0.0", 1620 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 1621 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 1622 | "dev": true 1623 | }, 1624 | "strip-json-comments": { 1625 | "version": "2.0.1", 1626 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1627 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 1628 | "dev": true 1629 | }, 1630 | "supports-color": { 1631 | "version": "5.5.0", 1632 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1633 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1634 | "dev": true, 1635 | "requires": { 1636 | "has-flag": "^3.0.0" 1637 | } 1638 | }, 1639 | "table": { 1640 | "version": "5.2.3", 1641 | "resolved": "https://registry.npmjs.org/table/-/table-5.2.3.tgz", 1642 | "integrity": "sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==", 1643 | "dev": true, 1644 | "requires": { 1645 | "ajv": "^6.9.1", 1646 | "lodash": "^4.17.11", 1647 | "slice-ansi": "^2.1.0", 1648 | "string-width": "^3.0.0" 1649 | }, 1650 | "dependencies": { 1651 | "ansi-regex": { 1652 | "version": "4.1.1", 1653 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", 1654 | "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", 1655 | "dev": true 1656 | }, 1657 | "string-width": { 1658 | "version": "3.1.0", 1659 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 1660 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 1661 | "dev": true, 1662 | "requires": { 1663 | "emoji-regex": "^7.0.1", 1664 | "is-fullwidth-code-point": "^2.0.0", 1665 | "strip-ansi": "^5.1.0" 1666 | } 1667 | }, 1668 | "strip-ansi": { 1669 | "version": "5.2.0", 1670 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 1671 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 1672 | "dev": true, 1673 | "requires": { 1674 | "ansi-regex": "^4.1.0" 1675 | } 1676 | } 1677 | } 1678 | }, 1679 | "text-table": { 1680 | "version": "0.2.0", 1681 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 1682 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 1683 | "dev": true 1684 | }, 1685 | "through": { 1686 | "version": "2.3.8", 1687 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 1688 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 1689 | "dev": true 1690 | }, 1691 | "tmp": { 1692 | "version": "0.0.33", 1693 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 1694 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 1695 | "dev": true, 1696 | "requires": { 1697 | "os-tmpdir": "~1.0.2" 1698 | } 1699 | }, 1700 | "to-regex-range": { 1701 | "version": "5.0.1", 1702 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1703 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1704 | "dev": true, 1705 | "requires": { 1706 | "is-number": "^7.0.0" 1707 | } 1708 | }, 1709 | "touch": { 1710 | "version": "3.1.1", 1711 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", 1712 | "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", 1713 | "dev": true 1714 | }, 1715 | "tslib": { 1716 | "version": "1.9.3", 1717 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 1718 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", 1719 | "dev": true 1720 | }, 1721 | "type-check": { 1722 | "version": "0.3.2", 1723 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 1724 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 1725 | "dev": true, 1726 | "requires": { 1727 | "prelude-ls": "~1.1.2" 1728 | } 1729 | }, 1730 | "undefsafe": { 1731 | "version": "2.0.5", 1732 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 1733 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 1734 | "dev": true 1735 | }, 1736 | "uri-js": { 1737 | "version": "4.2.2", 1738 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 1739 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 1740 | "dev": true, 1741 | "requires": { 1742 | "punycode": "^2.1.0" 1743 | } 1744 | }, 1745 | "validate-npm-package-license": { 1746 | "version": "3.0.4", 1747 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", 1748 | "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", 1749 | "dev": true, 1750 | "requires": { 1751 | "spdx-correct": "^3.0.0", 1752 | "spdx-expression-parse": "^3.0.0" 1753 | } 1754 | }, 1755 | "which": { 1756 | "version": "1.3.1", 1757 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1758 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1759 | "dev": true, 1760 | "requires": { 1761 | "isexe": "^2.0.0" 1762 | } 1763 | }, 1764 | "wordwrap": { 1765 | "version": "1.0.0", 1766 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", 1767 | "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", 1768 | "dev": true 1769 | }, 1770 | "wrappy": { 1771 | "version": "1.0.2", 1772 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1773 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1774 | "dev": true 1775 | }, 1776 | "write": { 1777 | "version": "1.0.3", 1778 | "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", 1779 | "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", 1780 | "dev": true, 1781 | "requires": { 1782 | "mkdirp": "^0.5.1" 1783 | } 1784 | } 1785 | } 1786 | } 1787 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-music", 3 | "version": "1.0.0", 4 | "description": "An exercise in building a GraphQL API", 5 | "author": "Nathan Chapman (https://nathanchapman.dev)", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "start": "nodemon ." 9 | }, 10 | "license": "MIT", 11 | "repository": "https://github.com/nathanchapman/graphql-music", 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "eslint": "5.16.0", 15 | "eslint-config-airbnb": "17.1.0", 16 | "eslint-plugin-import": "2.17.3", 17 | "eslint-plugin-jsx-a11y": "6.2.1", 18 | "eslint-plugin-react": "7.13.0", 19 | "nodemon": "3.1.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { ApolloServer, gql } = require('apollo-server'); 2 | 3 | const typeDefs = gql` 4 | type Query { 5 | greet(name: String): String! 6 | } 7 | `; 8 | 9 | const resolvers = { 10 | Query: { 11 | greet: (_, { name }) => `Hello ${name || 'World'}`, 12 | }, 13 | }; 14 | 15 | const server = new ApolloServer({ 16 | typeDefs, 17 | resolvers, 18 | }); 19 | 20 | server.listen().then(({ url }) => { 21 | console.log(`🚀 Server ready at ${url}`); 22 | }); 23 | --------------------------------------------------------------------------------