├── .eslintrc.js ├── .github └── workflows │ ├── main.yml │ └── pr.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── __tests__ ├── gql │ ├── mutations │ │ ├── create-non-null.gql │ │ ├── create.gql │ │ ├── delete-alt.gql │ │ ├── delete.gql │ │ ├── optional.gql │ │ ├── unimplemented.gql │ │ ├── update-non-null.gql │ │ └── update.gql │ ├── queries │ │ ├── context.gql │ │ ├── interface-inline-fragment.gql │ │ ├── interface-non-null.gql │ │ ├── interface.gql │ │ ├── object-kitchen-sink.gql │ │ ├── object-non-null.gql │ │ ├── object.gql │ │ ├── objects-nested-non-null.gql │ │ ├── objects-non-null.gql │ │ ├── objects.gql │ │ ├── relay-connection.gql │ │ ├── relay-non-null-edges-connection.gql │ │ ├── relay-non-null-nodes-connection.gql │ │ ├── scalar-non-null.gql │ │ ├── scalar-optional-resolve.gql │ │ ├── scalar.gql │ │ ├── sorted-objects.gql │ │ ├── union-nested-non-null.gql │ │ ├── union-non-null.gql │ │ └── union.gql │ ├── schema.gql │ └── schema.js ├── integration │ ├── handler-test.js │ ├── mutations │ │ ├── create-object-test.js │ │ ├── delete-object-test.js │ │ ├── optional-test.js │ │ ├── unimplemented-test.js │ │ └── update-object-test.js │ ├── queries │ │ ├── interface-test.js │ │ ├── object-kitchen-sink-test.js │ │ ├── object-test.js │ │ ├── objects-test.js │ │ ├── relay-connection-test.js │ │ ├── relay-non-null-edges-connection-test.js │ │ ├── relay-non-null-nodes-connection-test.js │ │ ├── scalar-test.js │ │ ├── sorted-objects-test.js │ │ ├── union-nested-non-null-test.js │ │ ├── union-non-null-test.js │ │ └── union-test.js │ └── setup.js └── unit │ ├── handler-test.js │ ├── orm │ ├── models-test.js │ └── records-test.js │ ├── relay-pagination-test.js │ ├── resolvers │ ├── create-field-resolver-test.js │ ├── list-resolver-test.js │ └── mirage-field-resolver-test.js │ ├── setup.js │ └── utils-test.js ├── babel.config.js ├── build ├── imports-suffix.js └── index.js ├── jest.config.js ├── lib ├── __mocks__ │ └── utils.js ├── handler.js ├── index.js ├── orm │ ├── __mocks__ │ │ ├── models.js │ │ └── records.js │ ├── models.js │ └── records.js ├── relay-pagination.js ├── resolvers │ ├── __mocks__ │ │ ├── default.js │ │ ├── field.js │ │ ├── interface.js │ │ ├── list.js │ │ ├── mirage.js │ │ ├── object.js │ │ └── union.js │ ├── default.js │ ├── field.js │ ├── interface.js │ ├── list.js │ ├── mirage.js │ ├── mutation.js │ ├── object.js │ ├── relay.js │ └── union.js └── utils.js ├── package.json ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "babel-eslint", 4 | plugins: ["import"], 5 | extends: [ 6 | "eslint:recommended", 7 | "plugin:import/errors", 8 | "plugin:prettier/recommended", 9 | ], 10 | env: { 11 | es6: true, 12 | node: true, 13 | browser: true, 14 | }, 15 | rules: { 16 | camelcase: 0, 17 | "object-curly-spacing": 0, 18 | quotes: 0, 19 | "array-bracket-spacing": 0, 20 | "no-var": 0, 21 | "object-shorthand": 0, 22 | "arrow-parens": 0, 23 | "no-unused-vars": ["error", { args: "none" }], 24 | }, 25 | overrides: [ 26 | { 27 | files: ["jest.config.js"], 28 | env: { 29 | browser: false, 30 | node: true, 31 | }, 32 | }, 33 | { 34 | files: ["**/__mocks__/**", "__tests__/**"], 35 | plugins: ["jest"], 36 | env: { 37 | "jest/globals": true, 38 | }, 39 | extends: ["plugin:jest/recommended", "plugin:jest/style"], 40 | }, 41 | ], 42 | settings: { 43 | "import/resolver": { 44 | alias: [ 45 | ["@lib", "./lib"], 46 | ["@tests", "./__tests__"], 47 | ], 48 | node: { 49 | extensions: ["js"], 50 | }, 51 | }, 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | env: 12 | CI: true 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | 17 | - name: Get yarn cache 18 | id: yarn-cache 19 | run: echo "::set-output name=dir::$(yarn cache dir)" 20 | 21 | - uses: actions/cache@v1 22 | with: 23 | path: ${{ steps.yarn-cache.outputs.dir }} 24 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 25 | restore-keys: | 26 | ${{ runner.os }}-yarn- 27 | 28 | - name: Install Dependencies 29 | run: yarn install 30 | 31 | - name: Lint 32 | run: | 33 | yarn prettier:check 34 | yarn lint 35 | 36 | - name: Test 37 | run: yarn test 38 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR CI 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | env: 9 | CI: true 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | 14 | - name: Get yarn cache 15 | id: yarn-cache 16 | run: echo "::set-output name=dir::$(yarn cache dir)" 17 | 18 | - uses: actions/cache@v1 19 | with: 20 | path: ${{ steps.yarn-cache.outputs.dir }} 21 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 22 | restore-keys: | 23 | ${{ runner.os }}-yarn- 24 | 25 | - name: Install Dependencies 26 | run: yarn install 27 | 28 | - name: Lint 29 | run: | 30 | yarn prettier:check 31 | yarn lint 32 | 33 | - name: Test 34 | run: yarn test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /dist/ 3 | /node_modules/ 4 | 5 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /**/*__mocks__/ 2 | /.github/ 3 | /__tests__/ 4 | /build/ 5 | /coverage/ 6 | /.eslintrc.js 7 | /babel.config.js 8 | /jest.config.js 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rocky Neurock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mirage JS GraphQL 2 | 3 | ![npm package](https://img.shields.io/npm/v/@miragejs/graphql?color=success&label=npm%20package) 4 | ![build](https://img.shields.io/github/actions/workflow/status/miragejs/graphql/main.yml?branch=main) 5 | 6 | Use [Mirage JS](https://miragejs.com) with [GraphQL](https://graphql.org). 7 | 8 | ## Overview 9 | 10 | Mirage lets you simulate API responses by writing **route handlers**. A route handler is a function that returns data to fulfill a request. Mirage GraphQL provides the ability to create a GraphQL route handler based on your GraphQL and Mirage schemas. 11 | 12 | ```javascript 13 | import { createServer } from "miragejs" 14 | import { createGraphQLHandler } from "@miragejs/graphql" 15 | import graphQLSchema from "app/gql/schema.gql" 16 | 17 | export default function makeServer(config) { 18 | return createServer({ 19 | routes() { 20 | const graphQLHandler = createGraphQLHandler(graphQLSchema, this.schema) 21 | 22 | this.post("/graphql", graphQLHandler) 23 | } 24 | }) 25 | } 26 | 27 | ``` 28 | 29 | ### Highlights 30 | 31 | Mirage GraphQL tries to do a lot for you. Here are the highlights: 32 | 33 | * It fulfills GraphQL requests by fetching data from Mirage's database. 34 | * It filters records from Mirage's database by using arguments from your GraphQL queries. 35 | * It handles create, update and delete type mutations automatically based on [some conventions](#automatic-mutation-conventions). 36 | * It allows you to supply your own resolvers (for cases where the automatic query and mutation resolution isn't sufficient). 37 | 38 | ## Installation 39 | 40 | You should install both `miragejs` and `@miragejs/graphql`. 41 | 42 | ```sh 43 | # Using npm 44 | npm install --save-dev miragejs @miragejs/graphql 45 | 46 | # Using Yarn 47 | yarn add --dev miragejs @miragejs/graphql 48 | ``` 49 | 50 | ## Guide 51 | 52 | This guide assumes most of its readers are already using GraphQL in their apps and want to start using Mirage to mock out their backend. This guide will try to provide enough information to be useful but it's worth reading the [Mirage guides](https://miragejs.com/docs/getting-started/introduction/) to get a full understanding of everything Mirage can do. 53 | 54 | ### Table of Contents 55 | 56 | * [Mirage GraphQL Assumptions](#mirage-graphql-assumptions) 57 | * [You Don't Need Mirage Models](#you-dont-need-mirage-models) 58 | * [Arguments from GraphQL Queries Map to Field Names of the Return Type](#arguments-from-graphql-queries-map-to-field-names-of-the-return-type) 59 | * [Miscellaneous Assumptions](#miscellaneous-assumptions) 60 | * [Example Use Cases](#example-use-cases) 61 | * [Example Schema](#example-schema) 62 | * [Example: Find Person by ID](#example-find-person-by-id) 63 | * [Example: Get All People](#example-get-all-people) 64 | * [Example: Creating and Updating a Person](#example-creating-and-updating-a-person) 65 | * [Automatic Mutation Conventions](#automatic-mutation-conventions) 66 | * [Example: Filtering People](#example-filtering-people) 67 | * [Part 1: Filtering by Last Name](#part-1-filtering-by-last-name) 68 | * [Part 2: Sorting](#part-2-sorting) 69 | * [Example: Deleting a Person](#example-deleting-a-person) 70 | 71 | ### Mirage GraphQL Assumptions 72 | 73 | There are a couple of assumptions Mirage GraphQL makes concerning how it resolves GraphQL queries. It's important to understand these assumptions to avoid confusion based on its behavior. 74 | 75 | #### You Don't Need to Define Mirage Models 76 | 77 | In many cases, you need to [tell Mirage about the models](https://miragejs.com/docs/main-concepts/models/) that exist in your app but Mirage GraphQL assumes relationships between types from your GraphQL schema and creates models accordingly. You can still define Mirage models, if you'd like, and Mirage GraphQL won't try to create them on its own. 78 | 79 | #### Arguments from GraphQL Queries Map to Field Names of the Return Type 80 | 81 | Mirage GraphQL uses arguments to filter records from Mirage's database. This isn't very useful for testing, as you only need to seed Mirage's database with the exact records you need for a given test. It's more useful when using Mirage for development where filtering and pagination may be desired for a more realistic user experience. 82 | 83 | #### Miscellaneous Assumptions 84 | 85 | * Fields that should resolve to a single object of a union type are resolved by taking the first appropriate record from Mirage's database. This is how Mirage GraphQL automatically resolves in this scenario. As with all automatic resolution, if you need to include some additional logic, you'll need to supply your own resolver. 86 | 87 | ### Example Use Cases 88 | 89 | Notes: 90 | 91 | * For further reference, there are many more use cases covered by the integration tests. 92 | * The `graphql-request` library is used in the examples but is not a dependency installed by Mirage GraphQL. 93 | 94 | #### Example Schema 95 | 96 | For these examples, imagine we have a GraphQL schema that looks like this: 97 | 98 | ```graphql 99 | # app/gql/schema.gql 100 | 101 | input PersonInput { 102 | firstName: String 103 | lastName: String 104 | } 105 | 106 | type Mutation { 107 | createPerson(input: PersonInput!): Person 108 | updatePerson(id: ID!, input: PersonInput!): Person 109 | 110 | # Note: `deletePerson` can't automatically be resolved due to the Boolean 111 | # return type. We will need to implement a resolver for this. 112 | deletePerson(id: ID!): Boolean 113 | } 114 | 115 | type Person { 116 | id: ID! 117 | firstName: String! 118 | lastName: String! 119 | } 120 | 121 | type Query { 122 | allPeople: [Person] 123 | person(id: ID!): Person 124 | 125 | # Note: `people` can't automatically be resolved if the `sortBy` argument is 126 | # supplied to the query. We will need to implement a resolver for this. 127 | people(firstName: String, lastName: String, sortBy: String): [Person] 128 | } 129 | ``` 130 | 131 | and we create a Mirage server like this: 132 | 133 | ```javascript 134 | // app/mirage/server.js 135 | 136 | import { createServer } from "miragejs" 137 | import { createGraphQLHandler } from "@miragejs/graphql" 138 | import graphQLSchema from "app/gql/schema.gql" 139 | 140 | export function makeServer() { 141 | return createServer({ 142 | routes() { 143 | const graphQLHandler = createGraphQLHandler(graphQLSchema, this.schema) 144 | 145 | this.post("/graphql", graphQLHandler) 146 | } 147 | }) 148 | } 149 | ``` 150 | 151 | #### Example: Find Person by ID 152 | 153 | In this example, we can get a `Person` record by ID. 154 | 155 | ```javascript 156 | // app/components/person.js 157 | 158 | import { createServer } from "app/mirage/server" 159 | import { request } from "graphql-request" 160 | 161 | const server = createServer() 162 | 163 | server.create("person", { firstName: "Mikael", lastName: "Åkerfeldt" }) 164 | 165 | export default { 166 | // ...other component stuff 167 | 168 | personQuery: ` 169 | query Person($id: id) { 170 | person(id: $id) { 171 | id 172 | firstName 173 | lastName 174 | } 175 | } 176 | `, 177 | getPerson(id) { 178 | return request("/graphql", this.personQuery, { id }) 179 | } 180 | } 181 | ``` 182 | 183 | A call to `getPerson("1")` will cause Mirage GraphQL to respond with: 184 | 185 | ```json 186 | { 187 | "data": { 188 | "person": { 189 | "id": "1", 190 | "firstName": "Mikael", 191 | "lastName": "Åkerfeldt" 192 | } 193 | } 194 | } 195 | ``` 196 | 197 | ### Example: Get All People 198 | 199 | In this example, we can get all the `Person` records from Mirage's database. 200 | 201 | ```javascript 202 | // app/components/people.js 203 | 204 | import { createServer } from "app/mirage/server" 205 | import { request } from "graphql-request" 206 | 207 | const server = createServer() 208 | 209 | server.create("person", { firstName: "Mikael", lastName: "Åkerfeldt" }) 210 | server.create("person", { firstName: "Per", lastName: "Nilsson" }) 211 | server.create("person", { firstName: "Tomas", lastName: "Haake" }) 212 | 213 | export default { 214 | // ...other component stuff 215 | 216 | peopleQuery: ` 217 | query People { 218 | people { 219 | id 220 | firstName 221 | lastName 222 | } 223 | } 224 | `, 225 | getPeople() { 226 | return request("/graphql", this.peopleQuery) 227 | } 228 | } 229 | ``` 230 | 231 | A call to `getPeople()` will cause Mirage GraphQL to respond with: 232 | 233 | ```json 234 | { 235 | "data": { 236 | "people": [ 237 | { 238 | "id": "1", 239 | "firstName": "Mikael", 240 | "lastName": "Åkerfeldt" 241 | }, 242 | { 243 | "id": "2", 244 | "firstName": "Per", 245 | "lastName": "Nilsson" 246 | }, 247 | { 248 | "id": "3", 249 | "firstName": "Tomas", 250 | "lastName": "Haake" 251 | } 252 | ] 253 | } 254 | } 255 | ``` 256 | 257 | ### Example: Creating and Updating a Person 258 | 259 | In this example, we can create or update a `Person` record in Mirage's database. 260 | 261 | ```javascript 262 | // app/components/people.js 263 | 264 | import { createServer } from "app/mirage/server" 265 | import { request } from "graphql-request" 266 | 267 | const server = createServer() 268 | 269 | export default { 270 | // ...other component stuff 271 | 272 | createPersonMutation: ` 273 | mutation CreatePerson($input: PersonInput!) { 274 | createPerson(input: $input) { 275 | id 276 | firstName 277 | lastName 278 | } 279 | } 280 | `, 281 | updatePersonMutation: ` 282 | mutation UpdatePerson($id: ID!, $input: PersonInput!) { 283 | updatePerson(id: $id, input: $input) { 284 | id 285 | firstName 286 | lastName 287 | } 288 | } 289 | `, 290 | createPerson(input) { 291 | return request("/graphql", this.createPersonMutation, { input }) 292 | }, 293 | updatePerson(id, input) { 294 | return request("/graphql", this.updatePersonMutation, { id, input }) 295 | } 296 | } 297 | ``` 298 | 299 | A call to `createPerson({ firstName: "Ola", lastName: "Englund" })` will cause Mirage GraphQL to respond with: 300 | 301 | ```json 302 | { 303 | "data": { 304 | "createPerson": { 305 | "id": "1", 306 | "firstName": "Ola", 307 | "lastName": "Englund" 308 | } 309 | } 310 | } 311 | ``` 312 | 313 | If you then wanted to update that person, you could call `updatePerson("1", { lastName: "Strandberg" })` which would result in: 314 | 315 | ```json 316 | { 317 | "data": { 318 | "updatePerson": { 319 | "id": "1", 320 | "firstName": "Ola", 321 | "lastName": "Strandberg" 322 | } 323 | } 324 | } 325 | ``` 326 | 327 | #### Automatic Mutation Conventions 328 | 329 | Mirage GraphQL will automatically resolve these mutations per these conventions: 330 | 331 | * A mutation that returns an object type and has one argument, an input type, will create a record with the given input type attributes. 332 | * A mutation that returns an object type and has two arguments, an ID type and an input type, will update a record having that ID with the given input type attributes. 333 | * A mutation that returns an object type and has one argument, an ID type, will delete a record having that ID. 334 | 335 | Any other combination of arguments for a mutation requires a resolver. This can be seen in a later example. 336 | 337 | ### Example: Filtering People 338 | 339 | In this example, we can get filter `Person` records from Mirage's database. There will be two parts. In part 1, we'll filter by `lastName` which is an argument for the query and an attribute of `Person` records. In part 2, we'll add a `sortBy` argument which will require us to implement a resolver. 340 | 341 | #### Part 1: Filtering by Last Name 342 | 343 | In the following case, Mirage GraphQL can automatically filter the records from Mirage's database because the `lastName` argument for the query matches an attribute of the records. 344 | 345 | ```javascript 346 | // app/components/people.js 347 | 348 | import { createServer } from "app/mirage/server" 349 | import { request } from "graphql-request" 350 | 351 | const server = createServer() 352 | 353 | server.create("person", { firstName: "Mikael", lastName: "Åkerfeldt" }) 354 | server.create("person", { firstName: "Per", lastName: "Nilsson" }) 355 | server.create("person", { firstName: "Tomas", lastName: "Haake" }) 356 | 357 | export default { 358 | // ...other component stuff 359 | 360 | peopleQuery: ` 361 | query People($firstName: String, $lastName: String, $sortBy: String) { 362 | people(firstName: $firstName, lastName: $lastName, sortBy: $sortBy) { 363 | id 364 | firstName 365 | lastName 366 | } 367 | } 368 | `, 369 | getPeopleByLastName(lastName) { 370 | return request("/graphql", this.peopleQuery, { lastName }) 371 | } 372 | } 373 | ``` 374 | 375 | A call to `getPeopleByLastName("Haake")` will cause Mirage GraphQL to respond with: 376 | 377 | ```json 378 | { 379 | "data": { 380 | "people": [ 381 | { 382 | "id": "3", 383 | "firstName": "Tomas", 384 | "lastName": "Haake" 385 | } 386 | ] 387 | } 388 | } 389 | ``` 390 | 391 | #### Part 2: Sorting 392 | 393 | In the following case, Mirage GraphQL can't automatically resolve the query because the `sortBy` argument for the query doesn't match any attribute of the records. To do this, we need to add pass a resolver in when creating our GraphQL handler. 394 | 395 | In the Mirage server setup: 396 | 397 | ```javascript 398 | // app/mirage/server.js 399 | 400 | import { createServer } from "miragejs" 401 | import graphQLSchema from "app/gql/schema.gql" 402 | import { 403 | createGraphQLHandler, 404 | mirageGraphQLFieldResolver 405 | } from "@miragejs/graphql" 406 | 407 | export function makeServer() { 408 | return createServer({ 409 | routes() { 410 | const graphQLHandler = createGraphQLHandler(graphQLSchema, this.schema, { 411 | resolvers: { 412 | Query: { 413 | people(obj, args, context, info) { 414 | const { sortBy } = args 415 | 416 | delete args.sortBy 417 | 418 | const records = 419 | mirageGraphQLFieldResolver(obj, args, context, info) 420 | 421 | return records.sort((a, b) => a[sortBy].localeCompare(b[sortBy])) 422 | } 423 | } 424 | } 425 | }) 426 | 427 | this.post("/graphql", graphQLHandler) 428 | } 429 | }) 430 | } 431 | ``` 432 | 433 | Note: We can pass as many resolvers into `createGraphQLHandler` as we want. Additionally, we can compose resolvers by leaning on the default field resolver from Mirage GraphQL, as shown above. In this case, the default field resolver does most of the work to get the records and our custom resolver only has to sort them. 434 | 435 | Having added a resolver to handle the `sortBy` argument, the following component example will now work: 436 | 437 | ```javascript 438 | // app/components/people.js 439 | 440 | import { createServer } from "app/mirage/server" 441 | import { request } from "graphql-request" 442 | 443 | const server = createServer() 444 | 445 | server.create("person", { firstName: "Mikael", lastName: "Åkerfeldt" }) 446 | server.create("person", { firstName: "Per", lastName: "Nilsson" }) 447 | server.create("person", { firstName: "Tomas", lastName: "Haake" }) 448 | 449 | export default { 450 | // ...other component stuff 451 | 452 | peopleQuery: ` 453 | query People($firstName: String, $lastName: String, $sortBy: String) { 454 | people(firstName: $firstName, lastName: $lastName, sortBy: $sortBy) { 455 | id 456 | firstName 457 | lastName 458 | } 459 | } 460 | `, 461 | getSortedPeopleBy(sortBy) { 462 | return request("/graphql", this.peopleQuery, { sortBy }) 463 | } 464 | } 465 | ``` 466 | 467 | A call to `getSortedPeopleBy("lastName")` will cause Mirage GraphQL to respond with: 468 | 469 | ```json 470 | { 471 | "data": { 472 | "people": [ 473 | { 474 | "id": "1", 475 | "firstName": "Mikael", 476 | "lastName": "Åkerfeldt" 477 | }, 478 | { 479 | "id": "3", 480 | "firstName": "Tomas", 481 | "lastName": "Haake" 482 | }, 483 | { 484 | "id": "2", 485 | "firstName": "Per", 486 | "lastName": "Nilsson" 487 | } 488 | ] 489 | } 490 | } 491 | ``` 492 | 493 | ### Example: Deleting a Person 494 | 495 | If you read the section on automatically resolving mutations, you'll know that Mirage GraphQL can automatically handle conventional mutations that delete records. However, in our example schema, the `deletePerson` mutation is unconventional. It returns `Boolean` instead of a `Person`. In this case, we need to implement a resolver. 496 | 497 | In the Mirage server setup: 498 | 499 | ```javascript 500 | // app/mirage/server.js 501 | 502 | import { createServer } from "miragejs" 503 | import graphQLSchema from "app/gql/schema.gql" 504 | import { 505 | createGraphQLHandler, 506 | mirageGraphQLFieldResolver 507 | } from "@miragejs/graphql" 508 | 509 | export function makeServer() { 510 | return createServer({ 511 | routes() { 512 | const graphQLHandler = createGraphQLHandler(graphQLSchema, this.schema, { 513 | resolvers: { 514 | Mutation: { 515 | deletePerson(obj, args, context, info) { 516 | const person = context.mirageSchema.db.people.find(args.id) 517 | 518 | if (person) { 519 | context.mirageSchema.db.people.remove(args.id) 520 | 521 | return true 522 | } 523 | 524 | return false 525 | } 526 | } 527 | } 528 | }) 529 | 530 | this.post("/graphql", graphQLHandler) 531 | } 532 | }) 533 | } 534 | ``` 535 | 536 | Having added a resolver to handle the mutation, the following component example will now work: 537 | 538 | ```javascript 539 | // app/components/people.js 540 | 541 | import { createServer } from "app/mirage/server" 542 | import { request } from "graphql-request" 543 | 544 | const server = createServer() 545 | 546 | export default { 547 | // ...other component stuff 548 | 549 | deletePersonMutation: ` 550 | mutation DeletePerson($id: ID!) { 551 | deletePerson(id: $id) 552 | } 553 | `, 554 | deletePerson(id) { 555 | return request("/graphql", this.deletePersonMutation, { id }) 556 | } 557 | } 558 | ``` 559 | 560 | A call to `deletePerson("1")` will remove the record from Mirage's database and cause Mirage GraphQL to respond with: 561 | 562 | ```json 563 | { 564 | "data": { 565 | "deletePerson": true 566 | } 567 | } 568 | ``` 569 | 570 | ## Getting Help and Contributing 571 | 572 | Discussions are welcome anywhere including the Mirage Discord server's `#graphql` channel. Please feel free to reach out for help or to collaborate. 573 | 574 | Any contributions are welcome. The most helpful contributions come from new use cases and most often arrive in the form of GitHub issues. One great way to contribute a new use case is by adding a failing test. 575 | 576 | ## History 577 | 578 | As Mirage itself evolved from an Ember add-on ([ember-cli-mirage](https://ember-cli-mirage.com)) so too did Mirage GraphQL ([ember-cli-mirage-graphql](https://github.com/kloeckner-i/ember-cli-mirage-graphql)). 579 | 580 | ### Differences from `ember-cli-mirage-graphql` 581 | 582 | The `ember-cli-mirage-graphql` add-on doesn't leverage very many features of [GraphQL JS](https://github.com/graphql/graphql-js) and does quite a lot of custom work to resolve queries. 583 | 584 | There are several disadvantages to its approach, namely: 585 | 586 | * It doesn't use resolvers but rather uses the mocking feature from [GraphQL Tools](https://github.com/ardatan/graphql-tools). This can lead to some strange results, if every field isn't mocked properly. 587 | * It doesn't use much of GraphQL's API and re-implements a lot of existing functionality in a less robust way. 588 | * It doesn't use Mirage's ORM API which introduces many limitations on its ability to automatically resolve records. 589 | * The add-on's API includes custom field and variable mapping which can be avoided entirely by providing the ability to supply your own resolvers. 590 | 591 | ### Upgrading 592 | 593 | If you want to upgrade to Mirage GraphQL from `ember-cli-mirage-graphql`, you may need to make some significant changes in how you create the GraphQL handler. Firstly, you will need to pass in your Mirage schema as shown at the top of this README. 594 | 595 | If you used any of the options, `fieldsMap`, `varsMap` and `mutations`, you will need to re-implement them with resolvers; though, hopefully some mutations can be automatically resolved for you now. 596 | 597 | ## Special Thanks 598 | 599 | Special thanks for helping this library evolve go out to [Sam Selikoff](https://github.com/samselikoff), [Chad Carbert](https://github.com/chadian), [Jamie White](https://github.com/jgwhite), [Blake Gentry](https://github.com/bgentry), [Ruben Manrique](https://github.com/miwialex), [Louis-Michel Couture](https://github.com/louim), [David Mazza](https://github.com/dmzza), [Cameron Nicklaus](https://github.com/camnicklaus) and [Bert De Block](https://github.com/bertdeblock). 600 | -------------------------------------------------------------------------------- /__tests__/gql/mutations/create-non-null.gql: -------------------------------------------------------------------------------- 1 | mutation CreateTestObjectNonNull($input: TestObjectInput!) { 2 | createTestObjectNonNull(input: $input) { 3 | id 4 | size 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/gql/mutations/create.gql: -------------------------------------------------------------------------------- 1 | mutation CreateTestObject($input: TestObjectInput) { 2 | createTestObject(input: $input) { 3 | id 4 | size 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/gql/mutations/delete-alt.gql: -------------------------------------------------------------------------------- 1 | mutation DeleteTestObjectAlt($id: ID!) { 2 | deleteTestObjectAlt(id: $id) 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/gql/mutations/delete.gql: -------------------------------------------------------------------------------- 1 | mutation DeleteTestObject($id: ID!) { 2 | deleteTestObject(id: $id) { 3 | id 4 | size 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/gql/mutations/optional.gql: -------------------------------------------------------------------------------- 1 | mutation OptionallyMutateTestObject($id: ID!, $input: TestObjectInput!) { 2 | optionallyMutateTestObject(id: $id, input: $input) { 3 | id 4 | size 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/gql/mutations/unimplemented.gql: -------------------------------------------------------------------------------- 1 | mutation Unimplemented { 2 | unimplemented { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/gql/mutations/update-non-null.gql: -------------------------------------------------------------------------------- 1 | mutation UpdateTestObjectNonNull($id: ID!, $input: TestObjectInput!) { 2 | updateTestObjectNonNull(id: $id, input: $input) { 3 | id 4 | size 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/gql/mutations/update.gql: -------------------------------------------------------------------------------- 1 | mutation UpdateTestObject($id: ID!, $input: TestObjectInput) { 2 | updateTestObject(id: $id, input: $input) { 3 | id 4 | size 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/gql/queries/context.gql: -------------------------------------------------------------------------------- 1 | query { testContext } 2 | -------------------------------------------------------------------------------- /__tests__/gql/queries/interface-inline-fragment.gql: -------------------------------------------------------------------------------- 1 | query TestInterfaceInlineFragment($id: ID, $label: String) { 2 | testInterface(id: $id, label: $label) { 3 | ... on TestImplOne { 4 | id 5 | description 6 | label 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/gql/queries/interface-non-null.gql: -------------------------------------------------------------------------------- 1 | query TestInterfaceNonNull($label: String) { 2 | testInterfaceNonNull(label: $label) { 3 | id 4 | label 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/gql/queries/interface.gql: -------------------------------------------------------------------------------- 1 | query TestInterface($label: String) { 2 | testInterface(label: $label) { 3 | id 4 | label 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/gql/queries/object-kitchen-sink.gql: -------------------------------------------------------------------------------- 1 | fragment testOption on TestOption { 2 | id 3 | name 4 | } 5 | 6 | fragment testRelayConnection on TestRelayConnection { 7 | edges { 8 | cursor 9 | node { 10 | id 11 | } 12 | } 13 | pageInfo { 14 | hasPreviousPage 15 | hasNextPage 16 | startCursor 17 | endCursor 18 | } 19 | } 20 | 21 | fragment testUnion on TestUnion { 22 | ... on TestUnionOne { 23 | id 24 | oneName 25 | } 26 | ... on TestUnionTwo { 27 | id 28 | twoName 29 | } 30 | } 31 | 32 | query TestObject($id: ID!) { 33 | testObject(id: $id) { 34 | id 35 | size 36 | sizeNonNull 37 | belongsToField { 38 | id 39 | name 40 | } 41 | belongsToNonNullField { 42 | id 43 | name 44 | } 45 | hasManyField { 46 | ...testOption 47 | } 48 | hasManyFilteredField(name: "Foo") { 49 | ...testOption 50 | } 51 | hasManyNonNullField { 52 | ...testOption 53 | } 54 | hasManyNestedNonNullField { 55 | ...testOption 56 | } 57 | interfaceField { 58 | ... on TestImplOne { 59 | id 60 | label 61 | } 62 | } 63 | interfaceNonNullField { 64 | ... on TestImplOne { 65 | id 66 | label 67 | } 68 | } 69 | relayConnectionField(first: 1, after: "VGVzdFJlbGF5Tm9kZTox") { 70 | ...testRelayConnection 71 | } 72 | relayConnectionFilteredField(first: 1, color: "blue") { 73 | edges { 74 | cursor 75 | node { 76 | id 77 | color 78 | } 79 | } 80 | pageInfo { 81 | hasPreviousPage 82 | hasNextPage 83 | startCursor 84 | endCursor 85 | } 86 | } 87 | relayConnectionNonNullField(last: 1, before: "VGVzdFJlbGF5Tm9kZToz") { 88 | ...testRelayConnection 89 | } 90 | unionField { 91 | ...testUnion 92 | } 93 | unionNonNullField(oneName: "foo") { 94 | ...testUnion 95 | } 96 | unionNestedNonNullField(twoName: "bar") { 97 | ...testUnion 98 | } 99 | unionSingularField { 100 | ...testUnion 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /__tests__/gql/queries/object-non-null.gql: -------------------------------------------------------------------------------- 1 | query TestObjectNonNull($id: ID!) { 2 | testObjectNonNull(id: $id) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/gql/queries/object.gql: -------------------------------------------------------------------------------- 1 | query TestObject($id: ID!) { 2 | testObject(id: $id) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/gql/queries/objects-nested-non-null.gql: -------------------------------------------------------------------------------- 1 | query TestObjectsNestedNonNull($size: String) { 2 | testObjectsNestedNonNull(size: $size) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/gql/queries/objects-non-null.gql: -------------------------------------------------------------------------------- 1 | query TestObjectsNonNull($size: String) { 2 | testObjectsNonNull(size: $size) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/gql/queries/objects.gql: -------------------------------------------------------------------------------- 1 | query TestObjects($size: String) { 2 | testObjects(size: $size) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/gql/queries/relay-connection.gql: -------------------------------------------------------------------------------- 1 | query TestRelayConnection( 2 | $color: String 3 | $first: Int 4 | $last: Int 5 | $after: String 6 | $before: String 7 | ) { 8 | testRelayConnection( 9 | color: $color 10 | first: $first 11 | last: $last 12 | after: $after 13 | before: $before 14 | ) { 15 | edges { 16 | cursor 17 | node { 18 | id 19 | } 20 | } 21 | totalCount 22 | pageInfo { 23 | hasPreviousPage 24 | hasNextPage 25 | startCursor 26 | endCursor 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /__tests__/gql/queries/relay-non-null-edges-connection.gql: -------------------------------------------------------------------------------- 1 | query TestNonNullEdgesRelayConnection( 2 | $color: String 3 | $first: Int 4 | $last: Int 5 | $after: String 6 | $before: String 7 | ) { 8 | testNonNullEdgesRelayConnection( 9 | color: $color 10 | first: $first 11 | last: $last 12 | after: $after 13 | before: $before 14 | ) { 15 | edges { 16 | cursor 17 | node { 18 | id 19 | } 20 | } 21 | totalCount 22 | pageInfo { 23 | hasPreviousPage 24 | hasNextPage 25 | startCursor 26 | endCursor 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /__tests__/gql/queries/relay-non-null-nodes-connection.gql: -------------------------------------------------------------------------------- 1 | query TestNonNullNodesRelayConnection( 2 | $color: String 3 | $first: Int 4 | $last: Int 5 | $after: String 6 | $before: String 7 | ) { 8 | testNonNullNodesRelayConnection( 9 | color: $color 10 | first: $first 11 | last: $last 12 | after: $after 13 | before: $before 14 | ) { 15 | edges { 16 | cursor 17 | node { 18 | id 19 | } 20 | } 21 | totalCount 22 | pageInfo { 23 | hasPreviousPage 24 | hasNextPage 25 | startCursor 26 | endCursor 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /__tests__/gql/queries/scalar-non-null.gql: -------------------------------------------------------------------------------- 1 | query { testScalarNonNull } 2 | -------------------------------------------------------------------------------- /__tests__/gql/queries/scalar-optional-resolve.gql: -------------------------------------------------------------------------------- 1 | query { testScalarOptionalResolve } 2 | -------------------------------------------------------------------------------- /__tests__/gql/queries/scalar.gql: -------------------------------------------------------------------------------- 1 | query { testScalar } 2 | -------------------------------------------------------------------------------- /__tests__/gql/queries/sorted-objects.gql: -------------------------------------------------------------------------------- 1 | query TestSortedObjects { 2 | testSortedObjects { 3 | size 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/gql/queries/union-nested-non-null.gql: -------------------------------------------------------------------------------- 1 | query TestUnionNestedNonNull($oneName: String, $twoName: String) { 2 | testUnionNestedNonNull(oneName: $oneName, twoName: $twoName) { 3 | ... on TestUnionOne { 4 | oneName 5 | } 6 | ... on TestUnionTwo { 7 | twoName 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /__tests__/gql/queries/union-non-null.gql: -------------------------------------------------------------------------------- 1 | query TestUnionNonNull($oneName: String, $twoName: String) { 2 | testUnionNonNull(oneName: $oneName, twoName: $twoName) { 3 | ... on TestUnionOne { 4 | oneName 5 | } 6 | ... on TestUnionTwo { 7 | twoName 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /__tests__/gql/queries/union.gql: -------------------------------------------------------------------------------- 1 | query TestUnion($oneName: String, $twoName: String) { 2 | testUnion(oneName: $oneName, twoName: $twoName) { 3 | ... on TestUnionOne { 4 | oneName 5 | } 6 | ... on TestUnionTwo { 7 | twoName 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /__tests__/gql/schema.gql: -------------------------------------------------------------------------------- 1 | input TestObjectInput { 2 | size: String 3 | } 4 | 5 | interface TestInterface { 6 | id: ID! 7 | label: String! 8 | } 9 | 10 | type Mutation { 11 | createTestObject(input: TestObjectInput): TestObject 12 | createTestObjectNonNull(input: TestObjectInput!): TestObject 13 | deleteTestObjectAlt(id: ID!): Boolean 14 | deleteTestObject(id: ID!): TestObject 15 | optionallyMutateTestObject(id: ID!, input: TestObjectInput!): TestObject 16 | unimplemented: TestObject 17 | updateTestObject(id: ID!, input: TestObjectInput): TestObject 18 | updateTestObjectNonNull(id: ID!, input: TestObjectInput!): TestObject 19 | } 20 | 21 | type PageInfo { 22 | endCursor: String 23 | hasNextPage: Boolean 24 | hasPreviousPage: Boolean 25 | startCursor: String 26 | } 27 | 28 | type Query { 29 | testContext: String 30 | testInterface(id: ID, label: String): TestInterface 31 | testInterfaceNonNull(id: ID, label: String): TestInterface! 32 | testInterfaceOptional(id: ID!): TestInterface 33 | testObject(id: ID!): TestObject 34 | testObjectNonNull(id: ID!): TestObject! 35 | testObjects(size: String): [TestObject] 36 | testObjectsNonNull(size: String): [TestObject]! 37 | testObjectsNestedNonNull(size: String): [TestObject!]! 38 | testRelayConnection( 39 | color: String 40 | first: Int 41 | last: Int 42 | before: String 43 | after: String 44 | ): TestRelayConnection 45 | testNonNullEdgesRelayConnection( 46 | color: String 47 | first: Int 48 | last: Int 49 | before: String 50 | after: String 51 | ): TestNonNullEdgesRelayConnection 52 | testNonNullNodesRelayConnection( 53 | color: String 54 | first: Int 55 | last: Int 56 | before: String 57 | after: String 58 | ): TestNonNullNodesRelayConnection 59 | testScalar: String 60 | testScalarNonNull: String! 61 | testScalarOptionalResolve: String 62 | testSortedObjects: [TestObject] 63 | testUnion(oneName: String, twoName: String): [TestUnion] 64 | testUnionNonNull(oneName: String, twoName: String): [TestUnion]! 65 | testUnionNestedNonNull(oneName: String, twoName: String): [TestUnion!]! 66 | testUnionSingular(oneName: String, twoName: String): TestUnion 67 | } 68 | 69 | type TestCategory { 70 | id: ID! 71 | name: String! 72 | } 73 | 74 | type TestImplOne implements TestInterface { 75 | id: ID! 76 | description: String 77 | label: String! 78 | } 79 | 80 | type TestImplTwo implements TestInterface { 81 | id: ID! 82 | label: String! 83 | } 84 | 85 | type TestObject { 86 | id: ID! 87 | belongsToField: TestCategory 88 | belongsToNonNullField: TestCategory! 89 | hasManyField: [TestOption] 90 | hasManyFilteredField(name: String): [TestOption] 91 | hasManyNonNullField: [TestOption]! 92 | hasManyNestedNonNullField: [TestOption!]! 93 | interfaceField: TestInterface 94 | interfaceNonNullField: TestInterface! 95 | relayConnectionField(first: Int, after: String): TestRelayConnection 96 | relayConnectionFilteredField( 97 | first: Int 98 | after: String 99 | color: String 100 | ): TestRelayConnection 101 | relayConnectionNonNullField(last: Int, before: String): TestRelayConnection! 102 | size: String 103 | sizeNonNull: String! 104 | unionField: [TestUnion] 105 | unionNonNullField(oneName: String): [TestUnion]! 106 | unionNestedNonNullField(twoName: String): [TestUnion!]! 107 | unionSingularField: TestUnion 108 | } 109 | 110 | type TestOption { 111 | id: ID! 112 | name: String! 113 | } 114 | 115 | type TestRelayConnection { 116 | edges: [TestRelayEdge] 117 | totalCount: Int! 118 | pageInfo: PageInfo! 119 | } 120 | 121 | type TestNonNullEdgesRelayConnection { 122 | edges: [TestRelayEdge!]! 123 | totalCount: Int! 124 | pageInfo: PageInfo! 125 | } 126 | 127 | type TestNonNullNodesRelayConnection { 128 | edges: [TestNonNullNodesRelayEdge] 129 | totalCount: Int! 130 | pageInfo: PageInfo! 131 | } 132 | 133 | type TestRelayEdge { 134 | cursor: String! 135 | node: TestRelayNode 136 | } 137 | 138 | type TestNonNullNodesRelayEdge { 139 | cursor: String! 140 | node: TestRelayNode! 141 | } 142 | 143 | type TestRelayNode { 144 | id: ID! 145 | color: String 146 | } 147 | 148 | type TestUnionOne { 149 | id: ID! 150 | oneName: String 151 | } 152 | 153 | type TestUnionTwo { 154 | id: ID! 155 | twoName: String 156 | } 157 | 158 | union TestUnion = TestUnionOne | TestUnionTwo 159 | -------------------------------------------------------------------------------- /__tests__/gql/schema.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import schema from "./schema.gql"; 3 | import { buildASTSchema } from "graphql"; 4 | 5 | export const graphQLSchemaAST = gql` 6 | ${schema} 7 | `; 8 | export const graphQLSchema = buildASTSchema(graphQLSchemaAST); 9 | -------------------------------------------------------------------------------- /__tests__/integration/handler-test.js: -------------------------------------------------------------------------------- 1 | import { createGraphQLHandler } from "../../lib/handler"; 2 | import { createServer } from "miragejs"; 3 | import gql from "graphql-tag"; 4 | import { query } from "@tests/integration/setup"; 5 | 6 | let mirageSchema; 7 | let request; 8 | 9 | function startServer({ resolvers }) { 10 | const graphQLSchema = ` 11 | type Foo { bar: String } 12 | type Query { foo: Foo } 13 | `; 14 | const server = createServer({ 15 | routes() { 16 | this.post("/graphql", (_schema, _request) => { 17 | mirageSchema = this.schema; 18 | request = _request; 19 | 20 | const handler = createGraphQLHandler(graphQLSchema, mirageSchema, { 21 | resolvers, 22 | }); 23 | 24 | return handler(_schema, _request); 25 | }); 26 | }, 27 | }); 28 | 29 | server.logging = false; 30 | 31 | return server; 32 | } 33 | 34 | describe("Integration | handler", function () { 35 | test("resolver context gets Mirage schema and request", async function () { 36 | expect.assertions(2); 37 | 38 | const server = startServer({ 39 | resolvers: { 40 | Query: { 41 | foo(_obj, _args, context) { 42 | expect(context.mirageSchema).toBe(mirageSchema); 43 | expect(context.request).toBe(request); 44 | 45 | return { bar: "foo" }; 46 | }, 47 | }, 48 | }, 49 | }); 50 | 51 | server.logging = false; 52 | 53 | await query( 54 | gql` 55 | query { 56 | foo { 57 | bar 58 | } 59 | } 60 | ` 61 | ); 62 | 63 | server.shutdown(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /__tests__/integration/mutations/create-object-test.js: -------------------------------------------------------------------------------- 1 | import createTestObjectMutation from "@tests/gql/mutations/create.gql"; 2 | import createTestObjectNonNullMutation from "@tests/gql/mutations/create-non-null.gql"; 3 | import { mutate, startServer } from "@tests/integration/setup"; 4 | 5 | let server; 6 | 7 | describe("Integration | mutations | create", function () { 8 | beforeEach(function () { 9 | server = startServer(); 10 | }); 11 | 12 | afterEach(function () { 13 | server.shutdown(); 14 | }); 15 | 16 | it("can create a test object", async function () { 17 | const { createTestObject } = await mutate(createTestObjectMutation, { 18 | variables: { 19 | input: { size: "M" }, 20 | }, 21 | }); 22 | const record = server.schema.testObjects.first(); 23 | 24 | expect(createTestObject).toEqual({ id: "1", size: "M" }); 25 | expect(record.id).toBe("1"); 26 | expect(record.size).toBe("M"); 27 | }); 28 | 29 | it("can create a test object (non-null input type)", async function () { 30 | const { createTestObjectNonNull } = await mutate( 31 | createTestObjectNonNullMutation, 32 | { 33 | variables: { 34 | input: { size: "M" }, 35 | }, 36 | } 37 | ); 38 | const record = server.schema.testObjects.first(); 39 | 40 | expect(createTestObjectNonNull).toEqual({ id: "1", size: "M" }); 41 | expect(record.id).toBe("1"); 42 | expect(record.size).toBe("M"); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/integration/mutations/delete-object-test.js: -------------------------------------------------------------------------------- 1 | import deleteTestObjectMutation from "@tests/gql/mutations/delete.gql"; 2 | import deleteTestObjectAltMutation from "@tests/gql/mutations/delete-alt.gql"; 3 | import { mutate, startServer } from "@tests/integration/setup"; 4 | 5 | let server; 6 | 7 | describe("Integration | mutations | delete", function () { 8 | afterEach(function () { 9 | server.shutdown(); 10 | }); 11 | 12 | it("can delete a test object", async function () { 13 | server = startServer(); 14 | 15 | server.create("test-object", { size: "M" }); 16 | 17 | const { deleteTestObject } = await mutate(deleteTestObjectMutation, { 18 | variables: { id: "1" }, 19 | }); 20 | const record = server.schema.testObjects.first(); 21 | 22 | expect(deleteTestObject).toEqual({ id: "1", size: "M" }); 23 | expect(record).toBeNull(); 24 | }); 25 | 26 | it("can delete a test object and return a boolean value", async function () { 27 | server = startServer({ 28 | resolvers: { 29 | Mutation: { 30 | deleteTestObjectAlt(_obj, args, context) { 31 | context.mirageSchema.db.testObjects.remove(args.id); 32 | 33 | return true; 34 | }, 35 | }, 36 | }, 37 | }); 38 | 39 | server.create("test-object", { size: "M" }); 40 | 41 | const { deleteTestObjectAlt } = await mutate(deleteTestObjectAltMutation, { 42 | variables: { id: "1" }, 43 | }); 44 | const record = server.schema.testObjects.first(); 45 | 46 | expect(deleteTestObjectAlt).toEqual(true); 47 | expect(record).toBeNull(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/integration/mutations/optional-test.js: -------------------------------------------------------------------------------- 1 | import optionalTestObjectMutation from "@tests/gql/mutations/optional.gql"; 2 | import { mutate, startServer } from "@tests/integration/setup"; 3 | 4 | describe("Integration | mutations | optional", function () { 5 | it("can run optional mutations", async function () { 6 | const server = startServer({ 7 | resolvers: { 8 | Mutation: { 9 | optionallyMutateTestObject(_obj, { id, input }, context) { 10 | return context.mirageSchema.db.testObjects.update(id, input); 11 | }, 12 | }, 13 | }, 14 | }); 15 | 16 | server.create("test-object", { size: "S" }); 17 | 18 | const { optionallyMutateTestObject } = await mutate( 19 | optionalTestObjectMutation, 20 | { 21 | variables: { 22 | id: "1", 23 | input: { 24 | size: "M", 25 | }, 26 | }, 27 | } 28 | ); 29 | 30 | expect(optionallyMutateTestObject).toEqual({ id: "1", size: "M" }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/integration/mutations/unimplemented-test.js: -------------------------------------------------------------------------------- 1 | import unimplementedMutation from "@tests/gql/mutations/unimplemented.gql"; 2 | import { mutate, startServer } from "@tests/integration/setup"; 3 | 4 | describe("Integration | mutations | unimplemented", function () { 5 | it("throws an error if no default mutation is found", async function () { 6 | startServer(); 7 | 8 | await expect(() => mutate(unimplementedMutation)).rejects.toThrow( 9 | "Could not find a default resolver for unimplemented. Please supply a resolver for this mutation." 10 | ); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/integration/mutations/update-object-test.js: -------------------------------------------------------------------------------- 1 | import updateTestObjectMutation from "@tests/gql/mutations/update.gql"; 2 | import updateTestObjectNonNullMutation from "@tests/gql/mutations/update-non-null.gql"; 3 | import { mutate, startServer } from "@tests/integration/setup"; 4 | 5 | let server; 6 | 7 | describe("Integration | mutations | update", function () { 8 | beforeEach(function () { 9 | server = startServer(); 10 | }); 11 | 12 | afterEach(function () { 13 | server.shutdown(); 14 | }); 15 | 16 | it("can update a test object", async function () { 17 | server.create("test-object", { size: "M" }); 18 | 19 | const { updateTestObject } = await mutate(updateTestObjectMutation, { 20 | variables: { id: "1", input: { size: "L" } }, 21 | }); 22 | const record = server.schema.testObjects.first(); 23 | 24 | expect(updateTestObject).toEqual({ id: "1", size: "L" }); 25 | expect(record.size).toBe("L"); 26 | }); 27 | 28 | it("can update a test object (non-null input)", async function () { 29 | server.create("test-object", { size: "M" }); 30 | 31 | const { updateTestObjectNonNull } = await mutate( 32 | updateTestObjectNonNullMutation, 33 | { 34 | variables: { id: "1", input: { size: "L" } }, 35 | } 36 | ); 37 | const record = server.schema.testObjects.first(); 38 | 39 | expect(updateTestObjectNonNull).toEqual({ id: "1", size: "L" }); 40 | expect(record.size).toBe("L"); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /__tests__/integration/queries/interface-test.js: -------------------------------------------------------------------------------- 1 | import interfaceQuery from "@tests/gql/queries/interface.gql"; 2 | import interfaceNonNullQuery from "@tests/gql/queries/interface-non-null.gql"; 3 | import interfaceInlineFragmentQuery from "@tests/gql/queries/interface-inline-fragment.gql"; 4 | import { query, startServer } from "@tests/integration/setup"; 5 | 6 | let server; 7 | 8 | describe("Integration | queries | interface", function () { 9 | beforeEach(function () { 10 | server = startServer(); 11 | 12 | server.create("test-impl-one", { description: "foo", label: "bar" }); 13 | server.create("test-impl-two", { label: "baz" }); 14 | }); 15 | 16 | afterEach(function () { 17 | server.shutdown(); 18 | }); 19 | 20 | test("query for interface (inline fragment)", async function () { 21 | const { testInterface } = await query(interfaceInlineFragmentQuery, { 22 | variables: { id: "1" }, 23 | }); 24 | 25 | expect(testInterface).toEqual({ 26 | id: "1", 27 | description: "foo", 28 | label: "bar", 29 | }); 30 | }); 31 | 32 | test("query for filtered interface", async function () { 33 | const { testInterface } = await query(interfaceQuery, { 34 | variables: { label: "bar" }, 35 | }); 36 | 37 | expect(testInterface).toEqual({ 38 | id: "1", 39 | label: "bar", 40 | }); 41 | }); 42 | 43 | test("query for filtered, non-null interface", async function () { 44 | const { testInterfaceNonNull } = await query(interfaceNonNullQuery, { 45 | variables: { label: "baz" }, 46 | }); 47 | 48 | expect(testInterfaceNonNull).toEqual({ 49 | id: "1", 50 | label: "baz", 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /__tests__/integration/queries/object-kitchen-sink-test.js: -------------------------------------------------------------------------------- 1 | import objectKitchenSinkQuery from "@tests/gql/queries/object-kitchen-sink.gql"; 2 | import { query, startServer } from "@tests/integration/setup"; 3 | 4 | function seedUnassociatedRecords(server) { 5 | server.createList("test-option", 2); 6 | server.create("test-union-one"); 7 | server.create("test-union-two"); 8 | } 9 | 10 | describe("Integration | queries | object", function () { 11 | test("query for test object", async function () { 12 | const server = startServer(); 13 | const testCategory = server.create("test-category", { name: "cat" }); 14 | const testImpl = server.create("test-impl-one", { label: "impl" }); 15 | const testOptions = server.createList("test-option", 1, { name: "opt" }); 16 | const filterableTestOptions = [ 17 | ...testOptions, 18 | server.create("test-option", { name: "Foo" }), 19 | ]; 20 | const blueTestRelayNode = server.create("test-relay-node", { 21 | color: "blue", 22 | }); 23 | const testRelayNodes = [ 24 | blueTestRelayNode, 25 | ...server.createList("test-relay-node", 2), 26 | ]; 27 | const testUnions = [ 28 | server.create("test-union-one", { oneName: "foo" }), 29 | server.create("test-union-two", { twoName: "bar" }), 30 | ]; 31 | 32 | seedUnassociatedRecords(server); 33 | 34 | server.create("test-object", { 35 | size: "XL", 36 | sizeNonNull: "XL", 37 | belongsToField: testCategory, 38 | belongsToNonNullField: testCategory, 39 | hasManyField: testOptions, 40 | hasManyFilteredField: filterableTestOptions, 41 | hasManyNonNullField: testOptions, 42 | hasManyNestedNonNullField: testOptions, 43 | interfaceField: testImpl, 44 | interfaceNonNullField: testImpl, 45 | relayConnectionField: testRelayNodes, 46 | relayConnectionFilteredField: testRelayNodes, 47 | relayConnectionNonNullField: testRelayNodes, 48 | unionField: testUnions, 49 | unionNonNullField: testUnions, 50 | unionNestedNonNullField: testUnions, 51 | unionSingularField: testUnions[0], 52 | }); 53 | 54 | const { testObject } = await query(objectKitchenSinkQuery, { 55 | variables: { id: "1" }, 56 | }); 57 | 58 | expect(testObject).toEqual({ 59 | id: "1", 60 | size: "XL", 61 | sizeNonNull: "XL", 62 | belongsToField: { id: "1", name: "cat" }, 63 | belongsToNonNullField: { id: "1", name: "cat" }, 64 | hasManyField: [{ id: "1", name: "opt" }], 65 | hasManyFilteredField: [{ id: "2", name: "Foo" }], 66 | hasManyNonNullField: [{ id: "1", name: "opt" }], 67 | hasManyNestedNonNullField: [{ id: "1", name: "opt" }], 68 | interfaceField: { id: "1", label: "impl" }, 69 | interfaceNonNullField: { id: "1", label: "impl" }, 70 | relayConnectionField: { 71 | edges: [{ cursor: "VGVzdFJlbGF5Tm9kZToy", node: { id: "2" } }], 72 | pageInfo: { 73 | hasPreviousPage: true, 74 | hasNextPage: true, 75 | startCursor: "VGVzdFJlbGF5Tm9kZToy", 76 | endCursor: "VGVzdFJlbGF5Tm9kZToy", 77 | }, 78 | }, 79 | relayConnectionFilteredField: { 80 | edges: [ 81 | { 82 | cursor: "VGVzdFJlbGF5Tm9kZTox", 83 | node: { id: "1", color: "blue" }, 84 | }, 85 | ], 86 | pageInfo: { 87 | hasPreviousPage: false, 88 | hasNextPage: false, 89 | startCursor: "VGVzdFJlbGF5Tm9kZTox", 90 | endCursor: "VGVzdFJlbGF5Tm9kZTox", 91 | }, 92 | }, 93 | relayConnectionNonNullField: { 94 | edges: [{ cursor: "VGVzdFJlbGF5Tm9kZToy", node: { id: "2" } }], 95 | pageInfo: { 96 | hasPreviousPage: true, 97 | hasNextPage: true, 98 | startCursor: "VGVzdFJlbGF5Tm9kZToy", 99 | endCursor: "VGVzdFJlbGF5Tm9kZToy", 100 | }, 101 | }, 102 | unionField: [ 103 | { id: "1", oneName: "foo" }, 104 | { id: "1", twoName: "bar" }, 105 | ], 106 | unionNonNullField: [{ id: "1", oneName: "foo" }], 107 | unionNestedNonNullField: [{ id: "1", twoName: "bar" }], 108 | unionSingularField: { id: "1", oneName: "foo" }, 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /__tests__/integration/queries/object-test.js: -------------------------------------------------------------------------------- 1 | import objectQuery from "@tests/gql/queries/object.gql"; 2 | import objectNonNullQuery from "@tests/gql/queries/object-non-null.gql"; 3 | import { query, startServer } from "@tests/integration/setup"; 4 | 5 | let server; 6 | 7 | describe("Integration | queries | object", function () { 8 | beforeEach(function () { 9 | server = startServer(); 10 | }); 11 | 12 | afterEach(function () { 13 | server.shutdown(); 14 | }); 15 | 16 | test("query for test object", async function () { 17 | server.create("test-object"); 18 | 19 | const { testObject } = await query(objectQuery, { 20 | variables: { id: "1" }, 21 | }); 22 | 23 | expect(testObject).toEqual({ id: "1" }); 24 | }); 25 | 26 | test("query for non-null test object", async function () { 27 | server.create("test-object"); 28 | 29 | const { testObjectNonNull } = await query(objectNonNullQuery, { 30 | variables: { id: "1" }, 31 | }); 32 | 33 | expect(testObjectNonNull).toEqual({ id: "1" }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/integration/queries/objects-test.js: -------------------------------------------------------------------------------- 1 | import objectsQuery from "@tests/gql/queries/objects.gql"; 2 | import objectsQueryNonNull from "@tests/gql/queries/objects-non-null.gql"; 3 | import objectsQueryNestedNonNull from "@tests/gql/queries/objects-nested-non-null.gql"; 4 | import { query, startServer } from "@tests/integration/setup"; 5 | 6 | let server; 7 | 8 | describe("Integration | queries | objects", function () { 9 | beforeEach(function () { 10 | server = startServer(); 11 | }); 12 | 13 | afterEach(function () { 14 | server.shutdown(); 15 | }); 16 | 17 | test("query for test objects", async function () { 18 | server.createList("test-object", 2); 19 | 20 | const { testObjects } = await query(objectsQuery); 21 | 22 | expect(testObjects).toEqual([{ id: "1" }, { id: "2" }]); 23 | }); 24 | 25 | test("query for filtering test objects", async function () { 26 | server.create("test-object", { size: "M" }); 27 | 28 | const smallObject = server.create("test-object", { size: "S" }); 29 | const { testObjects } = await query(objectsQuery, { 30 | variables: { size: "S" }, 31 | }); 32 | 33 | expect(testObjects).toEqual([ 34 | { 35 | id: smallObject.id, 36 | }, 37 | ]); 38 | }); 39 | 40 | describe("non-null", function () { 41 | test("query for test objects", async function () { 42 | server.createList("test-object", 2); 43 | 44 | const { testObjectsNonNull } = await query(objectsQueryNonNull); 45 | 46 | expect(testObjectsNonNull).toEqual([{ id: "1" }, { id: "2" }]); 47 | }); 48 | 49 | test("query for filtering test objects", async function () { 50 | server.create("test-object", { size: "M" }); 51 | 52 | const smallObject = server.create("test-object", { size: "S" }); 53 | const { testObjectsNonNull } = await query(objectsQueryNonNull, { 54 | variables: { size: "S" }, 55 | }); 56 | 57 | expect(testObjectsNonNull).toEqual([ 58 | { 59 | id: smallObject.id, 60 | }, 61 | ]); 62 | }); 63 | }); 64 | 65 | describe("nested non-null", function () { 66 | test("query for test objects", async function () { 67 | server.createList("test-object", 2); 68 | 69 | const { testObjectsNestedNonNull } = await query( 70 | objectsQueryNestedNonNull 71 | ); 72 | 73 | expect(testObjectsNestedNonNull).toEqual([{ id: "1" }, { id: "2" }]); 74 | }); 75 | 76 | test("query for filtering test objects", async function () { 77 | server.create("test-object", { size: "M" }); 78 | 79 | const smallObject = server.create("test-object", { size: "S" }); 80 | const { testObjectsNestedNonNull } = await query( 81 | objectsQueryNestedNonNull, 82 | { 83 | variables: { size: "S" }, 84 | } 85 | ); 86 | 87 | expect(testObjectsNestedNonNull).toEqual([ 88 | { 89 | id: smallObject.id, 90 | }, 91 | ]); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /__tests__/integration/queries/relay-connection-test.js: -------------------------------------------------------------------------------- 1 | import relayConnectionQuery from "@tests/gql/queries/relay-connection.gql"; 2 | import { query, startServer } from "@tests/integration/setup"; 3 | 4 | let server; 5 | 6 | describe("Integration | queries | Relay connection", function () { 7 | beforeEach(function () { 8 | server = startServer(); 9 | 10 | server.create("test-relay-node", { color: "blue" }); 11 | server.createList("test-relay-node", 2); 12 | }); 13 | 14 | afterEach(function () { 15 | server.shutdown(); 16 | }); 17 | 18 | test("query for Relay connection", async function () { 19 | const { testRelayConnection } = await query(relayConnectionQuery); 20 | 21 | expect(testRelayConnection).toEqual({ 22 | edges: [ 23 | { 24 | cursor: "VGVzdFJlbGF5Tm9kZTox", 25 | node: { id: "1" }, 26 | }, 27 | { 28 | cursor: "VGVzdFJlbGF5Tm9kZToy", 29 | node: { id: "2" }, 30 | }, 31 | { 32 | cursor: "VGVzdFJlbGF5Tm9kZToz", 33 | node: { id: "3" }, 34 | }, 35 | ], 36 | totalCount: 3, 37 | pageInfo: { 38 | hasPreviousPage: false, 39 | hasNextPage: false, 40 | startCursor: "VGVzdFJlbGF5Tm9kZTox", 41 | endCursor: "VGVzdFJlbGF5Tm9kZToz", 42 | }, 43 | }); 44 | }); 45 | 46 | describe("filtering", function () { 47 | test("query for Relay connection by color", async function () { 48 | const { testRelayConnection } = await query(relayConnectionQuery, { 49 | variables: { color: "blue" }, 50 | }); 51 | 52 | expect(testRelayConnection).toEqual({ 53 | edges: [ 54 | { 55 | cursor: "VGVzdFJlbGF5Tm9kZTox", 56 | node: { id: "1" }, 57 | }, 58 | ], 59 | totalCount: 1, 60 | pageInfo: { 61 | hasPreviousPage: false, 62 | hasNextPage: false, 63 | startCursor: "VGVzdFJlbGF5Tm9kZTox", 64 | endCursor: "VGVzdFJlbGF5Tm9kZTox", 65 | }, 66 | }); 67 | }); 68 | 69 | test("query for Relay connection by first/after", async function () { 70 | const { testRelayConnection } = await query(relayConnectionQuery, { 71 | variables: { first: 1, after: "VGVzdFJlbGF5Tm9kZTox" }, 72 | }); 73 | 74 | expect(testRelayConnection).toEqual({ 75 | edges: [ 76 | { 77 | cursor: "VGVzdFJlbGF5Tm9kZToy", 78 | node: { id: "2" }, 79 | }, 80 | ], 81 | totalCount: 3, 82 | pageInfo: { 83 | hasPreviousPage: true, 84 | hasNextPage: true, 85 | startCursor: "VGVzdFJlbGF5Tm9kZToy", 86 | endCursor: "VGVzdFJlbGF5Tm9kZToy", 87 | }, 88 | }); 89 | }); 90 | 91 | test("query for Relay connection by last/before", async function () { 92 | const { testRelayConnection } = await query(relayConnectionQuery, { 93 | variables: { last: 1, before: "VGVzdFJlbGF5Tm9kZToz" }, 94 | }); 95 | 96 | expect(testRelayConnection).toEqual({ 97 | edges: [ 98 | { 99 | cursor: "VGVzdFJlbGF5Tm9kZToy", 100 | node: { id: "2" }, 101 | }, 102 | ], 103 | totalCount: 3, 104 | pageInfo: { 105 | hasPreviousPage: true, 106 | hasNextPage: true, 107 | startCursor: "VGVzdFJlbGF5Tm9kZToy", 108 | endCursor: "VGVzdFJlbGF5Tm9kZToy", 109 | }, 110 | }); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /__tests__/integration/queries/relay-non-null-edges-connection-test.js: -------------------------------------------------------------------------------- 1 | import relayNonNullEdgesConnectionQuery from "@tests/gql/queries/relay-non-null-edges-connection.gql"; 2 | import { query, startServer } from "@tests/integration/setup"; 3 | 4 | let server; 5 | 6 | describe("Integration | queries | Relay connection (non-null edges)", function () { 7 | beforeEach(function () { 8 | server = startServer(); 9 | 10 | server.create("test-relay-node", { color: "blue" }); 11 | server.createList("test-relay-node", 2); 12 | }); 13 | 14 | afterEach(function () { 15 | server.shutdown(); 16 | }); 17 | 18 | test("query for Relay connection with non-null edges", async function () { 19 | const { testNonNullEdgesRelayConnection } = await query( 20 | relayNonNullEdgesConnectionQuery 21 | ); 22 | 23 | expect(testNonNullEdgesRelayConnection).toEqual({ 24 | edges: [ 25 | { 26 | cursor: "VGVzdFJlbGF5Tm9kZTox", 27 | node: { id: "1" }, 28 | }, 29 | { 30 | cursor: "VGVzdFJlbGF5Tm9kZToy", 31 | node: { id: "2" }, 32 | }, 33 | { 34 | cursor: "VGVzdFJlbGF5Tm9kZToz", 35 | node: { id: "3" }, 36 | }, 37 | ], 38 | totalCount: 3, 39 | pageInfo: { 40 | hasPreviousPage: false, 41 | hasNextPage: false, 42 | startCursor: "VGVzdFJlbGF5Tm9kZTox", 43 | endCursor: "VGVzdFJlbGF5Tm9kZToz", 44 | }, 45 | }); 46 | }); 47 | 48 | describe("filtering non-null edges", function () { 49 | test("query for Relay connection by color", async function () { 50 | const { testNonNullEdgesRelayConnection } = await query( 51 | relayNonNullEdgesConnectionQuery, 52 | { 53 | variables: { color: "blue" }, 54 | } 55 | ); 56 | 57 | expect(testNonNullEdgesRelayConnection).toEqual({ 58 | edges: [ 59 | { 60 | cursor: "VGVzdFJlbGF5Tm9kZTox", 61 | node: { id: "1" }, 62 | }, 63 | ], 64 | totalCount: 1, 65 | pageInfo: { 66 | hasPreviousPage: false, 67 | hasNextPage: false, 68 | startCursor: "VGVzdFJlbGF5Tm9kZTox", 69 | endCursor: "VGVzdFJlbGF5Tm9kZTox", 70 | }, 71 | }); 72 | }); 73 | 74 | test("query for Relay connection by first/after", async function () { 75 | const { testNonNullEdgesRelayConnection } = await query( 76 | relayNonNullEdgesConnectionQuery, 77 | { 78 | variables: { first: 1, after: "VGVzdFJlbGF5Tm9kZTox" }, 79 | } 80 | ); 81 | 82 | expect(testNonNullEdgesRelayConnection).toEqual({ 83 | edges: [ 84 | { 85 | cursor: "VGVzdFJlbGF5Tm9kZToy", 86 | node: { id: "2" }, 87 | }, 88 | ], 89 | totalCount: 3, 90 | pageInfo: { 91 | hasPreviousPage: true, 92 | hasNextPage: true, 93 | startCursor: "VGVzdFJlbGF5Tm9kZToy", 94 | endCursor: "VGVzdFJlbGF5Tm9kZToy", 95 | }, 96 | }); 97 | }); 98 | 99 | test("query for Relay connection by last/before", async function () { 100 | const { testNonNullEdgesRelayConnection } = await query( 101 | relayNonNullEdgesConnectionQuery, 102 | { 103 | variables: { last: 1, before: "VGVzdFJlbGF5Tm9kZToz" }, 104 | } 105 | ); 106 | 107 | expect(testNonNullEdgesRelayConnection).toEqual({ 108 | edges: [ 109 | { 110 | cursor: "VGVzdFJlbGF5Tm9kZToy", 111 | node: { id: "2" }, 112 | }, 113 | ], 114 | totalCount: 3, 115 | pageInfo: { 116 | hasPreviousPage: true, 117 | hasNextPage: true, 118 | startCursor: "VGVzdFJlbGF5Tm9kZToy", 119 | endCursor: "VGVzdFJlbGF5Tm9kZToy", 120 | }, 121 | }); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /__tests__/integration/queries/relay-non-null-nodes-connection-test.js: -------------------------------------------------------------------------------- 1 | import relayNonNullNodesConnectionQuery from "@tests/gql/queries/relay-non-null-nodes-connection.gql"; 2 | import { query, startServer } from "@tests/integration/setup"; 3 | 4 | let server; 5 | 6 | describe("Integration | queries | Relay connection (non-null edges and nodes)", function () { 7 | beforeEach(function () { 8 | server = startServer(); 9 | 10 | server.create("test-relay-node", { color: "blue" }); 11 | server.createList("test-relay-node", 2); 12 | }); 13 | 14 | afterEach(function () { 15 | server.shutdown(); 16 | }); 17 | 18 | test("query for Relay connection with non-null nodes", async function () { 19 | const { testNonNullNodesRelayConnection } = await query( 20 | relayNonNullNodesConnectionQuery 21 | ); 22 | 23 | expect(testNonNullNodesRelayConnection).toEqual({ 24 | edges: [ 25 | { 26 | cursor: "VGVzdFJlbGF5Tm9kZTox", 27 | node: { id: "1" }, 28 | }, 29 | { 30 | cursor: "VGVzdFJlbGF5Tm9kZToy", 31 | node: { id: "2" }, 32 | }, 33 | { 34 | cursor: "VGVzdFJlbGF5Tm9kZToz", 35 | node: { id: "3" }, 36 | }, 37 | ], 38 | totalCount: 3, 39 | pageInfo: { 40 | hasPreviousPage: false, 41 | hasNextPage: false, 42 | startCursor: "VGVzdFJlbGF5Tm9kZTox", 43 | endCursor: "VGVzdFJlbGF5Tm9kZToz", 44 | }, 45 | }); 46 | }); 47 | 48 | describe("filtering non-null nodes", function () { 49 | test("query for Relay connection by color", async function () { 50 | const { testNonNullNodesRelayConnection } = await query( 51 | relayNonNullNodesConnectionQuery, 52 | { 53 | variables: { color: "blue" }, 54 | } 55 | ); 56 | 57 | expect(testNonNullNodesRelayConnection).toEqual({ 58 | edges: [ 59 | { 60 | cursor: "VGVzdFJlbGF5Tm9kZTox", 61 | node: { id: "1" }, 62 | }, 63 | ], 64 | totalCount: 1, 65 | pageInfo: { 66 | hasPreviousPage: false, 67 | hasNextPage: false, 68 | startCursor: "VGVzdFJlbGF5Tm9kZTox", 69 | endCursor: "VGVzdFJlbGF5Tm9kZTox", 70 | }, 71 | }); 72 | }); 73 | 74 | test("query for Relay connection by first/after", async function () { 75 | const { testNonNullNodesRelayConnection } = await query( 76 | relayNonNullNodesConnectionQuery, 77 | { 78 | variables: { first: 1, after: "VGVzdFJlbGF5Tm9kZTox" }, 79 | } 80 | ); 81 | 82 | expect(testNonNullNodesRelayConnection).toEqual({ 83 | edges: [ 84 | { 85 | cursor: "VGVzdFJlbGF5Tm9kZToy", 86 | node: { id: "2" }, 87 | }, 88 | ], 89 | totalCount: 3, 90 | pageInfo: { 91 | hasPreviousPage: true, 92 | hasNextPage: true, 93 | startCursor: "VGVzdFJlbGF5Tm9kZToy", 94 | endCursor: "VGVzdFJlbGF5Tm9kZToy", 95 | }, 96 | }); 97 | }); 98 | 99 | test("query for Relay connection by last/before", async function () { 100 | const { testNonNullNodesRelayConnection } = await query( 101 | relayNonNullNodesConnectionQuery, 102 | { 103 | variables: { last: 1, before: "VGVzdFJlbGF5Tm9kZToz" }, 104 | } 105 | ); 106 | 107 | expect(testNonNullNodesRelayConnection).toEqual({ 108 | edges: [ 109 | { 110 | cursor: "VGVzdFJlbGF5Tm9kZToy", 111 | node: { id: "2" }, 112 | }, 113 | ], 114 | totalCount: 3, 115 | pageInfo: { 116 | hasPreviousPage: true, 117 | hasNextPage: true, 118 | startCursor: "VGVzdFJlbGF5Tm9kZToy", 119 | endCursor: "VGVzdFJlbGF5Tm9kZToy", 120 | }, 121 | }); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /__tests__/integration/queries/scalar-test.js: -------------------------------------------------------------------------------- 1 | import contextQuery from "@tests/gql/queries/context.gql"; 2 | import scalarQuery from "@tests/gql/queries/scalar.gql"; 3 | import scalarNonNullQuery from "@tests/gql/queries/scalar-non-null.gql"; 4 | import scalarOptionalResolveQuery from "@tests/gql/queries/scalar-optional-resolve.gql"; 5 | import { query, startServer } from "@tests/integration/setup"; 6 | 7 | let server; 8 | 9 | describe("Integration | queries | scalars", function () { 10 | beforeEach(function () { 11 | server = startServer(); 12 | }); 13 | 14 | afterEach(function () { 15 | server.shutdown(); 16 | }); 17 | 18 | test("query for scalar from context", async function () { 19 | const { testContext } = await query(contextQuery, { 20 | url: "/graphql-scalars", 21 | }); 22 | 23 | expect(testContext).toBe("foo"); 24 | }); 25 | 26 | test("query for scalar on root object", async function () { 27 | const { testScalar } = await query(scalarQuery, { 28 | url: "/graphql-scalars", 29 | }); 30 | 31 | expect(testScalar).toBe("foo"); 32 | }); 33 | 34 | test("query for non-null scalar on root object", async function () { 35 | const { testScalarNonNull } = await query(scalarNonNullQuery, { 36 | url: "/graphql-scalars", 37 | }); 38 | 39 | expect(testScalarNonNull).toBe("foo"); 40 | }); 41 | 42 | test("query for optional resolver scalar on root object", async function () { 43 | const { 44 | testScalarOptionalResolve, 45 | } = await query(scalarOptionalResolveQuery, { url: "/graphql-scalars" }); 46 | 47 | expect(testScalarOptionalResolve).toBe("foo"); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/integration/queries/sorted-objects-test.js: -------------------------------------------------------------------------------- 1 | import mirageGraphQLFieldResolver from "@lib/resolvers/mirage"; 2 | import sortedObjectsQuery from "@tests/gql/queries/sorted-objects.gql"; 3 | import { query, startServer } from "@tests/integration/setup"; 4 | 5 | const SORT_VALUES = { S: 0, M: 1, L: 2 }; 6 | 7 | describe("Integration | queries | sorted objects", function () { 8 | test("resolvers can be composed from the Mirage resolver", async function () { 9 | const server = startServer({ 10 | resolvers: { 11 | Query: { 12 | testSortedObjects() { 13 | const records = mirageGraphQLFieldResolver(...arguments); 14 | 15 | return records.sort(function (a, b) { 16 | return SORT_VALUES[a.size] - SORT_VALUES[b.size]; 17 | }); 18 | }, 19 | }, 20 | }, 21 | }); 22 | 23 | server.create("test-object", { size: "L" }); 24 | server.create("test-object", { size: "M" }); 25 | server.create("test-object", { size: "S" }); 26 | 27 | const { testSortedObjects } = await query(sortedObjectsQuery); 28 | 29 | expect(testSortedObjects).toEqual([ 30 | { size: "S" }, 31 | { size: "M" }, 32 | { size: "L" }, 33 | ]); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/integration/queries/union-nested-non-null-test.js: -------------------------------------------------------------------------------- 1 | import unionNestedNonNullQuery from "@tests/gql/queries/union-nested-non-null.gql"; 2 | import { query, startServer } from "@tests/integration/setup"; 3 | 4 | let server; 5 | 6 | describe("Integration | queries | union", function () { 7 | beforeEach(function () { 8 | server = startServer(); 9 | 10 | server.create("test-union-one", { oneName: "foo" }); 11 | server.create("test-union-two", { twoName: "bar" }); 12 | }); 13 | 14 | afterEach(function () { 15 | server.shutdown(); 16 | }); 17 | 18 | test("query for nested non-null union type", async function () { 19 | const { testUnionNestedNonNull } = await query(unionNestedNonNullQuery); 20 | 21 | expect(testUnionNestedNonNull).toEqual([ 22 | { oneName: "foo" }, 23 | { twoName: "bar" }, 24 | ]); 25 | }); 26 | 27 | describe("filtering nested non-null", function () { 28 | test("by oneName arg", async function () { 29 | const { testUnionNestedNonNull } = await query(unionNestedNonNullQuery, { 30 | variables: { oneName: "foo" }, 31 | }); 32 | 33 | expect(testUnionNestedNonNull).toEqual([{ oneName: "foo" }]); 34 | }); 35 | 36 | test("by twoName arg", async function () { 37 | const { testUnionNestedNonNull } = await query(unionNestedNonNullQuery, { 38 | variables: { twoName: "bar" }, 39 | }); 40 | 41 | expect(testUnionNestedNonNull).toEqual([{ twoName: "bar" }]); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/integration/queries/union-non-null-test.js: -------------------------------------------------------------------------------- 1 | import unionNonNullQuery from "@tests/gql/queries/union-non-null.gql"; 2 | import { query, startServer } from "@tests/integration/setup"; 3 | 4 | let server; 5 | 6 | describe("Integration | queries | union (non-null)", function () { 7 | beforeEach(function () { 8 | server = startServer(); 9 | 10 | server.create("test-union-one", { oneName: "foo" }); 11 | server.create("test-union-two", { twoName: "bar" }); 12 | }); 13 | 14 | afterEach(function () { 15 | server.shutdown(); 16 | }); 17 | 18 | test("query for non-null union type", async function () { 19 | const { testUnionNonNull } = await query(unionNonNullQuery); 20 | 21 | expect(testUnionNonNull).toEqual([{ oneName: "foo" }, { twoName: "bar" }]); 22 | }); 23 | 24 | describe("filtering non-null", function () { 25 | test("by oneName arg", async function () { 26 | const { testUnionNonNull } = await query(unionNonNullQuery, { 27 | variables: { oneName: "foo" }, 28 | }); 29 | 30 | expect(testUnionNonNull).toEqual([{ oneName: "foo" }]); 31 | }); 32 | 33 | test("by twoName arg", async function () { 34 | const { testUnionNonNull } = await query(unionNonNullQuery, { 35 | variables: { twoName: "bar" }, 36 | }); 37 | 38 | expect(testUnionNonNull).toEqual([{ twoName: "bar" }]); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/integration/queries/union-test.js: -------------------------------------------------------------------------------- 1 | import unionQuery from "@tests/gql/queries/union.gql"; 2 | import { query, startServer } from "@tests/integration/setup"; 3 | 4 | let server; 5 | 6 | describe("Integration | queries | union", function () { 7 | beforeEach(function () { 8 | server = startServer(); 9 | 10 | server.create("test-union-one", { oneName: "foo" }); 11 | server.create("test-union-two", { twoName: "bar" }); 12 | }); 13 | 14 | afterEach(function () { 15 | server.shutdown(); 16 | }); 17 | 18 | test("query for union type", async function () { 19 | const { testUnion } = await query(unionQuery); 20 | 21 | expect(testUnion).toEqual([{ oneName: "foo" }, { twoName: "bar" }]); 22 | }); 23 | 24 | describe("filtering", function () { 25 | test("by oneName arg", async function () { 26 | const { testUnion } = await query(unionQuery, { 27 | variables: { oneName: "foo" }, 28 | }); 29 | 30 | expect(testUnion).toEqual([{ oneName: "foo" }]); 31 | }); 32 | 33 | test("by twoName arg", async function () { 34 | const { testUnion } = await query(unionQuery, { 35 | variables: { twoName: "bar" }, 36 | }); 37 | 38 | expect(testUnion).toEqual([{ twoName: "bar" }]); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/integration/setup.js: -------------------------------------------------------------------------------- 1 | import { createGraphQLHandler } from "@lib/handler"; 2 | import { graphQLSchema } from "@tests/gql/schema"; 3 | import { request } from "graphql-request"; 4 | import { Server } from "miragejs"; 5 | 6 | export function query(queryDocument, { url = "/graphql", variables } = {}) { 7 | const { 8 | loc: { 9 | source: { body }, 10 | }, 11 | } = queryDocument; 12 | 13 | return request(url, body, variables); 14 | } 15 | 16 | export const mutate = query; 17 | 18 | export function startServer({ resolvers } = {}) { 19 | const server = new Server({ 20 | routes() { 21 | const testGraphQLHandler = createGraphQLHandler( 22 | graphQLSchema, 23 | this.schema, 24 | { resolvers } 25 | ); 26 | const scalarTestGraphQLHandler = createGraphQLHandler( 27 | graphQLSchema, 28 | this.schema, 29 | { 30 | context: { foo: "foo" }, 31 | resolvers: { 32 | Query: { 33 | testContext: (_obj, _args, context) => context.foo, 34 | testScalarOptionalResolve: () => "foo", 35 | }, 36 | }, 37 | root: { 38 | testScalar: "foo", 39 | testScalarNonNull: "foo", 40 | }, 41 | } 42 | ); 43 | 44 | this.post("/graphql-scalars", scalarTestGraphQLHandler); 45 | this.post("/graphql", testGraphQLHandler); 46 | }, 47 | }); 48 | 49 | server.logging = false; 50 | 51 | return server; 52 | } 53 | -------------------------------------------------------------------------------- /__tests__/unit/handler-test.js: -------------------------------------------------------------------------------- 1 | jest.mock("@lib/orm/models"); 2 | jest.mock("@lib/resolvers/field"); 3 | jest.mock("@lib/utils"); 4 | jest.mock("graphql", () => ({ 5 | __esModule: true, 6 | graphql: jest.fn(function () { 7 | throw new Error("foo"); 8 | }), 9 | })); 10 | 11 | import { Response } from "miragejs"; 12 | import createFieldResolver from "@lib/resolvers/field"; 13 | import { createGraphQLHandler } from "@lib/handler"; 14 | import { ensureExecutableGraphQLSchema } from "@lib/utils"; 15 | import { ensureModels } from "@lib/orm/models"; 16 | import { graphql } from "graphql"; // eslint-disable-line no-unused-vars 17 | import { graphQLSchema } from "@tests/unit/setup"; 18 | 19 | describe("Unit | handler", function () { 20 | const mirageSchema = {}; 21 | const resolvers = {}; 22 | const graphQLHandler = createGraphQLHandler(graphQLSchema, mirageSchema, { 23 | resolvers, 24 | }); 25 | 26 | it("creates a GraphQL field resolver", function () { 27 | expect(createFieldResolver).toHaveBeenCalledWith(resolvers); 28 | }); 29 | 30 | it("ensures the GraphQL schema is executable", function () { 31 | expect(ensureExecutableGraphQLSchema).toHaveBeenCalledWith(graphQLSchema); 32 | }); 33 | 34 | it("ensures models are created in the Mirage schema", function () { 35 | expect(ensureModels).toHaveBeenCalledWith({ graphQLSchema, mirageSchema }); 36 | }); 37 | 38 | it("responds with 500 if GraphQL throws an exception", function () { 39 | const response = graphQLHandler(null, { requestBody: "{}" }); 40 | 41 | expect(response).toBeInstanceOf(Response); 42 | expect(response.code).toBe(500); 43 | expect(response.data.errors[0].message).toBe("foo"); 44 | }); 45 | 46 | it("supports creating handler with no options provided", function () { 47 | expect(() => createGraphQLHandler(graphQLSchema, mirageSchema)).not.toThrow( 48 | "Cannot destructure property `context` of 'undefined' or 'null'." 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/unit/orm/models-test.js: -------------------------------------------------------------------------------- 1 | import { ensureModels, createModels } from "@lib/orm/models"; 2 | import { graphQLSchema } from "@tests/gql/schema"; 3 | 4 | describe("Unit | ORM | models", function () { 5 | describe("ensureModels", function () { 6 | const registeredModels = {}; 7 | const mirageSchema = { 8 | hasModelForModelName: () => false, 9 | registerModel(typeName, model) { 10 | registeredModels[typeName] = model; 11 | }, 12 | }; 13 | 14 | ensureModels({ graphQLSchema, mirageSchema }); 15 | 16 | it("does not register a model for the mutation type", function () { 17 | expect(registeredModels.Mutation).toBeUndefined(); 18 | }); 19 | 20 | it("does not register a model for the query type", function () { 21 | expect(registeredModels.Query).toBeUndefined(); 22 | }); 23 | 24 | it("does not register a model for the subscription type", function () { 25 | expect(registeredModels.Subscription).toBeUndefined(); 26 | }); 27 | 28 | it("does register the TestObject model", function () { 29 | expect(registeredModels.TestObject).toBeDefined(); 30 | }); 31 | 32 | it("does register the TestCategory model", function () { 33 | expect(registeredModels.TestCategory).toBeDefined(); 34 | }); 35 | 36 | it("does register the TestOption model", function () { 37 | expect(registeredModels.TestOption).toBeDefined(); 38 | }); 39 | 40 | it("does not register the TestInterface model", function () { 41 | expect(registeredModels.TestInterface).toBeUndefined(); 42 | }); 43 | 44 | it("does register the TestImplOne model", function () { 45 | expect(registeredModels.TestImplOne).toBeDefined(); 46 | }); 47 | 48 | it("does register the TestImplTwo model", function () { 49 | expect(registeredModels.TestImplTwo).toBeDefined(); 50 | }); 51 | 52 | it("does not register the TestUnion model", function () { 53 | expect(registeredModels.TestUnion).toBeUndefined(); 54 | }); 55 | 56 | it("does register the TestUnionOne model", function () { 57 | expect(registeredModels.TestUnionOne).toBeDefined(); 58 | }); 59 | 60 | it("does register the TestUnionTwo model", function () { 61 | expect(registeredModels.TestUnionTwo).toBeDefined(); 62 | }); 63 | 64 | describe("TestObject relationships", function () { 65 | const model = new registeredModels.TestObject({}, "TestObject"); 66 | 67 | function testRelationship( 68 | name, 69 | type, 70 | modelName, 71 | { isPolymorphic = false } = {} 72 | ) { 73 | expect(model.__proto__[name].constructor.name).toBe(type); 74 | expect(model.__proto__[name].modelName).toBe(modelName); 75 | expect(model.__proto__[name].opts.polymorphic).toBe(isPolymorphic); 76 | } 77 | 78 | // eslint-disable-next-line jest/expect-expect 79 | it("belongs to test category", function () { 80 | testRelationship("belongsToField", "BelongsTo", "test-category"); 81 | }); 82 | 83 | // eslint-disable-next-line jest/expect-expect 84 | it("has many test options", function () { 85 | testRelationship("hasManyField", "HasMany", "test-option"); 86 | }); 87 | 88 | // eslint-disable-next-line jest/expect-expect 89 | it("belongs to test interface", function () { 90 | testRelationship("interfaceField", "BelongsTo", "test-interface", { 91 | isPolymorphic: true, 92 | }); 93 | }); 94 | 95 | // eslint-disable-next-line jest/expect-expect 96 | it("has many test relay nodes", function () { 97 | testRelationship("relayConnectionField", "HasMany", "test-relay-node"); 98 | }); 99 | 100 | // eslint-disable-next-line jest/expect-expect 101 | it("has many test unions", function () { 102 | testRelationship("unionField", "HasMany", "test-union", { 103 | isPolymorphic: true, 104 | }); 105 | }); 106 | }); 107 | }); 108 | 109 | describe("createModels", function () { 110 | const models = createModels({ graphQLSchema }); 111 | 112 | function findModel(name) { 113 | return models.find((model) => model.name == name); 114 | } 115 | 116 | it("does not create a model for the mutation type", function () { 117 | expect(findModel("Mutation")).toBeUndefined(); 118 | }); 119 | 120 | it("does not create a model for the query type", function () { 121 | expect(findModel("Query")).toBeUndefined(); 122 | }); 123 | 124 | it("does not create a model for the subscription type", function () { 125 | expect(findModel("Subscription")).toBeUndefined(); 126 | }); 127 | 128 | it("does create the TestObject model", function () { 129 | expect(findModel("TestObject")).toBeDefined(); 130 | }); 131 | 132 | it("does create the TestCategory model", function () { 133 | expect(findModel("TestCategory")).toBeDefined(); 134 | }); 135 | 136 | it("does create the TestOption model", function () { 137 | expect(findModel("TestOption")).toBeDefined(); 138 | }); 139 | 140 | it("does not create the TestInterface model", function () { 141 | expect(findModel("TestInterface")).toBeUndefined(); 142 | }); 143 | 144 | it("does create the TestImplOne model", function () { 145 | expect(findModel("TestImplOne")).toBeDefined(); 146 | }); 147 | 148 | it("does create the TestImplTwo model", function () { 149 | expect(findModel("TestImplTwo")).toBeDefined(); 150 | }); 151 | 152 | it("does not create the TestUnion model", function () { 153 | expect(findModel("TestUnion")).toBeUndefined(); 154 | }); 155 | 156 | it("does create the TestUnionOne model", function () { 157 | expect(findModel("TestUnionOne")).toBeDefined(); 158 | }); 159 | 160 | it("does create the TestUnionTwo model", function () { 161 | expect(findModel("TestUnionTwo")).toBeDefined(); 162 | }); 163 | 164 | describe("TestObject relationships", function () { 165 | const model = findModel("TestObject"); 166 | 167 | function findRelationship(model, fieldName) { 168 | return model.associations.find((item) => item.fieldName == fieldName); 169 | } 170 | 171 | function testRelationship( 172 | fieldName, 173 | type, 174 | associationName, 175 | { isPolymorphic = false } = {} 176 | ) { 177 | const rel = findRelationship(model, fieldName); 178 | expect(rel).toBeDefined(); 179 | expect(rel.name).toBe(associationName); 180 | expect(rel.type).toBe(type); 181 | expect(rel.options.polymorphic).toBe(isPolymorphic); 182 | } 183 | 184 | // eslint-disable-next-line jest/expect-expect 185 | it("belongs to test category", function () { 186 | testRelationship("belongsToField", "belongsTo", "testCategory"); 187 | }); 188 | 189 | // eslint-disable-next-line jest/expect-expect 190 | it("has many test options", function () { 191 | testRelationship("hasManyField", "hasMany", "testOption"); 192 | }); 193 | 194 | // eslint-disable-next-line jest/expect-expect 195 | it("belongs to test interface", function () { 196 | testRelationship("interfaceField", "belongsTo", "testInterface", { 197 | isPolymorphic: true, 198 | }); 199 | }); 200 | 201 | // eslint-disable-next-line jest/expect-expect 202 | it("has many test relay nodes", function () { 203 | testRelationship("relayConnectionField", "hasMany", "testRelayNode"); 204 | }); 205 | 206 | // eslint-disable-next-line jest/expect-expect 207 | it("has many test unions", function () { 208 | testRelationship("unionField", "hasMany", "testUnion", { 209 | isPolymorphic: true, 210 | }); 211 | }); 212 | }); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /__tests__/unit/orm/records-test.js: -------------------------------------------------------------------------------- 1 | import { adaptRecord, adaptRecords, getRecords } from "@lib/orm/records"; 2 | 3 | describe("Unit | ORM | records", function () { 4 | describe("adapt records", function () { 5 | it("can adapt a db record for use with GraphQL", function () { 6 | const record = { 7 | associations: { bars: "HasMany" }, 8 | attrs: { name: "foo" }, 9 | bars: [{ name: "bar" }], 10 | modelName: "foo", 11 | }; 12 | 13 | expect(adaptRecord(record, "Foo")).toEqual({ 14 | bars: record.bars, 15 | name: "foo", 16 | __typename: "Foo", 17 | }); 18 | }); 19 | 20 | it("can adapt a list of records for use with GraphQL", function () { 21 | const records = [ 22 | { 23 | associations: { bars: "HasMany" }, 24 | attrs: { name: "foo" }, 25 | bars: [{ name: "bar" }], 26 | modelName: "foo", 27 | }, 28 | { 29 | associations: { baz: "belongsTo" }, 30 | attrs: { name: "bar" }, 31 | baz: { name: "baz" }, 32 | modelName: "foo", 33 | }, 34 | ]; 35 | 36 | expect(adaptRecords(records, "Foo")).toEqual([ 37 | { 38 | bars: records[0].bars, 39 | name: "foo", 40 | __typename: "Foo", 41 | }, 42 | { 43 | baz: records[1].baz, 44 | name: "bar", 45 | __typename: "Foo", 46 | }, 47 | ]); 48 | }); 49 | }); 50 | 51 | describe("get records", function () { 52 | const models = [ 53 | { attrs: { name: "Foo1" }, modelName: "foo" }, 54 | { attrs: { name: "Foo2" }, modelName: "foo" }, 55 | ]; 56 | const mirageSchema = { 57 | foos: { 58 | where: jest.fn(({ name }) => ({ 59 | models: name ? models.slice(0, 1) : models, 60 | })), 61 | }, 62 | toCollectionName: jest.fn(() => "foos"), 63 | }; 64 | const type = { 65 | name: "Foo", 66 | getFields: () => ({ name: {} }), 67 | }; 68 | 69 | it("can get and adapt records", function () { 70 | const records = getRecords(type, {}, mirageSchema); 71 | 72 | expect(mirageSchema.toCollectionName).toHaveBeenCalledWith("Foo"); 73 | expect(records).toEqual([ 74 | { 75 | name: "Foo1", 76 | __typename: "Foo", 77 | }, 78 | { 79 | name: "Foo2", 80 | __typename: "Foo", 81 | }, 82 | ]); 83 | }); 84 | 85 | it("can get, filter and adapt records", function () { 86 | const records = getRecords(type, { name: "Foo1" }, mirageSchema); 87 | 88 | expect(mirageSchema.toCollectionName).toHaveBeenCalledWith("Foo"); 89 | expect(mirageSchema.foos.where).toHaveBeenCalledWith({ name: "Foo1" }); 90 | expect(records).toEqual([ 91 | { 92 | name: "Foo1", 93 | __typename: "Foo", 94 | }, 95 | ]); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /__tests__/unit/relay-pagination-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | getEdges, 3 | getPageInfo, 4 | getRelayArgs, 5 | isRelayConnectionType, 6 | isRelayEdgeType, 7 | isRelayType, 8 | } from "@lib/relay-pagination"; 9 | 10 | describe("Unit | Relay pagination", function () { 11 | const connectionType = { 12 | name: "RelayConnection", 13 | getFields: () => ({ edges: [] }), 14 | }; 15 | const edgeType = { 16 | name: "RelayEdge", 17 | getFields: () => ({ node: {} }), 18 | }; 19 | 20 | describe("is Relay type", function () { 21 | test("if Relay connection", function () { 22 | expect(isRelayType(connectionType)).toBe(true); 23 | }); 24 | 25 | test("if Relay edge type", function () { 26 | expect(isRelayType(edgeType)).toBe(true); 27 | }); 28 | 29 | test("if Relay page info type", function () { 30 | const pageInfoType = { 31 | name: "PageInfo", 32 | getFields: () => ({ startCursor: "" }), 33 | }; 34 | 35 | expect(isRelayType(pageInfoType)).toBe(true); 36 | }); 37 | 38 | test("if non Relay type", function () { 39 | const nonRelayType = { name: "Foo" }; 40 | const otherNonRelayType = { name: "Connection", getFields: () => ({}) }; 41 | 42 | expect(isRelayType(nonRelayType)).toBe(false); 43 | expect(isRelayType(otherNonRelayType)).toBe(false); 44 | }); 45 | }); 46 | 47 | it("can determine if a type is a Relay connection", function () { 48 | expect(isRelayConnectionType(connectionType)).toBe(true); 49 | expect(isRelayConnectionType(edgeType)).toBe(false); 50 | }); 51 | 52 | it("can determine if a type is a Relay edge", function () { 53 | expect(isRelayEdgeType(edgeType)).toBe(true); 54 | expect(isRelayConnectionType(edgeType)).toBe(false); 55 | }); 56 | 57 | it("can separate Relay pagination arguments", function () { 58 | const args = { 59 | first: 10, 60 | last: 10, 61 | after: "12345", 62 | before: "54321", 63 | foo: "bar", 64 | }; 65 | const { relayArgs, nonRelayArgs } = getRelayArgs(args); 66 | 67 | expect(nonRelayArgs).toEqual({ foo: "bar" }); 68 | expect(relayArgs).toEqual({ 69 | first: 10, 70 | last: 10, 71 | after: "12345", 72 | before: "54321", 73 | }); 74 | }); 75 | 76 | describe("page info", function () { 77 | test("when previous page", function () { 78 | const records = [{ id: 1 }, { id: 12 }]; 79 | const edges = [ 80 | { cursor: 11, node: { id: 11 } }, 81 | { cursor: 12, node: { id: 12 } }, 82 | ]; 83 | 84 | expect(getPageInfo(records, edges)).toEqual({ 85 | hasPreviousPage: true, 86 | hasNextPage: false, 87 | startCursor: 11, 88 | endCursor: 12, 89 | }); 90 | }); 91 | 92 | test("when next page", function () { 93 | const records = [{ id: 1 }, { id: 3 }]; 94 | const edges = [ 95 | { cursor: 1, node: { id: 1 } }, 96 | { cursor: 2, node: { id: 2 } }, 97 | ]; 98 | 99 | expect(getPageInfo(records, edges)).toEqual({ 100 | hasPreviousPage: false, 101 | hasNextPage: true, 102 | startCursor: 1, 103 | endCursor: 2, 104 | }); 105 | }); 106 | 107 | test("when both previous and next page", function () { 108 | const records = [{ id: 1 }, { id: 4 }]; 109 | const edges = [ 110 | { cursor: 2, node: { id: 2 } }, 111 | { cursor: 3, node: { id: 3 } }, 112 | ]; 113 | 114 | expect(getPageInfo(records, edges)).toEqual({ 115 | hasPreviousPage: true, 116 | hasNextPage: true, 117 | startCursor: 2, 118 | endCursor: 3, 119 | }); 120 | }); 121 | 122 | test("when neither previous or next page", function () { 123 | const records = [{ id: 1 }, { id: 2 }]; 124 | const edges = [ 125 | { cursor: 1, node: { id: 1 } }, 126 | { cursor: 2, node: { id: 2 } }, 127 | ]; 128 | 129 | expect(getPageInfo(records, edges)).toEqual({ 130 | hasPreviousPage: false, 131 | hasNextPage: false, 132 | startCursor: 1, 133 | endCursor: 2, 134 | }); 135 | }); 136 | }); 137 | 138 | describe("set edges of a connection", function () { 139 | const encode = (id) => id; 140 | 141 | function createRecords(n) { 142 | let i = 1; 143 | const records = []; 144 | 145 | while (i <= n) { 146 | records.push({ id: `${i}`, foo: `bar${i}` }); 147 | i++; 148 | } 149 | 150 | return records; 151 | } 152 | 153 | test("with no args", function () { 154 | const args = {}; 155 | const records = createRecords(1); 156 | 157 | expect(getEdges(records, args, "Foo", encode)).toEqual([ 158 | { 159 | cursor: "Foo:1", 160 | node: { id: "1", foo: "bar1" }, 161 | }, 162 | ]); 163 | }); 164 | 165 | test("with first/after args", function () { 166 | const args = { first: 2, after: "Foo:2" }; 167 | const records = createRecords(5); 168 | 169 | expect(getEdges(records, args, "Foo", encode)).toEqual([ 170 | { 171 | cursor: "Foo:3", 172 | node: { id: "3", foo: "bar3" }, 173 | }, 174 | { 175 | cursor: "Foo:4", 176 | node: { id: "4", foo: "bar4" }, 177 | }, 178 | ]); 179 | }); 180 | 181 | test("with last/before args", function () { 182 | const args = { last: 2, before: "Foo:4" }; 183 | const records = createRecords(5); 184 | 185 | expect(getEdges(records, args, "Foo", encode)).toEqual([ 186 | { 187 | cursor: "Foo:2", 188 | node: { id: "2", foo: "bar2" }, 189 | }, 190 | { 191 | cursor: "Foo:3", 192 | node: { id: "3", foo: "bar3" }, 193 | }, 194 | ]); 195 | }); 196 | 197 | test("with first/before args", function () { 198 | const args = { first: 3, before: "Foo:3" }; 199 | const records = createRecords(5); 200 | 201 | expect(getEdges(records, args, "Foo", encode)).toEqual([ 202 | { 203 | cursor: "Foo:1", 204 | node: { id: "1", foo: "bar1" }, 205 | }, 206 | { 207 | cursor: "Foo:2", 208 | node: { id: "2", foo: "bar2" }, 209 | }, 210 | ]); 211 | }); 212 | 213 | test("with last/after args", function () { 214 | const args = { last: 3, after: "Foo:3" }; 215 | const records = createRecords(5); 216 | 217 | expect(getEdges(records, args, "Foo", encode)).toEqual([ 218 | { 219 | cursor: "Foo:4", 220 | node: { id: "4", foo: "bar4" }, 221 | }, 222 | { 223 | cursor: "Foo:5", 224 | node: { id: "5", foo: "bar5" }, 225 | }, 226 | ]); 227 | }); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /__tests__/unit/resolvers/create-field-resolver-test.js: -------------------------------------------------------------------------------- 1 | jest.mock("@lib/resolvers/mirage"); 2 | 3 | import createFieldResolver from "@lib/resolvers/field"; 4 | import mirageGraphQLFieldResolver from "@lib/resolvers/mirage"; 5 | 6 | describe("Unit | resolvers | create field resolver", function () { 7 | test("field resolver can delegate to optional resolver", function () { 8 | const optionalResolvers = { Foo: { bar: jest.fn(() => {}) } }; 9 | const fieldResolver = createFieldResolver(optionalResolvers); 10 | const obj = {}; 11 | const args = {}; 12 | const context = {}; 13 | const info = { fieldName: "bar", parentType: { name: "Foo" } }; 14 | 15 | fieldResolver(obj, args, context, info); 16 | 17 | expect(optionalResolvers.Foo.bar).toHaveBeenCalledWith( 18 | obj, 19 | args, 20 | context, 21 | info 22 | ); 23 | }); 24 | 25 | test("field resolver calls Mirage field resolver by default", function () { 26 | const fieldResolver = createFieldResolver(); 27 | const obj = {}; 28 | const args = {}; 29 | const context = {}; 30 | const info = { fieldName: "bar", parentType: { name: "Foo" } }; 31 | 32 | fieldResolver(obj, args, context, info); 33 | 34 | expect(mirageGraphQLFieldResolver).toHaveBeenCalledWith( 35 | obj, 36 | args, 37 | context, 38 | info 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/unit/resolvers/list-resolver-test.js: -------------------------------------------------------------------------------- 1 | jest.mock("@lib/orm/records"); 2 | 3 | import { filterRecords, getRecords } from "@lib/orm/records"; 4 | import resolveList from "@lib/resolvers/list"; 5 | 6 | describe("Unit | resolvers | list", function () { 7 | describe("without a parent record", function () { 8 | const obj = undefined; 9 | const args = {}; 10 | const type = {}; 11 | const context = { mirageSchema: {} }; 12 | 13 | it("finds records for the given type and args", function () { 14 | resolveList(obj, args, context, undefined, type); 15 | 16 | expect(getRecords).toHaveBeenCalledWith(type, args, context.mirageSchema); 17 | }); 18 | }); 19 | 20 | describe("parent records", function () { 21 | it("returns edges of parent, if type is a Relay edge", function () { 22 | const obj = { edges: [] }; 23 | const type = { 24 | name: "TestEdge", 25 | getFields: () => ({ node: undefined }), 26 | }; 27 | const edges = resolveList(obj, undefined, undefined, undefined, type); 28 | 29 | expect(edges).toEqual(obj.edges); 30 | }); 31 | 32 | it("can filter records included with the parent record", function () { 33 | const obj = { bars: [] }; 34 | const args = {}; 35 | const info = { fieldName: "bars" }; 36 | const type = { name: "Foo" }; 37 | 38 | resolveList(obj, args, undefined, info, type); 39 | 40 | expect(filterRecords).toHaveBeenCalledWith(obj.bars, args); 41 | }); 42 | 43 | it("can filter records related to a Mirage record", function () { 44 | const obj = { bars: { models: [] } }; 45 | const args = {}; 46 | const info = { fieldName: "bars" }; 47 | const type = { name: "Foo" }; 48 | 49 | resolveList(obj, args, undefined, info, type); 50 | 51 | expect(filterRecords).toHaveBeenCalledWith(obj.bars.models, args); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/unit/resolvers/mirage-field-resolver-test.js: -------------------------------------------------------------------------------- 1 | jest.mock("@lib/resolvers/default"); 2 | jest.mock("@lib/resolvers/list"); 3 | jest.mock("@lib/resolvers/object"); 4 | jest.mock("@lib/resolvers/interface"); 5 | jest.mock("@lib/resolvers/union"); 6 | 7 | import defaultFieldResolver from "@lib/resolvers/default"; 8 | import { graphQLSchema } from "@tests/gql/schema"; 9 | import mirageGraphQLFieldResolver from "@lib/resolvers/mirage"; 10 | import resolveList from "@lib/resolvers/list"; 11 | import resolveObject from "@lib/resolvers/object"; 12 | import resolveInterface from "@lib/resolvers/interface"; 13 | import resolveUnion from "@lib/resolvers/union"; 14 | 15 | describe("Unit | resolvers | mirage field resolver", function () { 16 | const obj = {}; 17 | const args = {}; 18 | const context = {}; 19 | const typeMap = graphQLSchema.getTypeMap(); 20 | const queryFields = typeMap.Query.getFields(); 21 | 22 | describe("object types", function () { 23 | const { type } = queryFields.testObject; 24 | 25 | it("can resolve an object type", function () { 26 | const info = { returnType: type }; 27 | 28 | mirageGraphQLFieldResolver(obj, args, context, info); 29 | 30 | expect(resolveObject).toHaveBeenCalledWith( 31 | obj, 32 | args, 33 | context, 34 | info, 35 | type 36 | ); 37 | }); 38 | 39 | it("can resolve a non-null object type", function () { 40 | const { type: nonNullType } = queryFields.testObjectNonNull; 41 | const info = { returnType: nonNullType }; 42 | 43 | mirageGraphQLFieldResolver(obj, args, context, info); 44 | 45 | expect(resolveObject).toHaveBeenCalledWith( 46 | obj, 47 | args, 48 | context, 49 | info, 50 | type 51 | ); 52 | }); 53 | 54 | it("can resolve a list of objects", function () { 55 | const { type: listType } = queryFields.testObjects; 56 | const info = { returnType: listType }; 57 | 58 | mirageGraphQLFieldResolver(obj, args, context, info); 59 | 60 | expect(resolveList).toHaveBeenCalledWith(obj, args, context, info, type); 61 | }); 62 | }); 63 | 64 | describe("polymorphic types", function () { 65 | it("can resolve union object types", function () { 66 | const info = { returnType: queryFields.testUnionSingular.type }; 67 | 68 | mirageGraphQLFieldResolver(obj, args, context, info); 69 | 70 | expect(resolveUnion).toHaveBeenCalledWith( 71 | obj, 72 | args, 73 | context, 74 | info, 75 | false, 76 | info.returnType 77 | ); 78 | }); 79 | 80 | it("can resolve union list types", function () { 81 | const { 82 | type: { ofType: unionType }, 83 | } = queryFields.testUnion; 84 | const info = { returnType: queryFields.testUnion.type }; 85 | 86 | mirageGraphQLFieldResolver(obj, args, context, info); 87 | 88 | expect(resolveUnion).toHaveBeenCalledWith( 89 | obj, 90 | args, 91 | context, 92 | info, 93 | true, 94 | unionType 95 | ); 96 | }); 97 | 98 | it("can resolve interface types", function () { 99 | const type = typeMap.TestInterface; 100 | const info = { returnType: queryFields.testInterface.type }; 101 | 102 | mirageGraphQLFieldResolver(obj, args, context, info); 103 | 104 | expect(resolveInterface).toHaveBeenCalledWith( 105 | obj, 106 | args, 107 | context, 108 | info, 109 | type 110 | ); 111 | }); 112 | }); 113 | 114 | describe("scalar types", function () { 115 | it("can resolve scalar types", function () { 116 | const { type } = queryFields.testScalar; 117 | const info = { returnType: type }; 118 | 119 | mirageGraphQLFieldResolver(obj, args, context, info); 120 | 121 | expect(defaultFieldResolver).toHaveBeenCalledWith( 122 | obj, 123 | args, 124 | context, 125 | info 126 | ); 127 | }); 128 | 129 | it("can resolve non-null scalar types", function () { 130 | const { type: nonNullType } = queryFields.testScalarNonNull; 131 | const info = { returnType: nonNullType }; 132 | 133 | mirageGraphQLFieldResolver(obj, args, context, info); 134 | 135 | expect(defaultFieldResolver).toHaveBeenCalledWith( 136 | obj, 137 | args, 138 | context, 139 | info 140 | ); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /__tests__/unit/setup.js: -------------------------------------------------------------------------------- 1 | export const graphQLSchema = {}; 2 | -------------------------------------------------------------------------------- /__tests__/unit/utils-test.js: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLScalarType } from "graphql"; 2 | import { graphQLSchemaAST, graphQLSchema } from "@tests/gql/schema"; 3 | import { 4 | capitalize, 5 | ensureExecutableGraphQLSchema, 6 | unwrapType, 7 | } from "@lib/utils"; 8 | 9 | describe("Unit | utils", function () { 10 | describe("ensure executable GraphQL schema", function () { 11 | function testSchema(schemaToTest) { 12 | const schema = ensureExecutableGraphQLSchema(schemaToTest); 13 | const typeMap = schema.getTypeMap(); 14 | const { size } = typeMap.TestObject.getFields(); 15 | const { testObject } = typeMap.Query.getFields(); 16 | 17 | expect(size.name).toBe("size"); 18 | expect(size.type).toBeInstanceOf(GraphQLScalarType); 19 | expect(testObject.name).toBe("testObject"); 20 | expect(testObject.type).toBeInstanceOf(GraphQLObjectType); 21 | } 22 | 23 | // eslint-disable-next-line jest/expect-expect 24 | test("if the schema is an AST", function () { 25 | testSchema(graphQLSchemaAST); 26 | }); 27 | 28 | // eslint-disable-next-line jest/expect-expect 29 | test("if the schema is a string", function () { 30 | testSchema(` 31 | type Query { 32 | testObject: TestObject 33 | } 34 | 35 | type TestObject { 36 | size: String 37 | } 38 | `); 39 | }); 40 | 41 | // eslint-disable-next-line jest/expect-expect 42 | test("if the schema is already executable", function () { 43 | testSchema(graphQLSchema); 44 | }); 45 | }); 46 | 47 | describe("unwrap type", function () { 48 | const typeMap = graphQLSchema.getTypeMap(); 49 | const queryFields = typeMap.Query.getFields(); 50 | 51 | it("can unwrap non-null types", function () { 52 | const { type: nonNullType } = queryFields.testObjectNonNull; 53 | const type = typeMap.TestObject; 54 | 55 | expect(unwrapType(nonNullType)).toEqual({ isList: false, type }); 56 | }); 57 | 58 | it("can unwrap list types", function () { 59 | const { type: listType } = queryFields.testObjects; 60 | const type = typeMap.TestObject; 61 | 62 | expect(unwrapType(listType)).toEqual({ isList: true, type }); 63 | }); 64 | 65 | it("can unwrap a non-null list", function () { 66 | const { type: nonNullListType } = queryFields.testObjectsNonNull; 67 | const type = typeMap.TestObject; 68 | 69 | expect(unwrapType(nonNullListType)).toEqual({ isList: true, type }); 70 | }); 71 | 72 | it("can unwrap a non-null list of non-null types", function () { 73 | const { 74 | type: nonNullListOfNonNullType, 75 | } = queryFields.testObjectsNestedNonNull; 76 | const type = typeMap.TestObject; 77 | 78 | expect(unwrapType(nonNullListOfNonNullType)).toEqual({ 79 | isList: true, 80 | type, 81 | }); 82 | }); 83 | 84 | it("can unwrap Relay node types", function () { 85 | const { type: connectionType } = queryFields.testRelayConnection; 86 | const nodeType = typeMap.TestRelayNode; 87 | 88 | expect(unwrapType(connectionType, { considerRelay: true })).toEqual({ 89 | isList: true, 90 | type: nodeType, 91 | }); 92 | }); 93 | 94 | it("can unwrap non-null Relay edges", function () { 95 | const { 96 | type: connectionType, 97 | } = queryFields.testNonNullEdgesRelayConnection; 98 | const nonNullNodeType = typeMap.TestRelayNode; 99 | 100 | expect(unwrapType(connectionType, { considerRelay: true })).toEqual({ 101 | isList: true, 102 | type: nonNullNodeType, 103 | }); 104 | }); 105 | 106 | it("can unwrap non-null Relay nodes", function () { 107 | const { 108 | type: connectionType, 109 | } = queryFields.testNonNullNodesRelayConnection; 110 | const nonNullNodeType = typeMap.TestRelayNode; 111 | 112 | expect(unwrapType(connectionType, { considerRelay: true })).toEqual({ 113 | isList: true, 114 | type: nonNullNodeType, 115 | }); 116 | }); 117 | }); 118 | 119 | describe("capitalize string", function () { 120 | it("capitalizes the first letter of a string", function () { 121 | expect(capitalize("foo bar")).toBe("Foo bar"); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current", 8 | }, 9 | }, 10 | ], 11 | ], 12 | overrides: [ 13 | { 14 | plugins: [ 15 | ["@babel/plugin-transform-destructuring", { loose: true }], 16 | ["@babel/plugin-transform-spread", { loose: true }], 17 | ], 18 | env: { 19 | cjs: { 20 | presets: [["@babel/preset-env", { modules: "commonjs" }]], 21 | }, 22 | mjs: { 23 | presets: [["@babel/preset-env", { modules: false }]], 24 | plugins: [["./build/imports-suffix", { suffix: "-mjs" }]], 25 | }, 26 | }, 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /build/imports-suffix.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Adds suffix to all paths imported inside MJS files 5 | * 6 | * Transforms: 7 | * import { foo } from "./bar"; 8 | * export { foo } from "./bar"; 9 | * 10 | * to: 11 | * import { foo } from "./bar-mjs"; 12 | * export { foo } from "./bar-mjs"; 13 | */ 14 | module.exports = function addExtensionToImportPaths(context, { suffix }) { 15 | const { types } = context; 16 | 17 | function replaceImportPath(path) { 18 | if (!path.node.source) { 19 | return; 20 | } 21 | 22 | const source = path.node.source.value; 23 | 24 | if (source.startsWith("./") || source.startsWith("../")) { 25 | const newSourceNode = types.stringLiteral(source + suffix); 26 | 27 | path.get("source").replaceWith(newSourceNode); 28 | } 29 | } 30 | 31 | return { 32 | visitor: { 33 | ImportDeclaration: replaceImportPath, 34 | ExportNamedDeclaration: replaceImportPath, 35 | }, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const babel = require("@babel/core"); 4 | const fs = require("fs"); 5 | const path = require("path"); 6 | 7 | const DIST_PATH = "./dist"; 8 | const SRC_PATH = "./lib"; 9 | 10 | function babelBuild(srcPath, options) { 11 | options.comments = false; 12 | 13 | return babel.transformFileSync(srcPath, options).code + "\n"; 14 | } 15 | 16 | function readdirRecursive(dirPath, opts = {}) { 17 | const result = []; 18 | const { ignoreDir } = opts; 19 | 20 | for (const dirent of fs.readdirSync(dirPath, { withFileTypes: true })) { 21 | const { name } = dirent; 22 | 23 | if (!dirent.isDirectory()) { 24 | result.push(name); 25 | continue; 26 | } 27 | 28 | if (ignoreDir && ignoreDir.test(name)) { 29 | continue; 30 | } 31 | 32 | const list = readdirRecursive(path.join(dirPath, name), opts).map((f) => 33 | path.join(name, f) 34 | ); 35 | 36 | result.push(...list); 37 | } 38 | 39 | return result; 40 | } 41 | 42 | function rmdirRecursive(dirPath) { 43 | if (fs.existsSync(dirPath)) { 44 | for (const dirent of fs.readdirSync(dirPath, { withFileTypes: true })) { 45 | const fullPath = path.join(dirPath, dirent.name); 46 | 47 | if (dirent.isDirectory()) { 48 | rmdirRecursive(fullPath); 49 | } else { 50 | fs.unlinkSync(fullPath); 51 | } 52 | } 53 | 54 | fs.rmdirSync(dirPath); 55 | } 56 | } 57 | 58 | if (require.main === module) { 59 | rmdirRecursive(DIST_PATH); 60 | 61 | fs.mkdirSync(DIST_PATH); 62 | 63 | const srcFiles = readdirRecursive(SRC_PATH, { ignoreDir: /^__.*__$/ }); 64 | 65 | for (const filepath of srcFiles) { 66 | const srcPath = path.join(SRC_PATH, filepath); 67 | const destPath = path.join(DIST_PATH, filepath); 68 | const cjs = babelBuild(srcPath, { envName: "cjs" }); 69 | const mjs = babelBuild(srcPath, { envName: "mjs" }); 70 | 71 | fs.mkdirSync(path.dirname(destPath), { recursive: true }); 72 | fs.writeFileSync(destPath, cjs); 73 | fs.writeFileSync(destPath.replace(/\.js$/, "-mjs.js"), mjs); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | collectCoverageFrom: ["lib/**/*.js"], 4 | coveragePathIgnorePatterns: [ 5 | "/lib/index.js", 6 | "/lib/resolvers/default.js", 7 | ], 8 | coverageThreshold: { 9 | global: { 10 | lines: 90, 11 | }, 12 | }, 13 | moduleNameMapper: { 14 | "^@lib/(.*)$": "/lib/$1", 15 | "^@tests/(.*)$": "/__tests__/$1", 16 | }, 17 | testMatch: ["**/__tests__/**/*-test.js"], 18 | transform: { 19 | "\\.gql$": "jest-transform-graphql", 20 | "^.+\\.[t|j]s$": "babel-jest", 21 | }, 22 | watchPlugins: [ 23 | "jest-watch-typeahead/filename", 24 | "jest-watch-typeahead/testname", 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /lib/__mocks__/utils.js: -------------------------------------------------------------------------------- 1 | import { graphQLSchema } from "@tests/unit/setup"; 2 | 3 | export const ensureExecutableGraphQLSchema = jest.fn(() => graphQLSchema); 4 | -------------------------------------------------------------------------------- /lib/handler.js: -------------------------------------------------------------------------------- 1 | import { Response } from "miragejs"; 2 | import createFieldResolver from "./resolvers/field"; 3 | import { ensureExecutableGraphQLSchema } from "./utils"; 4 | import { ensureModels } from "./orm/models"; 5 | import { graphql } from "graphql"; 6 | 7 | /** 8 | * Handles GraphQL requests. It returns either a successful GraphQL response or 9 | * a response with a 500 status code and messages from any caught exceptions. 10 | * 11 | * @callback graphQLHandler 12 | * @param {Object} _mirageSchema Mirage passes this in; though, it goes unused. 13 | * @param {Object} request The request object passed in by Mirage. 14 | * @returns {Object} A response object. 15 | */ 16 | 17 | /** 18 | * A higher-order function that returns a request handler for GraphQL queries. 19 | * It accepts both a GraphQL and Mirage schema along with a hash of options. 20 | * Options include: 21 | * 22 | * 1. `resolvers` - A resolver map for cases where the default Mirage resolvers 23 | * aren't sufficient. Such cases include: resolving root-level scalar values, 24 | * sorting records and complex mutations. 25 | * 2. `context` - A context object that GraphQL will pass into each resolver. A 26 | * common use case for this is to supply current user information to 27 | * resolvers. By default, whatever context you pass in will be appended with 28 | * a reference to the Mirage schema and the request being handled. 29 | * 3. `root` - A root level value that GraphQL will use as the parent object for 30 | * fields at the highest level. 31 | * 32 | * The GraphQL schema param may be a string, an AST or an executable 33 | * GraphQL schema. This library ensures the schema is executable in any case. 34 | * 35 | * This also ensures models are added to the Mirage schema for each appropriate 36 | * type from the GraphQL schema. Since the GraphQL schema already defines types 37 | * and relationships, it may be redundant to define Mirage models when using 38 | * GraphQL. You may still define Mirage models, though, if you'd like. 39 | * 40 | * Lastly, it creates a field resolver that GraphQL will use to resolve every 41 | * field from a query. If an optional resolver isn't supplied for a given field, 42 | * this field resolver will be used. It does its best to resolve queries and 43 | * mutations automatically based on the information from the GraphQL schema and 44 | * the records in Mirage's database. 45 | * 46 | * @function createGraphQLHandler 47 | * @param {Object|string} graphQLSchema 48 | * @param {Object} mirageSchema 49 | * @param {{context: Object, resolvers: Object, root: Object}} 50 | * @returns {graphQLHandler} 51 | */ 52 | export function createGraphQLHandler( 53 | graphQLSchema, 54 | mirageSchema, 55 | { context = {}, resolvers, root } = {} 56 | ) { 57 | const contextValue = { ...context, mirageSchema }; 58 | const fieldResolver = createFieldResolver(resolvers); 59 | 60 | graphQLSchema = ensureExecutableGraphQLSchema(graphQLSchema); 61 | 62 | ensureModels({ graphQLSchema, mirageSchema }); 63 | 64 | return function graphQLHandler(_mirageSchema, request) { 65 | try { 66 | const { query, variables } = JSON.parse(request.requestBody); 67 | 68 | return graphql({ 69 | contextValue: { ...contextValue, request }, 70 | fieldResolver, 71 | rootValue: root, 72 | schema: graphQLSchema, 73 | source: query, 74 | variableValues: variables, 75 | }); 76 | } catch (ex) { 77 | return new Response(500, {}, { errors: [ex] }); 78 | } 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import { createGraphQLHandler } from "./handler"; 2 | import mirageGraphQLFieldResolver from "./resolvers/mirage"; 3 | 4 | export { createGraphQLHandler, mirageGraphQLFieldResolver }; 5 | -------------------------------------------------------------------------------- /lib/orm/__mocks__/models.js: -------------------------------------------------------------------------------- 1 | export const ensureModels = jest.fn(() => {}); 2 | -------------------------------------------------------------------------------- /lib/orm/__mocks__/records.js: -------------------------------------------------------------------------------- 1 | export const filterRecords = jest.fn(() => {}); 2 | export const getRecords = jest.fn(() => {}); 3 | -------------------------------------------------------------------------------- /lib/orm/models.js: -------------------------------------------------------------------------------- 1 | import { 2 | Model, 3 | belongsTo, 4 | hasMany, 5 | _utilsInflectorCamelize as camelize, 6 | } from "miragejs"; 7 | import { isInterfaceType, isUnionType, isObjectType } from "graphql"; 8 | import { isRelayType } from "../relay-pagination"; 9 | import { unwrapType } from "../utils"; 10 | 11 | const ASSOCIATION_TYPE_CHECKS = [isInterfaceType, isObjectType, isUnionType]; 12 | 13 | function isAssociationType(type) { 14 | return ASSOCIATION_TYPE_CHECKS.find((checkType) => checkType(type)); 15 | } 16 | 17 | function createAssociationForFields(fields) { 18 | return function (associations, fieldName) { 19 | const fieldType = fields[fieldName].type; 20 | const { isList, type: unwrappedType } = unwrapType(fieldType, { 21 | considerRelay: true, 22 | }); 23 | 24 | if (isAssociationType(unwrappedType)) { 25 | const associationName = camelize(unwrappedType.name); 26 | const options = { 27 | polymorphic: 28 | isInterfaceType(unwrappedType) || isUnionType(unwrappedType), 29 | }; 30 | 31 | associations.push({ 32 | name: associationName, 33 | fieldName: fieldName, 34 | type: isList ? "hasMany" : "belongsTo", 35 | options, 36 | }); 37 | } 38 | 39 | return associations; 40 | }; 41 | } 42 | 43 | function createAssociations(type) { 44 | const fields = type.getFields(); 45 | return Object.keys(fields).reduce(createAssociationForFields(fields), []); 46 | } 47 | 48 | function createModel(type) { 49 | return { 50 | name: type.name, 51 | camelizedName: camelize(type.name), 52 | associations: createAssociations(type), 53 | }; 54 | } 55 | 56 | function shouldCreateModel(type) { 57 | return ( 58 | isObjectType(type) && !isRelayType(type) && !type.name.startsWith("__") 59 | ); 60 | } 61 | 62 | function shouldRegisterModel(mirageSchema, name) { 63 | return !mirageSchema.hasModelForModelName(name); 64 | } 65 | 66 | function createAssociationOptions(model) { 67 | return model.associations.reduce(function (options, association) { 68 | options[association.fieldName] = 69 | association.type === "hasMany" 70 | ? hasMany(association.name, association.options) 71 | : belongsTo(association.name, association.options); 72 | 73 | return options; 74 | }, {}); 75 | } 76 | 77 | function registerModel(mirageSchema, model) { 78 | const options = createAssociationOptions(model); 79 | mirageSchema.registerModel(model.name, Model.extend(options)); 80 | } 81 | 82 | /** 83 | * Create a POJO for all model definitions from the Graphql Schema. It figures 84 | * out all the models and their relationships that need to be established for 85 | * the Mirage Schema. 86 | * 87 | * Use this data to generate Mirage Models. This is the underlining 88 | * implementation of `ensureModels`. 89 | * 90 | * @function createModels 91 | * @param {{graphQLSchema: Object} options 92 | */ 93 | export function createModels({ graphQLSchema }) { 94 | const graphQLSchemaQueryTypes = [ 95 | graphQLSchema.getMutationType(), 96 | graphQLSchema.getQueryType(), 97 | graphQLSchema.getSubscriptionType(), 98 | ]; 99 | const typeMap = graphQLSchema.getTypeMap(); 100 | 101 | return Object.keys(typeMap).reduce(function (models, typeName) { 102 | const { type } = unwrapType(typeMap[typeName]); 103 | const isQueryType = graphQLSchemaQueryTypes.includes(type); 104 | 105 | if (shouldCreateModel(type) && !isQueryType) { 106 | models.push(createModel(type)); 107 | } 108 | return models; 109 | }, []); 110 | } 111 | 112 | /** 113 | * Ensures models exist in Mirage's schema for each appropriate type in the 114 | * GraphQL schema. We do this for 2 main reasons: 115 | * 116 | * 1. It saves us from having to specify models in the Mirage server setup that 117 | * essentially duplicate information we already have in the GraphQL schema. 118 | * 2. It ensures relationships are properly established in the Mirage schema. 119 | * 120 | * You can still specify models in your Mirage server setup. Any existing models 121 | * in the Mirage schema are skipped here. 122 | * 123 | * @function ensureModels 124 | * @param {{graphQLSchema: Object, mirageSchema: Object}} options 125 | */ 126 | export function ensureModels({ graphQLSchema, mirageSchema }) { 127 | const models = createModels({ graphQLSchema }); 128 | 129 | models.forEach(function (model) { 130 | if (shouldRegisterModel(mirageSchema, model.name)) { 131 | registerModel(mirageSchema, model); 132 | } 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /lib/orm/records.js: -------------------------------------------------------------------------------- 1 | import { capitalize } from "../utils"; 2 | import { _utilsInflectorCamelize as camelize } from "miragejs"; 3 | 4 | /** 5 | * Adapts a record from Mirage's database to return in the GraphQL response. It 6 | * flattens the attributes and relationships in a copy of the record. It also 7 | * ensures the `__typename` attribute is added to the copy so GraphQL can 8 | * resolve records of a polymorphic type. 9 | * 10 | * @function adaptRecord 11 | * @param {Object} record 12 | * @returns {Object} A copy of the record, adapted for the response. 13 | */ 14 | export function adaptRecord(record) { 15 | if (record == null) return; 16 | 17 | const { attrs, associations } = record; 18 | const __typename = capitalize(camelize(record.modelName)); 19 | const clone = { ...attrs, __typename }; 20 | 21 | for (let field in associations) { 22 | clone[field] = record[field]; 23 | } 24 | 25 | return clone; 26 | } 27 | 28 | /** 29 | * Adapts a list of records from Mirage's database to return in the GraphQL 30 | * response. 31 | * 32 | * @function adaptRecords 33 | * @param {Object[]} records 34 | * @see adaptRecord 35 | * @returns {Object[]} A list of adapted records. 36 | */ 37 | export function adaptRecords(records) { 38 | return records.reduce(function (adaptedRecords, record) { 39 | return [...adaptedRecords, adaptRecord(record)]; 40 | }, []); 41 | } 42 | 43 | /** 44 | * Gets records from Mirage's database for the given GraphQL type. It can filter 45 | * the records by the given GraphQL arguments, if supplied. Filtering assumes 46 | * each key in `args` corresponds to an attribute of the record. 47 | * 48 | * For more advanced filtering needs, or sorting, for example, you will need to 49 | * implement your own resolver. 50 | * 51 | * @function getRecords 52 | * @param {Object} type The GraphQL type. 53 | * @param {Object} args The GraphQL args. These may be empty. 54 | * @param {Object} mirageSchema 55 | * @see adaptedRecords 56 | * @see createGraphQLHandler 57 | * @returns {Object[]} A list of filtered, adapted records. 58 | */ 59 | export function getRecords(type, args, mirageSchema) { 60 | const collectionName = mirageSchema.toCollectionName(type.name); 61 | const records = mirageSchema[collectionName].where(args).models; 62 | 63 | return adaptRecords(records); 64 | } 65 | 66 | /** 67 | * Filters records by a hash of arguments. This is useful in cases where you 68 | * have a list of records and don't need to fetch them from Mirage's database. 69 | * Filtering assumes each key in `args` corresponds to an attribute of the 70 | * record. 71 | * 72 | * For more advanced filtering needs, or sorting, for example, you will need to 73 | * implement your own resolver. 74 | * 75 | * @function filterRecords 76 | * @param {Object[]} records The records to filter. 77 | * @param {Object} args Args by which to filter. 78 | * @returns {Object[]} A list of filtered, adapted records. 79 | */ 80 | export function filterRecords(records, args) { 81 | if (args) { 82 | records = records.filter(function (record) { 83 | return Object.keys(args).reduce(function (isMatch, arg) { 84 | return !isMatch ? isMatch : record[arg] === args[arg]; 85 | }, true); 86 | }); 87 | } 88 | 89 | return adaptRecords(records); 90 | } 91 | -------------------------------------------------------------------------------- /lib/relay-pagination.js: -------------------------------------------------------------------------------- 1 | const ENCODE = 2 | typeof btoa !== "undefined" 3 | ? btoa 4 | : typeof Buffer !== "undefined" 5 | ? (str) => Buffer.from(str).toString("base64") 6 | : (str) => str; 7 | 8 | const RELAY_ARGS = ["after", "before", "first", "last"]; 9 | 10 | function getIndexOfRecord(records, cursor, typeName, encode) { 11 | let index = null; 12 | 13 | if (cursor == null) return index; 14 | 15 | for (let i = 0; i < records.length; i++) { 16 | if (encode(`${typeName}:${records[i].id}`) === cursor) { 17 | index = i; 18 | break; 19 | } 20 | } 21 | 22 | return index; 23 | } 24 | 25 | function hasField(type, fieldName) { 26 | return fieldName in type.getFields(); 27 | } 28 | 29 | /** 30 | * Given a list of records and a hash of Relay pagination arguments, it creates 31 | * a list of filtered Relay connection edges. It also accepts an encoding 32 | * function to create cursors. If no encoding function is passed in, a default 33 | * is used. 34 | * 35 | * @function getEdges 36 | * @param {Object[]} records 37 | * @param {{first: integer, last: integer, after: string, before: string}} args 38 | * @param {string} typeName 39 | * @param {encode} [encode] 40 | * @see {@link https://relay.dev/graphql/connections.htm#sec-Edge-Types} 41 | * @returns {Object[]} A list of Relay connection edges mapped from the records. 42 | */ 43 | export function getEdges(records, args, typeName, encode = ENCODE) { 44 | const { after, before, first, last } = args; 45 | const afterIndex = getIndexOfRecord(records, after, typeName, encode); 46 | const beforeIndex = getIndexOfRecord(records, before, typeName, encode); 47 | 48 | if (afterIndex != null) records = records.slice(afterIndex + 1); 49 | if (beforeIndex != null) records = records.slice(0, beforeIndex); 50 | if (first != null) records = records.slice(0, first); 51 | if (last != null) records = records.slice(-last); 52 | 53 | return records.map((record) => ({ 54 | cursor: encode(`${typeName}:${record.id}`), 55 | node: record, 56 | })); 57 | } 58 | 59 | /** 60 | * Given a list of records and a list of edges, this function compares the two 61 | * lists and builds page info for a Relay connection. 62 | * 63 | * @function getPageInfo 64 | * @param {Object[]} records 65 | * @param {Object[]} edges 66 | * @see {@link https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo} 67 | * @returns {{hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string, endCursor: string}} 68 | */ 69 | export function getPageInfo(records, edges) { 70 | const pageInfo = { 71 | hasPreviousPage: false, 72 | hasNextPage: false, 73 | startCursor: null, 74 | endCursor: null, 75 | }; 76 | 77 | if (edges && edges.length) { 78 | const [firstEdge] = edges; 79 | const lastEdge = edges[edges.length - 1]; 80 | 81 | pageInfo.startCursor = firstEdge.cursor; 82 | pageInfo.endCursor = lastEdge.cursor; 83 | pageInfo.hasPreviousPage = firstEdge.node.id !== records[0].id; 84 | pageInfo.hasNextPage = lastEdge.node.id !== records[records.length - 1].id; 85 | } 86 | 87 | return pageInfo; 88 | } 89 | 90 | /** 91 | * Given a list of arguments, it separates Relay pagination args (first, last, 92 | * after, before) from any other arguments. 93 | * 94 | * @function getRelayArgs 95 | * @param {Object} args 96 | * @returns {{relayArgs: Object, nonRelayArgs: Object}} 97 | */ 98 | export function getRelayArgs(args) { 99 | return Object.keys(args).reduce( 100 | function (separatedArgs, arg) { 101 | const argsSet = RELAY_ARGS.includes(arg) ? "relayArgs" : "nonRelayArgs"; 102 | 103 | separatedArgs[argsSet][arg] = args[arg]; 104 | 105 | return separatedArgs; 106 | }, 107 | { relayArgs: {}, nonRelayArgs: {} } 108 | ); 109 | } 110 | 111 | /** 112 | * Utility function to determine if a given type is a Relay connection. 113 | * 114 | * @function isRelayEdgeType 115 | * @param {Object} type 116 | * @returns {boolean} 117 | */ 118 | export function isRelayConnectionType(type) { 119 | return type.name.endsWith("Connection") && hasField(type, "edges"); 120 | } 121 | 122 | /** 123 | * Utility function to determine if a given type is a Relay connection edge. 124 | * 125 | * @function isRelayEdgeType 126 | * @param {Object} type 127 | * @returns {boolean} 128 | */ 129 | export function isRelayEdgeType(type) { 130 | return type.name.endsWith("Edge") && hasField(type, "node"); 131 | } 132 | 133 | /** 134 | * Utility function to determine if a given type is Relay connection page info. 135 | * 136 | * @function isRelayPageInfoType 137 | * @param {Object} type 138 | * @returns {boolean} 139 | */ 140 | export function isRelayPageInfoType(type) { 141 | return type.name === "PageInfo" && hasField(type, "startCursor"); 142 | } 143 | 144 | /** 145 | * Utility function to determine if a given type is a Relay connection, a Relay 146 | * connection edge or Relay connection page info. 147 | * 148 | * @function isRelayType 149 | * @param {Object} type 150 | * @return {boolean} 151 | */ 152 | export function isRelayType(type) { 153 | return ( 154 | type.name && 155 | (isRelayConnectionType(type) || 156 | isRelayEdgeType(type) || 157 | isRelayPageInfoType(type)) 158 | ); 159 | } 160 | -------------------------------------------------------------------------------- /lib/resolvers/__mocks__/default.js: -------------------------------------------------------------------------------- 1 | export default jest.fn(() => {}); 2 | -------------------------------------------------------------------------------- /lib/resolvers/__mocks__/field.js: -------------------------------------------------------------------------------- 1 | export default jest.fn(() => {}); 2 | -------------------------------------------------------------------------------- /lib/resolvers/__mocks__/interface.js: -------------------------------------------------------------------------------- 1 | export default jest.fn(() => {}); 2 | -------------------------------------------------------------------------------- /lib/resolvers/__mocks__/list.js: -------------------------------------------------------------------------------- 1 | export default jest.fn(() => {}); 2 | -------------------------------------------------------------------------------- /lib/resolvers/__mocks__/mirage.js: -------------------------------------------------------------------------------- 1 | export default jest.fn(() => {}); 2 | -------------------------------------------------------------------------------- /lib/resolvers/__mocks__/object.js: -------------------------------------------------------------------------------- 1 | export default jest.fn(() => {}); 2 | -------------------------------------------------------------------------------- /lib/resolvers/__mocks__/union.js: -------------------------------------------------------------------------------- 1 | export default jest.fn(() => {}); 2 | -------------------------------------------------------------------------------- /lib/resolvers/default.js: -------------------------------------------------------------------------------- 1 | import { defaultFieldResolver } from "graphql"; 2 | 3 | export default defaultFieldResolver; 4 | -------------------------------------------------------------------------------- /lib/resolvers/field.js: -------------------------------------------------------------------------------- 1 | import mirageGraphQLFieldResolver from "./mirage"; 2 | 3 | function getOptionalResolver(info, optionalResolvers) { 4 | const { fieldName, parentType } = info; 5 | 6 | return ( 7 | optionalResolvers && 8 | optionalResolvers[parentType.name] && 9 | optionalResolvers[parentType.name][fieldName] 10 | ); 11 | } 12 | 13 | /** 14 | * The field resolver to be used for all GraphQL queries. It delegates to an 15 | * optional resolver passed into the GraphQL handler or the high-level Mirage 16 | * resolver. 17 | * 18 | * @callback fieldResolver 19 | * @param {Object} obj 20 | * @param {Object} args 21 | * @param {Object} context 22 | * @param {Object} info 23 | * @returns {*} 24 | */ 25 | 26 | /** 27 | * A higher order function that accepts a hash of optional resolvers passed into 28 | * the GraphQL handler and returns a field resolver function to be used for all 29 | * GraphQL queries. 30 | * 31 | * @function createFieldResolver 32 | * @param {Object} optionalResolvers 33 | * @return {fieldResolver} 34 | */ 35 | export default function createFieldResolver(optionalResolvers) { 36 | return function fieldResolver(_obj, _args, _context, info) { 37 | const optionalResolver = getOptionalResolver(info, optionalResolvers); 38 | 39 | if (optionalResolver) { 40 | return optionalResolver(...arguments); 41 | } 42 | 43 | return mirageGraphQLFieldResolver(...arguments); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /lib/resolvers/interface.js: -------------------------------------------------------------------------------- 1 | import resolveObject from "./object"; 2 | 3 | function getTypeFromInlineFragment(info) { 4 | const selection = info.fieldNodes[0].selectionSet.selections.find( 5 | ({ kind }) => kind === "InlineFragment" 6 | ); 7 | 8 | if (selection) { 9 | const { 10 | typeCondition: { 11 | name: { value: typeName }, 12 | }, 13 | } = selection; 14 | 15 | return info.schema.getTypeMap()[typeName]; 16 | } 17 | } 18 | 19 | function resolveFromImplementations(obj, args, context, info, type) { 20 | const { objects: implementations } = info.schema.getImplementations(type); 21 | 22 | return implementations 23 | .map((implType) => resolveObject(obj, args, context, info, implType)) 24 | .find((record) => record != null); 25 | } 26 | 27 | /** 28 | * Resolves a field that returns an interface types. If the query includes an 29 | * inline fragment, it uses that to determine the implementation type by which 30 | * to resolve. If no inline fragment is specified, it gets all implementation 31 | * types and looks for a record matching any of those. 32 | * 33 | * The latter case could be unreliable and in such cases it is advised that an 34 | * optional resolver be passed into the handler for the particular field. In 35 | * either case, it delegates to `resolveObject`. 36 | * 37 | * @function resolveInterface 38 | * @param {Object} obj 39 | * @param {Object} args 40 | * @param {Object} context 41 | * @param {Object} info 42 | * @param {Object} type An unwrapped type. 43 | * @see {@link https://graphql.org/learn/execution/#root-fields-resolvers} 44 | * @see resolveObject 45 | * @returns {Object} A record from Mirage's database. 46 | */ 47 | export default function resolveInterface(obj, args, context, info, type) { 48 | const implType = getTypeFromInlineFragment(info); 49 | 50 | return implType 51 | ? resolveObject(obj, args, context, info, implType) 52 | : resolveFromImplementations(obj, args, context, info, type); 53 | } 54 | -------------------------------------------------------------------------------- /lib/resolvers/list.js: -------------------------------------------------------------------------------- 1 | import { filterRecords, getRecords } from "../orm/records"; 2 | import { isRelayEdgeType } from "../relay-pagination"; 3 | 4 | function getRelatedRecords(obj, fieldName) { 5 | return obj[fieldName].models || obj[fieldName]; 6 | } 7 | 8 | /** 9 | * Resolves fields that return a list type. Note: If there's no parent object, 10 | * this only works for lists that return a list of records from Mirage's 11 | * database. If a query should return a list of scalar values, for example, an 12 | * optional resolver should be passed into the GraphQL handler. 13 | * 14 | * @function resolveList 15 | * @param {Object} obj 16 | * @param {Object} args 17 | * @param {Object} context 18 | * @param {Object} info 19 | * @param {Object} type An unwrapped type. 20 | * @see {@link https://graphql.org/learn/execution/#root-fields-resolvers} 21 | * @returns {Object[]} A list of records from Mirage's database. 22 | */ 23 | export default function resolveList(obj, args, context, info, type) { 24 | return !obj 25 | ? getRecords(type, args, context.mirageSchema) 26 | : isRelayEdgeType(type) 27 | ? obj.edges 28 | : filterRecords(getRelatedRecords(obj, info.fieldName), args); 29 | } 30 | -------------------------------------------------------------------------------- /lib/resolvers/mirage.js: -------------------------------------------------------------------------------- 1 | import { isInterfaceType, isObjectType, isUnionType } from "graphql"; 2 | import resolveDefault from "./default"; 3 | import resolveList from "./list"; 4 | import resolveObject from "./object"; 5 | import resolveInterface from "./interface"; 6 | import resolveUnion from "./union"; 7 | import { unwrapType } from "../utils"; 8 | 9 | /** 10 | * Resolves all fields from queries handled by the GraphQL handler. It unwraps 11 | * the return type, if need be, and delegates resolution to different resolvers, 12 | * depending on the type. If no suitable resolver exists in this library, it 13 | * delegates to GraphQL's default field resolver (in cases like fields that 14 | * return scalar values, for example). 15 | * 16 | * This resolver is useful when composing optional resolvers to pass into the 17 | * GraphQL handler as it does a lot of the heavy lifting. For example, 18 | * implementing a resolver that sorts a list of filtered records becomes trivial 19 | * if the records are fetched using this resolver before sorting them. 20 | * 21 | * @function mirageGraphQLFieldResolver 22 | * @param {Object} obj 23 | * @param {Object} args 24 | * @param {Object} context 25 | * @param {Object} info 26 | * @see {@link https://graphql.org/learn/execution/#root-fields-resolvers} 27 | * @returns {*} 28 | */ 29 | export default function mirageGraphQLFieldResolver(obj, args, context, info) { 30 | let { isList, type } = unwrapType(info.returnType); 31 | 32 | return isInterfaceType(type) 33 | ? resolveInterface(obj, args, context, info, type) 34 | : isUnionType(type) 35 | ? resolveUnion(obj, args, context, info, isList, type) 36 | : !isObjectType(type) 37 | ? resolveDefault(obj, args, context, info) 38 | : isList 39 | ? resolveList(obj, args, context, info, type) 40 | : resolveObject(obj, args, context, info, type); 41 | } 42 | -------------------------------------------------------------------------------- /lib/resolvers/mutation.js: -------------------------------------------------------------------------------- 1 | import { isInputObjectType, isSpecifiedScalarType } from "graphql"; 2 | 3 | function getMutationVarTypes(variableDefinitions, typeMap) { 4 | return variableDefinitions.reduce(function (vars, definition) { 5 | const typeInfo = definition.type.type || definition.type; 6 | const type = typeMap[typeInfo.name.value]; 7 | 8 | return [...vars, type]; 9 | }, []); 10 | } 11 | 12 | function hasCreateVars(varTypes) { 13 | return varTypes.length === 1 && isInputObjectType(varTypes[0]); 14 | } 15 | 16 | function hasDeleteVars(varTypes) { 17 | return varTypes.length === 1 && isIdVar(varTypes[0]); 18 | } 19 | 20 | function hasUpdateVars(varTypes) { 21 | return ( 22 | varTypes.length === 2 && 23 | varTypes.reduce(function (hasUpdateVars, varType) { 24 | if (hasUpdateVars === false) return hasUpdateVars; 25 | 26 | return isInputObjectType(varType) || isIdVar(varType); 27 | }, null) 28 | ); 29 | } 30 | 31 | function isIdVar(varType) { 32 | return isSpecifiedScalarType(varType) && varType.name === "ID"; 33 | } 34 | 35 | function resolveCreateMutation(args, table) { 36 | const input = args[Object.keys(args)[0]]; 37 | 38 | return table.insert(input); 39 | } 40 | 41 | function resolveDeleteMutation(args, table) { 42 | const record = table.find(args.id); 43 | 44 | table.remove(args.id); 45 | 46 | return record; 47 | } 48 | 49 | function resolveUpdateMutation(args, table) { 50 | const input = args[Object.keys(args).find((arg) => arg !== "id")]; 51 | 52 | return table.update(args.id, input); 53 | } 54 | 55 | function throwUnimplemented(info) { 56 | throw new Error( 57 | `Could not find a default resolver for ${info.fieldName}. Please supply a resolver for this mutation.` 58 | ); 59 | } 60 | 61 | /** 62 | * Resolves mutations in a default way. There are three types of mutations this 63 | * library will try to resolve automatically: 64 | * 65 | * 1. Create. A mutation with one input type argument. 66 | * 2. Update. A mutation with two arguments: ID type and input type. 67 | * 3. Delete. A mutation with one ID type argument. 68 | * 69 | * Each of these default mutations will return the affected record. Any 70 | * mutations with different arguments, or mutations where the above assumptions 71 | * don't apply, must be resolved by an optional resolver passed into the GraphQL 72 | * handler. 73 | * 74 | * @function resolveMutation 75 | * @param {Object} args 76 | * @param {Object} context 77 | * @param {Object} info 78 | * @param {String} typeName 79 | * @see {@link https://graphql.org/learn/execution/#root-fields-resolvers} 80 | * @throws It will throw an error if the mutation cannot be handled by default. 81 | * @returns {Object} The affected record from Mirage's database. 82 | */ 83 | export function resolveMutation(args, context, info, typeName) { 84 | const collectionName = context.mirageSchema.toCollectionName(typeName); 85 | const table = context.mirageSchema.db[collectionName]; 86 | const varTypes = getMutationVarTypes( 87 | info.operation.variableDefinitions, 88 | info.schema.getTypeMap() 89 | ); 90 | 91 | return hasCreateVars(varTypes) 92 | ? resolveCreateMutation(args, table) 93 | : hasDeleteVars(varTypes) 94 | ? resolveDeleteMutation(args, table) 95 | : hasUpdateVars(varTypes) 96 | ? resolveUpdateMutation(args, table) 97 | : throwUnimplemented(info); 98 | } 99 | -------------------------------------------------------------------------------- /lib/resolvers/object.js: -------------------------------------------------------------------------------- 1 | import { adaptRecord } from "../orm/records"; 2 | import { resolveMutation } from "./mutation"; 3 | import { resolveRelayConnection } from "./relay"; 4 | import { unwrapType } from "../utils"; 5 | import { 6 | isRelayConnectionType, 7 | isRelayEdgeType, 8 | isRelayPageInfoType, 9 | } from "../relay-pagination"; 10 | 11 | function findRecord(args, context, typeName) { 12 | const collectionName = context.mirageSchema.toCollectionName(typeName); 13 | const record = context.mirageSchema[collectionName].findBy(args); 14 | 15 | return adaptRecord(record, typeName); 16 | } 17 | 18 | function isMutation(info) { 19 | return info.parentType === info.schema.getMutationType(); 20 | } 21 | 22 | /** 23 | * Resolves a field that returns an object type. 24 | * 25 | * @function resolveObject 26 | * @param {Object} obj 27 | * @param {Object} args 28 | * @param {Object} context 29 | * @param {Object} info 30 | * @see {@link https://graphql.org/learn/execution/#root-fields-resolvers} 31 | * @returns {Object} A record from Mirage's database. 32 | */ 33 | export default function resolveObject(obj, args, context, info, type) { 34 | const { type: parentType } = unwrapType(info.parentType); 35 | 36 | return isMutation(info) 37 | ? resolveMutation(args, context, info, type.name) 38 | : isRelayConnectionType(type) 39 | ? resolveRelayConnection(obj, args, context, info, type) 40 | : !obj 41 | ? findRecord(args, context, type.name) 42 | : isRelayEdgeType(parentType) 43 | ? obj.node 44 | : isRelayPageInfoType(type) 45 | ? obj.pageInfo 46 | : adaptRecord(obj[info.fieldName]); 47 | } 48 | -------------------------------------------------------------------------------- /lib/resolvers/relay.js: -------------------------------------------------------------------------------- 1 | import { getEdges, getPageInfo, getRelayArgs } from "../relay-pagination"; 2 | import { filterRecords, getRecords } from "../orm/records"; 3 | import { unwrapType } from "../utils"; 4 | 5 | /** 6 | * Resolves a field that returns a Relay connection type. It determines the type 7 | * of records to fetch and filter from Mirage's database and builds a list of 8 | * edges and page info for the connection. 9 | * 10 | * @function resolveRelayConnection 11 | * @param {Object} obj 12 | * @param {Object} args 13 | * @param {Object} context 14 | * @param {Object} info 15 | * @param {Object} type 16 | * @see {@link https://relay.dev/graphql/connections.htm#sec-Connection-Types} 17 | * @see {@link https://graphql.org/learn/execution/#root-fields-resolvers} 18 | * @returns {{edges: Object[], pageInfo: Object}} 19 | */ 20 | export function resolveRelayConnection(obj, args, context, info, type) { 21 | const { edges: edgesField } = type.getFields(); 22 | const { type: edgeType } = unwrapType(edgesField.type); 23 | const { relayArgs, nonRelayArgs } = getRelayArgs(args); 24 | const { type: nodeType } = unwrapType(edgeType.getFields().node.type); 25 | const records = 26 | obj && obj[info.fieldName] && obj[info.fieldName].models 27 | ? filterRecords(obj[info.fieldName].models, nonRelayArgs) 28 | : getRecords(nodeType, nonRelayArgs, context.mirageSchema); 29 | const edges = getEdges(records, relayArgs, nodeType.name); 30 | 31 | return { 32 | edges, 33 | pageInfo: getPageInfo(records, edges), 34 | totalCount: records.length, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /lib/resolvers/union.js: -------------------------------------------------------------------------------- 1 | import { adaptRecord, filterRecords, getRecords } from "../orm/records"; 2 | 3 | function getRecordsForTypes(types, args, mirageSchema) { 4 | return types.reduce(function (records, type) { 5 | return [...records, ...getRecords(type, args, mirageSchema)]; 6 | }, []); 7 | } 8 | 9 | /** 10 | * Resolves a field that returns a union type. For each type in the union, it 11 | * fetches and filters records from Mirage's database. 12 | * 13 | * @function resolveUnion 14 | * @param {Object} obj 15 | * @param {Object} args 16 | * @param {Object} context 17 | * @param {Object} info 18 | * @param {Boolean} isList 19 | * @param {Object} type 20 | * @see {@link https://graphql.org/learn/execution/#root-fields-resolvers} 21 | * @returns {Object[]} A list of records of many types from Mirage's database. 22 | */ 23 | export default function resolveUnion(obj, args, context, info, isList, type) { 24 | return !obj 25 | ? getRecordsForTypes(type.getTypes(), args, context.mirageSchema) 26 | : isList 27 | ? filterRecords(obj[info.fieldName].models, args) 28 | : adaptRecord(obj[info.fieldName]); 29 | } 30 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | import { isRelayType } from "./relay-pagination"; 2 | import { 3 | GraphQLSchema, 4 | buildASTSchema, 5 | isListType, 6 | isNonNullType, 7 | parse, 8 | } from "graphql"; 9 | 10 | /** 11 | * Capitalize a string 12 | * 13 | * @function capitalize 14 | * @param {String} str 15 | * @returns {String} The capitalized string. 16 | */ 17 | export function capitalize(str) { 18 | return `${str.charAt(0).toUpperCase()}${str.slice(1)}`; 19 | } 20 | 21 | /** 22 | * Given a GraphQL schema which may be a string or an AST, it returns an 23 | * executable version of that schema. If the schema passed in is already 24 | * executable, it returns the schema as-is. 25 | * 26 | * @function ensureExecutableGraphQLSchema 27 | * @param {Object} graphQLSchema 28 | * @returns {Object} The executable GraphQL schema. 29 | */ 30 | export function ensureExecutableGraphQLSchema(graphQLSchema) { 31 | if (!(graphQLSchema instanceof GraphQLSchema)) { 32 | if (typeof graphQLSchema === "string") { 33 | graphQLSchema = parse(graphQLSchema); 34 | } 35 | 36 | graphQLSchema = buildASTSchema(graphQLSchema, { 37 | commentDescriptions: true, 38 | }); 39 | } 40 | 41 | return graphQLSchema; 42 | } 43 | 44 | /** 45 | * Unwraps GraphQL types, e.g., a non-null list of objects, to determine the 46 | * underlying type. It also considers types like Relay connections and edges. 47 | * This is useful when querying for list or non-null types and needing to know 48 | * which underlying type of record(s) to fetch from Mirage's database. 49 | * 50 | * It returns a hash containing a Boolean attribute, isList, which tells us the 51 | * underlying type was wrapped in a list type. It also contains a type attribute 52 | * that refers to the actual underlying type. 53 | * 54 | * @function unwrapType 55 | * @param {Object} type 56 | * @param {{considerRelay: boolean, isList: boolean}} options 57 | * @returns {{isList: boolean, type: Object}} 58 | */ 59 | export function unwrapType( 60 | type, 61 | options = { considerRelay: false, isList: false } 62 | ) { 63 | if (options.considerRelay && isRelayType(type)) { 64 | const fields = type.getFields(); 65 | 66 | return fields.edges 67 | ? unwrapType(fields.edges.type, options) 68 | : unwrapType(fields.node.type, options); 69 | } 70 | 71 | const isList = isListType(type); 72 | 73 | if (isList || isNonNullType(type)) { 74 | if (!options.isList) { 75 | options.isList = isList; 76 | } 77 | 78 | return unwrapType(type.ofType, options); 79 | } 80 | 81 | return { isList: options.isList, type }; 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@miragejs/graphql", 3 | "version": "0.1.13", 4 | "description": "A library for handling GraphQL requests with Mirage JS", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "module": "dist/index-mjs.js", 8 | "keywords": [ 9 | "pretender", 10 | "prototype", 11 | "server", 12 | "testing", 13 | "mirage", 14 | "graphql" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/miragejs/graphql.git" 19 | }, 20 | "license": "MIT", 21 | "author": "Rocky Neurock", 22 | "bugs": { 23 | "url": "https://github.com/miragejs/graphql/issues" 24 | }, 25 | "scripts": { 26 | "build": "node build && tsc", 27 | "lint": "eslint .", 28 | "prepublishOnly": "node build && tsc", 29 | "prettier:check": "prettier --list-different '**/*.js'", 30 | "prettier:update": "prettier --write .", 31 | "test": "jest" 32 | }, 33 | "dependencies": { 34 | "graphql": "^15.0.0", 35 | "miragejs": "^0.1.0" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.5.5", 39 | "@babel/preset-env": "^7.9.0", 40 | "babel-eslint": "^10.0.2", 41 | "babel-jest": "^26.0.1", 42 | "eslint": "^6.1.0", 43 | "eslint-config-prettier": "^6.3.0", 44 | "eslint-import-resolver-alias": "^1.1.2", 45 | "eslint-import-resolver-node": "^0.3.2", 46 | "eslint-plugin-import": "^2.18.2", 47 | "eslint-plugin-jest": "^23.6.0", 48 | "eslint-plugin-node": "^11.0.0", 49 | "eslint-plugin-prettier": "^3.1.0", 50 | "graphql-request": "^2.0.0", 51 | "graphql-tag": "^2.10.3", 52 | "jest": "^26.6.3", 53 | "jest-transform-graphql": "^2.1.0", 54 | "jest-watch-typeahead": "^0.6.0", 55 | "prettier": "^2.0.2", 56 | "typescript": "^4.0.3" 57 | }, 58 | "engines": { 59 | "node": "6.* || 8.* || >= 10.*" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["lib/**/*"], 3 | "exclude": ["**/__mocks__/**/*"], 4 | 5 | "compilerOptions": { 6 | "allowJs": true, 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "outDir": "dist" 10 | } 11 | } 12 | --------------------------------------------------------------------------------