diff --git a/gatsby-config.js b/gatsby-config.ts similarity index 78% rename from gatsby-config.js rename to gatsby-config.ts index 1e52a8232f..973a60abd4 100644 --- a/gatsby-config.js +++ b/gatsby-config.ts @@ -1,4 +1,6 @@ -module.exports = { +import type { GatsbyConfig } from "gatsby" + +const config: GatsbyConfig = { siteMetadata: { title: "GraphQL", description: @@ -87,24 +89,26 @@ module.exports = { author: byline, }) ), - query: `{ - allMarkdownRemark( - filter: {frontmatter: {layout: {eq: "blog"}}} - sort: {frontmatter: {date: DESC}} - ) { - edges { - node { - excerpt - frontmatter { - title - date - permalink - byline - } - } - } - } -}`, + query: /* GraphQL */ ` + { + allMarkdownRemark( + filter: { frontmatter: { layout: { eq: "blog" } } } + sort: { frontmatter: { date: DESC } } + ) { + edges { + node { + excerpt + frontmatter { + title + date + permalink + byline + } + } + } + } + } + `, output: "/blog/rss.xml", title: "Blog | GraphQL", feed_url: "http://graphql.org/blog/rss.xml", @@ -115,3 +119,5 @@ module.exports = { }, ], } + +export default config diff --git a/gatsby-node.js b/gatsby-node.js index c6894f6127..d0f301f8e3 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,18 +1,18 @@ -const path = require("path") +const path = require("node:path") const sortLibs = require("./scripts/sort-libraries") const globby = require("globby") const frontmatterParser = require("parser-front-matter") -const { readFile } = require("fs-extra") -const { promisify } = require("util") +const { readFile } = require("node:fs/promises") +const { promisify } = require("node:util") -exports.createSchemaCustomization = ({ actions, schema }) => { - const gql = String.raw; - const { createTypes } = actions; +const parse$ = promisify(frontmatterParser.parse) + +exports.createSchemaCustomization = ({ actions }) => { + const gql = String.raw + const { createTypes } = actions createTypes(gql` - type BlogPost implements Node - @childOf(types: ["MarkdownRemark"]) - { + type BlogPost implements Node @childOf(types: ["MarkdownRemark"]) { postId: String! title: String! tags: [String!]! @@ -21,8 +21,8 @@ exports.createSchemaCustomization = ({ actions, schema }) => { guestBio: String remark: MarkdownRemark! @link # backlink to the parent } - `); -}; + `) +} // Transform nodes, each of logic inside here can be extracted to a separated plugin later. exports.onCreateNode = async ({ @@ -32,60 +32,61 @@ exports.onCreateNode = async ({ createNodeId, createContentDigest, }) => { - const { createNode, createParentChildLink } = actions; + const { createNode, createParentChildLink } = actions // Derive content nodes from remark nodes - if (node.internal.type === 'MarkdownRemark') { - if (node.frontmatter.layout === 'blog') { - const nodeId = createNodeId(`${node.id} >>> BlogPost`); - - const permalink = node.frontmatter.permalink; - if (!permalink?.startsWith('/blog/')) { - reporter.panicOnBuild(`${permalink} is not valid permalink for blog post`); - return; - } - - // It contains a kind of transform logic. However, those logics can be extracted to resolvers into ahead of sourcing (createTypes) - const blogPostContent = { - id: nodeId, - postId: permalink.replace('/blog/', '').replace(/\/$/, ''), - title: node.frontmatter.title, - tags: node.frontmatter.tags ?? [], - date: node.frontmatter.date, - authors: (node.frontmatter.byline ?? '') - .split(',') - .map(name => name.trim()) - .filter(Boolean), - guestBio: node.frontmatter.guestBio ?? null, - }; - - createNode({ - ...blogPostContent, - remark: node.id, - parent: node.id, - children: [], - internal: { - type: 'BlogPost', - contentDigest: createContentDigest(blogPostContent), - }, - }); + if ( + node.internal.type === "MarkdownRemark" && + node.frontmatter.layout === "blog" + ) { + const nodeId = createNodeId(`${node.id} >>> BlogPost`) + + const { permalink } = node.frontmatter + if (!permalink?.startsWith("/blog/")) { + reporter.panicOnBuild(`${permalink} is not valid permalink for blog post`) + return + } - createParentChildLink({ - parent: node, - child: blogPostContent, - }); + // It contains a kind of transform logic. However, those logics can be extracted to resolvers into ahead of sourcing (createTypes) + const blogPostContent = { + id: nodeId, + postId: permalink.replace("/blog/", "").replace(/\/$/, ""), + title: node.frontmatter.title, + tags: node.frontmatter.tags ?? [], + date: node.frontmatter.date, + authors: (node.frontmatter.byline ?? "") + .split(",") + .map(name => name.trim()) + .filter(Boolean), + guestBio: node.frontmatter.guestBio ?? null, } + + createNode({ + ...blogPostContent, + remark: node.id, + parent: node.id, + children: [], + internal: { + type: "BlogPost", + contentDigest: createContentDigest(blogPostContent), + }, + }) + + createParentChildLink({ + parent: node, + child: blogPostContent, + }) } -}; +} exports.onCreatePage = async ({ page, actions }) => { // trying to refactor code to be "the Gatsby way". // from the paths on ready, ignores a bunch of existing custom logic below. - if (page.path.startsWith('/blog')) { - return; + if (page.path.startsWith("/blog")) { + return } - if (page.path.startsWith('/tags')) { - return; + if (page.path.startsWith("/tags")) { + return } const { createPage, deletePage } = actions @@ -98,14 +99,13 @@ exports.onCreatePage = async ({ page, actions }) => { const markdownFilePaths = await globby("src/content/code/**/*.md") const codeData = {} const slugMap = require("./src/content/code/slug-map.json") - const parse$ = promisify(frontmatterParser.parse) await Promise.all( markdownFilePaths.map(async markdownFilePath => { const markdownFileContent = await readFile(markdownFilePath, "utf-8") let { data: { name, description, url, github, npm, gem }, content: howto, - } = await parse$(markdownFileContent, undefined) + } = await parse$(markdownFileContent) howto = howto.trim() const pathArr = markdownFilePath.split("/") if (markdownFilePath.includes("language-support")) { @@ -113,15 +113,13 @@ exports.onCreatePage = async ({ page, actions }) => { const languageNameSlugIndex = languageSupportDirIndex + 1 const languageNameSlug = pathArr[languageNameSlugIndex] const languageName = slugMap[languageNameSlug] - codeData.Languages = codeData.Languages || {} - codeData.Languages[languageName] = - codeData.Languages[languageName] || {} + codeData.Languages ||= {} + codeData.Languages[languageName] ||= {} const categoryNameSlugIndex = languageSupportDirIndex + 2 const categoryNameSlug = pathArr[categoryNameSlugIndex] const categoryName = slugMap[categoryNameSlug] - codeData.Languages[languageName][categoryName] = - codeData.Languages[languageName][categoryName] || [] + codeData.Languages[languageName][categoryName] ||= [] codeData.Languages[languageName][categoryName].push({ name, description, @@ -132,12 +130,34 @@ exports.onCreatePage = async ({ page, actions }) => { gem, sourcePath: markdownFilePath, }) + } else if (markdownFilePath.includes("tools")) { + const toolSupportDirIndex = pathArr.indexOf("tools") + const toolNameSlugIndex = toolSupportDirIndex + 1 + const toolNameSlug = pathArr[toolNameSlugIndex] + const toolName = slugMap[toolNameSlug] + codeData.ToolsNew ||= {} + codeData.ToolsNew[toolName] ||= {} + const categoryToolsNameSlugIndex = toolSupportDirIndex + 2 + const categoryToolsNameSlug = pathArr[categoryToolsNameSlugIndex] + const categoryToolsName = slugMap[categoryToolsNameSlug] + codeData.ToolsNew[toolName][categoryToolsName] ||= [] + + codeData.ToolsNew[toolName][categoryToolsName].push({ + name, + description, + howto, + url, + github, + npm, + gem, + sourcePath: markdownFilePath, + }) } else { const codeDirIndex = pathArr.indexOf("code") const categoryNameSlugIndex = codeDirIndex + 1 const categoryNameSlug = pathArr[categoryNameSlugIndex] const categoryName = slugMap[categoryNameSlug] - codeData[categoryName] = codeData[categoryName] || [] + codeData[categoryName] ||= [] codeData[categoryName].push({ name, description, @@ -152,29 +172,41 @@ exports.onCreatePage = async ({ page, actions }) => { }) ) const languageList = [] - let sortedTools = [] + const toolList = [] await Promise.all([ - Promise.all( - Object.keys(codeData.Languages).map(async languageName => { - const libraryCategoryMap = codeData.Languages[languageName] - let languageTotalStars = 0 - await Promise.all( - Object.keys(libraryCategoryMap).map(async libraryCategoryName => { - const libraries = libraryCategoryMap[libraryCategoryName] - const { sortedLibs, totalStars } = await sortLibs(libraries) - libraryCategoryMap[libraryCategoryName] = sortedLibs - languageTotalStars += totalStars || 0 - }) - ) - languageList.push({ - name: languageName, - totalStars: languageTotalStars, - categoryMap: libraryCategoryMap, + ...Object.keys(codeData.Languages).map(async languageName => { + const libraryCategoryMap = codeData.Languages[languageName] + let languageTotalStars = 0 + await Promise.all( + Object.keys(libraryCategoryMap).map(async libraryCategoryName => { + const libraries = libraryCategoryMap[libraryCategoryName] + const { sortedLibs, totalStars } = await sortLibs(libraries) + libraryCategoryMap[libraryCategoryName] = sortedLibs + languageTotalStars += totalStars || 0 + }) + ) + languageList.push({ + name: languageName, + totalStars: languageTotalStars, + categoryMap: libraryCategoryMap, + }) + }), + ...Object.keys(codeData.ToolsNew).map(async toolName => { + const toolCategoryMap = codeData.ToolsNew[toolName] + let toolTotalStars = 0 + await Promise.all( + Object.keys(toolCategoryMap).map(async toolCategoryName => { + const tools = toolCategoryMap[toolCategoryName] + const { sortedLibs, totalStars } = await sortLibs(tools) + toolCategoryMap[toolCategoryName] = sortedLibs + toolTotalStars += totalStars || 0 }) + ) + toolList.push({ + name: toolName, + totalStars: toolTotalStars, + categoryMap: toolCategoryMap, }) - ), - sortLibs(codeData.Tools).then(({ sortedLibs }) => { - sortedTools = sortedLibs }), ]) @@ -182,13 +214,22 @@ exports.onCreatePage = async ({ page, actions }) => { ...context, otherLibraries: { Services: codeData.Services, - Tools: sortedTools, "More Stuff": codeData["More Stuff"], }, languageList: languageList.sort((a, b) => { if (a.totalStars > b.totalStars) { return -1 - } else if (a.totalStars < b.totalStars) { + } + if (a.totalStars < b.totalStars) { + return 1 + } + return 0 + }), + toolList: toolList.sort((a, b) => { + if (a.totalStars > b.totalStars) { + return -1 + } + if (a.totalStars < b.totalStars) { return 1 } return 0 @@ -247,7 +288,7 @@ exports.createPages = async ({ graphql, actions }) => { } const tags = result.data.allBlogPost.group.map(group => group.fieldValue) - tags.forEach(tag => { + for (const tag of tags) { createPage({ path: `/tags/${tag.toLowerCase()}/`, component: path.resolve("./src/templates/{BlogPost.tags}.tsx"), @@ -255,7 +296,7 @@ exports.createPages = async ({ graphql, actions }) => { tag, }, }) - }) + } const markdownPages = result.data.allMarkdownRemark.edges @@ -333,16 +374,11 @@ exports.createPages = async ({ graphql, actions }) => { return } - if (!pagesGroupedByFolder[relativeDirectory]) { - pagesGroupedByFolder = { - ...pagesGroupedByFolder, - [relativeDirectory]: [node], - } - } else { - pagesGroupedByFolder = { - ...pagesGroupedByFolder, - [relativeDirectory]: [...pagesGroupedByFolder[relativeDirectory], node], - } + pagesGroupedByFolder = { + ...pagesGroupedByFolder, + [relativeDirectory]: pagesGroupedByFolder[relativeDirectory] + ? [...pagesGroupedByFolder[relativeDirectory], node] + : [node], } allPages.push({ @@ -364,7 +400,8 @@ exports.createPages = async ({ graphql, actions }) => { const bDate = new Date(b.frontmatter.date || Date.now()) if (aDate > bDate) { return -1 - } else if (aDate < bDate) { + } + if (aDate < bDate) { return 1 } return 0 @@ -385,7 +422,6 @@ exports.createPages = async ({ graphql, actions }) => { const permalink = page.frontmatter.permalink if (!previousPagesMap[permalink] && !firstPage) { firstPage = page - return } }) @@ -400,10 +436,8 @@ exports.createPages = async ({ graphql, actions }) => { let i = 0 while (page && i++ < 1000) { const { frontmatter } = page - const { - category: definedCategory, - next: definedNextPageUrl, - } = frontmatter + const { category: definedCategory, next: definedNextPageUrl } = + frontmatter let category = definedCategory || folder if (!currentCategory || category !== currentCategory.name) { if (currentCategory) { @@ -443,28 +477,28 @@ exports.createPages = async ({ graphql, actions }) => { // Use all the set up data to now tell Gatsby to create pages // on the site - allPages - .filter(page => !page.permalink.startsWith('/blog')) - .forEach(page => { - createPage({ - path: `${page.permalink}`, - component: docTemplate, - context: { - permalink: page.permalink, - nextPermalink: page.nextPermalink, - sideBarData: sideBardata[page.relativeDirectory], - sourcePath: page.sourcePath, - }, - }) - }) + for (const page of allPages) { + if (!page.permalink.startsWith("/blog")) { + createPage({ + path: `${page.permalink}`, + component: docTemplate, + context: { + permalink: page.permalink, + nextPermalink: page.nextPermalink, + sideBarData: sideBardata[page.relativeDirectory], + sourcePath: page.sourcePath, + }, + }) + } + } } exports.onCreateWebpackConfig = ({ actions }) => { actions.setWebpackConfig({ resolve: { fallback: { - "assert": require.resolve("assert/"), - } - } + assert: require.resolve("assert/"), + }, + }, }) } diff --git a/scripts/sort-libraries.js b/scripts/sort-libraries.js index ed71188605..92388609d3 100644 --- a/scripts/sort-libraries.js +++ b/scripts/sort-libraries.js @@ -207,7 +207,8 @@ const sortLibs = async libs => { } if (bScore > aScore) { return 1 - } else if (bScore < aScore) { + } + if (bScore < aScore) { return -1 } return 0 diff --git a/src/assets/css/_css/code.less b/src/assets/css/_css/code.less index 7cd96810e9..be691a9158 100644 --- a/src/assets/css/_css/code.less +++ b/src/assets/css/_css/code.less @@ -94,6 +94,9 @@ } } } + .tools-title { + margin: 0; + } .inner-content { max-width: 100%; } diff --git a/src/content/code/slug-map.json b/src/content/code/slug-map.json index a4895135eb..41a51ab8d6 100644 --- a/src/content/code/slug-map.json +++ b/src/content/code/slug-map.json @@ -26,5 +26,14 @@ "client": "Client", "server": "Server", "tools": "Tools", - "services": "Services" + "services": "Services", + "gqt": "GQT", + "graphql-armor": "Graphql-Armor", + "graphql-code-generator": "GraphQL Code Generator", + "microcks": "Microcks", + "quicktype": "QuickType", + "schemathesis": "Schemathesis", + "gateways-supergraphs": "GatewaysAndSupergraphs", + "general": "General", + "wundergraph": "WunderGraph" } diff --git a/src/content/code/tools/gqt.md b/src/content/code/tools/gqt/general/gqt.md similarity index 100% rename from src/content/code/tools/gqt.md rename to src/content/code/tools/gqt/general/gqt.md diff --git a/src/content/code/tools/graphql-armor.md b/src/content/code/tools/graphql-armor/general/graphql-armor.md similarity index 100% rename from src/content/code/tools/graphql-armor.md rename to src/content/code/tools/graphql-armor/general/graphql-armor.md diff --git a/src/content/code/tools/graphql-code-generator.md b/src/content/code/tools/graphql-code-generator/general/graphql-code-generator.md similarity index 100% rename from src/content/code/tools/graphql-code-generator.md rename to src/content/code/tools/graphql-code-generator/general/graphql-code-generator.md diff --git a/src/content/code/tools/microcks.md b/src/content/code/tools/microcks/general/microcks.md similarity index 100% rename from src/content/code/tools/microcks.md rename to src/content/code/tools/microcks/general/microcks.md diff --git a/src/content/code/tools/quicktype.md b/src/content/code/tools/quicktype/general/quicktype.md similarity index 100% rename from src/content/code/tools/quicktype.md rename to src/content/code/tools/quicktype/general/quicktype.md diff --git a/src/content/code/tools/schemathesis.md b/src/content/code/tools/schemathesis/general/schemathesis.md similarity index 100% rename from src/content/code/tools/schemathesis.md rename to src/content/code/tools/schemathesis/general/schemathesis.md diff --git a/src/content/code/tools/wundergraph/gateways-supergraphs/wundergraph.md b/src/content/code/tools/wundergraph/gateways-supergraphs/wundergraph.md new file mode 100644 index 0000000000..4ed57a1e4b --- /dev/null +++ b/src/content/code/tools/wundergraph/gateways-supergraphs/wundergraph.md @@ -0,0 +1,87 @@ +--- +name: WunderGraph +description: WunderGraph is an open-source GraphQL Gateway that is able to compose Apollo Federation, GraphQL, REST APIs, Databases, Kafka and more. +url: https://wundergraph.com +github: wundergraph/wundergraph +--- + +[WunderGraph](https://wundergraph.com) composes all your APIs into a single unified GraphQL API and +allows you to expose your Graph as a [secure and type-safe JSON-RPC API](https://docs.wundergraph.com/docs/features/graphql-to-json-rpc-compiler). + +To get started with WunderGraph, you can use `create-wundergraph-app` to bootstrap a new project: + +```bash +npx create-wundergraph-app my-project -E nextjs-swr +``` + +On the client side, WunderGraph's JSON-RPC API integrates very well with frameworks like [Next.js, SWR](https://github.com/wundergraph/wundergraph/tree/main/examples/nextjs-swr) and React Query, +while one the backend, we're able to leverage the power of "Server-Side-Only GraphQL". +Handle authentication, authorization, validation, joins and more right in the Query Layer. + +```graphql +mutation ( + $name: String! @fromClaim(name: NAME) + $email: String! @fromClaim(name: EMAIL) + $message: String! @jsonSchema(pattern: "^[a-zA-Z 0-9]+$") +) { + createOnepost( + data: { + message: $message + user: { + connectOrCreate: { + where: { email: $email } + create: { email: $email, name: $name } + } + } + } + ) { + id + message + user { + id + name + } + } +} +``` + +The Query above requires the user to be authenticated, +injects the user's name and email from the JWT token and validates the message against a JSON Schema. + +Here's another example showcasing how we can use Server-Side GraphQL with WunderGraph's unique [join capabilities](https://docs.wundergraph.com/docs/features/cross-api-joins-to-compose-apis), +composing data from two different APIs into a single GraphQL response. + +```graphql +query ( + $continent: String! + # the @internal directive removes the $capital variable from the public API + # this means, the user can't set it manually + # this variable is our JOIN key + $capital: String! @internal +) { + countries_countries(filter: { continent: { eq: $continent } }) { + code + name + # using the @export directive, we can export the value of the field `capital` into the JOIN key ($capital) + capital @export(as: "capital") + # the _join field returns the type Query! + # it exists on every object type so you can everywhere in your Query documents + _join { + # once we're inside the _join field, we can use the $capital variable to join the weather API + weather_getCityByName(name: $capital) { + weather { + temperature { + max + } + summary { + title + description + } + } + } + } + } +} +``` + +The full [example can be found on GitHub](https://github.com/wundergraph/wundergraph/tree/main/examples/cross-api-joins). diff --git a/src/pages/code.tsx b/src/pages/code.tsx index f64b9487b0..2af271ab02 100644 --- a/src/pages/code.tsx +++ b/src/pages/code.tsx @@ -31,11 +31,21 @@ interface Language { } } +interface Tool { + name: string + totalStars: number + categoryMap: { + GatewaysSupergraphs: ILibrary[] + General: ILibrary[] + } +} + interface PageContext { languageList: Language[] + toolList: Tool[] otherLibraries: { Services: ILibrary[] - Tools: ILibrary[] + Tools?: ILibrary[] } sourcePath: string } @@ -147,21 +157,51 @@ export function Library({ data }: { data: ILibrary }) { ) } +const categorySlugMap = [ + ["Server", toSlug("Server")], + ["Client", toSlug("Client")], + ["Tools", toSlug("Tools")], + ["Gateways-supergraphs", toSlug("Gateways-supergraphs")], + ["General", toSlug("General")], +] + export function LibraryList({ data }: { data: ILibrary[] }) { return (
{data.map(library => ( - + ))}
) } -const categorySlugMap = [ - ["Server", toSlug("Server")], - ["Client", toSlug("Client")], - ["Tools", toSlug("Tools")], -] +interface ToolsListProps { + pageContext: PageContext + type: "General" | "GatewaysAndSupergraphs" +} + +export function ToolsList({ pageContext, type }: ToolsListProps) { + return ( + <> +

+ {type === "GatewaysAndSupergraphs" ? "Gateways / Supergraphs" : type} +

+ {pageContext.toolList.map( + tool => ( + console.log(tool.categoryMap, tool.name), + ( +
+ {Object.entries(tool.categoryMap).map( + ([categoryName, data]) => + categoryName === type && + )} +
+ ) + ) + )} + + ) +} export default ({ pageContext }: PageProps<{}, PageContext>) => { return ( @@ -196,7 +236,6 @@ export default ({ pageContext }: PageProps<{}, PageContext>) => { -

Language Support

@@ -278,14 +317,33 @@ export default ({ pageContext }: PageProps<{}, PageContext>) => { ) })} -

- - Tools - - # - -

- +
+
+

Tools

+

+ + + Gateways/Supergraphs + + {" / "} + + General + + +

+
+
+ +

Services