mirror of
https://github.com/vale981/apollo-server
synced 2025-03-06 02:01:40 -05:00
Merge pull request #1967 from apollographql/platform-cleanup
Move relevant best practices guides into server docs
This commit is contained in:
commit
50a559ebf1
17 changed files with 622 additions and 1365 deletions
|
@ -25,7 +25,8 @@ sidebar_categories:
|
||||||
- features/unions-interfaces
|
- features/unions-interfaces
|
||||||
- features/directives
|
- features/directives
|
||||||
- features/creating-directives
|
- features/creating-directives
|
||||||
- features/test-utils
|
- features/authentication
|
||||||
|
- features/testing
|
||||||
# Schema stitching:
|
# Schema stitching:
|
||||||
# - features/schema-stitching
|
# - features/schema-stitching
|
||||||
# - features/remote-schemas
|
# - features/remote-schemas
|
||||||
|
@ -36,15 +37,7 @@ sidebar_categories:
|
||||||
- deployment/heroku
|
- deployment/heroku
|
||||||
- deployment/lambda
|
- deployment/lambda
|
||||||
- deployment/now
|
- deployment/now
|
||||||
Related Guides:
|
|
||||||
- title: Monitoring
|
|
||||||
href: https://www.apollographql.com/docs/guides/monitoring.html
|
|
||||||
- title: Versioning
|
|
||||||
href: https://www.apollographql.com/docs/guides/versioning.html
|
|
||||||
- title: Access Control
|
|
||||||
href: https://www.apollographql.com/docs/guides/access-control.html
|
|
||||||
- title: Security
|
|
||||||
href: https://www.apollographql.com/docs/guides/security.html
|
|
||||||
API Reference:
|
API Reference:
|
||||||
- api/apollo-server
|
- api/apollo-server
|
||||||
- api/graphql-tools
|
- api/graphql-tools
|
||||||
|
@ -63,13 +56,5 @@ root: /docs/apollo-server/
|
||||||
|
|
||||||
public_dir: public/docs/apollo-server
|
public_dir: public/docs/apollo-server
|
||||||
|
|
||||||
redirects:
|
|
||||||
/docs/apollo-server/v2/features/cdn.html:
|
|
||||||
docs/guides/performance
|
|
||||||
/docs/apollo-server/v2/features/apq.html:
|
|
||||||
docs/guides/performance
|
|
||||||
/docs/apollo-server/v2/features/file-uploads.html:
|
|
||||||
docs/guides/file-uploads
|
|
||||||
|
|
||||||
versioned-netlify-redirects:
|
versioned-netlify-redirects:
|
||||||
netlify_site_id: apollo-server-docs
|
netlify_site_id: apollo-server-docs
|
||||||
|
|
107
docs/package-lock.json
generated
107
docs/package-lock.json
generated
|
@ -520,7 +520,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
|
||||||
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
|
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"hoek": "2.x.x"
|
"hoek": "2.x.x"
|
||||||
}
|
}
|
||||||
|
@ -887,7 +886,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
|
||||||
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
|
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -1158,8 +1156,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"depd": {
|
"depd": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
|
@ -1457,8 +1454,7 @@
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
|
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"filename-regex": {
|
"filename-regex": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
|
@ -1604,8 +1600,7 @@
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
@ -1626,14 +1621,12 @@
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
@ -1648,20 +1641,17 @@
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
@ -1778,8 +1768,7 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
@ -1791,7 +1780,6 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -1806,7 +1794,6 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
|
@ -1814,14 +1801,12 @@
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
@ -1840,7 +1825,6 @@
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
@ -1921,8 +1905,7 @@
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
@ -1934,7 +1917,6 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
@ -2020,8 +2002,7 @@
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
@ -2057,7 +2038,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
|
@ -2077,7 +2057,6 @@
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
@ -2121,14 +2100,12 @@
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2993,9 +2970,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hexo-versioned-netlify-redirects": {
|
"hexo-versioned-netlify-redirects": {
|
||||||
"version": "1.0.7",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/hexo-versioned-netlify-redirects/-/hexo-versioned-netlify-redirects-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/hexo-versioned-netlify-redirects/-/hexo-versioned-netlify-redirects-1.1.0.tgz",
|
||||||
"integrity": "sha512-9N0FCU7O5ZpmYx5MC0DJ5VM7HPucL0kIzBtLTk7SfH7MkFmUEbxI8yZc22g3WEev9vmUOPaTDF3XXOXO/OKw9g==",
|
"integrity": "sha512-kfY19ZZDwBnRaZFA6KQgt5DhVBpTQG1yBoq01zaACFHIFuSd9OjhiHCkJ3it+TMvGl+JGTtgKhIwZoQUJHyv/g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"url-join": "^4.0.0"
|
"url-join": "^4.0.0"
|
||||||
}
|
}
|
||||||
|
@ -3010,8 +2987,7 @@
|
||||||
"version": "2.16.3",
|
"version": "2.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
|
||||||
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
|
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"hosted-git-info": {
|
"hosted-git-info": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
|
@ -3317,8 +3293,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
|
||||||
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
|
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"is-wsl": {
|
"is-wsl": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
@ -4180,22 +4155,19 @@
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
|
||||||
"integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
|
"integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"array-unique": {
|
"array-unique": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
|
||||||
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
|
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
||||||
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
|
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -4307,15 +4279,13 @@
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
|
||||||
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
|
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"braces": {
|
"braces": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
|
||||||
"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
|
"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"arr-flatten": "^1.1.0",
|
"arr-flatten": "^1.1.0",
|
||||||
"array-unique": "^0.3.2",
|
"array-unique": "^0.3.2",
|
||||||
|
@ -4334,7 +4304,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-extendable": "^0.1.0"
|
"is-extendable": "^0.1.0"
|
||||||
}
|
}
|
||||||
|
@ -4508,7 +4477,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
|
||||||
"integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
|
"integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"extend-shallow": "^2.0.1",
|
"extend-shallow": "^2.0.1",
|
||||||
"is-number": "^3.0.0",
|
"is-number": "^3.0.0",
|
||||||
|
@ -4521,7 +4489,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-extendable": "^0.1.0"
|
"is-extendable": "^0.1.0"
|
||||||
}
|
}
|
||||||
|
@ -4587,8 +4554,7 @@
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"is-glob": {
|
"is-glob": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
|
@ -4605,7 +4571,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
||||||
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"kind-of": "^3.0.2"
|
"kind-of": "^3.0.2"
|
||||||
},
|
},
|
||||||
|
@ -4615,7 +4580,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||||
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
|
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-buffer": "^1.1.5"
|
"is-buffer": "^1.1.5"
|
||||||
}
|
}
|
||||||
|
@ -4626,15 +4590,13 @@
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
||||||
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
|
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"micromatch": {
|
"micromatch": {
|
||||||
"version": "3.1.10",
|
"version": "3.1.10",
|
||||||
|
@ -4747,8 +4709,7 @@
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -4772,7 +4733,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
@ -5469,7 +5429,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
|
||||||
"integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
|
"integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-descriptor": "^1.0.0"
|
"is-descriptor": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -5479,7 +5438,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
|
||||||
"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
|
"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"kind-of": "^6.0.0"
|
"kind-of": "^6.0.0"
|
||||||
}
|
}
|
||||||
|
@ -5489,7 +5447,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
|
||||||
"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
|
"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"kind-of": "^6.0.0"
|
"kind-of": "^6.0.0"
|
||||||
}
|
}
|
||||||
|
@ -5499,7 +5456,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
|
||||||
"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
|
"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-accessor-descriptor": "^1.0.0",
|
"is-accessor-descriptor": "^1.0.0",
|
||||||
"is-data-descriptor": "^1.0.0",
|
"is-data-descriptor": "^1.0.0",
|
||||||
|
@ -5510,15 +5466,13 @@
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
||||||
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
|
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5527,7 +5481,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
|
||||||
"integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
|
"integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"kind-of": "^3.2.0"
|
"kind-of": "^3.2.0"
|
||||||
}
|
}
|
||||||
|
@ -5990,7 +5943,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
||||||
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"kind-of": "^3.0.2"
|
"kind-of": "^3.0.2"
|
||||||
}
|
}
|
||||||
|
@ -6342,8 +6294,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"hexo": {
|
"hexo": {
|
||||||
"version": "3.7.1"
|
"version": "3.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"apollo-hexo-config": "1.0.8",
|
"apollo-hexo-config": "1.0.8",
|
||||||
|
@ -24,6 +24,6 @@
|
||||||
"test": "npm run clean; npm run build"
|
"test": "npm run clean; npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hexo-versioned-netlify-redirects": "1.0.7"
|
"hexo-versioned-netlify-redirects": "1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
docs/source/_redirects
Normal file
3
docs/source/_redirects
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# The test-utils lived at its current home for about 5 days.
|
||||||
|
# It at least exists on some slides and Tweets.
|
||||||
|
/docs/apollo-server/features/test-utils.html /docs/apollo-server/features/testing.html
|
|
@ -1,248 +0,0 @@
|
||||||
---
|
|
||||||
title: Auth
|
|
||||||
description: Securing our app and serving our users
|
|
||||||
---
|
|
||||||
|
|
||||||
<h2 id="auth-background">Background: Authentication vs. Authorization</h2>
|
|
||||||
|
|
||||||
**Authentication** describes a process where an application proves the identity of a user, meaning someone claiming to be a certain user through the client is the actual user that has permission to make a request to the server. In most systems, a user and server share a handshake and token that uniquely pairs them together, ensuring both sides know they are communicating with their intended target.
|
|
||||||
|
|
||||||
**Authorization** defines what a user, such as admin or user, is allowed to do. Generally a server will authenticate users and provide them an authorization role that permits the user to perform a subset of all possible operations, such as read and not write.
|
|
||||||
|
|
||||||
<h2>Auth in GraphQL</h2>
|
|
||||||
|
|
||||||
GraphQL offers similar authentication and authorization mechanics as REST and other data fetching solutions with the possibility to control more fine grain access within a single request. There are two common approaches: schema authorization and operation authorization.
|
|
||||||
|
|
||||||
**Schema authorization** follows a similar guidance to REST, where the entire request and response is checked for an authenticated user and authorized to access the servers data.
|
|
||||||
|
|
||||||
**Operation authorization** takes advantage of the flexibility of GraphQL to provide public portions of the schema that don't require any authorization and private portions that require authentication and authorization.
|
|
||||||
|
|
||||||
> Authorization within our GraphQL resolvers is a great first line of defense for securing our application. We recommended having similar authorization patterns within our data fetching models to ensure a user is authorized at every level of data fetching and updating.
|
|
||||||
|
|
||||||
<h2>Authenticating users</h2>
|
|
||||||
|
|
||||||
All of the approaches require that users be authenticated with the server. If our system already has login method setup to authenticate users and provide credentials that can be used in subsequent requests, we can use this same system to authenticate GraphQL requests. With that said, if we are creating a new infrastructure for user authentication, we can follow the existing best practice to authenticate users. For a full example of authentication, follow [this example](#auth-example), which uses [passport.js](http://www.passportjs.org/).
|
|
||||||
|
|
||||||
<h2>Schema Authorization</h2>
|
|
||||||
|
|
||||||
Schema authorization is useful for GraphQL endpoints that require known users and allow access to all fields inside of a GraphQL endpoint. This approach is useful for internal applications, which are used by a group that is known and generally trusted. Additionally it's common to have separate GraphQL services for different features or products that are entirely available to users, meaning if a user is authenticated, they are authorized to access all the data. Since schema authorization does not need to be aware of the GraphQL layer, our server can add a middleware in front of the GraphQL layer to ensure authorization.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// authenticate for schema usage
|
|
||||||
const context = ({ req }) => {
|
|
||||||
const user = myAuthenticationLookupCode(req);
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("You need to be authenticated to access this schema!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { user }
|
|
||||||
};
|
|
||||||
|
|
||||||
const server = new ApolloServer({ typeDefs, resolvers, context });
|
|
||||||
|
|
||||||
server.listen().then(({ url }) => {
|
|
||||||
console.log(`🚀 Server ready at ${url}`)
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Currently this server will allow any authenticated user to request all fields in the schema, which means that authorization is all or nothing. While some applications provide a shared view of the data to all users, many use cases require scoping authorizations and limiting what some users can see. The authorization scope is shared across all resolvers, so this code adds the user id and scope to the context.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const { ForbiddenError } = require("apollo-server");
|
|
||||||
|
|
||||||
const context = ({ req }) => {
|
|
||||||
const user = myAuthenticationLookupCode(req);
|
|
||||||
if (!user) {
|
|
||||||
throw new ForbiddenError(
|
|
||||||
"You need to be authenticated to access this schema!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const scope = lookupScopeForUser(user);
|
|
||||||
|
|
||||||
return { user, scope };
|
|
||||||
};
|
|
||||||
|
|
||||||
const server = new ApolloServer({
|
|
||||||
typeDefs,
|
|
||||||
resolvers,
|
|
||||||
context
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen().then(({ url }) => {
|
|
||||||
console.log(`🚀 Server ready at ${url}`);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Now within a resolver, we are able to check the user's scope. If the user is not an administrator and `allTodos` are requested, a GraphQL specific forbidden error is thrown. Apollo Server will handle associate the error with the particular path and return it along with any other data successfully requested, such as `myTodos`, to the client.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const { ForbiddenError } = require("apollo-server");
|
|
||||||
|
|
||||||
const resolvers = {
|
|
||||||
Query: {
|
|
||||||
allTodos: (source, args, context) => {
|
|
||||||
if (context.scope !== "ADMIN") {
|
|
||||||
throw ForbiddenError("Need Administrator Privileges");
|
|
||||||
}
|
|
||||||
return context.Todos.getAll();
|
|
||||||
},
|
|
||||||
myTodos: (source, args, context) => {
|
|
||||||
return context.Todos.getById(context.user_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
The major downside to schema authorization is that all requests must be authenticated, which prevents unauthenticated requests to access information that should be publicly accessible, such as a home page. The next approach, partial query authorization, enables a portion of the schema to be public and authorize portions of the schema to authenticated users.
|
|
||||||
|
|
||||||
## Operation Authorization
|
|
||||||
|
|
||||||
Operation authorization removes the catch all portion of our context function that throws an unauthenticated error, moving the authorization check within resolvers. The instantiation of the server becomes:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const context = ({ req }) => {
|
|
||||||
const user = myAuthenticationLookupCode(req);
|
|
||||||
if (!user) {
|
|
||||||
return { user: null, scope: null }
|
|
||||||
}
|
|
||||||
|
|
||||||
const scope = lookupScopeForUser(user);
|
|
||||||
return { user, scope }
|
|
||||||
};
|
|
||||||
|
|
||||||
const server = new ApolloServer({
|
|
||||||
typeDefs,
|
|
||||||
resolvers,
|
|
||||||
context
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen().then(({ url }) => {
|
|
||||||
console.log(`🚀 Serverready at ${url}`)
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
The benefit of doing operation authorization is that private and public data is more easily managed an enforced. Take for example a schema that allows finding `allTodos` in the app (an administrative action), seeing any `publicTodos` which requires no authorization, and returning just a single users todos via `myTodos`. Using Apollo Server, we can easily build complex authorization models like so:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const { ForbiddenError, AuthenticationError } = require("apollo-server");
|
|
||||||
|
|
||||||
const resolvers = {
|
|
||||||
Query: {
|
|
||||||
allTodos: (source, args, context) => {
|
|
||||||
if (!context.scope) {
|
|
||||||
throw AuthenticationError("You must be logged in to see all todos");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.scope !== "ADMIN") {
|
|
||||||
throw ForbiddenError("You must be an administrator to see all todos");
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.Todos.getAllTodos();
|
|
||||||
},
|
|
||||||
publicTodos: (source, args, context) => {
|
|
||||||
return context.Todos.getPublicTodos();
|
|
||||||
},
|
|
||||||
myTodos: (source, args, context) => {
|
|
||||||
if (!context.scope) {
|
|
||||||
throw AuthenticationError("You must be logged in to see all todos");
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.Todos.getByUserId(context.user.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Should I send a password in a mutation?
|
|
||||||
|
|
||||||
Since GraphQL queries are sent to a server in the same manner as REST requests, the same policies apply to sending sensitive data over the wire. The current best practice is to provide an encrypted connection over https or wss if we are using websockets. Provided we setup this layer, passwords and other sensitive information should be secure.
|
|
||||||
|
|
||||||
## Auth Example
|
|
||||||
|
|
||||||
If you are new setting up new infrastructure or would like to understand an example of how to adapt your existing login system, you can follow this example using passport.js. We will use this example of authentication in the subsequent sections. To skip this section, jump down to the
|
|
||||||
|
|
||||||
```shell
|
|
||||||
npm install --save express passport body-parser express-session node-uuid passport-local apollo-server graphql
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
const bodyParser = require('body-parser');
|
|
||||||
const express = require('express');
|
|
||||||
const passport = require('passport');
|
|
||||||
const session = require('express-session');
|
|
||||||
const uuid = require('node-uuid');
|
|
||||||
```
|
|
||||||
|
|
||||||
After installing and importing the necessary packages, this code checks the user's password and attaches their id to the request.
|
|
||||||
|
|
||||||
```js
|
|
||||||
let LocalStrategy = require('passport-local').Strategy;
|
|
||||||
const { DB } = require('./schema/db.js');
|
|
||||||
|
|
||||||
passport.use(
|
|
||||||
'local',
|
|
||||||
new LocalStrategy(function(username, password, done) {
|
|
||||||
let checkPassword = DB.Users.checkPassword(username, password);
|
|
||||||
let getUser = checkPassword
|
|
||||||
.then(is_login_valid => {
|
|
||||||
if (is_login_valid) return DB.Users.getUserByUsername(username);
|
|
||||||
else throw new Error('invalid username or password');
|
|
||||||
})
|
|
||||||
.then(user => done(null, user))
|
|
||||||
.catch(err => done(err));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
passport.serializeUser((user, done) => done(null, user.id));
|
|
||||||
|
|
||||||
passport.deserializeUser((id, done) =>
|
|
||||||
DB.Users.get(id).then((user, err) => done(err, user))
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Now that passport has been setup, we initialize the server application to use the passport middleware, attaching the user id to the request.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
//passport's session piggy-backs on express-session
|
|
||||||
app.use(
|
|
||||||
session({
|
|
||||||
genid: function(req) {
|
|
||||||
return uuid.v4();
|
|
||||||
},
|
|
||||||
secret: 'Z3]GJW!?9uP"/Kpe',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
//Provide authentication and user information to all routes
|
|
||||||
app.use(passport.initialize());
|
|
||||||
app.use(passport.session());
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally we provide the login route and start Apollo Server.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const { typeDefs, resolvers } = require('./schema');
|
|
||||||
|
|
||||||
//login route for passport
|
|
||||||
app.use('/login', bodyParser.urlencoded({ extended: true }));
|
|
||||||
app.post(
|
|
||||||
'/login',
|
|
||||||
passport.authenticate('local', {
|
|
||||||
successRedirect: '/',
|
|
||||||
failureRedirect: '/login',
|
|
||||||
failureFlash: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
//Depending on the authorization model chosen, you may include some extra middleware here before you instantiate the server
|
|
||||||
|
|
||||||
//Create and start your apollo server
|
|
||||||
const server = new ApolloServer({ typeDefs, resolvers, app });
|
|
||||||
|
|
||||||
server.listen().then(({ url }) => {
|
|
||||||
console.log(`🚀 Server ready at ${url}`)
|
|
||||||
});
|
|
||||||
```
|
|
|
@ -1,23 +0,0 @@
|
||||||
---
|
|
||||||
title: Caching
|
|
||||||
description: Caching operations
|
|
||||||
---
|
|
||||||
|
|
||||||
One of the best ways we can speed up our application is to implement caching into it. Apollo Client has a intelligent cache which greatly lowers the work the client needs to do to fetch and manage data, but what about our server? Caching in Apollo Server can be done in a number of ways, but we recommend three in particular that have a good balance between complexity to manage and benefit of use.
|
|
||||||
|
|
||||||
<h2 id="whole-query">Whole query caching</h2>
|
|
||||||
|
|
||||||
GraphQL operations on a client are best when they are statically defined and used in an application. When this is the case, often times there will be operations that could easily be cached as a full result of the the request. We call this *whole query caching* and it is incredibly easy to implement with Apollo Server. Unlike custom REST endpoints, using Apollo Server allows us to define the cacheability of our resources and dynamically calculate the best possible cache timing for any given operation.
|
|
||||||
|
|
||||||
- For more information about setting up Apollo Engine with Apollo Server, [read this guide]()
|
|
||||||
- For more information about setting up whole query caching with Apollo Engine, [read this guide](https://www.apollographql.com/docs/engine/caching.html)
|
|
||||||
|
|
||||||
<h2 id="cdn-caching">CDN integration</h2>
|
|
||||||
|
|
||||||
If our application has a lot of public data that doesn’t change very frequently, and it’s important for it to load quickly, we will probably benefit from using a CDN to cache our API results. This can be particularly important for media or content companies like news sites and blogs.
|
|
||||||
|
|
||||||
A CDN will store our API result close to the “edge” of the network — that is, close to the region the user is in — and deliver a cached result much faster than it would have required to do a full round-trip to our actual server. As an added benefit, we get to save on server load since that query doesn’t actually hit our API.
|
|
||||||
|
|
||||||
- Setting up CDN caching with Apollo Server is incredibly easy, simply setup Apollo Engine then follow this [guide](https://www.apollographql.com/docs/engine/cdn.html)
|
|
||||||
- For more information about using a CDN with Apollo Engine, check out this [article](https://blog.apollographql.com/caching-graphql-results-in-your-cdn-54299832b8e2)
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
title: Monitoring
|
|
||||||
---
|
|
||||||
|
|
||||||
Intro about what to watch for?
|
|
||||||
|
|
||||||
## ENGINE
|
|
||||||
|
|
||||||
## formatError
|
|
|
@ -1,261 +0,0 @@
|
||||||
---
|
|
||||||
title: Organizing your code
|
|
||||||
description: Scaling your Apollo Server from a single file to your entire team
|
|
||||||
---
|
|
||||||
|
|
||||||
The GraphQL schema defines the api for Apollo Server, providing the single source of truth between client and server. A complete schema contains type definitions and resolvers. Type definitions are written and documented in the [Schema Definition Language(SDL)]() to define the valid server entry points. Corresponding to one to one with type definition fields, resolvers are functions that retrieve the data described by the type definitions.
|
|
||||||
|
|
||||||
To accommodate this tight coupling, type definitions and resolvers should be kept together in the same file. This collocation allows developers to modify fields and resolvers with atomic schema changes without unexpected consequences. At the end to build a complete schema, the type definitions are combined in an array and resolvers are merged together. Throughout all the examples, the resolvers delegate to a data model, as explained in [this section]().
|
|
||||||
|
|
||||||
> Note: This schema separation should be done by product or real-world domain, which create natural boundaries that are easier to reason about.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
* essentials/schema for connection between:
|
|
||||||
* GraphQL Types
|
|
||||||
* Resolvers
|
|
||||||
|
|
||||||
<h2 id="organizing-types">Organizing schema types</h2>
|
|
||||||
|
|
||||||
With large schemas, defining types in different files and merging them to create the complete schema may become necessary. We accomplish this by importing and exporting schema strings, combining them into arrays as necessary. The following example demonstrates separating the type definitions of [this schema](#first-example-schema) found at the end of the page.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// comment.js
|
|
||||||
const typeDefs = gql`
|
|
||||||
type Comment {
|
|
||||||
id: ID!
|
|
||||||
message: String
|
|
||||||
author: String
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export typeDefs;
|
|
||||||
```
|
|
||||||
|
|
||||||
The `Post` includes a reference to `Comment`, which is added to the array of type definitions and exported:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// post.js
|
|
||||||
const typeDefs = gql`
|
|
||||||
type Post {
|
|
||||||
id: ID!
|
|
||||||
title: String
|
|
||||||
content: String
|
|
||||||
author: String
|
|
||||||
comments: [Comment]
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Export Post and all dependent types
|
|
||||||
export typeDefs;
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally the root Query type, which uses Post, is created and passed to the server instantiation:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// schema.js
|
|
||||||
const Comment = require('./comment');
|
|
||||||
const Post = require('./post');
|
|
||||||
|
|
||||||
const RootQuery = gql`
|
|
||||||
type Query {
|
|
||||||
post(id: ID!): Post
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const server = new ApolloServer({
|
|
||||||
typeDefs: [RootQuery, Post.typeDefs, Comment.typeDefs],
|
|
||||||
resolvers, //defined in next section
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen().then(({ url }) => {
|
|
||||||
console.log(`🚀 Server ready at ${url}`)
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
<h2 id="organizing-resolvers">Organizing resolvers</h2>
|
|
||||||
|
|
||||||
For the type definitions above, we can accomplish the same modularity with resolvers by combining each type's resolvers together with Lodash's `merge` or another equivalent. The [end of this page](#first-example-resolvers) contains a complete view of the resolver map.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// comment.js
|
|
||||||
const CommentModel = require('./models/comment');
|
|
||||||
|
|
||||||
const resolvers = {
|
|
||||||
Comment: {
|
|
||||||
votes: (parent) => CommentModel.getVotesById(parent.id)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export resolvers;
|
|
||||||
```
|
|
||||||
|
|
||||||
The `Post` type:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// post.js
|
|
||||||
const PostModel = require('./models/post');
|
|
||||||
|
|
||||||
const resolvers = {
|
|
||||||
Post: {
|
|
||||||
comments: (parent) => PostModel.getCommentsById(parent.id)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export resolvers;
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally, the Query type's resolvers are merged and the result is passed to the server instantiation:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// schema.js
|
|
||||||
const { merge } = require('lodash');
|
|
||||||
const Post = require('./post');
|
|
||||||
const Comment = require('./comment');
|
|
||||||
|
|
||||||
const PostModel = require('./models/post');
|
|
||||||
|
|
||||||
// Merge all of the resolver objects together
|
|
||||||
const resolvers = merge({
|
|
||||||
Query: {
|
|
||||||
post: (_, args) => PostModel.getPostById(args.id)
|
|
||||||
}
|
|
||||||
}, Post.resolvers, Comment.resolvers);
|
|
||||||
|
|
||||||
const server = new ApolloServer({
|
|
||||||
typeDefs, //defined in previous section
|
|
||||||
resolvers,
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen().then(({ url }) => {
|
|
||||||
console.log(`🚀 Server ready at ${url}`)
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
<h2 id="extend-types">Extending types</h2>
|
|
||||||
|
|
||||||
The `extend` keyword provides the ability to add fields to existing types. Using `extend` is particularly useful in avoiding a large list of fields on root Queries and Mutations.
|
|
||||||
|
|
||||||
```js
|
|
||||||
//schema.js
|
|
||||||
const bookTypeDefs = gql`
|
|
||||||
extend type Query {
|
|
||||||
books: [Book]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Book {
|
|
||||||
id: ID!
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// These type definitions are often in a separate file
|
|
||||||
const authorTypeDefs = gql`
|
|
||||||
extend type Query {
|
|
||||||
authors: [Author]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Author {
|
|
||||||
id: ID!
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
export const typeDefs = [bookTypeDefs, authorTypeDefs]
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
const {typeDefs, resolvers} = require('./schema');
|
|
||||||
|
|
||||||
const rootQuery = gql`
|
|
||||||
"Query can and must be defined once per schema to be extended"
|
|
||||||
type Query {
|
|
||||||
_empty: String
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const server = new ApolloServer({
|
|
||||||
typeDefs: [RootQuery].concat(typeDefs),
|
|
||||||
resolvers,
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen().then(({ url }) => {
|
|
||||||
console.log(`🚀 Server ready at ${url}`)
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
> Note: In the current version of GraphQL, you can’t have an empty type even if you intend to extend it later. So we need to make sure the Query type has at least one field — in this case we can add a fake `_empty` field. Hopefully in future versions it will be possible to have an empty type to be extended later.
|
|
||||||
|
|
||||||
<h2 id="descriptions">Documenting a Schema</h2>
|
|
||||||
|
|
||||||
In addition to modularization, documentation within the SDL enables the schema to be effective as the single source of truth between client and server. GraphQL GUIs have built-in support for displaying docstrings with markdown syntax, such as those found in the following schema.
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
"""
|
|
||||||
Description for the type
|
|
||||||
"""
|
|
||||||
type MyObjectType {
|
|
||||||
"""
|
|
||||||
Description for field
|
|
||||||
Supports multi-line description
|
|
||||||
"""
|
|
||||||
myField: String!
|
|
||||||
|
|
||||||
otherField(
|
|
||||||
"""
|
|
||||||
Description for argument
|
|
||||||
"""
|
|
||||||
arg: Int
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<h2 id="api">API</h2>
|
|
||||||
|
|
||||||
Apollo Server pass `typeDefs` and `resolvers` to the `graphql-tools`'s `makeExecutableSchema`.
|
|
||||||
|
|
||||||
TODO point at graphql-tools `makeExecutableSchema` api
|
|
||||||
|
|
||||||
<h2 id="example-app">Example Application Details</h2>
|
|
||||||
|
|
||||||
<h3 id="example-schema">Schema</h3>
|
|
||||||
|
|
||||||
The full type definitions for the first example:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
type Comment {
|
|
||||||
id: ID!
|
|
||||||
message: String
|
|
||||||
author: String
|
|
||||||
votes: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Post {
|
|
||||||
id: ID!
|
|
||||||
title: String
|
|
||||||
content: String
|
|
||||||
author: String
|
|
||||||
comments: [Comment]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
post(id: ID!): Post
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<h3 id="example-resolvers">Resolvers</h3>
|
|
||||||
|
|
||||||
The full resolver map for the first example:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const CommentModel = require('./models/comment');
|
|
||||||
const PostModel = require('./models/post');
|
|
||||||
|
|
||||||
const resolvers = {
|
|
||||||
Comment: {
|
|
||||||
votes: (parent) => CommentModel.getVotesById(parent.id)
|
|
||||||
}
|
|
||||||
Post: {
|
|
||||||
comments: (parent) => PostModel.getCommentsById(parent.id)
|
|
||||||
}
|
|
||||||
Query: {
|
|
||||||
post: (_, args) => PostModel.getPostById(args.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,210 +0,0 @@
|
||||||
---
|
|
||||||
title: Performance
|
|
||||||
description: Reduce requests and speeding up applications
|
|
||||||
---
|
|
||||||
|
|
||||||
GraphQL offers performance benefits for most applications. By reducing round-trips when fetching data, lower the amount of data we are sending back, and make it easier to batch data lookups. Since GraphQL is often built as a stateless request-response pattern, scaling our app horizontally becomes much easier. In this section, we will dive into some benefits that Apollo Server brings to our app, and some patterns for speeding up our service.
|
|
||||||
|
|
||||||
## Prevent over-fetching
|
|
||||||
|
|
||||||
Rest endpoints often return all of the fields for whatever data they are returning. As applications grow, their data needs grow as well, which leads to a lot of unnecessary data being downloaded by our client applications. With GraphQL this isn't a problem because Apollo Server will only return the data that we ask for when making a request! Take for example a screen which shows an avatar of the currently logged in user. In a rest app we may make a request to `/api/v1/currentUser` which would return a response like this:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"firstName": "James",
|
|
||||||
"lastName": "Baxley",
|
|
||||||
"suffix": "III",
|
|
||||||
"avatar": "/photos/profile.jpg",
|
|
||||||
"friendIds": [2, 3, 4, 5, 6, 7],
|
|
||||||
"homeId": 1,
|
|
||||||
"occupation": "farmer",
|
|
||||||
// and so on for every field on this model that our client **could** use
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Contrast that to the request a client would send to Apollo Server and the response they would receive:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
query GetAvatar {
|
|
||||||
currentUser {
|
|
||||||
avatar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"currentUser": {
|
|
||||||
"avatar": "/photos/profile.jpg"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
No matter how much our data grows, this query will always only return the smallest bit of data that the client application actually needs! This will make our app faster and our end users data plan much happier!
|
|
||||||
|
|
||||||
## Reducing round-trips
|
|
||||||
|
|
||||||
Applications typically need to fetch multiple resources to load any given screen for a user. When building an app on top of a REST API, screens need to fetch the first round of data, then using that information, make another request to load related information. A common example of this would be to load a user, then load their friends:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const userAndFriends = fetch("/api/v1/user/currentUser").then(user => {
|
|
||||||
const friendRequest = Promise.all(
|
|
||||||
user.friendIds.map(id => fetch(`/api/vi/user/${id}`))
|
|
||||||
);
|
|
||||||
|
|
||||||
return friendRequest.then(friends => {
|
|
||||||
user.friends = friends;
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The above code would make at minimum two requests, one for the logged in user and one for a single friend. With more friends, the number of requests jumps up quite a lot! To get around this, custom endpoints are added into a RESTful API. In this example, a `/api/v1/friends/:userId` may be added to make fetching friends a single request per user instead of one per friend.
|
|
||||||
|
|
||||||
With GraphQL this is easily done in a single request! Given a schema like this:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
type User {
|
|
||||||
id: ID!
|
|
||||||
name: String!
|
|
||||||
friends: [User]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
currentUser: User
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
We can easily fetch the current user and all of their friends in a single request!
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
query LoadUserAndFriends {
|
|
||||||
currentUser {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
friends {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Batching data lookups
|
|
||||||
|
|
||||||
If we take the above query we may think GraphQL simply moves the waterfall of requests from the client to the server. Even if this was true, application speeds would still be improved. However, Apollo Server makes it possible to make applications even faster by batching data requests.
|
|
||||||
|
|
||||||
The most common way to batch requests is by using Facebook's [`dataloader`](https://github.com/facebook/dataloader) library. Let's explore a few options for request batching the previous operation:
|
|
||||||
|
|
||||||
<h3 id="custom-resolvers">Custom resolvers for batching</h3>
|
|
||||||
|
|
||||||
The simplest (and often easiest) way to speed up a GraphQL service is to create resolvers that optimistically fetch the needed data. Often times the best thing to do is to write the simplest resolver possible to look up data, profile it with a tool like Apollo Engine, then improve slow resolvers with logic tuned for the way our schema is used. Take the above query, for example:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const User = {
|
|
||||||
friends: (user, args, context) => {
|
|
||||||
// A simple approach to find each friend.
|
|
||||||
return user.friendIds.map(id => context.UserModel.findById(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The above resolver will make a database lookup for the initial user and then one lookup for every friend that our user has. This would quickly turn into an expensive resolver to call so lets look at how we could speed it up! First, lets take a simple, but proven technique:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const User = {
|
|
||||||
friends: (user, args, context) => {
|
|
||||||
// a custom model method for looking up multiple users
|
|
||||||
return context.UserModel.findByIds(user.friendIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Instead of fetching each user independently, we could fetch all users at once in a single lookup. This would be analogous to `SELECT * FROM users WHERE id IN (1,2,3,4)` vs the previous query would have been multiple versions of `SELECT * FROM users WHERE id = 1`.
|
|
||||||
|
|
||||||
Often times, custom resolvers are enough to speed up our server to the levels we want. However, there may be times where we want to be even more efficient when batching data. Lets say we expanded our operation to include more information:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
query LoadUserAndFriends {
|
|
||||||
currentUser {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
friends {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
family {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Assuming that `family` returns more `User` types, we now are making at minimum three database calls: 1) the user, 2) the batch of friends, and 3) the batch of family members. If we expand the query deeper:
|
|
||||||
|
|
||||||
```
|
|
||||||
query LoadUserAndFriends {
|
|
||||||
currentUser {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
friends {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
...peopleTheyCareAbout
|
|
||||||
}
|
|
||||||
family {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
...peopleTheyCareAbout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment peopleTheyCareAbout on User {
|
|
||||||
family {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
friends {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
We are now looking at any number of database calls! The more friends and families that are connected in our app, the more expensive this query gets. Using a library like `dataloader`, we can reduce this operation to a maximum of three database lookups. Let's take a look at how to implement it to understand what is happening:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const DataLoader = require('dataloader');
|
|
||||||
|
|
||||||
// give this to ApolloServer's context
|
|
||||||
const UserModelLoader = new DataLoader(UserModel.findByIds);
|
|
||||||
|
|
||||||
// in the User resolvers
|
|
||||||
const User = {
|
|
||||||
friends: (user, args, context) => {
|
|
||||||
return context.UserModelLoader.loadMany(user.friendIds);
|
|
||||||
},
|
|
||||||
family: (user, args, context) => {
|
|
||||||
return context.UserModelLoader.loadMany(user.familyIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After the first data request returns with our current user's information, we execute the resolvers for `friends` and `family` within the same "tick" of the event loop, which is technical talk for "pretty much at the same time". DataLoader will delay making a data request (in this case the `UserModel.findByIds` call) long enough for it to capture the request to look up both friends and families at once! It will combine the two arrays of ids into one so our `SELECT * FROM users WHERE id IN ...` request will contain the ids of both friends **and** families!
|
|
||||||
|
|
||||||
The friends and families request will return at the same time so when we select friends and families for all of previously returned users, the same batching can occur across all of the new users requests! So instead of potentially hundreds of data lookups, we can only perform 3 for a query like this!
|
|
||||||
|
|
||||||
|
|
||||||
## Scaling our app
|
|
||||||
|
|
||||||
Horizontal scaling is a fantastic way to increase the amount of load that our servers can handle without having to purchase more expensive computing resources to handling it. Apollo Server can scale extremely well like this as long as a couple of concerns are handled:
|
|
||||||
|
|
||||||
- Every request should ensure it has access to the required data source. If we are building on top of a HTTP endpoint this isn't a problem, but when using a database it is a good practice to verify our connection on each request. This helps to make our app more fault tolerant and easily scale up a new service which will connect as soon as requests start!
|
|
||||||
- Any state should be saved into a shared stateful datastore like redis. By sharing state, we can easily add more and more servers into our infrastructure without fear of losing any kind of state between scale up and scale down.
|
|
|
@ -1,371 +0,0 @@
|
||||||
---
|
|
||||||
title: Schema Design
|
|
||||||
description: The best way to fetch data, update it, and keep things running for a long time
|
|
||||||
---
|
|
||||||
|
|
||||||
GraphQL schemas are at their best when they are designed around the need of client applications, instead of the shape of how the data is stored. Often times teams will create schemas that are literal mappings on top of their collections or tables with CRUD like root fields. While this may be a fast way to get up and running, a strong long term GraphQL schema is built around the products usage.
|
|
||||||
|
|
||||||
## Style conventions
|
|
||||||
|
|
||||||
The GraphQL specification is flexible in the style that it dictates and doesn't impose specific naming guidelines. In order to facilitate development and continuity across GraphQL deployments, we suggest the following style conventions :
|
|
||||||
|
|
||||||
- **Fields**: are recommended to be written in `camelCase`, since the majority of consumers will be client applications written in JavaScript.
|
|
||||||
- **Types**: should be `PascalCase`.
|
|
||||||
- **Enums**: should have their name in `PascalCase` and their values in `ALL_CAPS` to denote their special meaning.
|
|
||||||
|
|
||||||
## Using interfaces
|
|
||||||
|
|
||||||
Interfaces are a powerful way to build and use GraphQL schemas through the use of _abstract types_. Abstract types can't be used directly in schema, but can be used as building blocks for creating explicit types.
|
|
||||||
|
|
||||||
Consider an example where different types of books share a common set of attributes, such as _text books_ and _coloring books_. A simple foundation for these books might be represented as the following `interface`:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
interface Book {
|
|
||||||
title: String
|
|
||||||
author: Author
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
We won't be able to directly use this interface to query for a book, but we can use it to implement concrete types. Imagine a screen within an application which needs to display a feed of all books, without regard to their (more specific) type. To create such functionality, we could define the following:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
type TextBook implements Book {
|
|
||||||
title: String
|
|
||||||
author: Author
|
|
||||||
classes: [Class]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ColoringBook implements Book {
|
|
||||||
title: String
|
|
||||||
author: Author
|
|
||||||
colors: [Color]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
schoolBooks: [Book]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, we've used the `Book` interface as the foundation for the `TextBook` and `ColoringBook` types. Then, a `schoolBooks` field simply expresses that it returns a list of books (i.e. `[Book]`).
|
|
||||||
|
|
||||||
Implementing the book feed example is now simplified since we've removed the need to worry about what kind of `Book`s will be returned. A query against this schema, which could return _text books_ and _coloring_ books, might look like:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
query GetBooks {
|
|
||||||
schoolBooks {
|
|
||||||
title
|
|
||||||
author
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is really helpful for feeds of common content, user role systems, and more!
|
|
||||||
|
|
||||||
Furthermore, if we need to return fields which are only provided by either `TextBook`s or `ColoringBook`s (not both) we can request fragments from the abstract types in the query. Those fragments will be filled in only as appropriate; in the case of the example, only coloring books will be returned with `colors`, and only text books will have `classes`:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
query GetBooks {
|
|
||||||
schoolBooks {
|
|
||||||
title
|
|
||||||
... on TextBook {
|
|
||||||
classes {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
... on ColoringBook {
|
|
||||||
colors {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To see an interface in practice, check out this [example]()
|
|
||||||
|
|
||||||
## A `Node` interface
|
|
||||||
|
|
||||||
A so-called "`Node` interface" is an implementation of a generic interface, on which other types can be built on, which enables the ability to fetch other _types_ in a schema by only providing an `id`. This interface isn't provided automatically by GraphQL (not does it _have_ to be called `Node`), but we highly recommend schemas consider implementing one.
|
|
||||||
|
|
||||||
To understand its value, we'll present an example with two collections: _authors_ and _posts_, though the usefulness of such an interface grows as more collections are introduced. As is common with most database collections, each of these collections have unique `id` columns which uniquely represent the individual documents within the collection.
|
|
||||||
|
|
||||||
To implement a so-called "`Node` interface", we'll add a `Node` interface to the schema, as follows:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
interface Node {
|
|
||||||
id: ID!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This `interface` declaration has the only field it will ever need: an `ID!` field, which is required to be non-null in all operations (as indicated by the `!`).
|
|
||||||
|
|
||||||
To take advantage of this new interface, we can use as the underlying implementation for the other types that our schema will define. For our example, this means we'll use it to build `Post` and `Author` object types:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
type Post implements Node {
|
|
||||||
id: ID!
|
|
||||||
title: String!
|
|
||||||
author: Author!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Author implements Node {
|
|
||||||
id: ID!
|
|
||||||
name: String!
|
|
||||||
posts: [Post]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
By implementing the `Node` interface as the foundation for `Post` and `Author`, we know that anytime a client has obtained an `id` (from either type), we can send it back to the server and retrieve that exact piece of data back!
|
|
||||||
|
|
||||||
<h3 id="global-ids">Global Ids</h3>
|
|
||||||
|
|
||||||
When using the `Node` interface, we will want to create schema unique `id` fields. The most common way to do this is to take the `id` from the datasource and join it with the type name where it is being exposed (i.e `Post:1`, `Author:1`). In doing so, even though the database `id` is the same for the first Post and first Author, the client can refetch each successfully!
|
|
||||||
|
|
||||||
Global Ids are often encoded into a base64 string after joined together. This is for consistency but also to denote that the client shouldn't try to parse and use the information as the shape of the `id` may change over time with schema revisions, but the uniqueness of it will not.
|
|
||||||
|
|
||||||
<h3 id"using-node">Using the node interface</h3>
|
|
||||||
|
|
||||||
Now that we have the `Node` interface, we need a way to globally refetch any id that the client can send. To do this, we add a field called `node` to our `Query` which returns a `Node` abstract type:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
type Query {
|
|
||||||
node(id: ID!): Node
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Now our client can refetch any type they want to as long as they have an `id` value for it:
|
|
||||||
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
query GetAuthor($authorId: ID!) {
|
|
||||||
node(id: $authorId) {
|
|
||||||
id
|
|
||||||
... on Author {
|
|
||||||
name
|
|
||||||
posts {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Using the `Node` interface can remove a ton of unnecessary fields on the `Query` type, as well as solve common patterns like data fetching for routing. Say we had a route showing content our user has liked: `/favorites` and then we wanted to drill down into those likes: `/favorites/:id` to show more information. Instead of creating a route for each kind of liked content (i.e `/favories/authors/:id`, `/favorites/posts/:id`), we can use the `node` field to request any type of liked content:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
query GetLikedContent($id: ID!){
|
|
||||||
favorite: node(id: $id){
|
|
||||||
id
|
|
||||||
... on Author {
|
|
||||||
pageTitle: name
|
|
||||||
}
|
|
||||||
... on Post {
|
|
||||||
pageTitle: title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Thanks to the `Node` interface and field aliasing, my response data is easily used by my UI no matter what my likes are:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ id: "Author:1", pageTitle: "Sashko" },
|
|
||||||
{ id: "Post:1", pageTitle: "GraphQL is great!" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
To see this in practice, check out the following [example]()
|
|
||||||
|
|
||||||
## Mutation responses
|
|
||||||
|
|
||||||
Mutations are an incredibly powerful part of GraphQL as they can easily return both information about the data updating transaction, as well as the actual data that has changed very easily. One pattern that we recommend to make this consistent is to have a `MutationResponse` interface that can be easily implemented for any `Mutation` fields. The `MutationResponse` is designed to allow transactional information alongside returning valuable data to make client side updates automatic! The interface looks like this:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
interface MutationResponse {
|
|
||||||
code: String!
|
|
||||||
success: Boolean!
|
|
||||||
message: String!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
An implementing type would look like this:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
type AddPostMutationResponse {
|
|
||||||
code: String!
|
|
||||||
success: Boolean!
|
|
||||||
message: String!
|
|
||||||
post: Post
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Lets break this down by field:
|
|
||||||
|
|
||||||
- **code** is a string representing a transactional value explaning details about the status of the data change. Think of this like HTTP status codes.
|
|
||||||
- **success** is a boolean telling the client if the update was successful. It is a coarse check that makes it easy for the client application to respond to failures
|
|
||||||
- **message** is a string that is meant to be a human readable description of the status of the transaction. It is intended to be used in the UI of the product
|
|
||||||
- **post** is added by the implementing type `AddPostMutationResponse` to return back the newly created post for the client to use!
|
|
||||||
|
|
||||||
Following this pattern for mutations provides detailed information about the data that has changed and how the operation to change it went! Client developers can easily react to failures and fetch the information they need to update their local cache.
|
|
||||||
|
|
||||||
<h2 id="organization">Organizing your schema</h2>
|
|
||||||
|
|
||||||
When schemas get large, we can start to define types in different files and import them to create the complete schema. We accomplish this by importing and exporting schema strings, combining them into arrays as necessary.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// comment.js
|
|
||||||
const typeDefs = gql`
|
|
||||||
type Comment {
|
|
||||||
id: Int!
|
|
||||||
message: String
|
|
||||||
author: String
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export typeDefs;
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
// post.js
|
|
||||||
const Comment = require('./comment');
|
|
||||||
|
|
||||||
const typeDefs = [`
|
|
||||||
type Post {
|
|
||||||
id: Int!
|
|
||||||
title: String
|
|
||||||
content: String
|
|
||||||
author: String
|
|
||||||
comments: [Comment]
|
|
||||||
}
|
|
||||||
`].concat(Comment.typeDefs);
|
|
||||||
|
|
||||||
// we export Post and all types it depends on
|
|
||||||
// in order to make sure we don't forget to include
|
|
||||||
// a dependency
|
|
||||||
export typeDefs;
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
// schema.js
|
|
||||||
const Post = require('./post.js');
|
|
||||||
|
|
||||||
const RootQuery = `
|
|
||||||
type RootQuery {
|
|
||||||
post(id: Int!): Post
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SchemaDefinition = `
|
|
||||||
schema {
|
|
||||||
query: RootQuery
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const server = new ApolloServer({
|
|
||||||
//we may destructure Post if supported by our Node version
|
|
||||||
typeDefs: [SchemaDefinition, RootQuery].concat(Post.typeDefs),
|
|
||||||
resolvers,
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen().then(({ url }) => {
|
|
||||||
console.log(`🚀 Server ready at ${url}`)
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
<h3 id="extend-types">Extending types</h3>
|
|
||||||
|
|
||||||
The `extend` keyword provides the ability to add fields to existing types. Using `extend` is particularly useful in avoiding a large list of fields on root Queries and Mutations.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const barTypeDefs = `
|
|
||||||
"Query can and must be defined once per schema to be extended"
|
|
||||||
type Query {
|
|
||||||
bars: [Bar]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Bar {
|
|
||||||
id: String
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const fooTypeDefs = `
|
|
||||||
type Foo {
|
|
||||||
id: String
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Query {
|
|
||||||
foos: [Foo]
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const typeDefs = [barTypeDefs, fooTypeDefs]
|
|
||||||
```
|
|
||||||
|
|
||||||
<h3 id="share-types">Sharing types</h3>
|
|
||||||
|
|
||||||
Schemas often contain circular dependencies or a shared type that has been hoisted to be referenced in separate files. When exporting array of schema strings with circular dependencies, the array can be wrapped in a function. The Apollo Server will only include each type definition once, even if it is imported multiple times by different types. Preventing deduplication of type definitions means that domains can be self contained and fully functional regardless of how they are combined.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// author.js
|
|
||||||
const Book = require('./book');
|
|
||||||
|
|
||||||
const Author = `
|
|
||||||
type Author {
|
|
||||||
id: Int!
|
|
||||||
firstName: String
|
|
||||||
lastName: String
|
|
||||||
books: [Book]
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// we export Author and all types it depends on
|
|
||||||
// in order to make sure we don't forget to include
|
|
||||||
// a dependency and we wrap it in a function
|
|
||||||
// to avoid strings deduplication
|
|
||||||
export const typeDefs = () => [Author].concat(Book.typeDefs);
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
// book.js
|
|
||||||
const Author = require('./author');
|
|
||||||
|
|
||||||
const Book = `
|
|
||||||
type Book {
|
|
||||||
title: String
|
|
||||||
author: Author
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const typeDefs = () => [Book].concat(Author.typeDefs);
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
// schema.js
|
|
||||||
const Author = require('./author.js');
|
|
||||||
|
|
||||||
const RootQuery = `
|
|
||||||
type RootQuery {
|
|
||||||
author(id: Int!): Author
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SchemaDefinition = `
|
|
||||||
schema {
|
|
||||||
query: RootQuery
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const server = new ApolloServer({
|
|
||||||
//we may destructure Post if supported by our Node version
|
|
||||||
typeDefs: [SchemaDefinition, RootQuery].concat(Author.typeDefs),
|
|
||||||
resolvers,
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen().then(({ url }) => {
|
|
||||||
console.log(`🚀 Server ready at ${url}`)
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
---
|
|
||||||
title: Security
|
|
||||||
---
|
|
||||||
|
|
||||||
Apollo Server is a safer way to build applications thanks to GraphQL's strong typing and the conversion of raw operations into a trusted syntax tree. By validating each part of an operation, GraphQL is mostly exempt from injection-attacks which are of concern in other data-driven applications.
|
|
||||||
|
|
||||||
This guide will discuss additional security measures which further harden the excellent foundation which GraphQL is already built upon. While Apollo Server will enable some additional protections automatically, others require attention on the part of the developer.
|
|
||||||
|
|
||||||
<h2 id="introspection">Introspection in production</h2>
|
|
||||||
|
|
||||||
Introspection is a powerful tool to have enabled during development and allows developers to get real-time visibility of a GraphQL server's capabilities.
|
|
||||||
|
|
||||||
In production, such insight might be less desireable unless the server is intended to be a "public" API.
|
|
||||||
|
|
||||||
Therefore, Apollo Server introspection is automatically disabled when the `NODE_ENV` is set to `production` in order to reduce visibility into the API.
|
|
||||||
|
|
||||||
Of course, no system should rely solely on so-called "security through obscurity" and this practice should be combined with other security techniques like open security and security by design.
|
|
||||||
|
|
||||||
<h2 id="ssl">Securing with SSL/TLS</h2>
|
|
||||||
|
|
||||||
You can secure all communication between the clients and your GraphQL server by using SSL/TLS. Apollo Server, with subscriptions, can be configured to use the `https` module with `apollo-server-express`. See [example server code](../essentials/server.html#ssl).
|
|
||||||
|
|
||||||
Alternatively, you can use a reverse proxy solution like [NGINX](https://www.nginx.com/) or [Traefik](https://traefik.io/). An additional benefit of using Traefik is that you can use a free [Let's Encrypt SSL certificate](http://niels.nu/blog/2017/traefik-https-letsencrypt.html).
|
|
||||||
|
|
||||||
<h2 id="injection">Injection prevention</h2>
|
|
||||||
|
|
||||||
As we build out our schema, it may be tempting to allow for shortcut arguments to creep in which have security risks. This most commonly happens on filters and on mutation inputs:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
query OhNo {
|
|
||||||
users(filter: "id = 1;' sql injection goes here!") {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mutation Dang {
|
|
||||||
updateUser(user: { firstName: "James", id: 1 }) {
|
|
||||||
success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In the first operation we are passing a filter that is a database filter directly as a string. This opens the door for SQL injection since the string is preserved from the client to the server.
|
|
||||||
|
|
||||||
In the second operation we are passing an id value which may let an attacker update information for someone else! This often happens if generic Input Types are created for corresponding data sources:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
# used for both creating and updating a user
|
|
||||||
input UserInput {
|
|
||||||
id: Int
|
|
||||||
firstName: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
createUser(user: UserInput): User
|
|
||||||
updateUser(user: UserInput): User
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The fix for both of these attack vectors is to create more detailed arguments and let the validation step of Apollo Server filter out bad values as well as **never** pass raw values from a client into our datasource.
|
|
||||||
|
|
||||||
<h2 id="dos">Denial-of-Service (DoS) Protection</h2>
|
|
||||||
|
|
||||||
Apollo Server is a Node.js application and standard precautions should be taken in order to avoid Denial-of-Service (DoS) attacks.
|
|
||||||
|
|
||||||
Since GraphQL involves the traversal of a graph in which circular relationships of arbitrary depths might be accessible, some additional precautions can be taken to limit the risks of Complexity Denial-of-Service (CDoS) attacks, where a bad actor could craft expensive operations and lock up resources indefinitely.
|
|
||||||
|
|
||||||
There are two common techniques to mitigate CDoS risks, and can be enabled together:
|
|
||||||
|
|
||||||
1. **Operation white-listing**
|
|
||||||
|
|
||||||
By hashing the potential operations a client might send (e.g. based on field names) and storing these "permitted" hashes on the server (or a shared cache), it becomes possible to check incoming operations against the permitted hashes and skip execution if the hash is not allowed.
|
|
||||||
|
|
||||||
Since many consumers of non-public APIs have their operations statically defined within their source code, this technique is often sufficient and is best implemented as an automated deployment step.
|
|
||||||
|
|
||||||
2. **Complexity limits**
|
|
||||||
|
|
||||||
These can be used to limit the use of queries which, for example, request a list of books including the authors of each book, plus the books of those authors, and _their_ authors, and so on. By limiting operations to an application-defined depth of "_n_", these can be easily prevented.
|
|
||||||
|
|
||||||
We suggest implementing complexity limits using community-provided packages like [graphql-depth-limit](https://github.com/stems/graphql-depth-limit) and [graphql-validation-complexity](https://github.com/4Catalyzer/graphql-validation-complexity).
|
|
||||||
|
|
||||||
> For additional information on securing a GraphQL server deployment, check out [Securing your GraphQL API from malicious queries](https://blog.apollographql.com/securing-your-graphql-api-from-malicious-queries-16130a324a6b) by Spectrum co-founder, Max Stoiber.
|
|
|
@ -1,13 +0,0 @@
|
||||||
---
|
|
||||||
title: Testing
|
|
||||||
---
|
|
||||||
|
|
||||||
Intro section about separation of concerns making GraphQL ideal for unit testing as well integration testing
|
|
||||||
|
|
||||||
> (James) Add API for ApolloServer to make it easy to run integration tests against? Dependency injection anyone?
|
|
||||||
|
|
||||||
## Unit testing resolvers
|
|
||||||
|
|
||||||
## Integration testing operations
|
|
||||||
|
|
||||||
## Using your schema to mock data for client testing
|
|
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
title: Versioning
|
|
||||||
description: How to add and remove parts of your schema without breaking your clients
|
|
||||||
---
|
|
||||||
|
|
||||||
tl;dr don't. Use a tool like Engine (one day) to help you iterate
|
|
||||||
|
|
||||||
## Why versioning isn't needed
|
|
||||||
|
|
||||||
## Practical examples of field rollovers
|
|
|
@ -3,7 +3,7 @@ title: Understanding schema concepts
|
||||||
sidebar_title: Writing a schema
|
sidebar_title: Writing a schema
|
||||||
---
|
---
|
||||||
|
|
||||||
> Estimated time: About 6 minutes.
|
> Estimated time: About 10 minutes.
|
||||||
|
|
||||||
A GraphQL schema is at the center of any GraphQL server implementation and describes the functionality available to the clients which connect to it.
|
A GraphQL schema is at the center of any GraphQL server implementation and describes the functionality available to the clients which connect to it.
|
||||||
|
|
||||||
|
@ -242,6 +242,276 @@ Introspection is an **optional** feature, enabled by default during development,
|
||||||
|
|
||||||
By allowing the consumer of the API to view the full possibilities of the API, developers can easily write new queries, or add new fields to existing ones.
|
By allowing the consumer of the API to view the full possibilities of the API, developers can easily write new queries, or add new fields to existing ones.
|
||||||
|
|
||||||
|
One of the main aspects of GraphQL is that it allows you to describe the space of data available in your system with a strongly typed schema. While GraphQL makes it possible to evolve your API over time without breaking your clients, it's always easier if you think about some schema design decisions up front to reduce the amount of refactoring you need to do later.
|
||||||
|
|
||||||
|
This article details some practices around schema design which will help you design a great GraphQL API to stand the test of time.
|
||||||
|
|
||||||
|
<h2 id="style">Style conventions</h2>
|
||||||
|
|
||||||
|
The GraphQL specification is flexible and doesn't impose specific naming guidelines. However, in order to facilitate development and continuity across GraphQL deployments, it's useful to have a general set of conventions. We suggest the following:
|
||||||
|
|
||||||
|
* **Fields** should be named in `camelCase`, since the majority of consumers will be client applications written in JavaScript, Java, Kotlin, or Swift, all of which recommend `camelCase` for variable names.
|
||||||
|
* **Types**: should be `PascalCase`, to match how classes are defined in the languages above.
|
||||||
|
* **Enums**: should have their type name in `PascalCase`, and their value names in `ALL_CAPS`, since they are similar to constants.
|
||||||
|
|
||||||
|
If you use the conventions above, you won't need to have any extra logic in your clients to convert names to match the conventions of these languages.
|
||||||
|
|
||||||
|
<h2 id="design-for-client">Design for client needs</h2>
|
||||||
|
|
||||||
|
GraphQL schemas are at their best when they are designed around the needs of client applications. When a team is building their first GraphQL schema, they might be tempted to create literal mappings on top of existing database collections or tables using CRUD-like root fields. While this literal database-to-schema mapping may be a fast way to get up and running, we strongly suggest avoiding it and instead building the schema based on how the GraphQL API will be used by the front-end.
|
||||||
|
|
||||||
|
If a database has fields or relationships that the client doesn't yet need, don’t include them in the schema up front. Adding fields later is much easier than removing them, so add fields to your API as your clients need them rather than exposing all of the possible data up front. This is especially useful because GraphQL allows you to create associations between your data that don't exist in the underlying data, enabling you to move complex data manipulation logic out of your clients.
|
||||||
|
|
||||||
|
For example, let's say you want to create a view that lists some events, their locations, and the weather at that location. In that case, you might want to do a query like this:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query EventList {
|
||||||
|
upcomingEvents {
|
||||||
|
name
|
||||||
|
date
|
||||||
|
location {
|
||||||
|
name
|
||||||
|
weather {
|
||||||
|
temperature
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The desire to display this data could inform the design of a schema like the following:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type Query {
|
||||||
|
upcomingEvents: [Event]
|
||||||
|
# Other fields, etc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event {
|
||||||
|
name: String
|
||||||
|
date: String
|
||||||
|
location: Location
|
||||||
|
}
|
||||||
|
|
||||||
|
type Location {
|
||||||
|
name: String
|
||||||
|
weather: WeatherInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeatherInfo {
|
||||||
|
temperature: Float
|
||||||
|
description: String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This doesn't necessarily need to match the data returned from a single REST endpoint or database. For example, if you have a REST endpoint exposing a list of events and their locations, but not weather information, you would just need to fetch the weather information from a second endpoint (or even a 3rd party API) in your resolvers. This way, you can design a schema that will allow your frontend to be as simple as possible, without limiting yourself to the exact shape of data that's in your underlying data sources.
|
||||||
|
|
||||||
|
<h2 id="mutations">Designing mutations</h2>
|
||||||
|
|
||||||
|
The `Mutation` type is a core type in GraphQL which specializes in _modifying_ data, which contrasts the `Query` type used for _fetching_ data.
|
||||||
|
|
||||||
|
Unlike REST, where the behavior can be more ad-hoc, the `Mutation` type is designed with the expectation that there will be a response object. This ensures that the client receives the most current data without a subsequent round-trip re-query.
|
||||||
|
|
||||||
|
A mutation for updating the age of a `User` might look like this:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type Mutation {
|
||||||
|
updateUserAge(id: ID!, age: Int!): User
|
||||||
|
}
|
||||||
|
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
age: Int!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With this definition, the following mutation becomes possible:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
mutation updateMyUser {
|
||||||
|
updateUserAge(id: 1, age: 25){
|
||||||
|
id
|
||||||
|
age
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once executed by the server, the response returned to the client might be:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"updateUserAge": {
|
||||||
|
"id": "1",
|
||||||
|
"age": "25",
|
||||||
|
"name": "Jane Doe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
While it's not mandatory to return the object which has been updated, the inclusion of the updated information allows the client to confidently update its local state without performing additional requests.
|
||||||
|
|
||||||
|
As with queries, it's best to design mutations with the client in mind and in response to a user's action. In simple cases, this might only result in changes to a single document, however in many cases there will be updates to multiple documents from different resources, for example, a `likePost` mutation might update the total likes for a user as well as their post.
|
||||||
|
|
||||||
|
In order to provide a consistent shape of response data, we recommend adopting a pattern which returns a standardized response format which supports returning any number of documents from each resource which was modified. We'll outline a recommended pattern for this in the next section.
|
||||||
|
|
||||||
|
<h3 id="mutation-responses">Responses</h3>
|
||||||
|
|
||||||
|
GraphQL mutations can return any information the developer wishes, but designing mutation responses in a consistent and robust structure makes them more approachable by humans and less complicated to traverse in client code. There are two guiding principles which we have combined into our suggested mutation response structure.
|
||||||
|
|
||||||
|
First, while mutations might only modify a single resource type, they often need to touch several at a time. It makes sense for this to happen in a single round-trip to the server and this is one of the strengths of GraphQL! When different resources are modified, the client code can benefit from having updated fields returned from each type and the response format should support that.
|
||||||
|
|
||||||
|
Secondly, mutations have a higher chance of causing errors than queries since they are modifying data. If only a portion of a mutation update succeeds, whether that is a partial update to a single document's fields or a failed update to an entire document, it's important to convey that information to the client to avoid stale local state on the client.
|
||||||
|
|
||||||
|
A common way to handle errors during a mutation is to simply `throw` an error. While that's fine, throwing an error in a resolver will return an error for the entire operation to the caller and prevent a more meaningful response. Consider the following mutation example, which tries to update a user's `name` and `age`:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
mutation updateUser {
|
||||||
|
updateUser(id: 1, user: { age: -1, name: "Foo Bar" }){
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With validation in place, this mutation might cause an error since the `age` is a negative value. While it’s possible that the entire operation should be stopped, there’s an opportunity to partially update the user’s record with the new `name` and return the updated record with the `age` left untouched.
|
||||||
|
|
||||||
|
Fortunately, the powerful structure of GraphQL mutations accommodates this use case and can return transactional information about the update alongside the records which have been changed which enables client-side updates to occur automatically.
|
||||||
|
|
||||||
|
In order to provide consistency across a schema, we suggest introducing a `MutationResponse` interface which can be implemented on every mutation response in a schema and enables transactional information to be returned in addition to the normal mutation response object.
|
||||||
|
|
||||||
|
A `MutationResponse` interface would look like this:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
interface MutationResponse {
|
||||||
|
code: String!
|
||||||
|
success: Boolean!
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
An implementing type would look like this:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type UpdateUserMutationResponse implements MutationResponse {
|
||||||
|
code: String!
|
||||||
|
success: Boolean!
|
||||||
|
message: String!
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Calling a mutation that returns that `UpdateUserMutationResponse` type would result in a response that looks something like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"updateUser": {
|
||||||
|
"code": "200",
|
||||||
|
"success": true,
|
||||||
|
"message": "User was successfully updated",
|
||||||
|
"user": {
|
||||||
|
"id": "1",
|
||||||
|
"name": "Jane Doe",
|
||||||
|
"age": 35
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let’s break this down, field by field:
|
||||||
|
|
||||||
|
* `code` is a string representing a transactional value explaining details about the status of the data change. Think of this like an HTTP status code.
|
||||||
|
* `success` is a boolean indicating whether the update was successful or not. This allows a coarse check by the client to know if there were failures.
|
||||||
|
* `message` is a string that is meant to be a human-readable description of the status of the transaction. It is intended to be used in the UI of the product.
|
||||||
|
* `user` is added by the implementing type `UpdateUserMutationResponse` to return back the newly created user for the client to use!
|
||||||
|
|
||||||
|
For mutations which have touched multiple types, this same structure can be used to return updated objects from each one. For example, a `likePost` type, which could affect a user's "reputation" and also update the post itself, might implement `MutationResponse` in the following manner:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type LikePostMutationResponse implements MutationResponse {
|
||||||
|
code: String!
|
||||||
|
success: Boolean!
|
||||||
|
message: String!
|
||||||
|
post: Post
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this response type, we've provided the expectation that both the `user` and the `post` would be returned and an actual response to a `likePost` mutation could be:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"likePost": {
|
||||||
|
"code": "200",
|
||||||
|
"success": true,
|
||||||
|
"message": "Thanks!",
|
||||||
|
"post": {
|
||||||
|
"likes": 5040
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"reputation": 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Following this pattern for mutations provides detailed information about the data that has changed and feedback on whether the operation was successful or not. Armed with this information, developers can easily react to failures within the client
|
||||||
|
|
||||||
|
<h3 id="mutation-input-types">Input types</h3>
|
||||||
|
|
||||||
|
Input types are a special type in GraphQL which allows an object to be passed as an argument to both queries and mutations and is helpful when simple scalar types aren't sufficient.
|
||||||
|
|
||||||
|
This allows arguments to be structured in an more manageable way, similar to how switching to an `options` argument might be appreciated when `function` arguments become too iterative.
|
||||||
|
|
||||||
|
For example, consider this mutation which creates a post along with its accompanying media URLs (e.g. images):
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type Mutation {
|
||||||
|
createPost(title: String, body: String, mediaUrls: [String]): Post
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This could be easier to digest, and the arguments would be easier to re-use within the mutation, by using an `input` type with the relevant fields.
|
||||||
|
|
||||||
|
An input type is defined like a normal object type but using the `input` keyword. To introduce an `input` type for this example, we'd do:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type Mutation {
|
||||||
|
createPost(post: PostAndMediaInput): Post
|
||||||
|
}
|
||||||
|
|
||||||
|
input PostAndMediaInput {
|
||||||
|
title: String
|
||||||
|
body: String
|
||||||
|
mediaUrls: [String]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Not only does this facilitate passing the `PostAndMediaInput` around within the schema, it also provides a basis for annotating fields with descriptions which are automatically exposed by GraphQL-enabled tools:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
input PostAndMediaInput {
|
||||||
|
"A main title for the post"
|
||||||
|
title: String
|
||||||
|
"The textual body of the post."
|
||||||
|
body: String
|
||||||
|
"A list of URLs to render in the post."
|
||||||
|
mediaUrls: [String]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Input types can also be used when different operations require the exact same information, though we urge caution on over-using this technique since changes to `input` types are breaking changes for all operations which utilize them.
|
||||||
|
|
||||||
|
Additionally, while it is possible to reuse an `input` type between a query and mutation which target the same resource, it's often best to avoid this since in many cases certain null fields might be tolerated for one but not the other.
|
||||||
|
|
||||||
<h2 id="next-steps">Next steps</h2>
|
<h2 id="next-steps">Next steps</h2>
|
||||||
|
|
||||||
At this point, we hope to have explained the basic information necessary to understand a GraphQL schema.
|
At this point, we hope to have explained the basic information necessary to understand a GraphQL schema.
|
||||||
|
|
257
docs/source/features/authentication.md
Normal file
257
docs/source/features/authentication.md
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
---
|
||||||
|
title: Authentication
|
||||||
|
description: How to authorize users and control permissions in your GraphQL API
|
||||||
|
---
|
||||||
|
|
||||||
|
At some point (probably pretty early on) when building a GraphQL endpoint, you’ll probably have to face the question of how to control who can see and interact with the data in your API.
|
||||||
|
|
||||||
|
**Authentication** is determining whether a user is logged in or not, and subsequently figuring out _which_ user someone is. **Authorization** is then deciding what the user has permission to do or see.
|
||||||
|
|
||||||
|
This article will primarily be focusing on how to set up authorization for your schema once you know about the user trying to make the request, but we’ll go through one example of authentication just to get some _context_ for what we’re doing.
|
||||||
|
|
||||||
|
<h2 id="context">Putting user info on the context</h2>
|
||||||
|
|
||||||
|
Before we get into figuring out user permissions, we have to figure out how to recognize a user first. From HTTP headers, to JSON web tokens, there are a number of ways to handle authentication of users, but once you have your user, controlling access looks pretty similar.
|
||||||
|
|
||||||
|
We’ll be using a login token in an HTTP authorization header as an example.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// using apollo-server 2.x
|
||||||
|
const { ApolloServer } = require('apollo-server');
|
||||||
|
|
||||||
|
const server = new ApolloServer({
|
||||||
|
typeDefs,
|
||||||
|
resolvers,
|
||||||
|
context: ({ req }) => {
|
||||||
|
// get the user token from the headers
|
||||||
|
const token = req.headers.authorization || '';
|
||||||
|
|
||||||
|
// try to retrieve a user with the token
|
||||||
|
const user = getUser(token);
|
||||||
|
|
||||||
|
// add the user to the context
|
||||||
|
return { user };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen().then(({ url }) => {
|
||||||
|
console.log(`🚀 Server ready at ${url}`)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
So what’s happening here, exactly? This block of code is setting up a new GraphQL server, using Apollo Server 2.0. This new version of Apollo Server simplifies the API for creating new servers, and has some more intelligent defaults. You can read more about it [here](https://blog.apollographql.com/apollo-server-2-0-30c9bbb4ab5e)!
|
||||||
|
|
||||||
|
In this constructor, we pass type definitions and resolvers to the constructor as well as a function to build our `context` object. The `context` object is one that gets passed to every single resolver at every level, so we can access it anywhere in our schema code. It’s where we can store things like data fetchers, database connections, and (conveniently) information about the user making the request.
|
||||||
|
|
||||||
|
Since the context is generated again with every new request, we don’t have to worry about cleaning up user data at the end of execution.
|
||||||
|
|
||||||
|
The context function here looks at the request headers, pulls off the header named `authorization`, and stores it to a variable. It then calls a `getUser` function with that token, and expects a user to be returned if the token is valid. After that, it returns a context object containing the (potential) user, for all of our resolvers to use.
|
||||||
|
|
||||||
|
The specifics of retrieving a user will look different for each method of authentication, but the final part will look about the same every time. The authorization needs for your schema may require you to put nothing more than `{ loggedIn: true }` into context, but also may require an id or roles, like `{ user: { id: 12345, roles: ['user', 'admin'] } }`.
|
||||||
|
|
||||||
|
In the next section, we’ll look at ways to use the user information we now have to secure your schema.
|
||||||
|
|
||||||
|
<h2 id="schema-auth">Schema authorization</h2>
|
||||||
|
|
||||||
|
Once we have information about the user making a request, the most basic thing we can do is deny them the ability to run a query at all based on their roles. This is an all-or-nothing approach to authorization that we’ll start with because it’s the simplest. If you choose to block users like this, no fields will be publicly queryable.
|
||||||
|
|
||||||
|
We would want to do this only on very restrictive environments where there is no public access to the schema or any fields, like an internal tool or maybe an independent micro service that we don’t want exposed to the public.
|
||||||
|
|
||||||
|
To do this kind of authorization, we can just modify the context function.
|
||||||
|
|
||||||
|
```js
|
||||||
|
context: ({ req }) => {
|
||||||
|
// get the user token from the headers
|
||||||
|
const token = req.headers.authorization || '';
|
||||||
|
|
||||||
|
// try to retrieve a user with the token
|
||||||
|
const user = getUser(token);
|
||||||
|
|
||||||
|
// optionally block the user
|
||||||
|
// we could also check user roles/permissions here
|
||||||
|
if (!user) throw new AuthorizationError('you must be logged in');
|
||||||
|
|
||||||
|
// add the user to the context
|
||||||
|
return { user };
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
The only difference from the basic context function is the check for the user. If no user exists or if lookup fails, the function throws an error, and none of the query gets executed.
|
||||||
|
|
||||||
|
<h2 id="resolver-auth">Authorization in resolvers</h2>
|
||||||
|
|
||||||
|
Schema authorization may be useful in specific instances, but more commonly, GraphQL schemas will have some fields that need to be public. An example of this would be a news site that wants to show article previews to anyone, but restrict the full body of articles to paying customers only.
|
||||||
|
|
||||||
|
Luckily, GraphQL offers very granular control over data. In GraphQL servers, individual field resolvers have the ability to check user roles and make decisions as to what to return for each user. In the previous sections, we saw how to attach user information to the context object. In the rest of the article, we’ll discuss how to use that context object.
|
||||||
|
|
||||||
|
For our first example, let’s look at a resolver that’s only accessible with a valid user:
|
||||||
|
|
||||||
|
```js
|
||||||
|
users: (root, args, context) => {
|
||||||
|
// In this case, we'll pretend there is no data when
|
||||||
|
// we're not logged in. Another option would be to
|
||||||
|
// throw an error.
|
||||||
|
if (!context.user) return [];
|
||||||
|
|
||||||
|
return ['bob', 'jake'];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This example is a field in our schema named `users` that returns a list of users’ names. The `if` check on the first line of the function looks at the `context` generated from our request, checks for a `user` object, and if one doesn’t exist, returns `null` for the whole field.
|
||||||
|
|
||||||
|
One choice to make when building out our resolvers is what an unauthorized field should return. In some use cases, returning `null` here is perfectly valid. Alternatives to this would be to return an empty array, `[]` or to throw an error, telling the client that they’re not allowed to access that field. For the sake of simplicity, we just returned `[]` in this example.
|
||||||
|
|
||||||
|
Now let’s expand that example a little further, and only allow users with an `admin` role to look at our user list. After all, we probably don’t want just anyone to have access to all our users.
|
||||||
|
|
||||||
|
```js
|
||||||
|
users: (root, args, context) => {
|
||||||
|
if (!context.user || !context.user.roles.includes('admin')) return null;
|
||||||
|
return context.models.User.getAll();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This example looks almost the same as the previous one, with one addition: it expects the `roles` array on a user to include an `admin` role. Otherwise, it returns `null`. The benefit of doing authorization like this is that we can short-circuit our resolvers and not even call lookup functions when we don’t have permission to use them, limiting the possible errors that could expose sensitive data.
|
||||||
|
|
||||||
|
Because our resolvers have access to everything in the context, an important question we need to ask is how much information we want in the context. For example, we don’t need the user’s id, name, or age (at least not yet). It’s best to keep things out of the context until they’re needed, since they’re easy to add back in later.
|
||||||
|
|
||||||
|
<h2 id="models-auth">Authorization in data models</h2>
|
||||||
|
|
||||||
|
As our server gets more complex, there will probably be multiple places in the schema that need to fetch the same kind of data. In our last example, you may have noticed the return array was replaced with a call to `context.models.User.getAll()`.
|
||||||
|
|
||||||
|
Since the very beginning, [we’ve recommended](https://www.apollographql.com/docs/graphql-tools/connectors.html) moving the actual data fetching and transformation logic from resolvers to centralized Model objects that each represent a concept from your application: User, Post, etc. This allows you to make your resolvers a thin routing layer, and put all of your business logic in one place.
|
||||||
|
|
||||||
|
For example, a model file for `User` would include all the logic for operating on users, and may look something like…
|
||||||
|
|
||||||
|
```js
|
||||||
|
export const User = {
|
||||||
|
getAll: () => { /* fetching/transform logic for all users */ },
|
||||||
|
getById: (id) => { /* fetching/transform logic for a single user */ },
|
||||||
|
getByGroupId: (id) => { /* fetching/transform logic for a group of users */ },
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
In the following example, our schema has multiple ways to request a single user…
|
||||||
|
|
||||||
|
```js
|
||||||
|
type Query {
|
||||||
|
user (id: ID!): User
|
||||||
|
article (id: ID!): Article
|
||||||
|
}
|
||||||
|
|
||||||
|
type Article {
|
||||||
|
author: User
|
||||||
|
}
|
||||||
|
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rather than having the same fetching logic for a single user in two separate places, it usually makes sense to move that logic to the model file. You may have guessed, with all this talk of model files in an authorization article, that authorization is another great thing to delegate to the model, just like data fetching. You would be right.
|
||||||
|
|
||||||
|
**Delegating authorization to models**
|
||||||
|
|
||||||
|
You may have noticed that our models also exist on the context, alongside the user object we added earlier. We can add the models to the context in exactly the same way as we did the user.
|
||||||
|
|
||||||
|
```js
|
||||||
|
context: ({ req }) => {
|
||||||
|
// get the user token from the headers
|
||||||
|
const token = req.headers.authentication || '';
|
||||||
|
|
||||||
|
// try to retrieve a user with the token
|
||||||
|
const user = getUser(token);
|
||||||
|
|
||||||
|
// optionally block the user
|
||||||
|
// we could also check user roles/permissions here
|
||||||
|
if (!user) throw new AuthorizationError('you must be logged in to query this schema');
|
||||||
|
|
||||||
|
// add the user to the context
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
models: {
|
||||||
|
User: generateUserModel({ user }),
|
||||||
|
...
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Starting to generate our models with a function requires a small refactor, that would leave our User model looking something like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export const generateUserModel = ({ user }) => ({
|
||||||
|
getAll: () => { /* fetching/transform logic for all users */ },
|
||||||
|
getById: (id) => { /* fetching/transform logic for a single user */ },
|
||||||
|
getByGroupId: (id) => { /* fetching/transform logic for a group of users */ },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Now any model method in `User` has access to the same `user` information that resolvers already had, allowing us to refactor the `getAll` function to do the permissions check directly rather than having to put it in the resolver:
|
||||||
|
|
||||||
|
```js
|
||||||
|
getAll: () => {
|
||||||
|
if(!user || !user.roles.includes('admin') return null;
|
||||||
|
return fetch('http://myurl.com/users');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<h2 id="directives-auth">Authorization via Custom Directives</h2>
|
||||||
|
|
||||||
|
Another way to go about authorization is via GraphQL Schema Directives. A directive is an identifier preceded by a `@` character, optionally followed by a list of named arguments, which can appear after almost any form of syntax in the GraphQL query or schema languages.
|
||||||
|
|
||||||
|
Check out this example of an authorization directive:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const typeDefs = `
|
||||||
|
directive @auth(requires: Role = ADMIN) on OBJECT | FIELD_DEFINITION
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
ADMIN
|
||||||
|
REVIEWER
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
|
||||||
|
type User @auth(requires: USER) {
|
||||||
|
name: String
|
||||||
|
banned: Boolean @auth(requires: ADMIN)
|
||||||
|
canPost: Boolean @auth(requires: REVIEWER)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
```
|
||||||
|
|
||||||
|
The `@auth` directive can be called directly on the type, or on the fields if you want to limit access to specific fields as shown in the example above. The logic behind authorization is hidden away in the directive implementation.
|
||||||
|
|
||||||
|
One way of implementing the `@auth` directive is via the [SchemaDirectiveVisitor](https://www.apollographql.com/docs/graphql-tools/schema-directives.html) class from [graphql-tools](https://github.com/apollographql/graphql-tools). Ben Newman covered creating a sample `@deprecated` and `@rest` directive in this [excellent article](https://blog.apollographql.com/reusable-graphql-schema-directives-131fb3a177d1). You can draw inspiration from these examples.
|
||||||
|
|
||||||
|
|
||||||
|
<h2 id="rest-auth">Authorization outside of GraphQL</h2>
|
||||||
|
|
||||||
|
If you’re using a REST API that has built-in authorization, like with an HTTP header, you have one more option. Rather than doing any authentication or authorization work in the GraphQL layer (in resolvers/models), it’s possible to simply pass through the headers or cookies to your REST endpoint and let it do the work.
|
||||||
|
|
||||||
|
Here’s an example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/server.js
|
||||||
|
context: ({ req }) => {
|
||||||
|
// pass the request information through to the model
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
models: {
|
||||||
|
User: generateUserModel({ req }),
|
||||||
|
...
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/models/user.js
|
||||||
|
export const generateUserModel = ({ req }) => ({
|
||||||
|
getAll: () => {
|
||||||
|
return fetch('http://myurl.com/users', { headers: req.headers });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If your REST endpoint is already backed by some form of authorization, this cuts down a lot of the logic that needs to get built in the GraphQL layer. This can be a great option when building a GraphQL API over an existing REST API that has everything you need already built in.
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: Test Utilities
|
title: Integration testing
|
||||||
description: Making apollo-server easier to test
|
description: Utilities for testing Apollo Server
|
||||||
---
|
---
|
||||||
|
|
||||||
Testing `apollo-server` can be done in many ways. The `apollo-server-testing` package provides tooling to make testing easier and accessible to users of all of the `apollo-server` integrations.
|
Testing `apollo-server` can be done in many ways. The `apollo-server-testing` package provides tooling to make testing easier and accessible to users of all of the `apollo-server` integrations.
|
|
@ -77,72 +77,90 @@ A possible query for these result could appear as follows. This query demonstrat
|
||||||
|
|
||||||
## Interface type
|
## Interface type
|
||||||
|
|
||||||
An `Interface` type provides the ability to describe fields that are shared across different types. It is best used to show that all types implementing an interface always contain the interface's fields. In other words, it is the semantic opposite of a union. For example, in this example `Vehicle` interface type is used by members `Airplane` and `Car`:
|
Interfaces are a powerful way to build and use GraphQL schemas through the use of _abstract types_. Abstract types can't be used directly in schema, but can be used as building blocks for creating explicit types.
|
||||||
|
|
||||||
|
Consider an example where different types of books share a common set of attributes, such as _text books_ and _coloring books_. A simple foundation for these books might be represented as the following `interface`:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
interface Book {
|
||||||
|
title: String
|
||||||
|
author: Author
|
||||||
|
}
|
||||||
```
|
```
|
||||||
interface Vehicle {
|
|
||||||
maxSpeed: Int
|
We won't be able to directly use this interface to query for a book, but we can use it to implement concrete types. Imagine a screen within an application which needs to display a feed of all books, without regard to their (more specific) type. To create such functionality, we could define the following:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type TextBook implements Book {
|
||||||
|
title: String
|
||||||
|
author: Author
|
||||||
|
classes: [Class]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Airplane implements Vehicle {
|
type ColoringBook implements Book {
|
||||||
maxSpeed: Int
|
title: String
|
||||||
wingspan: Int
|
author: Author
|
||||||
}
|
colors: [Color]
|
||||||
|
|
||||||
type Car implements Vehicle {
|
|
||||||
maxSpeed: Int
|
|
||||||
licensePlate: String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
vehicle: Vehicle
|
schoolBooks: [Book]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Similarly to the `Union`, `Interface` requires an extra `__resolveType` field in the resolver map.
|
In this example, we've used the `Book` interface as the foundation for the `TextBook` and `ColoringBook` types. Then, a `schoolBooks` field simply expresses that it returns a list of books (i.e. `[Book]`).
|
||||||
|
|
||||||
|
Similarly to the `Union`, `Interface` requires an extra `__resolveType` field in the resolver map to determine which type the interface should resolve to.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Vehicle: {
|
Book: {
|
||||||
__resolveType(obj, context, info){
|
__resolveType(book, context, info){
|
||||||
if(obj.wingspan){
|
if(book.classes){
|
||||||
return 'Airplane';
|
return 'TextBook';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(obj.licensePlate){
|
if(book.colors){
|
||||||
return 'Car';
|
return 'ColoringBook';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
vehicle: () => { ... }
|
schoolBooks: () => { ... }
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const server = new ApolloServer({
|
|
||||||
typeDefs,
|
|
||||||
resolvers,
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen().then(({ url }) => {
|
|
||||||
console.log(`🚀 Server ready at ${url}`)
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
A possible query could appear as follows. Notice that `maxSpeed` is shared, so it can be included directly:
|
Implementing the book feed example is now simplified since we've removed the need to worry about what kind of `Book`s will be returned. A query against this schema, which could return _text books_ and _coloring_ books, might look like:
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
{
|
query GetBooks {
|
||||||
vehicle {
|
schoolBooks {
|
||||||
maxSpeed
|
title
|
||||||
|
author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
... on Car {
|
This is really helpful for feeds of common content, user role systems, and more!
|
||||||
licensePlate
|
|
||||||
|
Furthermore, if we need to return fields which are only provided by either `TextBook`s or `ColoringBook`s (not both) we can request fragments from the abstract types in the query. Those fragments will be filled in only as appropriate; in the case of the example, only coloring books will be returned with `colors`, and only textbooks will have `classes`:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query GetBooks {
|
||||||
|
schoolBooks {
|
||||||
|
title
|
||||||
|
... on TextBook {
|
||||||
|
classes {
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
... on Airplane {
|
... on ColoringBook {
|
||||||
wingspan
|
colors {
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue