├── .babelrc ├── .gitignore ├── .prettierrc.js ├── README.md ├── next-env.d.ts ├── package.json ├── schema ├── schema.graphql └── schema.json ├── scripts └── generateSchema.ts ├── src ├── __generated__ │ └── pagesQuery.graphql.ts ├── graphql │ ├── common │ │ └── types.ts │ ├── interface │ │ └── NodeInterface.ts │ ├── loaders │ │ ├── dataloaders.ts │ │ └── middleware.ts │ ├── modules │ │ └── user │ │ │ ├── UserLoader.ts │ │ │ ├── UserModel.ts │ │ │ ├── UserType.ts │ │ │ └── mock.ts │ ├── schema.ts │ └── type │ │ └── QueryType.ts ├── pages │ ├── _app.tsx │ ├── api │ │ └── graphql.ts │ └── index.tsx └── relay │ ├── environment.ts │ └── withQuery.tsx ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [["relay", { "artifactDirectory": "./src/__generated__" }]] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | .idea 36 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'es5', 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: false, 8 | }; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relay-next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "export": "next export", 10 | "relay": "relay-compiler --watchman false --src ./src --schema schema/schema.graphql --language typescript --artifactDirectory ./src/__generated__", 11 | "lint": "eslint src --ext .ts,.tsx", 12 | "lint:fix": "lint -- --fix", 13 | "generate-schema": "ts-node ./scripts/generateSchema.ts " 14 | }, 15 | "dependencies": { 16 | "apollo-server-micro": "^2.16.1", 17 | "dataloader": "^2.0.0", 18 | "graphql": "^15.3.0", 19 | "graphql-middleware": "^4.0.2", 20 | "graphql-relay": "^0.6.0", 21 | "next": "9.5.2", 22 | "react": "16.13.1", 23 | "react-dom": "16.13.1", 24 | "react-relay": "^10.0.1", 25 | "react-relay-network-modern": "^4.7.5", 26 | "relay-compiler": "^10.0.1", 27 | "relay-hooks": "^3.5.2", 28 | "relay-runtime": "^10.0.1" 29 | }, 30 | "devDependencies": { 31 | "@types/graphql-relay": "^0.6.0", 32 | "@types/node": "^14.6.0", 33 | "@types/react": "^16.9.46", 34 | "@types/react-relay": "^7.0.8", 35 | "@types/relay-runtime": "^10.0.3", 36 | "@typescript-eslint/eslint-plugin": "^3.9.1", 37 | "@typescript-eslint/parser": "^3.9.1", 38 | "babel-plugin-relay": "^10.0.1", 39 | "eslint": "^7.7.0", 40 | "eslint-config-prettier": "^6.11.0", 41 | "eslint-plugin-jsx-a11y": "^6.3.1", 42 | "eslint-plugin-prettier": "^3.1.4", 43 | "eslint-plugin-react": "^7.20.6", 44 | "eslint-plugin-react-hooks": "^4.1.0", 45 | "prettier": "^2.0.5", 46 | "relay-compiler-language-typescript": "^13.0.1", 47 | "ts-node": "^8.10.2", 48 | "typescript": "^3.9.7" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /schema/schema.graphql: -------------------------------------------------------------------------------- 1 | """The root of all... queries""" 2 | type Query { 3 | """Fetches an object given its ID""" 4 | node( 5 | """The ID of an object""" 6 | id: ID! 7 | ): Node 8 | me: User 9 | users(after: String, first: Int, before: String, last: Int): UserConnection 10 | } 11 | 12 | """An object with an ID""" 13 | interface Node { 14 | """The id of the object.""" 15 | id: ID! 16 | } 17 | 18 | """User data""" 19 | type User implements Node { 20 | """The ID of an object""" 21 | id: ID! 22 | name: String! 23 | email: String! 24 | } 25 | 26 | """A connection to a list of items.""" 27 | type UserConnection { 28 | """Information to aid in pagination.""" 29 | pageInfo: PageInfo! 30 | 31 | """A list of edges.""" 32 | edges: [UserEdge] 33 | } 34 | 35 | """Information about pagination in a connection.""" 36 | type PageInfo { 37 | """When paginating forwards, are there more items?""" 38 | hasNextPage: Boolean! 39 | 40 | """When paginating backwards, are there more items?""" 41 | hasPreviousPage: Boolean! 42 | 43 | """When paginating backwards, the cursor to continue.""" 44 | startCursor: String 45 | 46 | """When paginating forwards, the cursor to continue.""" 47 | endCursor: String 48 | } 49 | 50 | """An edge in a connection.""" 51 | type UserEdge { 52 | """The item at the end of the edge""" 53 | node: User 54 | 55 | """A cursor for use in pagination""" 56 | cursor: String! 57 | } 58 | -------------------------------------------------------------------------------- /schema/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "__schema": { 4 | "queryType": { 5 | "name": "Query" 6 | }, 7 | "mutationType": null, 8 | "subscriptionType": null, 9 | "types": [ 10 | { 11 | "kind": "OBJECT", 12 | "name": "Query", 13 | "description": "The root of all... queries", 14 | "fields": [ 15 | { 16 | "name": "node", 17 | "description": "Fetches an object given its ID", 18 | "args": [ 19 | { 20 | "name": "id", 21 | "description": "The ID of an object", 22 | "type": { 23 | "kind": "NON_NULL", 24 | "name": null, 25 | "ofType": { 26 | "kind": "SCALAR", 27 | "name": "ID", 28 | "ofType": null 29 | } 30 | }, 31 | "defaultValue": null 32 | } 33 | ], 34 | "type": { 35 | "kind": "INTERFACE", 36 | "name": "Node", 37 | "ofType": null 38 | }, 39 | "isDeprecated": false, 40 | "deprecationReason": null 41 | }, 42 | { 43 | "name": "me", 44 | "description": null, 45 | "args": [], 46 | "type": { 47 | "kind": "OBJECT", 48 | "name": "User", 49 | "ofType": null 50 | }, 51 | "isDeprecated": false, 52 | "deprecationReason": null 53 | }, 54 | { 55 | "name": "users", 56 | "description": null, 57 | "args": [ 58 | { 59 | "name": "after", 60 | "description": null, 61 | "type": { 62 | "kind": "SCALAR", 63 | "name": "String", 64 | "ofType": null 65 | }, 66 | "defaultValue": null 67 | }, 68 | { 69 | "name": "first", 70 | "description": null, 71 | "type": { 72 | "kind": "SCALAR", 73 | "name": "Int", 74 | "ofType": null 75 | }, 76 | "defaultValue": null 77 | }, 78 | { 79 | "name": "before", 80 | "description": null, 81 | "type": { 82 | "kind": "SCALAR", 83 | "name": "String", 84 | "ofType": null 85 | }, 86 | "defaultValue": null 87 | }, 88 | { 89 | "name": "last", 90 | "description": null, 91 | "type": { 92 | "kind": "SCALAR", 93 | "name": "Int", 94 | "ofType": null 95 | }, 96 | "defaultValue": null 97 | } 98 | ], 99 | "type": { 100 | "kind": "OBJECT", 101 | "name": "UserConnection", 102 | "ofType": null 103 | }, 104 | "isDeprecated": false, 105 | "deprecationReason": null 106 | } 107 | ], 108 | "inputFields": null, 109 | "interfaces": [], 110 | "enumValues": null, 111 | "possibleTypes": null 112 | }, 113 | { 114 | "kind": "INTERFACE", 115 | "name": "Node", 116 | "description": "An object with an ID", 117 | "fields": [ 118 | { 119 | "name": "id", 120 | "description": "The id of the object.", 121 | "args": [], 122 | "type": { 123 | "kind": "NON_NULL", 124 | "name": null, 125 | "ofType": { 126 | "kind": "SCALAR", 127 | "name": "ID", 128 | "ofType": null 129 | } 130 | }, 131 | "isDeprecated": false, 132 | "deprecationReason": null 133 | } 134 | ], 135 | "inputFields": null, 136 | "interfaces": [], 137 | "enumValues": null, 138 | "possibleTypes": [ 139 | { 140 | "kind": "OBJECT", 141 | "name": "User", 142 | "ofType": null 143 | } 144 | ] 145 | }, 146 | { 147 | "kind": "SCALAR", 148 | "name": "ID", 149 | "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", 150 | "fields": null, 151 | "inputFields": null, 152 | "interfaces": null, 153 | "enumValues": null, 154 | "possibleTypes": null 155 | }, 156 | { 157 | "kind": "OBJECT", 158 | "name": "User", 159 | "description": "User data", 160 | "fields": [ 161 | { 162 | "name": "id", 163 | "description": "The ID of an object", 164 | "args": [], 165 | "type": { 166 | "kind": "NON_NULL", 167 | "name": null, 168 | "ofType": { 169 | "kind": "SCALAR", 170 | "name": "ID", 171 | "ofType": null 172 | } 173 | }, 174 | "isDeprecated": false, 175 | "deprecationReason": null 176 | }, 177 | { 178 | "name": "name", 179 | "description": null, 180 | "args": [], 181 | "type": { 182 | "kind": "NON_NULL", 183 | "name": null, 184 | "ofType": { 185 | "kind": "SCALAR", 186 | "name": "String", 187 | "ofType": null 188 | } 189 | }, 190 | "isDeprecated": false, 191 | "deprecationReason": null 192 | }, 193 | { 194 | "name": "email", 195 | "description": null, 196 | "args": [], 197 | "type": { 198 | "kind": "NON_NULL", 199 | "name": null, 200 | "ofType": { 201 | "kind": "SCALAR", 202 | "name": "String", 203 | "ofType": null 204 | } 205 | }, 206 | "isDeprecated": false, 207 | "deprecationReason": null 208 | } 209 | ], 210 | "inputFields": null, 211 | "interfaces": [ 212 | { 213 | "kind": "INTERFACE", 214 | "name": "Node", 215 | "ofType": null 216 | } 217 | ], 218 | "enumValues": null, 219 | "possibleTypes": null 220 | }, 221 | { 222 | "kind": "SCALAR", 223 | "name": "String", 224 | "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", 225 | "fields": null, 226 | "inputFields": null, 227 | "interfaces": null, 228 | "enumValues": null, 229 | "possibleTypes": null 230 | }, 231 | { 232 | "kind": "OBJECT", 233 | "name": "UserConnection", 234 | "description": "A connection to a list of items.", 235 | "fields": [ 236 | { 237 | "name": "pageInfo", 238 | "description": "Information to aid in pagination.", 239 | "args": [], 240 | "type": { 241 | "kind": "NON_NULL", 242 | "name": null, 243 | "ofType": { 244 | "kind": "OBJECT", 245 | "name": "PageInfo", 246 | "ofType": null 247 | } 248 | }, 249 | "isDeprecated": false, 250 | "deprecationReason": null 251 | }, 252 | { 253 | "name": "edges", 254 | "description": "A list of edges.", 255 | "args": [], 256 | "type": { 257 | "kind": "LIST", 258 | "name": null, 259 | "ofType": { 260 | "kind": "OBJECT", 261 | "name": "UserEdge", 262 | "ofType": null 263 | } 264 | }, 265 | "isDeprecated": false, 266 | "deprecationReason": null 267 | } 268 | ], 269 | "inputFields": null, 270 | "interfaces": [], 271 | "enumValues": null, 272 | "possibleTypes": null 273 | }, 274 | { 275 | "kind": "OBJECT", 276 | "name": "PageInfo", 277 | "description": "Information about pagination in a connection.", 278 | "fields": [ 279 | { 280 | "name": "hasNextPage", 281 | "description": "When paginating forwards, are there more items?", 282 | "args": [], 283 | "type": { 284 | "kind": "NON_NULL", 285 | "name": null, 286 | "ofType": { 287 | "kind": "SCALAR", 288 | "name": "Boolean", 289 | "ofType": null 290 | } 291 | }, 292 | "isDeprecated": false, 293 | "deprecationReason": null 294 | }, 295 | { 296 | "name": "hasPreviousPage", 297 | "description": "When paginating backwards, are there more items?", 298 | "args": [], 299 | "type": { 300 | "kind": "NON_NULL", 301 | "name": null, 302 | "ofType": { 303 | "kind": "SCALAR", 304 | "name": "Boolean", 305 | "ofType": null 306 | } 307 | }, 308 | "isDeprecated": false, 309 | "deprecationReason": null 310 | }, 311 | { 312 | "name": "startCursor", 313 | "description": "When paginating backwards, the cursor to continue.", 314 | "args": [], 315 | "type": { 316 | "kind": "SCALAR", 317 | "name": "String", 318 | "ofType": null 319 | }, 320 | "isDeprecated": false, 321 | "deprecationReason": null 322 | }, 323 | { 324 | "name": "endCursor", 325 | "description": "When paginating forwards, the cursor to continue.", 326 | "args": [], 327 | "type": { 328 | "kind": "SCALAR", 329 | "name": "String", 330 | "ofType": null 331 | }, 332 | "isDeprecated": false, 333 | "deprecationReason": null 334 | } 335 | ], 336 | "inputFields": null, 337 | "interfaces": [], 338 | "enumValues": null, 339 | "possibleTypes": null 340 | }, 341 | { 342 | "kind": "SCALAR", 343 | "name": "Boolean", 344 | "description": "The `Boolean` scalar type represents `true` or `false`.", 345 | "fields": null, 346 | "inputFields": null, 347 | "interfaces": null, 348 | "enumValues": null, 349 | "possibleTypes": null 350 | }, 351 | { 352 | "kind": "OBJECT", 353 | "name": "UserEdge", 354 | "description": "An edge in a connection.", 355 | "fields": [ 356 | { 357 | "name": "node", 358 | "description": "The item at the end of the edge", 359 | "args": [], 360 | "type": { 361 | "kind": "OBJECT", 362 | "name": "User", 363 | "ofType": null 364 | }, 365 | "isDeprecated": false, 366 | "deprecationReason": null 367 | }, 368 | { 369 | "name": "cursor", 370 | "description": "A cursor for use in pagination", 371 | "args": [], 372 | "type": { 373 | "kind": "NON_NULL", 374 | "name": null, 375 | "ofType": { 376 | "kind": "SCALAR", 377 | "name": "String", 378 | "ofType": null 379 | } 380 | }, 381 | "isDeprecated": false, 382 | "deprecationReason": null 383 | } 384 | ], 385 | "inputFields": null, 386 | "interfaces": [], 387 | "enumValues": null, 388 | "possibleTypes": null 389 | }, 390 | { 391 | "kind": "SCALAR", 392 | "name": "Int", 393 | "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", 394 | "fields": null, 395 | "inputFields": null, 396 | "interfaces": null, 397 | "enumValues": null, 398 | "possibleTypes": null 399 | }, 400 | { 401 | "kind": "OBJECT", 402 | "name": "__Schema", 403 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", 404 | "fields": [ 405 | { 406 | "name": "description", 407 | "description": null, 408 | "args": [], 409 | "type": { 410 | "kind": "SCALAR", 411 | "name": "String", 412 | "ofType": null 413 | }, 414 | "isDeprecated": false, 415 | "deprecationReason": null 416 | }, 417 | { 418 | "name": "types", 419 | "description": "A list of all types supported by this server.", 420 | "args": [], 421 | "type": { 422 | "kind": "NON_NULL", 423 | "name": null, 424 | "ofType": { 425 | "kind": "LIST", 426 | "name": null, 427 | "ofType": { 428 | "kind": "NON_NULL", 429 | "name": null, 430 | "ofType": { 431 | "kind": "OBJECT", 432 | "name": "__Type", 433 | "ofType": null 434 | } 435 | } 436 | } 437 | }, 438 | "isDeprecated": false, 439 | "deprecationReason": null 440 | }, 441 | { 442 | "name": "queryType", 443 | "description": "The type that query operations will be rooted at.", 444 | "args": [], 445 | "type": { 446 | "kind": "NON_NULL", 447 | "name": null, 448 | "ofType": { 449 | "kind": "OBJECT", 450 | "name": "__Type", 451 | "ofType": null 452 | } 453 | }, 454 | "isDeprecated": false, 455 | "deprecationReason": null 456 | }, 457 | { 458 | "name": "mutationType", 459 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 460 | "args": [], 461 | "type": { 462 | "kind": "OBJECT", 463 | "name": "__Type", 464 | "ofType": null 465 | }, 466 | "isDeprecated": false, 467 | "deprecationReason": null 468 | }, 469 | { 470 | "name": "subscriptionType", 471 | "description": "If this server support subscription, the type that subscription operations will be rooted at.", 472 | "args": [], 473 | "type": { 474 | "kind": "OBJECT", 475 | "name": "__Type", 476 | "ofType": null 477 | }, 478 | "isDeprecated": false, 479 | "deprecationReason": null 480 | }, 481 | { 482 | "name": "directives", 483 | "description": "A list of all directives supported by this server.", 484 | "args": [], 485 | "type": { 486 | "kind": "NON_NULL", 487 | "name": null, 488 | "ofType": { 489 | "kind": "LIST", 490 | "name": null, 491 | "ofType": { 492 | "kind": "NON_NULL", 493 | "name": null, 494 | "ofType": { 495 | "kind": "OBJECT", 496 | "name": "__Directive", 497 | "ofType": null 498 | } 499 | } 500 | } 501 | }, 502 | "isDeprecated": false, 503 | "deprecationReason": null 504 | } 505 | ], 506 | "inputFields": null, 507 | "interfaces": [], 508 | "enumValues": null, 509 | "possibleTypes": null 510 | }, 511 | { 512 | "kind": "OBJECT", 513 | "name": "__Type", 514 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByUrl`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", 515 | "fields": [ 516 | { 517 | "name": "kind", 518 | "description": null, 519 | "args": [], 520 | "type": { 521 | "kind": "NON_NULL", 522 | "name": null, 523 | "ofType": { 524 | "kind": "ENUM", 525 | "name": "__TypeKind", 526 | "ofType": null 527 | } 528 | }, 529 | "isDeprecated": false, 530 | "deprecationReason": null 531 | }, 532 | { 533 | "name": "name", 534 | "description": null, 535 | "args": [], 536 | "type": { 537 | "kind": "SCALAR", 538 | "name": "String", 539 | "ofType": null 540 | }, 541 | "isDeprecated": false, 542 | "deprecationReason": null 543 | }, 544 | { 545 | "name": "description", 546 | "description": null, 547 | "args": [], 548 | "type": { 549 | "kind": "SCALAR", 550 | "name": "String", 551 | "ofType": null 552 | }, 553 | "isDeprecated": false, 554 | "deprecationReason": null 555 | }, 556 | { 557 | "name": "specifiedByUrl", 558 | "description": null, 559 | "args": [], 560 | "type": { 561 | "kind": "SCALAR", 562 | "name": "String", 563 | "ofType": null 564 | }, 565 | "isDeprecated": false, 566 | "deprecationReason": null 567 | }, 568 | { 569 | "name": "fields", 570 | "description": null, 571 | "args": [ 572 | { 573 | "name": "includeDeprecated", 574 | "description": null, 575 | "type": { 576 | "kind": "SCALAR", 577 | "name": "Boolean", 578 | "ofType": null 579 | }, 580 | "defaultValue": "false" 581 | } 582 | ], 583 | "type": { 584 | "kind": "LIST", 585 | "name": null, 586 | "ofType": { 587 | "kind": "NON_NULL", 588 | "name": null, 589 | "ofType": { 590 | "kind": "OBJECT", 591 | "name": "__Field", 592 | "ofType": null 593 | } 594 | } 595 | }, 596 | "isDeprecated": false, 597 | "deprecationReason": null 598 | }, 599 | { 600 | "name": "interfaces", 601 | "description": null, 602 | "args": [], 603 | "type": { 604 | "kind": "LIST", 605 | "name": null, 606 | "ofType": { 607 | "kind": "NON_NULL", 608 | "name": null, 609 | "ofType": { 610 | "kind": "OBJECT", 611 | "name": "__Type", 612 | "ofType": null 613 | } 614 | } 615 | }, 616 | "isDeprecated": false, 617 | "deprecationReason": null 618 | }, 619 | { 620 | "name": "possibleTypes", 621 | "description": null, 622 | "args": [], 623 | "type": { 624 | "kind": "LIST", 625 | "name": null, 626 | "ofType": { 627 | "kind": "NON_NULL", 628 | "name": null, 629 | "ofType": { 630 | "kind": "OBJECT", 631 | "name": "__Type", 632 | "ofType": null 633 | } 634 | } 635 | }, 636 | "isDeprecated": false, 637 | "deprecationReason": null 638 | }, 639 | { 640 | "name": "enumValues", 641 | "description": null, 642 | "args": [ 643 | { 644 | "name": "includeDeprecated", 645 | "description": null, 646 | "type": { 647 | "kind": "SCALAR", 648 | "name": "Boolean", 649 | "ofType": null 650 | }, 651 | "defaultValue": "false" 652 | } 653 | ], 654 | "type": { 655 | "kind": "LIST", 656 | "name": null, 657 | "ofType": { 658 | "kind": "NON_NULL", 659 | "name": null, 660 | "ofType": { 661 | "kind": "OBJECT", 662 | "name": "__EnumValue", 663 | "ofType": null 664 | } 665 | } 666 | }, 667 | "isDeprecated": false, 668 | "deprecationReason": null 669 | }, 670 | { 671 | "name": "inputFields", 672 | "description": null, 673 | "args": [], 674 | "type": { 675 | "kind": "LIST", 676 | "name": null, 677 | "ofType": { 678 | "kind": "NON_NULL", 679 | "name": null, 680 | "ofType": { 681 | "kind": "OBJECT", 682 | "name": "__InputValue", 683 | "ofType": null 684 | } 685 | } 686 | }, 687 | "isDeprecated": false, 688 | "deprecationReason": null 689 | }, 690 | { 691 | "name": "ofType", 692 | "description": null, 693 | "args": [], 694 | "type": { 695 | "kind": "OBJECT", 696 | "name": "__Type", 697 | "ofType": null 698 | }, 699 | "isDeprecated": false, 700 | "deprecationReason": null 701 | } 702 | ], 703 | "inputFields": null, 704 | "interfaces": [], 705 | "enumValues": null, 706 | "possibleTypes": null 707 | }, 708 | { 709 | "kind": "ENUM", 710 | "name": "__TypeKind", 711 | "description": "An enum describing what kind of type a given `__Type` is.", 712 | "fields": null, 713 | "inputFields": null, 714 | "interfaces": null, 715 | "enumValues": [ 716 | { 717 | "name": "SCALAR", 718 | "description": "Indicates this type is a scalar.", 719 | "isDeprecated": false, 720 | "deprecationReason": null 721 | }, 722 | { 723 | "name": "OBJECT", 724 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 725 | "isDeprecated": false, 726 | "deprecationReason": null 727 | }, 728 | { 729 | "name": "INTERFACE", 730 | "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", 731 | "isDeprecated": false, 732 | "deprecationReason": null 733 | }, 734 | { 735 | "name": "UNION", 736 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 737 | "isDeprecated": false, 738 | "deprecationReason": null 739 | }, 740 | { 741 | "name": "ENUM", 742 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 743 | "isDeprecated": false, 744 | "deprecationReason": null 745 | }, 746 | { 747 | "name": "INPUT_OBJECT", 748 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 749 | "isDeprecated": false, 750 | "deprecationReason": null 751 | }, 752 | { 753 | "name": "LIST", 754 | "description": "Indicates this type is a list. `ofType` is a valid field.", 755 | "isDeprecated": false, 756 | "deprecationReason": null 757 | }, 758 | { 759 | "name": "NON_NULL", 760 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 761 | "isDeprecated": false, 762 | "deprecationReason": null 763 | } 764 | ], 765 | "possibleTypes": null 766 | }, 767 | { 768 | "kind": "OBJECT", 769 | "name": "__Field", 770 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", 771 | "fields": [ 772 | { 773 | "name": "name", 774 | "description": null, 775 | "args": [], 776 | "type": { 777 | "kind": "NON_NULL", 778 | "name": null, 779 | "ofType": { 780 | "kind": "SCALAR", 781 | "name": "String", 782 | "ofType": null 783 | } 784 | }, 785 | "isDeprecated": false, 786 | "deprecationReason": null 787 | }, 788 | { 789 | "name": "description", 790 | "description": null, 791 | "args": [], 792 | "type": { 793 | "kind": "SCALAR", 794 | "name": "String", 795 | "ofType": null 796 | }, 797 | "isDeprecated": false, 798 | "deprecationReason": null 799 | }, 800 | { 801 | "name": "args", 802 | "description": null, 803 | "args": [], 804 | "type": { 805 | "kind": "NON_NULL", 806 | "name": null, 807 | "ofType": { 808 | "kind": "LIST", 809 | "name": null, 810 | "ofType": { 811 | "kind": "NON_NULL", 812 | "name": null, 813 | "ofType": { 814 | "kind": "OBJECT", 815 | "name": "__InputValue", 816 | "ofType": null 817 | } 818 | } 819 | } 820 | }, 821 | "isDeprecated": false, 822 | "deprecationReason": null 823 | }, 824 | { 825 | "name": "type", 826 | "description": null, 827 | "args": [], 828 | "type": { 829 | "kind": "NON_NULL", 830 | "name": null, 831 | "ofType": { 832 | "kind": "OBJECT", 833 | "name": "__Type", 834 | "ofType": null 835 | } 836 | }, 837 | "isDeprecated": false, 838 | "deprecationReason": null 839 | }, 840 | { 841 | "name": "isDeprecated", 842 | "description": null, 843 | "args": [], 844 | "type": { 845 | "kind": "NON_NULL", 846 | "name": null, 847 | "ofType": { 848 | "kind": "SCALAR", 849 | "name": "Boolean", 850 | "ofType": null 851 | } 852 | }, 853 | "isDeprecated": false, 854 | "deprecationReason": null 855 | }, 856 | { 857 | "name": "deprecationReason", 858 | "description": null, 859 | "args": [], 860 | "type": { 861 | "kind": "SCALAR", 862 | "name": "String", 863 | "ofType": null 864 | }, 865 | "isDeprecated": false, 866 | "deprecationReason": null 867 | } 868 | ], 869 | "inputFields": null, 870 | "interfaces": [], 871 | "enumValues": null, 872 | "possibleTypes": null 873 | }, 874 | { 875 | "kind": "OBJECT", 876 | "name": "__InputValue", 877 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", 878 | "fields": [ 879 | { 880 | "name": "name", 881 | "description": null, 882 | "args": [], 883 | "type": { 884 | "kind": "NON_NULL", 885 | "name": null, 886 | "ofType": { 887 | "kind": "SCALAR", 888 | "name": "String", 889 | "ofType": null 890 | } 891 | }, 892 | "isDeprecated": false, 893 | "deprecationReason": null 894 | }, 895 | { 896 | "name": "description", 897 | "description": null, 898 | "args": [], 899 | "type": { 900 | "kind": "SCALAR", 901 | "name": "String", 902 | "ofType": null 903 | }, 904 | "isDeprecated": false, 905 | "deprecationReason": null 906 | }, 907 | { 908 | "name": "type", 909 | "description": null, 910 | "args": [], 911 | "type": { 912 | "kind": "NON_NULL", 913 | "name": null, 914 | "ofType": { 915 | "kind": "OBJECT", 916 | "name": "__Type", 917 | "ofType": null 918 | } 919 | }, 920 | "isDeprecated": false, 921 | "deprecationReason": null 922 | }, 923 | { 924 | "name": "defaultValue", 925 | "description": "A GraphQL-formatted string representing the default value for this input value.", 926 | "args": [], 927 | "type": { 928 | "kind": "SCALAR", 929 | "name": "String", 930 | "ofType": null 931 | }, 932 | "isDeprecated": false, 933 | "deprecationReason": null 934 | } 935 | ], 936 | "inputFields": null, 937 | "interfaces": [], 938 | "enumValues": null, 939 | "possibleTypes": null 940 | }, 941 | { 942 | "kind": "OBJECT", 943 | "name": "__EnumValue", 944 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", 945 | "fields": [ 946 | { 947 | "name": "name", 948 | "description": null, 949 | "args": [], 950 | "type": { 951 | "kind": "NON_NULL", 952 | "name": null, 953 | "ofType": { 954 | "kind": "SCALAR", 955 | "name": "String", 956 | "ofType": null 957 | } 958 | }, 959 | "isDeprecated": false, 960 | "deprecationReason": null 961 | }, 962 | { 963 | "name": "description", 964 | "description": null, 965 | "args": [], 966 | "type": { 967 | "kind": "SCALAR", 968 | "name": "String", 969 | "ofType": null 970 | }, 971 | "isDeprecated": false, 972 | "deprecationReason": null 973 | }, 974 | { 975 | "name": "isDeprecated", 976 | "description": null, 977 | "args": [], 978 | "type": { 979 | "kind": "NON_NULL", 980 | "name": null, 981 | "ofType": { 982 | "kind": "SCALAR", 983 | "name": "Boolean", 984 | "ofType": null 985 | } 986 | }, 987 | "isDeprecated": false, 988 | "deprecationReason": null 989 | }, 990 | { 991 | "name": "deprecationReason", 992 | "description": null, 993 | "args": [], 994 | "type": { 995 | "kind": "SCALAR", 996 | "name": "String", 997 | "ofType": null 998 | }, 999 | "isDeprecated": false, 1000 | "deprecationReason": null 1001 | } 1002 | ], 1003 | "inputFields": null, 1004 | "interfaces": [], 1005 | "enumValues": null, 1006 | "possibleTypes": null 1007 | }, 1008 | { 1009 | "kind": "OBJECT", 1010 | "name": "__Directive", 1011 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", 1012 | "fields": [ 1013 | { 1014 | "name": "name", 1015 | "description": null, 1016 | "args": [], 1017 | "type": { 1018 | "kind": "NON_NULL", 1019 | "name": null, 1020 | "ofType": { 1021 | "kind": "SCALAR", 1022 | "name": "String", 1023 | "ofType": null 1024 | } 1025 | }, 1026 | "isDeprecated": false, 1027 | "deprecationReason": null 1028 | }, 1029 | { 1030 | "name": "description", 1031 | "description": null, 1032 | "args": [], 1033 | "type": { 1034 | "kind": "SCALAR", 1035 | "name": "String", 1036 | "ofType": null 1037 | }, 1038 | "isDeprecated": false, 1039 | "deprecationReason": null 1040 | }, 1041 | { 1042 | "name": "isRepeatable", 1043 | "description": null, 1044 | "args": [], 1045 | "type": { 1046 | "kind": "NON_NULL", 1047 | "name": null, 1048 | "ofType": { 1049 | "kind": "SCALAR", 1050 | "name": "Boolean", 1051 | "ofType": null 1052 | } 1053 | }, 1054 | "isDeprecated": false, 1055 | "deprecationReason": null 1056 | }, 1057 | { 1058 | "name": "locations", 1059 | "description": null, 1060 | "args": [], 1061 | "type": { 1062 | "kind": "NON_NULL", 1063 | "name": null, 1064 | "ofType": { 1065 | "kind": "LIST", 1066 | "name": null, 1067 | "ofType": { 1068 | "kind": "NON_NULL", 1069 | "name": null, 1070 | "ofType": { 1071 | "kind": "ENUM", 1072 | "name": "__DirectiveLocation", 1073 | "ofType": null 1074 | } 1075 | } 1076 | } 1077 | }, 1078 | "isDeprecated": false, 1079 | "deprecationReason": null 1080 | }, 1081 | { 1082 | "name": "args", 1083 | "description": null, 1084 | "args": [], 1085 | "type": { 1086 | "kind": "NON_NULL", 1087 | "name": null, 1088 | "ofType": { 1089 | "kind": "LIST", 1090 | "name": null, 1091 | "ofType": { 1092 | "kind": "NON_NULL", 1093 | "name": null, 1094 | "ofType": { 1095 | "kind": "OBJECT", 1096 | "name": "__InputValue", 1097 | "ofType": null 1098 | } 1099 | } 1100 | } 1101 | }, 1102 | "isDeprecated": false, 1103 | "deprecationReason": null 1104 | } 1105 | ], 1106 | "inputFields": null, 1107 | "interfaces": [], 1108 | "enumValues": null, 1109 | "possibleTypes": null 1110 | }, 1111 | { 1112 | "kind": "ENUM", 1113 | "name": "__DirectiveLocation", 1114 | "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", 1115 | "fields": null, 1116 | "inputFields": null, 1117 | "interfaces": null, 1118 | "enumValues": [ 1119 | { 1120 | "name": "QUERY", 1121 | "description": "Location adjacent to a query operation.", 1122 | "isDeprecated": false, 1123 | "deprecationReason": null 1124 | }, 1125 | { 1126 | "name": "MUTATION", 1127 | "description": "Location adjacent to a mutation operation.", 1128 | "isDeprecated": false, 1129 | "deprecationReason": null 1130 | }, 1131 | { 1132 | "name": "SUBSCRIPTION", 1133 | "description": "Location adjacent to a subscription operation.", 1134 | "isDeprecated": false, 1135 | "deprecationReason": null 1136 | }, 1137 | { 1138 | "name": "FIELD", 1139 | "description": "Location adjacent to a field.", 1140 | "isDeprecated": false, 1141 | "deprecationReason": null 1142 | }, 1143 | { 1144 | "name": "FRAGMENT_DEFINITION", 1145 | "description": "Location adjacent to a fragment definition.", 1146 | "isDeprecated": false, 1147 | "deprecationReason": null 1148 | }, 1149 | { 1150 | "name": "FRAGMENT_SPREAD", 1151 | "description": "Location adjacent to a fragment spread.", 1152 | "isDeprecated": false, 1153 | "deprecationReason": null 1154 | }, 1155 | { 1156 | "name": "INLINE_FRAGMENT", 1157 | "description": "Location adjacent to an inline fragment.", 1158 | "isDeprecated": false, 1159 | "deprecationReason": null 1160 | }, 1161 | { 1162 | "name": "VARIABLE_DEFINITION", 1163 | "description": "Location adjacent to a variable definition.", 1164 | "isDeprecated": false, 1165 | "deprecationReason": null 1166 | }, 1167 | { 1168 | "name": "SCHEMA", 1169 | "description": "Location adjacent to a schema definition.", 1170 | "isDeprecated": false, 1171 | "deprecationReason": null 1172 | }, 1173 | { 1174 | "name": "SCALAR", 1175 | "description": "Location adjacent to a scalar definition.", 1176 | "isDeprecated": false, 1177 | "deprecationReason": null 1178 | }, 1179 | { 1180 | "name": "OBJECT", 1181 | "description": "Location adjacent to an object type definition.", 1182 | "isDeprecated": false, 1183 | "deprecationReason": null 1184 | }, 1185 | { 1186 | "name": "FIELD_DEFINITION", 1187 | "description": "Location adjacent to a field definition.", 1188 | "isDeprecated": false, 1189 | "deprecationReason": null 1190 | }, 1191 | { 1192 | "name": "ARGUMENT_DEFINITION", 1193 | "description": "Location adjacent to an argument definition.", 1194 | "isDeprecated": false, 1195 | "deprecationReason": null 1196 | }, 1197 | { 1198 | "name": "INTERFACE", 1199 | "description": "Location adjacent to an interface definition.", 1200 | "isDeprecated": false, 1201 | "deprecationReason": null 1202 | }, 1203 | { 1204 | "name": "UNION", 1205 | "description": "Location adjacent to a union definition.", 1206 | "isDeprecated": false, 1207 | "deprecationReason": null 1208 | }, 1209 | { 1210 | "name": "ENUM", 1211 | "description": "Location adjacent to an enum definition.", 1212 | "isDeprecated": false, 1213 | "deprecationReason": null 1214 | }, 1215 | { 1216 | "name": "ENUM_VALUE", 1217 | "description": "Location adjacent to an enum value definition.", 1218 | "isDeprecated": false, 1219 | "deprecationReason": null 1220 | }, 1221 | { 1222 | "name": "INPUT_OBJECT", 1223 | "description": "Location adjacent to an input object type definition.", 1224 | "isDeprecated": false, 1225 | "deprecationReason": null 1226 | }, 1227 | { 1228 | "name": "INPUT_FIELD_DEFINITION", 1229 | "description": "Location adjacent to an input object field definition.", 1230 | "isDeprecated": false, 1231 | "deprecationReason": null 1232 | } 1233 | ], 1234 | "possibleTypes": null 1235 | } 1236 | ], 1237 | "directives": [ 1238 | { 1239 | "name": "include", 1240 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 1241 | "locations": [ 1242 | "FIELD", 1243 | "FRAGMENT_SPREAD", 1244 | "INLINE_FRAGMENT" 1245 | ], 1246 | "args": [ 1247 | { 1248 | "name": "if", 1249 | "description": "Included when true.", 1250 | "type": { 1251 | "kind": "NON_NULL", 1252 | "name": null, 1253 | "ofType": { 1254 | "kind": "SCALAR", 1255 | "name": "Boolean", 1256 | "ofType": null 1257 | } 1258 | }, 1259 | "defaultValue": null 1260 | } 1261 | ] 1262 | }, 1263 | { 1264 | "name": "skip", 1265 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 1266 | "locations": [ 1267 | "FIELD", 1268 | "FRAGMENT_SPREAD", 1269 | "INLINE_FRAGMENT" 1270 | ], 1271 | "args": [ 1272 | { 1273 | "name": "if", 1274 | "description": "Skipped when true.", 1275 | "type": { 1276 | "kind": "NON_NULL", 1277 | "name": null, 1278 | "ofType": { 1279 | "kind": "SCALAR", 1280 | "name": "Boolean", 1281 | "ofType": null 1282 | } 1283 | }, 1284 | "defaultValue": null 1285 | } 1286 | ] 1287 | }, 1288 | { 1289 | "name": "deprecated", 1290 | "description": "Marks an element of a GraphQL schema as no longer supported.", 1291 | "locations": [ 1292 | "FIELD_DEFINITION", 1293 | "ENUM_VALUE" 1294 | ], 1295 | "args": [ 1296 | { 1297 | "name": "reason", 1298 | "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", 1299 | "type": { 1300 | "kind": "SCALAR", 1301 | "name": "String", 1302 | "ofType": null 1303 | }, 1304 | "defaultValue": "\"No longer supported\"" 1305 | } 1306 | ] 1307 | }, 1308 | { 1309 | "name": "specifiedBy", 1310 | "description": "Exposes a URL that specifies the behaviour of this scalar.", 1311 | "locations": [ 1312 | "SCALAR" 1313 | ], 1314 | "args": [ 1315 | { 1316 | "name": "url", 1317 | "description": "The URL that specifies the behaviour of this scalar.", 1318 | "type": { 1319 | "kind": "NON_NULL", 1320 | "name": null, 1321 | "ofType": { 1322 | "kind": "SCALAR", 1323 | "name": "String", 1324 | "ofType": null 1325 | } 1326 | }, 1327 | "defaultValue": null 1328 | } 1329 | ] 1330 | } 1331 | ] 1332 | } 1333 | } 1334 | } -------------------------------------------------------------------------------- /scripts/generateSchema.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { graphql } from 'graphql'; 3 | import { getIntrospectionQuery, printSchema } from 'graphql/utilities'; 4 | import path from 'path'; 5 | import schema from '../src/graphql/schema'; 6 | 7 | // Save JSON of full schema introspection for Babel Relay Plugin to use 8 | (async () => { 9 | const result = await graphql(schema, getIntrospectionQuery()); 10 | if (result.errors) { 11 | console.error( 12 | 'ERROR introspecting schema: ', 13 | JSON.stringify(result.errors, null, 2) 14 | ); //eslint-disable-line no-console 15 | } else { 16 | fs.writeFileSync( 17 | path.join(__dirname, '../schema/schema.json'), 18 | JSON.stringify(result, null, 2) 19 | ); 20 | 21 | process.exit(0); 22 | } 23 | })(); 24 | 25 | // Save user readable type system shorthand of schema 26 | fs.writeFileSync( 27 | path.join(__dirname, '../schema/schema.graphql'), 28 | printSchema(schema) 29 | ); 30 | -------------------------------------------------------------------------------- /src/__generated__/pagesQuery.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ConcreteRequest } from "relay-runtime"; 6 | export type pagesQueryVariables = {}; 7 | export type pagesQueryResponse = { 8 | readonly users: { 9 | readonly edges: ReadonlyArray<{ 10 | readonly node: { 11 | readonly email: string; 12 | readonly id: string; 13 | } | null; 14 | } | null> | null; 15 | } | null; 16 | }; 17 | export type pagesQuery = { 18 | readonly response: pagesQueryResponse; 19 | readonly variables: pagesQueryVariables; 20 | }; 21 | 22 | 23 | 24 | /* 25 | query pagesQuery { 26 | users { 27 | edges { 28 | node { 29 | email 30 | id 31 | } 32 | } 33 | } 34 | } 35 | */ 36 | 37 | const node: ConcreteRequest = (function(){ 38 | var v0 = [ 39 | { 40 | "alias": null, 41 | "args": null, 42 | "concreteType": "UserConnection", 43 | "kind": "LinkedField", 44 | "name": "users", 45 | "plural": false, 46 | "selections": [ 47 | { 48 | "alias": null, 49 | "args": null, 50 | "concreteType": "UserEdge", 51 | "kind": "LinkedField", 52 | "name": "edges", 53 | "plural": true, 54 | "selections": [ 55 | { 56 | "alias": null, 57 | "args": null, 58 | "concreteType": "User", 59 | "kind": "LinkedField", 60 | "name": "node", 61 | "plural": false, 62 | "selections": [ 63 | { 64 | "alias": null, 65 | "args": null, 66 | "kind": "ScalarField", 67 | "name": "email", 68 | "storageKey": null 69 | }, 70 | { 71 | "alias": null, 72 | "args": null, 73 | "kind": "ScalarField", 74 | "name": "id", 75 | "storageKey": null 76 | } 77 | ], 78 | "storageKey": null 79 | } 80 | ], 81 | "storageKey": null 82 | } 83 | ], 84 | "storageKey": null 85 | } 86 | ]; 87 | return { 88 | "fragment": { 89 | "argumentDefinitions": [], 90 | "kind": "Fragment", 91 | "metadata": null, 92 | "name": "pagesQuery", 93 | "selections": (v0/*: any*/), 94 | "type": "Query", 95 | "abstractKey": null 96 | }, 97 | "kind": "Request", 98 | "operation": { 99 | "argumentDefinitions": [], 100 | "kind": "Operation", 101 | "name": "pagesQuery", 102 | "selections": (v0/*: any*/) 103 | }, 104 | "params": { 105 | "cacheID": "1f42b3a1e8d2f4c1c22bfe972b0b7557", 106 | "id": null, 107 | "metadata": {}, 108 | "name": "pagesQuery", 109 | "operationKind": "query", 110 | "text": "query pagesQuery {\n users {\n edges {\n node {\n email\n id\n }\n }\n }\n}\n" 111 | } 112 | }; 113 | })(); 114 | (node as any).hash = 'f90e888d328a7fa97e16c2e685867f5c'; 115 | export default node; 116 | -------------------------------------------------------------------------------- /src/graphql/common/types.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLDataloaders } from '../loaders/dataloaders'; 2 | import { User } from '../modules/user/UserModel'; 3 | 4 | export interface GraphQLContext { 5 | dataloaders: GraphQLDataloaders; 6 | user?: User; 7 | } 8 | -------------------------------------------------------------------------------- /src/graphql/interface/NodeInterface.ts: -------------------------------------------------------------------------------- 1 | import { fromGlobalId, nodeDefinitions } from 'graphql-relay'; 2 | import { GraphQLContext } from '../common/types'; 3 | import * as UserLoader from '../modules/user/UserLoader'; 4 | import UserType from '../modules/user/UserType'; 5 | 6 | const { nodeField, nodesField, nodeInterface } = nodeDefinitions( 7 | async (globalId, context: GraphQLContext) => { 8 | const { id, type } = fromGlobalId(globalId); 9 | if (type === 'User') return UserLoader.load(context, id); 10 | return null; 11 | }, 12 | (obj) => { 13 | if (obj instanceof UserLoader.default) return UserType; 14 | return null; 15 | } 16 | ); 17 | 18 | export const NodeInterface = nodeInterface; 19 | export const NodeField = nodeField; 20 | export const NodesField = nodesField; 21 | -------------------------------------------------------------------------------- /src/graphql/loaders/dataloaders.ts: -------------------------------------------------------------------------------- 1 | import DataLoader from 'dataloader'; 2 | 3 | import * as UserLoader from '../modules/user/UserLoader'; 4 | export type DataLoaderKey = string | object; 5 | 6 | export interface GraphQLDataloaders { 7 | UserLoader: DataLoader; 8 | } 9 | 10 | export { UserLoader }; 11 | -------------------------------------------------------------------------------- /src/graphql/loaders/middleware.ts: -------------------------------------------------------------------------------- 1 | import { IMiddleware } from 'graphql-middleware'; 2 | import { UserLoader } from './dataloaders'; 3 | 4 | export const getLoaders = () => ({ 5 | UserLoader: UserLoader.getLoader(), 6 | }); 7 | 8 | export const dataloadersMiddleware: IMiddleware = async ( 9 | resolve, 10 | root, 11 | args, 12 | ctx, 13 | info 14 | ) => { 15 | const dataloaders = getLoaders(); 16 | return resolve(root, args, { ...ctx, dataloaders }, info); 17 | }; 18 | -------------------------------------------------------------------------------- /src/graphql/modules/user/UserLoader.ts: -------------------------------------------------------------------------------- 1 | import DataLoader from 'dataloader'; 2 | import mock from './mock'; 3 | import { GraphQLContext } from '../../common/types'; 4 | import { DataLoaderKey } from '../../loaders/dataloaders'; 5 | import { User } from './UserModel'; 6 | import { ConnectionArguments, connectionFromArray } from 'graphql-relay'; 7 | 8 | export type { User } from './UserModel'; 9 | 10 | export default class UserLoader implements User { 11 | id: string; 12 | email: string; 13 | picture: string; 14 | name: string; 15 | 16 | constructor(data: User) { 17 | this.id = data.id; 18 | this.email = data.email; 19 | this.picture = data.picture; 20 | this.name = data.name; 21 | } 22 | } 23 | 24 | export const getLoader = () => 25 | new DataLoader(async (ids) => mock.filter(({ id }) => ids.includes(id))); 26 | 27 | const viewerCanSee = () => true; 28 | 29 | export const load = async (context: GraphQLContext, id: DataLoaderKey) => { 30 | if (!id) return null; 31 | try { 32 | const data = await context.dataloaders.UserLoader.load(id); 33 | return viewerCanSee() ? new UserLoader(data) : null; 34 | } catch (err) { 35 | return null; 36 | } 37 | }; 38 | 39 | export const clearCache = ( 40 | { dataloaders }: GraphQLContext, 41 | id: DataLoaderKey 42 | ) => { 43 | return dataloaders.UserLoader.clear(id.toString()); 44 | }; 45 | 46 | type LoadUsersArgs = ConnectionArguments; 47 | 48 | export const loadUsers = async (_: object, args: LoadUsersArgs) => { 49 | return connectionFromArray(mock, args); 50 | }; 51 | -------------------------------------------------------------------------------- /src/graphql/modules/user/UserModel.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string; 3 | name: string; 4 | email: string; 5 | picture: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/graphql/modules/user/UserType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import { globalIdField, connectionDefinitions } from 'graphql-relay'; 3 | import { NodeInterface } from '../../interface/NodeInterface'; 4 | import { User } from './UserModel'; 5 | import { GraphQLContext } from '../../common/types'; 6 | 7 | const UserType: GraphQLObjectType = new GraphQLObjectType( 8 | { 9 | name: 'User', 10 | description: 'User data', 11 | fields: () => ({ 12 | id: globalIdField('User'), 13 | name: { 14 | type: GraphQLNonNull(GraphQLString), 15 | }, 16 | email: { 17 | type: GraphQLNonNull(GraphQLString), 18 | }, 19 | }), 20 | interfaces: () => [NodeInterface], 21 | } 22 | ); 23 | 24 | export const UserConnection = connectionDefinitions({ 25 | name: 'User', 26 | nodeType: UserType, 27 | }); 28 | 29 | export default UserType; 30 | -------------------------------------------------------------------------------- /src/graphql/modules/user/mock.ts: -------------------------------------------------------------------------------- 1 | const SIZE = 10; 2 | 3 | export default new Array(SIZE).fill(null).map((_, index) => ({ 4 | id: index + 1, 5 | name: `User ${index + 1}`, 6 | email: `user_${index + 1}@domain.com`, 7 | picture: 'https://via.placeholder.com/150', 8 | })); 9 | -------------------------------------------------------------------------------- /src/graphql/schema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | import QueryType from './type/QueryType'; 3 | 4 | const schema = new GraphQLSchema({ 5 | query: QueryType, 6 | }); 7 | 8 | export default schema; 9 | -------------------------------------------------------------------------------- /src/graphql/type/QueryType.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | import { connectionArgs } from 'graphql-relay'; 3 | import { NodeField } from '../interface/NodeInterface'; 4 | 5 | import * as UserLoader from '../modules/user/UserLoader'; 6 | import UserType, { UserConnection } from '../modules/user/UserType'; 7 | 8 | export default new GraphQLObjectType({ 9 | name: 'Query', 10 | description: 'The root of all... queries', 11 | fields: () => ({ 12 | node: NodeField, 13 | me: { 14 | type: UserType, 15 | resolve: (_, __, context) => { 16 | return context && context.user; 17 | }, 18 | }, 19 | users: { 20 | type: UserConnection.connectionType, 21 | args: connectionArgs, 22 | resolve: UserLoader.loadUsers, 23 | }, 24 | }), 25 | }); 26 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppProps } from 'next/app'; 3 | import { RelayEnvironmentProvider } from 'react-relay/hooks'; 4 | import environment from '../relay/environment'; 5 | 6 | const MyApp = ({ Component, pageProps }: AppProps) => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /src/pages/api/graphql.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server-micro'; 2 | import { applyMiddleware } from 'graphql-middleware'; 3 | import schema from '../../graphql/schema'; 4 | import { dataloadersMiddleware } from '../../graphql/loaders/middleware'; 5 | 6 | const server = new ApolloServer({ 7 | schema: applyMiddleware(schema, dataloadersMiddleware), 8 | playground: { 9 | endpoint: '/api/graphql', 10 | }, 11 | introspection: true, 12 | }); 13 | 14 | export const config = { 15 | api: { 16 | bodyParser: false, 17 | }, 18 | }; 19 | 20 | export default server.createHandler({ path: '/api/graphql' }); 21 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-relay'; 3 | import { 4 | pagesQuery, 5 | pagesQueryResponse, 6 | } from '../__generated__/pagesQuery.graphql'; 7 | import withQuery, { getRelayStaticProps } from '../relay/withQuery'; 8 | 9 | const query = graphql` 10 | query pagesQuery { 11 | users { 12 | edges { 13 | node { 14 | email 15 | id 16 | } 17 | } 18 | } 19 | } 20 | `; 21 | 22 | type Props = { 23 | query: pagesQueryResponse; 24 | }; 25 | 26 | const Home: React.FC = ({ query }) => { 27 | return ( 28 | <> 29 |

