From daa5392a4108d0bef6076b062e7494d48fe7b9c3 Mon Sep 17 00:00:00 2001 From: Dmitriy Lazarev Date: Fri, 13 Sep 2024 17:32:59 +0500 Subject: [PATCH] Add missing docs, deprecate @discriminationAlias Signed-off-by: Dmitriy Lazarev --- www/README.md | 2 +- www/deno.lock | 165 +++++++-------- www/docs/codegen.mdx | 2 - www/docs/collections.mdx | 14 -- www/docs/discriminates.mdx | 25 ++- www/docs/extending.mdx | 21 +- www/docs/hydraphql.mdx | 12 +- www/docs/installation.mdx | 408 +++++++++++++++++++++++++++++++++++++ www/docs/introduction.mdx | 213 ------------------- www/docs/querying.mdx | 181 ++++++++++++---- www/docs/quickstart.mdx | 75 ------- www/docs/resolve.mdx | 10 +- www/docs/resolving.mdx | 35 +++- www/docs/structure.json | 6 +- www/docs/why.mdx | 237 +++++++++++++++++++++ www/routes/app.html.tsx | 2 +- 16 files changed, 946 insertions(+), 462 deletions(-) delete mode 100644 www/docs/codegen.mdx delete mode 100644 www/docs/collections.mdx create mode 100644 www/docs/installation.mdx delete mode 100644 www/docs/introduction.mdx delete mode 100644 www/docs/quickstart.mdx create mode 100644 www/docs/why.mdx diff --git a/www/README.md b/www/README.md index 0221b38..973fa92 100644 --- a/www/README.md +++ b/www/README.md @@ -1,4 +1,4 @@ -## Effection Website +## HydraphQL Website ### Development diff --git a/www/deno.lock b/www/deno.lock index 041c99e..bed3b4e 100644 --- a/www/deno.lock +++ b/www/deno.lock @@ -7,7 +7,7 @@ "npm:@twind/core@1.1.3": "npm:@twind/core@1.1.3", "npm:@twind/preset-tailwind@1.1.4": "npm:@twind/preset-tailwind@1.1.4_@twind+core@1.1.3", "npm:@twind/preset-typography@1.0.7": "npm:@twind/preset-typography@1.0.7_@twind+core@1.1.3", - "npm:@types/hast@^3.0.0": "npm:@types/hast@3.0.3", + "npm:@types/hast@^3.0.0": "npm:@types/hast@3.0.4", "npm:hast-util-select@6.0.1": "npm:hast-util-select@6.0.1", "npm:hast-util-to-html@9.0.0": "npm:hast-util-to-html@9.0.0", "npm:rehype-add-classes@1.0.0": "npm:rehype-add-classes@1.0.0", @@ -47,7 +47,7 @@ "@twind/core@1.1.3": { "integrity": "sha512-/B/aNFerMb2IeyjSJy3SJxqVxhrT77gBDknLMiZqXIRr4vNJqiuhx7KqUSRzDCwUmyGuogkamz+aOLzN6MeSLw==", "dependencies": { - "csstype": "csstype@3.1.2" + "csstype": "csstype@3.1.3" } }, "@twind/preset-tailwind@1.1.4_@twind+core@1.1.3": { @@ -84,14 +84,14 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dependencies": {} }, - "@types/hast@2.3.8": { - "integrity": "sha512-aMIqAlFd2wTIDZuvLbhUT+TGvMxrNC8ECUIVtH6xxy0sQLs3iu6NO8Kp/VT5je7i5ufnebXzdV1dNDMnvaH6IQ==", + "@types/hast@2.3.10": { + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", "dependencies": { "@types/unist": "@types/unist@2.0.10" } }, - "@types/hast@3.0.3": { - "integrity": "sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ==", + "@types/hast@3.0.4": { + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": { "@types/unist": "@types/unist@3.0.2" } @@ -102,8 +102,8 @@ "@types/unist": "@types/unist@2.0.10" } }, - "@types/mdast@4.0.3": { - "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", + "@types/mdast@4.0.4": { + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dependencies": { "@types/unist": "@types/unist@3.0.2" } @@ -132,14 +132,14 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dependencies": {} }, - "acorn-jsx@5.3.2_acorn@8.11.2": { + "acorn-jsx@5.3.2_acorn@8.12.1": { "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dependencies": { - "acorn": "acorn@8.11.2" + "acorn": "acorn@8.12.1" } }, - "acorn@8.11.2": { - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "acorn@8.12.1": { + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dependencies": {} }, "astring@1.8.6": { @@ -198,12 +198,12 @@ "integrity": "sha512-JjnG6/pdLJh3iqipq7kteNVtbIczsU2A1cNxb+VAiniSuNmrB/NI3us4rSCfArvlwRXYly+jZhUUfEoInSH9Qg==", "dependencies": {} }, - "csstype@3.1.2": { - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "csstype@3.1.3": { + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dependencies": {} }, - "debug@4.3.4": { - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "debug@4.3.6": { + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "ms@2.1.2" } @@ -224,8 +224,8 @@ "dequal": "dequal@2.0.3" } }, - "diff@5.1.0": { - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "diff@5.2.0": { + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dependencies": {} }, "direction@2.0.1": { @@ -290,10 +290,10 @@ "hast-util-from-parse5@7.1.2": { "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", "dependencies": { - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "@types/unist": "@types/unist@2.0.10", "hastscript": "hastscript@7.2.0", - "property-information": "property-information@6.4.0", + "property-information": "property-information@6.5.0", "vfile": "vfile@5.3.7", "vfile-location": "vfile-location@4.1.0", "web-namespaces": "web-namespaces@2.0.1" @@ -302,13 +302,13 @@ "hast-util-from-parse5@8.0.1": { "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", "dependencies": { - "@types/hast": "@types/hast@3.0.3", + "@types/hast": "@types/hast@3.0.4", "@types/unist": "@types/unist@3.0.2", "devlop": "devlop@1.1.0", "hastscript": "hastscript@8.0.0", - "property-information": "property-information@6.4.0", - "vfile": "vfile@6.0.1", - "vfile-location": "vfile-location@5.0.2", + "property-information": "property-information@6.5.0", + "vfile": "vfile@6.0.2", + "vfile-location": "vfile-location@5.0.3", "web-namespaces": "web-namespaces@2.0.1" } }, @@ -323,13 +323,13 @@ "hast-util-has-property@3.0.0": { "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", "dependencies": { - "@types/hast": "@types/hast@3.0.3" + "@types/hast": "@types/hast@3.0.4" } }, "hast-util-heading-rank@2.1.1": { "integrity": "sha512-iAuRp+ESgJoRFJbSyaqsfvJDY6zzmFoEnL1gtz1+U8gKtGGj1p0CVlysuUAUjq95qlZESHINLThwJzNGmgGZxA==", "dependencies": { - "@types/hast": "@types/hast@2.3.8" + "@types/hast": "@types/hast@2.3.10" } }, "hast-util-is-element@1.1.0": { @@ -339,36 +339,36 @@ "hast-util-is-element@2.1.3": { "integrity": "sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==", "dependencies": { - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "@types/unist": "@types/unist@2.0.10" } }, "hast-util-parse-selector@3.1.1": { "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", "dependencies": { - "@types/hast": "@types/hast@2.3.8" + "@types/hast": "@types/hast@2.3.10" } }, "hast-util-parse-selector@4.0.0": { "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "dependencies": { - "@types/hast": "@types/hast@3.0.3" + "@types/hast": "@types/hast@3.0.4" } }, - "hast-util-raw@9.0.1": { - "integrity": "sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA==", + "hast-util-raw@9.0.4": { + "integrity": "sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==", "dependencies": { - "@types/hast": "@types/hast@3.0.3", + "@types/hast": "@types/hast@3.0.4", "@types/unist": "@types/unist@3.0.2", "@ungap/structured-clone": "@ungap/structured-clone@1.2.0", "hast-util-from-parse5": "hast-util-from-parse5@8.0.1", "hast-util-to-parse5": "hast-util-to-parse5@8.0.0", "html-void-elements": "html-void-elements@3.0.0", - "mdast-util-to-hast": "mdast-util-to-hast@13.0.2", + "mdast-util-to-hast": "mdast-util-to-hast@13.2.0", "parse5": "parse5@7.1.2", "unist-util-position": "unist-util-position@5.0.0", "unist-util-visit": "unist-util-visit@5.0.0", - "vfile": "vfile@6.0.1", + "vfile": "vfile@6.0.2", "web-namespaces": "web-namespaces@2.0.1", "zwitch": "zwitch@2.0.4" } @@ -392,7 +392,7 @@ "hast-util-select@6.0.1": { "integrity": "sha512-KPNOtLqeJCcFRyxQm9BakO3bdIQfremXraw4mh9jxsJ+L593v/VdP3G9Dfjahacl/bw8PPvIFseaXzElKOYRpA==", "dependencies": { - "@types/hast": "@types/hast@3.0.3", + "@types/hast": "@types/hast@3.0.4", "@types/unist": "@types/unist@3.0.2", "bcp-47-match": "bcp-47-match@2.0.3", "comma-separated-tokens": "comma-separated-tokens@2.0.3", @@ -404,7 +404,7 @@ "hast-util-whitespace": "hast-util-whitespace@3.0.0", "not": "not@0.1.0", "nth-check": "nth-check@2.1.1", - "property-information": "property-information@6.4.0", + "property-information": "property-information@6.5.0", "space-separated-tokens": "space-separated-tokens@2.0.2", "unist-util-visit": "unist-util-visit@5.0.0", "zwitch": "zwitch@2.0.4" @@ -415,7 +415,7 @@ "dependencies": { "@types/estree": "@types/estree@1.0.5", "@types/estree-jsx": "@types/estree-jsx@1.0.3", - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "@types/unist": "@types/unist@2.0.10", "comma-separated-tokens": "comma-separated-tokens@2.0.3", "estree-util-attach-comments": "estree-util-attach-comments@2.1.1", @@ -423,7 +423,7 @@ "hast-util-whitespace": "hast-util-whitespace@2.0.1", "mdast-util-mdx-expression": "mdast-util-mdx-expression@1.3.2", "mdast-util-mdxjs-esm": "mdast-util-mdxjs-esm@1.3.1", - "property-information": "property-information@6.4.0", + "property-information": "property-information@6.5.0", "space-separated-tokens": "space-separated-tokens@2.0.2", "style-to-object": "style-to-object@0.4.4", "unist-util-position": "unist-util-position@4.0.4", @@ -433,15 +433,15 @@ "hast-util-to-html@9.0.0": { "integrity": "sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw==", "dependencies": { - "@types/hast": "@types/hast@3.0.3", + "@types/hast": "@types/hast@3.0.4", "@types/unist": "@types/unist@3.0.2", "ccount": "ccount@2.0.1", "comma-separated-tokens": "comma-separated-tokens@2.0.3", - "hast-util-raw": "hast-util-raw@9.0.1", + "hast-util-raw": "hast-util-raw@9.0.4", "hast-util-whitespace": "hast-util-whitespace@3.0.0", "html-void-elements": "html-void-elements@3.0.0", - "mdast-util-to-hast": "mdast-util-to-hast@13.0.2", - "property-information": "property-information@6.4.0", + "mdast-util-to-hast": "mdast-util-to-hast@13.2.0", + "property-information": "property-information@6.5.0", "space-separated-tokens": "space-separated-tokens@2.0.2", "stringify-entities": "stringify-entities@4.0.3", "zwitch": "zwitch@2.0.4" @@ -450,10 +450,10 @@ "hast-util-to-parse5@8.0.0": { "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", "dependencies": { - "@types/hast": "@types/hast@3.0.3", + "@types/hast": "@types/hast@3.0.4", "comma-separated-tokens": "comma-separated-tokens@2.0.3", "devlop": "devlop@1.1.0", - "property-information": "property-information@6.4.0", + "property-information": "property-information@6.5.0", "space-separated-tokens": "space-separated-tokens@2.0.2", "web-namespaces": "web-namespaces@2.0.1", "zwitch": "zwitch@2.0.4" @@ -462,13 +462,13 @@ "hast-util-to-string@2.0.0": { "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==", "dependencies": { - "@types/hast": "@types/hast@2.3.8" + "@types/hast": "@types/hast@2.3.10" } }, "hast-util-to-string@3.0.0": { "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", "dependencies": { - "@types/hast": "@types/hast@3.0.3" + "@types/hast": "@types/hast@3.0.4" } }, "hast-util-whitespace@1.0.4": { @@ -482,26 +482,26 @@ "hast-util-whitespace@3.0.0": { "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "dependencies": { - "@types/hast": "@types/hast@3.0.3" + "@types/hast": "@types/hast@3.0.4" } }, "hastscript@7.2.0": { "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", "dependencies": { - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "comma-separated-tokens": "comma-separated-tokens@2.0.3", "hast-util-parse-selector": "hast-util-parse-selector@3.1.1", - "property-information": "property-information@6.4.0", + "property-information": "property-information@6.5.0", "space-separated-tokens": "space-separated-tokens@2.0.2" } }, "hastscript@8.0.0": { "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", "dependencies": { - "@types/hast": "@types/hast@3.0.3", + "@types/hast": "@types/hast@3.0.4", "comma-separated-tokens": "comma-separated-tokens@2.0.3", "hast-util-parse-selector": "hast-util-parse-selector@4.0.0", - "property-information": "property-information@6.4.0", + "property-information": "property-information@6.5.0", "space-separated-tokens": "space-separated-tokens@2.0.2" } }, @@ -652,7 +652,7 @@ "integrity": "sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==", "dependencies": { "@types/estree-jsx": "@types/estree-jsx@1.0.3", - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "@types/mdast": "@types/mdast@3.0.15", "mdast-util-from-markdown": "mdast-util-from-markdown@1.3.1", "mdast-util-to-markdown": "mdast-util-to-markdown@1.5.0" @@ -662,7 +662,7 @@ "integrity": "sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==", "dependencies": { "@types/estree-jsx": "@types/estree-jsx@1.0.3", - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "@types/mdast": "@types/mdast@3.0.15", "@types/unist": "@types/unist@2.0.10", "ccount": "ccount@2.0.1", @@ -689,7 +689,7 @@ "integrity": "sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==", "dependencies": { "@types/estree-jsx": "@types/estree-jsx@1.0.3", - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "@types/mdast": "@types/mdast@3.0.15", "mdast-util-from-markdown": "mdast-util-from-markdown@1.3.1", "mdast-util-to-markdown": "mdast-util-to-markdown@1.5.0" @@ -705,7 +705,7 @@ "mdast-util-to-hast@12.3.0": { "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", "dependencies": { - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "@types/mdast": "@types/mdast@3.0.15", "mdast-util-definitions": "mdast-util-definitions@5.1.2", "micromark-util-sanitize-uri": "micromark-util-sanitize-uri@1.2.0", @@ -715,17 +715,18 @@ "unist-util-visit": "unist-util-visit@4.1.2" } }, - "mdast-util-to-hast@13.0.2": { - "integrity": "sha512-U5I+500EOOw9e3ZrclN3Is3fRpw8c19SMyNZlZ2IS+7vLsNzb2Om11VpIVOR+/0137GhZsFEF6YiKD5+0Hr2Og==", + "mdast-util-to-hast@13.2.0": { + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", "dependencies": { - "@types/hast": "@types/hast@3.0.3", - "@types/mdast": "@types/mdast@4.0.3", + "@types/hast": "@types/hast@3.0.4", + "@types/mdast": "@types/mdast@4.0.4", "@ungap/structured-clone": "@ungap/structured-clone@1.2.0", "devlop": "devlop@1.1.0", "micromark-util-sanitize-uri": "micromark-util-sanitize-uri@2.0.0", "trim-lines": "trim-lines@3.0.1", "unist-util-position": "unist-util-position@5.0.0", - "unist-util-visit": "unist-util-visit@5.0.0" + "unist-util-visit": "unist-util-visit@5.0.0", + "vfile": "vfile@6.0.2" } }, "mdast-util-to-markdown@1.5.0": { @@ -888,11 +889,11 @@ "vfile-message": "vfile-message@3.1.4" } }, - "micromark-extension-mdxjs@1.0.1_acorn@8.11.2": { + "micromark-extension-mdxjs@1.0.1_acorn@8.12.1": { "integrity": "sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==", "dependencies": { - "acorn": "acorn@8.11.2", - "acorn-jsx": "acorn-jsx@5.3.2_acorn@8.11.2", + "acorn": "acorn@8.12.1", + "acorn-jsx": "acorn-jsx@5.3.2_acorn@8.12.1", "micromark-extension-mdx-expression": "micromark-extension-mdx-expression@1.0.8", "micromark-extension-mdx-jsx": "micromark-extension-mdx-jsx@1.0.5", "micromark-extension-mdx-md": "micromark-extension-mdx-md@1.0.1", @@ -1088,7 +1089,7 @@ "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", "dependencies": { "@types/debug": "@types/debug@4.1.12", - "debug": "debug@4.3.4", + "debug": "debug@4.3.6", "decode-named-character-reference": "decode-named-character-reference@1.0.2", "micromark-core-commonmark": "micromark-core-commonmark@1.1.0", "micromark-factory-space": "micromark-factory-space@1.1.0", @@ -1169,14 +1170,14 @@ "integrity": "sha512-BKU45RMZAA+3npkQ/VxEH7EeZImQcfV6rfKH0O4HkkDz3uqqz+689dbkjiWia00vK390MY6EARPS6TzNS4tXPg==", "dependencies": {} }, - "property-information@6.4.0": { - "integrity": "sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ==", + "property-information@6.5.0": { + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", "dependencies": {} }, "refractor@4.8.1": { "integrity": "sha512-/fk5sI0iTgFYlmVGYVew90AoYnNMP6pooClx/XKqyeeCQXrL0Kvgn8V0VEht5ccdljbzzF1i3Q213gcntkRExg==", "dependencies": { - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "@types/prismjs": "@types/prismjs@1.26.3", "hastscript": "hastscript@7.2.0", "parse-entities": "parse-entities@4.0.1" @@ -1191,7 +1192,7 @@ "rehype-autolink-headings@6.1.1": { "integrity": "sha512-NMYzZIsHM3sA14nC5rAFuUPIOfg+DFmf9EY1YMhaNlB7+3kK/ZlE6kqPfuxr1tsJ1XWkTrMtMoyHosU70d35mA==", "dependencies": { - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "extend": "extend@3.0.2", "hast-util-has-property": "hast-util-has-property@2.0.1", "hast-util-heading-rank": "hast-util-heading-rank@2.1.1", @@ -1203,7 +1204,7 @@ "rehype-parse@8.0.5": { "integrity": "sha512-Ds3RglaY/+clEX2U2mHflt7NlMA72KspZ0JLUJgBBLpRddBcEw3H8uYZQliQriku22NZpYMfjDdSgHcjxue24A==", "dependencies": { - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "hast-util-from-parse5": "hast-util-from-parse5@7.1.2", "parse5": "parse5@6.0.1", "unified": "unified@10.1.2" @@ -1223,7 +1224,7 @@ "rehype-slug@5.1.0": { "integrity": "sha512-Gf91dJoXneiorNEnn+Phx97CO7oRMrpi+6r155tTxzGuLtm+QrI4cTwCa9e1rtePdL4i9tSO58PeSS6HWfgsiw==", "dependencies": { - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "github-slugger": "github-slugger@2.0.0", "hast-util-has-property": "hast-util-has-property@2.0.1", "hast-util-heading-rank": "hast-util-heading-rank@2.1.1", @@ -1245,7 +1246,7 @@ "integrity": "sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==", "dependencies": { "mdast-util-mdx": "mdast-util-mdx@2.0.1", - "micromark-extension-mdxjs": "micromark-extension-mdxjs@1.0.1_acorn@8.11.2" + "micromark-extension-mdxjs": "micromark-extension-mdxjs@1.0.1_acorn@8.12.1" } }, "remark-parse@10.0.2": { @@ -1259,7 +1260,7 @@ "remark-rehype@10.1.0": { "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", "dependencies": { - "@types/hast": "@types/hast@2.3.8", + "@types/hast": "@types/hast@2.3.10", "@types/mdast": "@types/mdast@3.0.15", "mdast-util-to-hast": "mdast-util-to-hast@12.3.0", "unified": "unified@10.1.2" @@ -1300,8 +1301,8 @@ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "dependencies": {} }, - "trough@2.1.0": { - "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "trough@2.2.0": { + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "dependencies": {} }, "unified@10.1.2": { @@ -1312,7 +1313,7 @@ "extend": "extend@3.0.2", "is-buffer": "is-buffer@2.0.5", "is-plain-obj": "is-plain-obj@4.1.0", - "trough": "trough@2.1.0", + "trough": "trough@2.2.0", "vfile": "vfile@5.3.7" } }, @@ -1411,7 +1412,7 @@ "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", "dependencies": { "dequal": "dequal@2.0.3", - "diff": "diff@5.1.0", + "diff": "diff@5.2.0", "kleur": "kleur@4.1.5", "sade": "sade@1.8.1" } @@ -1423,11 +1424,11 @@ "vfile": "vfile@5.3.7" } }, - "vfile-location@5.0.2": { - "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", + "vfile-location@5.0.3": { + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", "dependencies": { "@types/unist": "@types/unist@3.0.2", - "vfile": "vfile@6.0.1" + "vfile": "vfile@6.0.2" } }, "vfile-message@3.1.4": { @@ -1453,8 +1454,8 @@ "vfile-message": "vfile-message@3.1.4" } }, - "vfile@6.0.1": { - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "vfile@6.0.2": { + "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==", "dependencies": { "@types/unist": "@types/unist@3.0.2", "unist-util-stringify-position": "unist-util-stringify-position@4.0.0", @@ -1652,6 +1653,7 @@ "https://deno.land/x/effection@3.0.0-beta.1/lib/run/frame.ts": "b9b85112e3168c0fc91e01b1df60f2e911ee1a44314678944d9cbfa71b0641de", "https://deno.land/x/effection@3.0.0-beta.1/lib/run/scope.ts": "0e164df8b9825ac1aef3ff1e35a85cf6c82ac48318ba9942d76bf477337895ca", "https://deno.land/x/effection@3.0.0-beta.1/lib/run/task.ts": "b4b019d6e32d4c22c83ea9d1cfd64a3d587587080d459aec00aa9e6ba9d49b2a", + "https://deno.land/x/effection@3.0.0-beta.1/lib/run/types.ts": "c6e413c941613e364604643a084eb432a7ba3ec468e5b0f5341c09b2f54510ef", "https://deno.land/x/effection@3.0.0-beta.1/lib/run/value.ts": "d57428b45dfeecc9df1e68dadf8697dbc33cd412e6ffcab9d0ba4368e8c1fbd6", "https://deno.land/x/effection@3.0.0-beta.1/lib/shift-sync.ts": "74ecefa9cb2e145a3c52f363319f8d6296b804600852044b7d14bd53bc10b512", "https://deno.land/x/effection@3.0.0-beta.1/lib/signal.ts": "acae6b2f348a9c8c6bc43a9a5bd53e0187c8048619d7e0c6d6cf70e3a46bb8bf", @@ -1701,6 +1703,7 @@ "https://deno.land/x/esbuild_deno_loader@0.8.2/src/plugin_deno_loader.ts": "166356133ee63d80e5559a10c18e10b625da96e39a4518b8c7adfef718bb4e32", "https://deno.land/x/esbuild_deno_loader@0.8.2/src/plugin_deno_resolver.ts": "0449ed23ae93db1ec74d015a46934aefd7ba7a8f719f7a4980b616cb3f5bbee4", "https://deno.land/x/esbuild_deno_loader@0.8.2/src/shared.ts": "33052684aeb542ebd24da372816bbbf885cd090a7ab0fde7770801f7f5b49572", + "https://deno.land/x/hastx@v0.0.10/deps.ts": "6c9b4e0a1d0120f3be92d74baa68b83b3730de22e7c4fdee7b2631189a8b3336", "https://deno.land/x/hastx@v0.0.10/html.ts": "54f86c5378dd7282142c9847efaf20b7ee244743f16af20a62900e912d1ae810", "https://deno.land/x/importmap@0.2.1/_util.ts": "ada9a9618b537e6c0316c048a898352396c882b9f2de38aba18fd3f2950ede89", "https://deno.land/x/importmap@0.2.1/mod.ts": "ae3d1cd7eabd18c01a4960d57db471126b020f23b37ef14e1359bbb949227ade", diff --git a/www/docs/codegen.mdx b/www/docs/codegen.mdx deleted file mode 100644 index 370dcb4..0000000 --- a/www/docs/codegen.mdx +++ /dev/null @@ -1,2 +0,0 @@ -TODO: How to add and use `codegen` config and examples -TODO: Reveal transforming directives and other graphql stuff diff --git a/www/docs/collections.mdx b/www/docs/collections.mdx deleted file mode 100644 index 0bc7a41..0000000 --- a/www/docs/collections.mdx +++ /dev/null @@ -1,14 +0,0 @@ -TODO: Difference between arrays and connections -TODO: Structure of Connection -TODO: Mention `@resolve/@relation` -TODO: Add link to faq -TODO: Link to codegen and transforming `Connection` to specific type - - -TODO: Cover cases like and how to handle them: - - single ref resolved to array - - single ref resolved to connection - - array of refs resolved to array - - array of refs resolved to connection - - each ref from array resolved to array - - each ref from array resolved to connection diff --git a/www/docs/discriminates.mdx b/www/docs/discriminates.mdx index e788ec7..916678a 100644 --- a/www/docs/discriminates.mdx +++ b/www/docs/discriminates.mdx @@ -88,28 +88,27 @@ option. With that option HydraphQL will generate opaque types for all interfaces const application = await createApplication({ generateOpaqueTypes: true }); ``` -### `@discriminationAlias` +### Discrimination aliases By default value from `with` argument is used to find a type as-is or converted to PascalCase. And it's fairly enough for most cases. But sometimes you need to -match the value with a type that has a different name. In this case, you can use -`@discriminationAlias` directive. +match the value with a type that has a different name. In this case, you can define +`aliases` argument. ```graphql interface API @implements(interface: "Node") - @discriminates(with: "spec.type") - @discriminationAlias(from: "openapi", to: "OpenAPI") { + @discriminates(with: "spec.type", aliases: [{ from: "grpc", to: "GrpcAPI" }]) { # ... } -type OpenAPI @implements(interface: "API") { +type GrpcAPI @implements(interface: "API") { # ... } ``` -This means, when `spec.type` equals to `openapi`, the `API` interface will be -resolved to `OpenAPI` type. +This means, when `spec.type` equals to `gprc`, the `API` interface will be +resolved to `GrpcAPI` type. ### Using `@discriminates` with multiple data sources @@ -128,9 +127,13 @@ const loader = createLoader({ ```graphql extend interface Node - @discriminates(with: "__source") - @discriminationAlias(value: "MyApi", type: "MyApiNode") - @discriminationAlias(value: "AnotherApi", type: "AnotherApiNode") {} + @discriminates( + with: "__source", + aliases: [ + { value: "MyApi", type: "MyApiNode" }, + { value: "AnotherApi", type: "AnotherApiNode" } + ] + ) {} type MyApiNode @implements(interface: "Node") { # ... diff --git a/www/docs/extending.mdx b/www/docs/extending.mdx index 060be64..c4de36a 100644 --- a/www/docs/extending.mdx +++ b/www/docs/extending.mdx @@ -39,13 +39,12 @@ The GraphQL API provides 2 syntaxes for defining edges: Use one-to-one syntax when a node is related to only one node. For example, a component can have only one owner is expressed in the following way. For more -information checkout [`@resolve`][resolve], There is also [`@relation`][relation] directive -implemented in the [Backstage Catalog GraphQL plugin][catalog-module] that resolves nodes -with slightly different way. +information checkout [`@relation`][relation], There is also [`@resolve`][resolve] directive +that could be used for resolving nodes from 3rd party data sources. ```graphql type Component { - owner: User @resolve(at: "ownerId") + owner: User @relation(name: "ownedBy") } ``` @@ -59,11 +58,11 @@ section for more information. ```graphql type Component { - modules: [Module!] @resolve(at: "modules") + modules: [Module!] @relation(name: "modulesOf") } type Group { - members: Connection @resolve(at: "members", from: "UserAPI") + members: Connection @relation(name: "hasMember", nodeType: "User") } ``` @@ -249,9 +248,13 @@ server.listen(4000, () => { ```graphql extend interface Node - @discriminates(with: "__source") - @discriminationAlias(value: "MyApi", type: "MyApiNode") - @discriminationAlias(value: "Catalog", type: "Entity") + @discriminates( + with: "__source", + aliases: [ + { value: "MyApi", type: "MyApiNode" }, + { value: "Catalog", type: "Entity" } + ] + ) type Entity @implements(interface: "Node") { title: String @field(at: "metadata.title") diff --git a/www/docs/hydraphql.mdx b/www/docs/hydraphql.mdx index 5da89cf..70ee955 100644 --- a/www/docs/hydraphql.mdx +++ b/www/docs/hydraphql.mdx @@ -269,10 +269,14 @@ In the last step we need to update our schema ```graphql extend interface Node - @discriminates(with: "__source") - @discriminationAlias(value: "Assets", type: "Asset") - @discriminationAlias(value: "Markets", type: "Market") - @discriminationAlias(value: "Exchanges", type: "Exchange") {} + @discriminates( + with: "__source", + aliases: [ + { value: "Assets", type: "Asset" }, + { value: "Markets", type: "Market" }, + { value: "Exchanges", type: "Exchange" } + ] + ) {} type Asset @implements(interface: "Node") { symbol: String! @field diff --git a/www/docs/installation.mdx b/www/docs/installation.mdx new file mode 100644 index 0000000..e299978 --- /dev/null +++ b/www/docs/installation.mdx @@ -0,0 +1,408 @@ +The [Backstage GraphQL plugin][graphql-backend] is designed for schema-first +development of the GraphQL API. It reduces work necessary to expand the schema +using schema directives. [Schema directives](https://the-guild.dev/graphql/tools/docs/schema-directives) +are extensions to GraphQL schema used to automate implementation of the GraphQL API. +In Backstage GraphQL Plugin, schema directives are used to automatically create +resolvers and reduce code duplication. [Resolvers](https://graphql.org/learn/execution/) +tell a GraphQL API how to provide data for a specific schema type or field. +The Backstage GraphQL Plugin uses what it knows about the Backstage catalog to +reduce the need for writing resolvers that call the catalog. + +### Getting started + +To start using the Backstage GraphQL plugin, you need to install the plugin itself +and the Catalog module. + +```bash +yarn workspace backend add \ + @frontside/backstage-plugin-graphql-backend \ + @frontside/backstage-plugin-graphql-backend-module-catalog +``` + +Then you need to add it them to your backend. Currently, Backstage implemented two +backend systems: + +- [Backstage backend system](https://backstage.io/docs/backend-system/) +- [Old Backstage plugins API](https://backstage.io/docs/plugins/backend-plugin) + +### Usage with Backstage backend system + +Adding GraphQL plugin to your Backstage backend is pretty straightforward. + +```typescript +// packages/backend/src/index.ts +import { graphqlPlugin } from '@frontside/backstage-plugin-graphql-backend' +import { graphqlModuleCatalog } from '@frontside/backstage-plugin-graphql-backend-module-catalog' + +// Initializing Backstage backend +const backend = createBackend() + +// Adding GraphQL plugin +backend.use(graphqlPlugin()) +// Adding Catalog module +backend.use(graphqlModuleCatalog()) +``` + +The GraphQL plugin starts [Yoga](https://the-guild.dev/graphql/yoga-server) +a full-featured GraphQL server which adds a new `/api/graphql` endpoint. +And the Catalog module extends the Core GraphQL schema with basic Catalog types +and Catalog Data Loader. + +If you'd like to extend the GraphQL schema with your own types, you can use +`graphqlModuleRelationResolver` module instead of `graphqlModuleCatalog`. + +```typescript +// packages/backend/src/index.ts +import { graphqlModuleRelationResolver } from '@frontside/backstage-plugin-graphql-backend-module-catalog' + +// ... + +backend.use(graphqlModuleRelationResolver()) +``` + +The only difference between those two modules is that `graphqlModuleRelationResolver` +doesn't extend the GraphQL schema with additional types. It only adds `@relation` +directive and Catalog Data Loader. + +### Usage with old Backstage backend plugins API + +If you haven't migrated to the new Backstage backend system yet, you can still +use the GraphQL plugin, but you need to do a little bit more work. As usual, +you need to declare `createPlugin` function where you can initialize GraphQL Plugin. +So `@frontside/backstage-plugin-graphql-backend` provides `createRouter` function +to initialize `/api/graphql` endpoint handler. Additionally you need to pass +`Catalog` GraphQL module and create Catalog Data Loader. + +```typescript +// packages/backend/src/plugins/graphql.ts +import { createRouter } from '@frontside/backstage-plugin-graphql-backend' +import { Catalog, createCatalogLoader } from '@frontside/backstage-plugin-graphql-backend-module-catalog' + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + modules: [Catalog], + loaders: { ...createCatalogLoader(env.catalogClient) }, + }) +} +``` + +Then you need to add this plugin to your backend. + +```typescript +// packages/backend/src/index.ts +import { createPlugin } from './plugins/graphql' + +// ... + +async function main() { + // ... + + const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')) + apiRouter.use('/graphql', await graphql(graphqlEnv)) + + // ... +} +``` + +As well as with the new backend system if you don't want to use predefined Catalog +types. You can pass `Relation` GraphQL module instead of `Catalog`. + +### GraphQL Playground (GraphiQL) + +After you've added GraphQL plugin to your backend, you can install +[Backstage GraphiQL plugin](https://github.com/backstage/backstage/tree/master/plugins/graphiql#readme) +The same plugin is installed in [Backstage Demo](http://demo.backstage.io/graphiql). +It allows you to play around with GraphQL API and explore the schema. + +```bash +yarn workspace app add @backstage/plugin-graphiql +``` + +After you installed it, you need to add `/graphiql` route to your app. + +```diff +# packages/app/src/App.tsx ++import { GraphiQLPage } from '@backstage/plugin-graphiql' + + const routes = ( + ++ } /> + + ) +``` + +Then add the GraphQL endpoint to GraphQLBrowse API. + +```typescript +// packages/app/src/apis.ts +import { createApiFactory, discoveryApiRef }from '@backstage/core-plugin-api' +import { graphQlBrowseApiRef, GraphQLEndpoints } from '@backstage/plugin-graphiql' + +export const apis = [ + createApiFactory({ + api: graphQlBrowseApiRef, + deps: { discovery: discoveryApiRef }, + factory: ({ discovery }) => + GraphQLEndpoints.from([ + GraphQLEndpoints.create({ + id: 'catalog', + title: 'Catalog', + fetcher: async (params: any) => { + const graphqlURL = await discovery.getBaseUrl('graphql') + return fetch(graphqlURL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + }).then(res => res.json()) + }, + }) + ]), + }), +] +``` + +Now you can start your app and open `http://localhost:3000/graphiql` to explore +the GraphQL API. + +### Frontend GraphQL client + +And here we come to the most interesting part. How to use GraphQL API from the +frontend. There are a few popular GraphQL clients: + +- [`graphql-request`](https://github.com/jasonkuhrt/graphql-request) - Minimal +GraphQL client supporting Node and browsers for scripts or simple apps. +- [`Apollo Client`](https://github.com/apollographql/apollo-client) - A fully-featured, +production ready caching GraphQL client for every UI framework and GraphQL server. +- [`urql`](https://github.com/urql-graphql/urql) - the highly customizable and +versatile GraphQL client with which you add on features like normalized caching +as you grow. +- [`Relay`](https://github.com/facebook/relay) - is a JavaScript framework for +building data-driven React applications. +- [`graphql-hooks`](https://github.com/nearform/graphql-hooks) - Minimal hooks-first +GraphQL client. + +We recommend start with `graphql-request` and then with increasing requirements and +complexity move to more powerful clients. + +```bash +yarn workspace app add graphql graphql-request +``` + +To start using it you need to define a new API through `createApiFactory` function. + +```typescript +// packages/app/src/apis.ts +import { GraphQLClient } from 'graphql-request' +import { createApiRef, createApiFactory, configApiRef }from '@backstage/core-plugin-api' + +export const graphqlApiRef = createApiRef({ + id: 'plugin.graphql.service', +}) + +export const apis = [ + // ... + createApiFactory({ + api: graphqlApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => { + // NOTE: `discoveryApi.getBaseUrl` returns a promise which can't be used in this function + const graphqlURL = `${configApi.getString('backend.baseUrl')}/api/graphql` + return new GraphQLClient(graphqlURL) + } + }), +] +``` + +Then you can use it in your components. + +```tsx +// packages/app/src/components/MyComponent.tsx +import { useQuery } from '@tanstack/react-query' +import { gql } from 'graphql-request' +import { useApi } from '@backstage/core-plugin-api' +import { parseEntityRef } from '@backstage/catalog-model' +import { graphqlApiRef } from '../apis' + +const query = gql` + query getEntity($kind: String!, $name: String!, $namespace: String) { + entity(name: $name, kind: $kind, namespace: $namespace) { + # ... + } + } +` + +export const MyComponent = ({ entityRef }) => { + const graphqlClient = useApi(graphqlApiRef) + + const { isPending, isError, data: { entity }, error } = useQuery({ + queryKey: ['entity', entityRef], + queryFn: () => graphqlClient.request(query, parseEntityRef(entityRef)), + }) + + return ( +
+ {isPending && /* Loading state */} + {isError && /* Error message */} + {entity && /* Render entity */} +
+ ) +} +``` + +`graphql-request` is pretty straightforward and easy to use. But it lacks some useful +features or requires additional work to use them. For example, caching or +fragments batching. So if you need more features you can switch to more powerful +clients like `urql` or `graphql-hooks`. More advanced techniques is covered in +[querying] section. + +### Backend GraphQL client + +For making GraphQL requests from the backend you can use `graphql-request` as well. +But it's not the best option. The much better is to use [`@graphql-codegen`][codegen] +to generate well-typed GraphQL client by using GraphQL schema. + +At first you need to install a few packages. + +```bash +yarn workspace backend add \ + @graphql-codegen/cli \ + @graphql-codegen/typescript \ + @graphql-codegen/typescript-operations \ + @graphql-codegen/typescript-graphql-request +``` + +Then you need to create `schema.ts` file for GraphQL schema. + +```typescript +// ./packages/backend/src/schema.ts +import { transformSchema } from "@frontside/hydraphql" +import { printSchemaWithDirectives } from "@graphql-tools/utils" +import { Catalog } from '@frontside/backstage-plugin-graphql-backend-module-catalog' + +export const schema = printSchemaWithDirectives( + transformSchema([Catalog()]) +) +``` + +After that you can create `codegen.ts` config file. + +```typescript +import type { CodegenConfig } from "@graphql-codegen/cli" +import { schema } from "./src/schema" + +const config: CodegenConfig = { + schema, + generates: { + "./src/client.ts": { + plugins: [ + "typescript", + "typescript-operations", + "typescript-graphql-request", + ], + }, + }, +} + +export default config; +``` + +And finally run codegen to generate GraphQL client. + +```bash +yarn workspace backend run graphql-codegen --config codegen.ts +``` + +Usage of the generated client is pretty straightforward. + +```typescript +import { GraphQLClient } from 'graphql-request' +import { getSdk } from './client' + +const graphqlClient = new GraphQLClient('http://localhost:7000/api/graphql') +const sdk = getSdk(graphqlClient) + +const { entity } = await sdk.entity({ + kind: 'Component', + name: 'backstage', +}) +``` + +### GraphQL Code Generation + +In the previous part we've already used GraphQL Code Generation to generate +GraphQL client for Backstage Backend. But it's not the only thing you can do +with it. You can generate TypeScript types for GraphQL schema and even customize +it with your own codegen plugins to process the schema whatever you like. + +Let's start with explaining what `./packages/backend/src/schema.ts` file is. +Because Backstage GraphQL Plugin has modular architecture and GraphQL schema +uses different GraphQL directives to enhance the schema. So it's not possible +to use schema directly without post-processing to unwind all directives. It +happens in runtime when you start the backend. And for codegen we need to get +the same schema. So we use `transformSchema` function from `@frontside/hydraphql` +which is the core package of Backstage GraphQL Plugin. + +With `@graphql-tools/schema` you can get GraphQL schema of your backend. + +```typescript +const config: CodegenConfig = { + /* ... */ + generates: { + "./__generated__/schema.graphql": { + plugins: ["schema-ast"], + }, + }, +} +``` + +And with `@graphql-codegen/client-preset` you can generate typed queries for +your frontend. + +```typescript +const config: CodegenConfig = { + schema: '../backend/__generated__/schema.graphql', + documents: ['src/**/*.tsx'], + ignoreNoDocuments: true, // for better experience with the watcher + generates: { + './src/gql/': { preset: 'client' }, + }, +} +``` + +And then use it in your components. + +```tsx +// packages/app/src/components/MyComponent.tsx +import { graphql } from '../gql' + +const query = graphql(/* GraphQL */` + query getEntity($kind: String!, $name: String!, $namespace: String) { + entity(name: $name, kind: $kind, namespace: $namespace) { + # ... + } + } +`) + +export const MyComponent = ({ entityRef }) => { + const graphqlClient = useApi(graphqlApiRef); + + // NOTE: `entity` is typed + const { isPending, isError, data: { entity }, error } = useQuery({ + queryKey: ['entity', entityRef], + queryFn: () => graphqlClient.request(query, parseEntityRef(entityRef)), + }) + + /* ... */ +} +``` + +[graphql-backend]: https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend +[catalog-module]: https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend-module-catalog +[catalog-schema]: https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend-module-catalog/src/catalog +[relation-schema]: https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend-module-catalog/src/relation +[codegen]: https://the-guild.dev/graphql/codegen +[extending]: ./extending +[querying]: ./querying diff --git a/www/docs/introduction.mdx b/www/docs/introduction.mdx deleted file mode 100644 index 43b1397..0000000 --- a/www/docs/introduction.mdx +++ /dev/null @@ -1,213 +0,0 @@ -### Graph as the medium - -The Backstage catalog model is deliberately designed to be open to extension -so that it can house whatever set of attributes a company might need to represent -its entities. This means that the `spec` property of an Entity can contain -anything (which is good), but at the cost of being awkward to work with because -it is just a blob of un-typed JSON data over which each client must layer its -own structure at the point of consumption. For example: - -```typescript -let employee = await client.getEntityByRef("employee:cowboyd"); - -// let's say that "address" was changed to "location" -employee.spec.address.country; // TypeError: property 'country' does not exist on undefined -``` - -If the shape of the data changes, the client side types will just be straight -up wrong, and as a result must be carefully kept in sync with the backend. - -The story is similar when it comes to working with relationships. The flexibility -of the catalog model in this regard is powerful in that any entity can be related -to any other entity, and the type of relation that can join two entities together -in this manner is unbounded. Once again however, [working] with [these relationships][these] -can [be awkward][awkward] using the `CatalogApi` by itself. - -Not only is it cumbersome, it can also be inefficient. This is because without a -[bespoke batching mechanism][batching] at the point of consumption, the lowest -activation energy solution is going to suffer from the N+1 problem by manually -dereferencing each relationship: - -```typescript -const [ownedByRelation] = getEntityRelations(entity, RELATION_OWNED_BY); -const owner = ownedByRelation - ? await client.getEntityByRef(ownedByRelation.targetRef) - : null; -``` - -### GraphQL API - -Compare how GraphQL works where if we attempt to query a field that does not exist, -it immediately triggers an error. This is because we cannot express the API -without also expressing its shape. There is more info about [querying] - -```typescript -let employee = await client.query( - `node(id: "employee:cowboyd") { address { country } }` -); // TypeError: field "address" does not exist on type "Employee" -``` - -Even better, we can use [GraphQL Codegen](https://the-guild.dev/graphql/codegen) -to surface this category of error at *compile time* which can yield enormous -times savings for each dev cycle. Check out how to setup [codegen]. - -In terms of relationships a GraphQL API has the possibility of both querying -and traversing relationships as the natural expression of its schema, we can -just access it directly: - -```typescript -const owner = entity.ownedBy; -``` - -And, because resolution happens on the server, it can be responsible for properly -batching entity loads according to the depth of the query, thereby removing -[the burden][burden] from the shoulders of the developer. - -### GraphQL Schema - -The entity format is great for representing the graph internally, but it imposes -unnecessary ceremony when working with it from the outside. As an API consumer, -nobody cares whether a description of a user's role is in `metadata.description`, -or if their name to display on a web page is inside `spec.profile.displayName`, -they just want the data. By the same token, in order to work with related entities, -nobody wants to have to go through the rigmarole of reading relations and -assembling them by hand. Instead, they just want to access the related records -directly. - -To make this happen, we define a set of [GraphQL directives][directives] that -allow us to directly map values from an entity to a GraphQL type and thereby -generate the resolvers for it automatically. To map the fields like the ones -above, we introduce a [`@field`][field] directive to pull data out of the envelope and -give it directly to the developer. - -```graphql -interface Entity { - name: String! @field(at: "metadata.name") - kind: String! @field(at: "kind") - namespace: String! @field(at: "metadata.namespace", default: "default") - title: String! @field(at: "metadata.title", default: "") - description: String! @field(at: "metadata.description", default: "") - tags: [String] @field(at: "metadata.tags") - links: [EntityLink] @field(at: "metadata.links") -} -``` - -> 💡By default the kind of an entity is determined by the GraphQL type's name, -> so in the preceding example, the entities to which this applies will be -> kind: "User" more on this mechanism later - -GraphQL provides the opportunity to navigate relationships in the same way as -you would a simple data structure, and because the Backstage catalog has a -normalized way of expressing relations between nodes, we can leverage it to -automatically write the resolvers for related records. To do this, we introduce -a [`@relation`][relation] directive. Take this example snippet from the proposed -catalog schema: - -```graphql -union Owner = User | Group - -type System { - owner: Owner! @relation(name: "ownedBy") - domain: Domain @relation(name: "partOf") - apis: Connection @relation(name: "hasPart", nodeType: "API", kind: "api") - components: Connection - @relation(name: "hasPart", nodeType: "Component", kind: "component") - resources: Connection - @relation(name: "hasPart", nodeType: "Resource", kind: "resource") -} -``` - -This will tell the catalog loader to look up the relation of type “ownedBy” and -use its target as the value of the `owner` property. - -```typescript -system.owner.name //=> "team-a" -``` - -With all advantages that GraphQL provides there are some aspects that might add -difficulties of maintaining GraphQL schema. One of that aspects is GraphQL -doesn't have type inheritance, instead it uses composition: - -```graphql -interface Node { - id: ID! -} - -interface Entity implements Node { - id: ID! - #... Entity fields ... -} - -type System implements Node & Entity { - id: ID! - #... Entity fields ... - #... System fields ... -} -``` - -Which means if type `System` implements `Node` and `Entity` interfaces developer -must declare all fields of implementing interfaces. As a result GraphQL schema -contains a lot of repeating code. To simplify work with types we introduce a -[`@implements`][implements] directive so code from above will be: - -```graphql -interface Node { - id: ID! -} - -interface Entity @implements(interface: "Node") { - #... Entity fields ... -} - -type System @implements(interface: "Entity") { - #... System fields ... -} -``` - -GraphQL Plugin has a schema mapper that process [`@implements`][implements] -directives and unwind them to the final schema - -Another aspect of working with GraphQL is an input data must be resolved to a -specific object type and developer have to implement type resolvers for each -interface they have. That leads to bunch of imperative code alongside with -declarative schema and as any other project's code it requires to be covered by -unit tests. In the most cases these code will be straight forward by matching -certain fields' values with object type names, so why not to make life easier -through a declarative approach. To make it real we introduce -[`@discriminates` and `@discriminationAlias`][discriminates] directives: - -```graphql -interface Entity @discriminates(with: "kind") { - #... Entity fields ... -} - -interface Component - @implements(interface: "Entity") - @discriminates(with: "spec.type") - @discriminationAlias(value: "service", type: "Service") - @discriminationAlias(value: "website", type: "Website") - @discriminationAlias(value: "library", type: "Library") { - #... Component fields ... -} -``` - -Let's dive into this. By `with` argument we are telling GraphQL application to -take a value from a field `kind` of input data and use it as a type name to -discriminate input data as a specific type. For example `kind == 'Component'` -and `Component` is an interface, but we need to resolve our data to object type, -so we have to go further. For `Component` interface we tell GraphQL application -to look at `spec.type` field and here we declared a few aliases to what type -data should be resolve for specific value. - -[working]: https://github.com/thefrontside/backstage/blob/master/plugins/catalog-react/src/utils/getEntityRelations.ts#L29-L48 -[these]: https://github.com/thefrontside/backstage/blob/cf28822/plugins/catalog/src/components/AboutCard/AboutContent.tsx#L84-L97 -[awkward]: https://github.com/thefrontside/backstage/blob/cf28822/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx#L172-L265 -[batching]: https://github.com/thefrontside/backstage/blob/master/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts -[burden]: https://github.com/thefrontside/backstage/blob/master/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts -[directives]: https://graphql.org/learn/queries/#directives -[field]: ./field -[relation]: ./relation -[implements]: ./implements -[discriminates]: ./discriminates -[querying]: ./querying -[codegen]: ./codegen diff --git a/www/docs/querying.mdx b/www/docs/querying.mdx index 4779ad4..bd4d0f9 100644 --- a/www/docs/querying.mdx +++ b/www/docs/querying.mdx @@ -1,17 +1,115 @@ -## Frontend Development +In [installation] we briefly covered how to query the GraphQL API from the frontend. +Here you can learn more about GraphQL queries and fragments. -### How to query the GraphQL API? +### Using urql for GraphQL queries + +[`urql`](https://formidable.com/open-source/urql/docs/) is more advanced GraphQL +client than `graphql-request` but at the same time is more powerful and flexible. +There is nice [article](https://the-guild.dev/blog/unleash-the-power-of-fragments-with-graphql-codegen) +that covered how to use `urql` with `graphql-codegen`. + +Let's start with installing required packages: + +```bash +yarn workspace backend add \ + urql \ + @graphql-codegen/cli \ + @graphql-codegen/client-preset +``` + +Then you need to create `codegen.ts` file. You can notice that in `schema` field +is used GraphQL schema which should be generated in the backend package by using +`@graphql-codegen/schema-ast` plugin. + +```typescript +// ./packages/app/codegen.ts +import type { CodegenConfig } from "@graphql-codegen/cli" + +const config: CodegenConfig = { + schema: '../backend/__generated__/schema.graphql', + documents: ['src/**/*.tsx?'], + ignoreNoDocuments: true, // for better experience with the watcher + generates: { + './src/gql/': { preset: 'client' }, + }, +} +``` + +Now you can add `codegen` script to the `package.json` file: -We provide a `useGraphQL` hook that you can use to send queries and mutations to the GraphQL API. +```json +// ./packages/app/package.json +{ + "scripts": { + "codegen": "graphql-codegen --config codegen.ts" + } +} +``` -The `useGraphQL` hook takes two arguments: +Right now since you haven't written any GraphQL queries the `codegen` script +generates mostly useless files. But before starting writing queries you need to +initialize `urql` client. The initiating process is the same as described in +[installation] section. + +```typescript +// packages/app/src/apis.ts +import { Client, cacheExchange, fetchExchange } from 'urql' +import { createApiRef, createApiFactory, configApiRef }from '@backstage/core-plugin-api' + +export const graphqlApiRef = createApiRef({ + id: 'plugin.graphql.service', +}) + +export const apis = [ + // ... + createApiFactory({ + api: graphqlApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => { + // NOTE: `discoveryApi.getBaseUrl` returns a promise which can't be used in this function + const graphqlURL = `${configApi.getString('backend.baseUrl')}/api/graphql` + return new Client({ + url: graphqlURL, + exchanges: [cacheExchange, fetchExchange], + }) + } + }), +] +``` -1. operation - which can be query or a mutation -2. variables - data that is passed to the query +Then you need to create a provider component with `urql` client and wrap your +application with it. ```tsx -import { graphql } from '../../../../__generated__'; -import { useGraphQL } from '../../../../hooks'; +// ./packages/app/src/App.tsx +import { Provider } from 'urql' +import { apis } from './apis' + +const GraphQLProvider = ({ children }) => { + const graphqlClient = useApi(graphqlApiRef) + return ( + + {children} + + ) +} + +// ... + +export default app.createRoot( + + {/* ... */} + , +) +``` + +Finally you can start writing queries. + +### How to query the GraphQL API? + +```tsx +import { useQuery } from 'urql'; +import { graphql } from './gql'; const MyComponentQuery = graphql(/* GraphQL */ ` query MyComponent($kind: String!, $name: String!, $namespace: String) { @@ -26,11 +124,15 @@ const MyComponentQuery = graphql(/* GraphQL */ ` `); export function MyComponent() { - const { isLoading, error, data } = useGraphQL(MyComponentQuery, { - kind: 'Component', - name: 'my-component', - namespace: 'default', + const [result] = useQuery({ + query: MyComponentQuery, + variables: { + kind: 'Component', + name: 'my-component', + namespace: 'default', + } }); + const { data, fetching, error } = result return data?.entity?.__typename === 'Component' ? data.entity.name : null; } @@ -38,15 +140,13 @@ export function MyComponent() { There are a few important pieces here, -1. `useGraphQL` hook expects that your operation will have a name, e.g. our example uses `query MyComponent(...)` instead of `query(...)`. It doesn't support anonymous operations to allow us to automatically handle caching. Note that global queries need to be globally unique. +1. `useQuery` hook expects that your query will have a name, e.g. our example uses `query MyComponent(...)` instead of `query(...)`. It doesn't support anonymous operations to allow us to automatically handle caching. Note that global queries need to be globally unique. 2. `graphql(/* GraphQL */ \`` - gives you syntax highlighting in VSCode. You need to install [GraphQL: Language Feature Support](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql) to see it. 3. `__typename` is a very useful field. It allows you to narrow the type so you don't have conditional types. Notice that after `data?.entity?.__typename === 'Component'`, `data.entity.name` is doesn't need optional chaining. -Under the hood, `useGraphQL` uses a combination of `react-query` and `graphql-request`. `react-query` handles retries, failures, and caching, among other things. It expects the query function as a `Promise`. `graphql-request` is a light helper on top of the fetch API which returns that expected `Promise`. - ### Type-safe queries with TypeScript -The project is configured to generate TypeScript types for GraphQL queries using `[graphql-codegen](https://the-guild.dev/graphql/codegen)`. It's configured as part of the `yarn dev` script. While running `yarn dev` or other `yarn dev:*` commands, `graphql-codegen` will watch `tsx` and `ts` files for strings tagged with `/* GraphQL */` to find operations and generate TypeScript types for these operations. The resulting TypeScript types will be generated to `packages/app/src/__generated__` directory. You can import `graphql` functon from there using relative imports. +The project is configured to generate TypeScript types for GraphQL queries using [`graphql-codegen`](https://the-guild.dev/graphql/codegen). It's configured as part of the `graphql-codegen` script. While running the script `graphql-codegen` will watch `tsx` and `ts` files for strings tagged with `/* GraphQL */` to find operations and generate TypeScript types for these operations. The resulting TypeScript types will be generated to `packages/app/src/gql` directory. You can import `graphql` function from there using relative imports. ### GraphQL Queries and Fragments @@ -56,10 +156,10 @@ Queries and Fragments are two related concepts that you should be aware of. A qu ### Graphql Queries -You can declare a query by passing a string to the `graphql` function imported from `packages/app/src/__generated__`. You have to use the relative import. +You can declare a query by passing a string to the `graphql` function imported from `packages/app/src/gql`. You have to use the relative import. ```jsx -import { graphql } from '../../../../__generated__'; +import { graphql } from './gql'; export const ComponentPageQuery = graphql(/* GraphQL */ ` query ComponentPage($name: String!, $namespace: String) { @@ -72,12 +172,13 @@ export const ComponentPageQuery = graphql(/* GraphQL */ ` `); ``` -You can fetch the query with `useGraphQL(ComponentPageQuery, { name: 'my-component'})` hook. +You can fetch the query with `useQuery({ query: ComponentPageQuery, variables: { name: 'my-component'} })` hook. ```tsx export function ComponentPage() { - const { isLoading, error, data } = useGraphQL(ComponentPageQuery, { - name: 'my-component', + const [{ fetching, error, data }] = useQuery({ + query: ComponentPageQuery, + variables: { name: 'my-component' } }); return data?.entity?.__typename === 'Component' ? data.entity.name : null; @@ -86,41 +187,45 @@ export function ComponentPage() { ### GraphQL Fragments -In the query below, we have multiple fragments, signified by the `...`. The first fragment, `... on GithubOrganization`, is defined within the backend graphql modules. It allows scoping a query to data on a specific type of entity. +In the query below, we have one of the fragments, signified by the `...`. The first fragment, `... on Component`, is defined within the backend graphql modules. It allows scoping a query to data on a specific type of entity. ```tsx -import { graphql } from '../../../../__generated__'; +import { graphql } from './gql'; const AboutCardFragment = graphql(/* GraphQL */ ` - fragment AboutCard on Component { - ref - name - description - system { - ref - name - } - domain { + fragment AboutCardFragment on Entity { + ... on Component { ref name + description + system { + ref + name + } + domain { + ref + name + } } } `); ``` -Within the component using this fragment, it requires the `useFragment()` hook and a few type parameters. We set the `` to be the `const` variable of the query, in this case `GithubOrganizationRepositoriesFragment`. The same is passed to the `useFragment` function as the first arg, and the respective props as the second arg. From here, `entity` should be fully typed and usuable within the React component. +Within the component using this fragment, it requires the `useFragment()` hook and a few type parameters. We set the `` to be the `const` variable of the query, in this case `AboutCardFragment`. The same is passed to the `useFragment` function as the first arg, and the respective props as the second arg. From here, `entity` should be fully typed and usuable within the React component. ```tsx import { type FragmentType, useFragment, graphql, -} from '../../../../__generated__'; +} from './gql'; + +type AboutCardProps = { + entity: FragmentType +} -export function AboutCard(props: { - entity: FragmentType; -}) { +export function AboutCard(props: UserListItemProps) { const entity = useFragment( AboutCardFragment, props.entity, @@ -129,3 +234,5 @@ export function AboutCard(props: { [...] } ``` + +[installation]: ./installation diff --git a/www/docs/quickstart.mdx b/www/docs/quickstart.mdx deleted file mode 100644 index 343bade..0000000 --- a/www/docs/quickstart.mdx +++ /dev/null @@ -1,75 +0,0 @@ -The [Backstage GraphQL plugin][graphql-backend] is designed for schema-first -development of the GraphQL API. It reduces work necessary to expand the schema -using schema directives. [Schema directives](https://the-guild.dev/graphql/tools/docs/schema-directives) -are extensions to GraphQL schema used to automate implementation of the GraphQL API. -In Backstage GraphQL Plugin, schema directives are used to automatically create -resolvers. [Resolvers](https://graphql.org/learn/execution/) tell a GraphQL API -how to provide data for a specific schema type or field. The Backstage GraphQL -Plugin uses what it knows about the Backstage catalog to reduce the need for -writing resolvers that call the catalog. - -Currently, Backstage implemented two backend systems: - -- [EXPERIMENTAL Backstage backend system](https://backstage.io/docs/backend-system/) -- [Backstage backend plugins](https://backstage.io/docs/plugins/backend-plugin) - -### Usage with backend system - -The full cover of using GraphQL with EXPERIMENTAL Backstage backend system you can -find in [readme](https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend/README.md) - -```typescript -// packages/backend/src/index.ts -import { graphqlPlugin } from '@frontside/backstage-plugin-graphql-backend'; -import { graphqlModuleCatalog } from '@frontside/backstage-plugin-graphql-backend-module-catalog'; - -// Initializing Backstage backend -const backend = createBackend(); - -// Adding GraphQL plugin -backend.use(graphqlPlugin()); -// Adding Catalog GraphQL module -backend.use(graphqlModuleCatalog()); -``` - -### Usage with backend plugins - -Using the old Backstage backend plugins system is also fully covered in -[readme](https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend/docs/backend-plugins.md) - -```typescript -// packages/backend/src/plugins/graphql.ts -import { createRouter } from '@frontside/backstage-plugin-graphql-backend'; -import { Catalog, createCatalogLoader } from '@frontside/backstage-plugin-graphql-backend-module-catalog'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - modules: [Catalog], - loaders: { ...createCatalogLoader(env.catalogClient) }, - }); -} -``` - -### Catalog module - -The single GraphQL plugin isn't useful by itself. To able query Backstage Catalog -using GraphQL queries you need to install the [Catalog module][catalog-module]. - -The Catalog module provides [`@relation`](./relation) schema directive and data -loader for Catalog API. It also has well written [Catalog GraphQL module][catalog-schema] -with most basic Backstage types. We recommend to use it as a starting point for -further [extending] GraphQL schema. But if you'd like to implement your own type -structure you can use [Relation GraphQL module][relation-schema]. Relation module -contains only [`@relation`](./relation) schema directive and Catalog data loader. -Then you can setup [codegen] to generate GraphQL schema for clients and TypeScript -types for the schema. - -[graphql-backend]: https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend -[catalog-module]: https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend-module-catalog -[catalog-schema]: https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend-module-catalog/src/catalog -[relation-schema]: https://github.com/thefrontside/playhouse/blob/main/plugins/graphql-backend-module-catalog/src/relation -[extending]: ./extending -[codegen]: ./codegen diff --git a/www/docs/resolve.mdx b/www/docs/resolve.mdx index f90049a..8346ad7 100644 --- a/www/docs/resolve.mdx +++ b/www/docs/resolve.mdx @@ -31,9 +31,13 @@ the data source that was used to resolve the node. You can use this field to ```graphql extend interface Node - @discriminates(with: "__source") - @discriminationAlias(value: "ComponentsAPI", type: "Component") - @discriminationAlias(value: "OwnersAPI", type: "Owner") {} + @discriminates( + with: "__source", + aliases: [ + { value: "ComponentsAPI", type: "Component" }, + { value: "OwnersAPI", type: "Owner" } + ] + ) {} ``` ### Resolving collection of nodes diff --git a/www/docs/resolving.mdx b/www/docs/resolving.mdx index 9d0278a..29eea50 100644 --- a/www/docs/resolving.mdx +++ b/www/docs/resolving.mdx @@ -1,8 +1,3 @@ -TODO: Describe how type resolving works in details -TODO: Describe how field resolving works P+1 and late resolving -TODO: How to combine with custom resolvers -TODO: Resolving Connection type - Let's start from that each node in HydraphQL has a unique `id` which contains encoded information about the type of the node, which data source use to fetch runtime data and also a reference to the data with optional query arguments. @@ -35,7 +30,7 @@ article. So for a field with `@field` directive HydraphQL generates a resolver function like: ```typescript -const customResolver = field.resolve; +const userDefinedResolver = field.resolve; field.resolve = async ( source: { id: string }, @@ -46,6 +41,32 @@ field.resolve = async ( const { loader } = context; const node = await loader.load(source.id); const rawData = _.get(node, directive.at) ?? directive.default; - return customResolver(rawData, args, context, info); + return userDefinedResolver(rawData, args, context, info); +} +``` + +With `@resolve` directive it generates + +```typescript +const userDefinedResolver = field.resolve; + +field.resolve = async ( + source: { id: string }, + args: any, + context: { loader: DataLoader }, + info: GraphQLResolveInfo, +): Promise => { + const { loader } = context; + const node = await loader.load(source.id); + const ref = _.get(node, directive.at); + + const source = directive.from ?? decodeId(id).source; + const typename = field.type.name; + + return { id: encodeId({ source, typename, query: { ref, args } }) }; } ``` + +Generated resolver for `@relation` directive is a special case of `@resolve` directive resolver. +But instead of taking `source` and value from `form` path we take entity ref from `relations` array +filtering by relation `name` and targetRef `kind` diff --git a/www/docs/structure.json b/www/docs/structure.json index 913ca82..47546e5 100644 --- a/www/docs/structure.json +++ b/www/docs/structure.json @@ -1,10 +1,9 @@ { "Getting Started": [ - ["introduction.mdx", "Introduction"], - ["quickstart.mdx", "Quick start"], + ["installation.mdx", "Installation"], ["extending.mdx", "Extending schema"], - ["codegen.mdx", "GraphQL/TypeScript codegen"], ["querying.mdx", "Querying GraphQL API"], + ["why.mdx", "Why GraphQL?"], ["faq.mdx", "FAQ"] ], "Directives": [ @@ -16,7 +15,6 @@ ], "Advanced": [ ["resolving.mdx", "Type/Field resolving"], - ["collections.mdx", "List and Connection types"], ["hydraphql.mdx", "HydraphQL"], ["server.mdx", "GraphQL Server"] ] diff --git a/www/docs/why.mdx b/www/docs/why.mdx new file mode 100644 index 0000000..d5999b4 --- /dev/null +++ b/www/docs/why.mdx @@ -0,0 +1,237 @@ +### Limitations of REST API + +The Backstage catalog model is deliberately designed to be open to extension +so that it can house whatever set of attributes a company might need to represent +its entities. Each entity has a few root fields `apiVersion`, `kind`, `metadata`, +`spec`, `relations` and `status`. But only `metadata` and `spec` might be used for +extending. + +```json +{ + "apiVersion": "backstage.io/v1alpha1", + "kind": "Component", + "metadata": { + "name": "backstage", + "namespace": "default", + "description": "The Backstage core", + "tags": ["backstage", "core"] + }, + "spec": { + "type": "service", + "owner": "team-a", + "lifecycle": "experimental" + }, + "relations": [ + { + "targetRef": "group:default/team-a", + "type": "ownedBy" + } + ] +} +``` + +- `metadata` contains a set of reserved fields with specific meaning. Although +the field is open for extension, but it's not recommended due to the risk of +colliding with future extensions and because this kind of extensions live +primarily in metadata labels and annotations. +- `spec` precise structure of the field depends on the `apiVersion` and `kind` +combination. Which makes it possible to store anything. But at the cost of being +awkward to work with because it is just a blob of un-typed JSON data over +which each client must layer its own structure at the point of consumption. +For example: + +```typescript +let employee = await client.getEntityByRef("employee:cowboyd"); + +// let's say that "address" was changed to "location" +employee.spec.address.country; // TypeError: property 'country' does not exist on undefined +``` + +If the shape of the data changes, the client side types will just be straight +up wrong, and as a result must be carefully kept in sync with the backend. + +The story is similar when it comes to working with relationships. The flexibility +of the catalog model in this regard is powerful in that any entity can be related +to any other entity, and the type of relation that can join two entities together +in this manner is unbounded. + +```typescript +let systemPartsRefs = getEntityRelations(entity, RELATION_HAS_PART) + .map(relation => relation.targetRef); +let systemParts = await client.getEntitiesByRefs( + { entityRefs: systemPartsRefs } +); +let systemComponents = systemParts.filter( + entity => entity.kind === "Component" +); +let systemResources = systemParts.filter( + entity => entity.kind === "Resource" +); +``` + +Once again however, [working] with [these relationships][these] +can [be awkward][awkward] using the `CatalogApi` by itself. + +Not only is it cumbersome, it can also be inefficient. This is because without a +[bespoke batching mechanism][batching] at the point of consumption, the lowest +activation energy solution is going to suffer from the N+1 problem by manually +dereferencing each relationship: + +```typescript +let [ownedByRelation] = getEntityRelations(entity, RELATION_OWNED_BY); +let owner = ownedByRelation + ? await client.getEntityByRef(ownedByRelation.targetRef) + : null; + +let group = owner.kind === "Group" ? owner : null; + +if (!group) { + let [memberOfRelation] = getEntityRelations(owner, RELATION_MEMBER_OF); + group = groupRef + ? await client.getEntityByRef(memberOfRelation.targetRef) + : null; +} +``` + +### GraphQL API Benefits + +Compare how GraphQL works where if we attempt to query a field that does not exist, +it immediately triggers an error. This is because we cannot express the API +without also expressing its shape. There is more info about [querying] +and you can play with GraphQL API in [Backstage Demo](http://demo.backstage.io/graphiql) + +```typescript +let employee = await client.query( + `node(id: "employee:cowboyd") { + ...on Employee { + address { country } + } + }` +); // GraphQLError: Cannot query field "address" on type "Employee". +``` + +Even better, we can use [GraphQL Codegen](https://the-guild.dev/graphql/codegen) +to surface this category of error at *compile time* which can yield enormous +times savings for each dev cycle. Check out how to setup [codegen]. + +```tsx +import { FragmentType, graphql, useFragment } from './gql' + +const EmployeeFragment = graphql(` + fragment EmployeeFragment on Node { + ...on Employee { + location { country } + } + } +`); + +type EmployeeCardProps = { + employee: FragmentType +} + +export function EmployeeCard(props: EmployeeCardProps) { + const employee = useFragment(EmployeeFragment, props.employee); + return
{employee.location.country}
; +} +``` + +In terms of relationships a GraphQL API has the possibility of both querying +and traversing relationships as the natural expression of its schema, we can +just access it directly: + +```tsx +import { FragmentType, graphql, useFragment } from './gql' + +const ComponentFragment = graphql(` + fragment ComponentFragment on Node { + ...on Component { + owner { + profile { displayName } + memberOf { + profile { displayName } + } + } + } + } +`); + +type ComponentPageProps = { + component: FragmentType +} + +export function EmployeeCard(props: ComponentPageProps) { + const component = useFragment(ComponentFragment, props.component); + return /* + Note: Render component owner's name and groups here + */; +} +``` + +And, because resolution happens on the server, it can be responsible for properly +batching entity loads according to the depth of the query, thereby removing +[the burden][burden] from the shoulders of the developer. + +### GraphQL Schema + +The entity format is great for representing the graph internally, but it imposes +unnecessary ceremony when working with it from the outside. As an API consumer, +nobody cares whether a description of a user's role is in `metadata.description`, +or if their name to display on a web page is inside `spec.profile.displayName`, +they just want the data. By the same token, in order to work with related entities, +nobody wants to have to go through the rigmarole of reading relations and +assembling them by hand. Instead, they just want to access the related records +directly. + +To make this happen, we define a set of [GraphQL directives][directives] that +allow us to directly map values from an entity to a GraphQL type and thereby +generate the resolvers for it automatically. + +```graphql +interface API + @implements(interface: "Entity") + @discriminates( + with: "spec.type", + opaqueType: "OpaqueAPI", + aliases: [ + { value: "openapi", type: "OpenAPI" }, + { value: "asyncapi", type: "AsyncAPI" }, + { value: "graphql", type: "GraphQL" }, + { value: "grpc", type: "GRPC" }, + ] + ) { + type: String! @field(at: "spec.type") + lifecycle: String! @field(at: "spec.lifecycle") + owner: Owner! @relation(name: "ownedBy") + definition: String! @field(at: "spec.definition") + system: System @relation(name: "partOf") + apiConsumedBy: Connection + @relation(name: "apiConsumedBy", nodeType: "Component") + apiProvidedBy: Connection + @relation(name: "apiProvidedBy", nodeType: "Component") +} +``` + +- To map the fields like the ones above, we introduce a [`@field`][field] directive +to pull data out of the envelope and give it directly to the developer. +- Backstage catalog has a normalized way of expressing relations between nodes. +We introduce a [`@relation`][relation] directive to automatically generate the +resolvers for related records. +- To simplify work with interface implementations and fields composition we introduce an +[`@implements`][implements] directive to automatically copies fields from implemented +interfaces. +- Another point is a runtime data must be resolved to a specific object type. We +introduce [`@discriminates`][discriminates] directives +to declaratively define how GraphQL should resolve data to a specific object type. + +[working]: https://github.com/thefrontside/backstage/blob/master/plugins/catalog-react/src/utils/getEntityRelations.ts#L29-L48 +[these]: https://github.com/thefrontside/backstage/blob/cf28822/plugins/catalog/src/components/AboutCard/AboutContent.tsx#L84-L97 +[awkward]: https://github.com/thefrontside/backstage/blob/cf28822/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx#L172-L265 +[batching]: https://github.com/thefrontside/backstage/blob/master/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts +[burden]: https://github.com/thefrontside/backstage/blob/master/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts +[directives]: https://graphql.org/learn/queries/#directives +[field]: ./field +[relation]: ./relation +[implements]: ./implements +[discriminates]: ./discriminates +[querying]: ./querying +[codegen]: ./codegen diff --git a/www/routes/app.html.tsx b/www/routes/app.html.tsx index 8a00930..10e99ac 100644 --- a/www/routes/app.html.tsx +++ b/www/routes/app.html.tsx @@ -111,7 +111,7 @@ export function* useAppHtml({
{children}
-