Nodes:

30 |
31 | {query.users?.edges?.map((edge) => ( 32 |

{edge?.node?.email}

33 | ))} 34 | 35 | ); 36 | }; 37 | 38 | export const getStaticProps = getRelayStaticProps(query); 39 | 40 | export default withQuery(Home, query); 41 | -------------------------------------------------------------------------------- /src/relay/environment.ts: -------------------------------------------------------------------------------- 1 | import { Store, RecordSource, Environment } from 'relay-runtime'; 2 | import { RecordMap } from 'relay-runtime/lib/store/RelayStoreTypes'; 3 | import { RelayNetworkLayer, urlMiddleware } from 'react-relay-network-modern'; 4 | 5 | const network = new RelayNetworkLayer([ 6 | urlMiddleware({ 7 | url: process.env.NEXT_GRAPHQL_URL as string, 8 | }), 9 | ]); 10 | 11 | const initEnvironment = (records?: RecordMap) => { 12 | return new Environment({ 13 | network, 14 | store: new Store(new RecordSource(records)), 15 | }); 16 | }; 17 | 18 | export default initEnvironment; 19 | -------------------------------------------------------------------------------- /src/relay/withQuery.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { OperationType } from 'relay-runtime'; 3 | import { RelayEnvironmentProvider } from 'react-relay/hooks'; 4 | import { NextPage } from 'next'; 5 | import environment from './environment'; 6 | import { RecordMap } from 'relay-runtime/lib/store/RelayStoreTypes'; 7 | import { GraphQLTaggedNode } from 'relay-runtime'; 8 | import { fetchQuery } from 'react-relay'; 9 | import { CacheConfig } from 'relay-runtime/lib/util/RelayRuntimeTypes'; 10 | import { useQuery } from 'relay-hooks/lib'; 11 | 12 | type Props

= { records: RecordMap } & P; 13 | 14 | type BaseComponentProps = { 15 | query: OperationType['response']; 16 | }; 17 | 18 | function withQuery

( 19 | Component: React.ComponentType

, 20 | query: GraphQLTaggedNode, 21 | variables?: Q['variables'] 22 | ): React.FC> { 23 | const WithRelayEnvironment: React.FC

= () => { 24 | const { props: queryResponse, error } = useQuery(query, variables, { 25 | fetchPolicy: 'store-only', 26 | }); 27 | if (error) return

An Error Occurred

; 28 | if (!queryResponse) return

Loading

; 29 | // @ts-ignore 30 | return ; 31 | }; 32 | 33 | const WithQuery: NextPage> = ({ records, ...props }) => { 34 | return ( 35 | 36 | 37 | 38 | ); 39 | }; 40 | return WithQuery; 41 | } 42 | 43 | export function getRelayStaticProps( 44 | query: GraphQLTaggedNode, 45 | variables: T['variables'] = {}, 46 | cacheConfig?: CacheConfig | null 47 | ) { 48 | return async () => { 49 | const env = environment(); 50 | await fetchQuery(env, query, variables, cacheConfig); 51 | 52 | return { 53 | props: { 54 | records: env.getStore().getSource().toJSON(), 55 | }, 56 | }; 57 | }; 58 | } 59 | export default withQuery; 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": ["dom", "es2017"], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noEmit": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "target": "esnext" 20 | }, 21 | "exclude": ["node_modules"], 22 | "include": ["**/*.ts", "**/*.tsx"] 23 | } 24 | --------------------------------------------------------------------------------