From 6215f63b5bf0ea8dbf14f9e1ce45ccaa17fd1f5d Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Wed, 30 Jul 2025 14:12:14 +0200 Subject: [PATCH 1/5] graphql-middleware-with-neo4j-graphql --- ...graphql-middleware-with-neo4j-graphql.adoc | 401 ++++++++++++++++++ 1 file changed, 401 insertions(+) create mode 100644 modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc diff --git a/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc new file mode 100644 index 00000000..5970bf00 --- /dev/null +++ b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc @@ -0,0 +1,401 @@ += Using GraphQL middleware with Neo4j GraphQL + +https://neo4j.com/docs/graphql-manual/current/[Neo4j GraphQL] is a +library that auto-generates GraphQL APIs from type definitions, creating +complete CRUD operations, relationship traversals, and optimized Cypher +queries without writing resolvers. If you’re new to Neo4j GraphQL, check +out the https://neo4j.com/docs/graphql-manual/current/[official +documentation] to get started. + +Neo4j GraphQL provides powerful built-in features, including but not +limited to: + +* Automatic CRUD operations +* Complex relationship traversals +* Optimized Cypher queries +* Built-in filtering and sorting +* Authentication +* Authorization +* Cypher directive for user-defined operations +* Custom resolvers + +This set of features already provides a significant amount of +flexibility to support various user requirements. However, sometimes +your requirements go beyond what these built-in features can achieve. At +this point, some users may consider abandoning the auto-generated +resolver entirely and writing their own custom logic. + +https://neo4j.com/docs/graphql-manual/current/[Neo4j GraphQL] is a +library that auto-generates GraphQL APIs from type definitions, creating +complete CRUD operations, relationship traversals, and optimized Cypher +queries without writing resolvers. If you’re new to Neo4j GraphQL, check +out the https://neo4j.com/docs/graphql-manual/current/[official +documentation] to get started. + +Neo4j GraphQL provides powerful built-in features, including but not +limited to: + +* Automatic CRUD operations +* Complex relationship traversals +* Optimized Cypher queries +* Built-in filtering and sorting +* Authentication +* Authorization +* Cypher directive for user-defined operations +* Custom resolvers + +This set of features provides a significant amount of flexibility to support various user requirements. +However, sometimes your requirements go beyond what these built-in features can achieve. +At this point, some users may consider abandoning the auto-generated resolver entirely and writing their own custom logic. + +However, you can wrap your auto-generated Neo4j GraphQL resolver with custom logic that intercepts specific GraphQL operations. +This approach allows you to retain all the benefits of auto-generation while adding the custom behavior you need. + + +== GraphQL middleware + +This page makes use of https://github.com/dimatill/graphql-middleware[`graphql-middleware`]. +GraphQL middleware is a library that provides a way to wrap and extend the behavior of your GraphQL resolvers. +It acts as a layer that allows you to apply reusable logic, such as logging, validation, or authentication, across multiple resolvers in a consistent and modular +way. + + +=== Logging every request + +Consider this Neo4j GraphQL setup: + +[source,typescript] +---- +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { applyMiddleware } from "graphql-middleware"; +import * as neo4j from "neo4j-driver"; +import { Neo4jGraphQL } from "@neo4j/graphql"; + +const typeDefs = /* GraphQL */ ` + type User @node { + id: ID! @id + name: String! + email: String! + posts: [Post!]! @relationship(type: "AUTHORED", direction: OUT) + } + + type Post @node { + id: ID! + title: String! + content: String! + author: [User!]! @relationship(type: "AUTHORED", direction: IN) + } + + type Query { + me: User @cypher(statement: "MATCH (u:User {id: $userId}) RETURN u", columnName: "u") + } +`; + +const driver = neo4j.driver("bolt://localhost:7687", neo4j.auth.basic("neo4j", "password")); + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, +}); + +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +const { url } = await startStandaloneServer(server, { + listen: { port: 4000 }, +}); +console.log(`πŸš€ Server ready at ${url}`); +---- + +Add logging to every single operation without touching the generated schema: + +[source,typescript] +---- +import { applyMiddleware } from "graphql-middleware"; + +/* ...existing code... */ + +const logMiddleware = async (resolve, root, args, context, info) => { + const start = Date.now(); + console.log(`πŸš€ ${info.fieldName} started`); + + try { + const result = await resolve(root, args, context, info); + console.log(`βœ… ${info.fieldName} completed in ${Date.now() - start}ms`); + return result; + } catch (error) { + console.log(`πŸ’₯ ${info.fieldName} failed`); + throw error; + } +}; + +// Wrap your executable schema +const schemaWithLogging = applyMiddleware(await neoSchema.getSchema(), { + Query: logMiddleware, + Mutation: logMiddleware, +}); + +const server = new ApolloServer({ schema: schemaWithLogging }); +---- + +*That’s it. Every query and mutation is now logged.* Your auto-generated +resolver is unchanged, but you’ve added custom behavior. + +Query the users: + +[source,graphql] +---- +{ + users { + name + } +} +---- + +You should see in your server: + +.... +πŸš€ users started +βœ… users completed in 23ms +.... + + +=== Email validation before database writes + +You can use middleware to enforce specific business rules before data is written to the database. +For example, you can ensure that email addresses provided during user creation are valid. +By using middleware, you can intercept and validate the input before it reaches the Neo4j GraphQL resolver. + +Add a middleware that validates the email input in the `createUsers` operation. +A validation error will be thrown before it reaches the Neo4j GraphQL resolver, and the GraphQL client will receive the error message "Invalid email addresses detected". + +[source,typescript] +---- +/* ...existing code... */ + +const validateEmails = async (resolve, root, args, context, info) => { + // Only check createUsers mutations + if (info.fieldName === "createUsers") { + // Note: This is a simplistic and intentionally flawed email validation example, but good for demonstration purposes. + const invalidEmails = args.input.filter((user) => !user.email.includes("@")); + if (invalidEmails.length > 0) { + throw new Error("Invalid email addresses detected"); + } + } + + return resolve(root, args, context, info); +}; + +const schema = applyMiddleware( + await neoSchema.getSchema(), + { + Query: logMiddleware, + Mutation: logMiddleware, + }, + { + Mutation: validateEmails, + } +); +---- + +Try to create a user with the email "not-an-email": + +[source,graphql] +---- +mutation createUsers { + createUsers(input: [{ email: "not-an-email.com", name: "firstname" }]) { + users { + email + } + } +} +---- + + +== Working with Neo4j GraphQL + +Most of the above is applicable even outside Neo4j GraphQL, but there is an important concept when writing middleware for Neo4j GraphQL resolvers. + +Here's the key difference from how traditional GraphQL resolvers are +usually built: + +In *traditional GraphQL*, each field resolver executes independently, potentially causing multiple database calls. +By contrast, in *Neo4j GraphQL* the root field resolver (like `users` or `createUsers`) analyzes the entire query tree and executes one optimized Cypher query. + +The N+1 problem is solved in Neo4j GraphQL by analyzing the entire GraphQL operation (via the `info` object) and generating optimized Cypher queries that fetch all requested data in a single database round-trip. + +Consider this query: + +[source,graphql] +---- +{ + users { + name + email + posts { + title + content + } + } +} +---- + +Neo4j GraphQL doesn't execute separate resolvers for `name`, `email`, `posts`, `title`, and `content`. +Instead, the `users` field resolver generates and executes a single Cypher query that returns all the data at once. +The nested field resolvers simply return the already fetched data from memory. + + +=== Timing matters + +Timing matters for middleware - by the time the individual field resolvers execute, the database has already been queried and the data is available in the resolver's result. + +Consider the `logMiddleware` from above: + +[source,typescript] +---- +const logMiddleware = async (resolve, root, args, context, info) => { + const start = Date.now(); + console.log(`πŸš€ ${info.fieldName} started`); + + try { + const result = await resolve(root, args, context, info); + console.log(`βœ… ${info.fieldName} completed in ${Date.now() - start}ms`); + return result; + } catch (error) { + console.log(`πŸ’₯ ${info.fieldName} failed: ${error.message}`); + throw error; + } +}; +---- + +Apply the `logMiddleware` to queries and the user's name: + +[source,typescript] +---- +const schema = applyMiddleware( + schema, + { + Query: logMiddleware, // wraps all the Queries and it's executed before the database round-trip + }, + { + User: { + name: logMiddleware, // wraps only the User's name field resolver and it's executed after the database roundtrip + }, + } +); +---- + +Run this query: + +[source,graphql] +---- +query { + users { + name + } +} +---- + +You should see: + +.... +πŸš€ users started +... Neo4j resolver generates and executes Cypher ... +βœ… users completed in 48ms +πŸš€ name started +βœ… name completed in 0ms +.... + +Note how the name resolution happens after the round-trip to the database. + +Note the following difference: + +* Query and mutation level middleware runs before and after the Neo4j GraphQL autogenerated resolvers. +* Type and field level middleware runs only after the Neo4j GraphQL autogenerated resolvers. + + +== Stack multiple middleware + +It's possible to apply multiple pieces of middleware for the same field. +For instance, you can apply diverse middleware to the same `users` resolver: + +[source,typescript] +---- +const schema = applyMiddleware( + schema, + { + Query: { + users: async (resolve, root, args, context, info) => { + console.log("A started"); + await resolve(root, args, context, info); + console.log("A completed"); + }, + }, + }, + { + Query: { + users: async (resolve, root, args, context, info) => { + console.log("B started"); + await resolve(root, args, context, info); + console.log("B completed"); + }, + }, + }, + { + Query: { + users: async (resolve, root, args, context, info) => { + console.log("C started"); + await resolve(root, args, context, info); + console.log("C completed"); + }, + }, + } +); +---- + +The order in which middleware is applied is important, as they execute one after the other. +Each middleware wraps the next one, creating a chain of execution from outermost to innermost. + +Run this query: + +[source,graphql] +---- +query { + users { + name + } +} +---- + +Schematic output: + +[source,bash] +---- +.... +A started +B started +C started +... Neo4j GraphQL user resolver ... +C completed +B completed +A completed +.... +---- + +The user's resolver is wrapped in three layers of middleware. + + +== Conclusion + +GraphQL middleware with Neo4j GraphQL gives you the best of both worlds: the power of auto-generated schemas and the flexibility to inject custom logic exactly where you need it. + +When you need custom logic, graphql-middleware lets you keep the rapid development benefits of Neo4j GraphQL while adding the custom behavior you need. + +The GraphQL ecosystem evolves rapidly. +https://the-guild.dev/[The Guild] has developed https://envelop.dev/[Envelop] with its own https://www.npmjs.com/package/@envelop/graphql-middleware[graphql-middleware +plugin]. + +This guide uses `graphql-middleware` because it's server-agnostic and delivers the clearest path to understanding middleware with Neo4j GraphQL. +If you need a more comprehensive plugin ecosystem, we recommend exploring envelop. \ No newline at end of file From 40e52de9f3beae205b5ac110b2d4a68b5338e70d Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Wed, 30 Jul 2025 14:18:25 +0200 Subject: [PATCH 2/5] removed duplicate intro sections --- ...graphql-middleware-with-neo4j-graphql.adoc | 38 ++----------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc index 5970bf00..d68c3316 100644 --- a/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc +++ b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc @@ -1,14 +1,9 @@ = Using GraphQL middleware with Neo4j GraphQL -https://neo4j.com/docs/graphql-manual/current/[Neo4j GraphQL] is a -library that auto-generates GraphQL APIs from type definitions, creating -complete CRUD operations, relationship traversals, and optimized Cypher -queries without writing resolvers. If you’re new to Neo4j GraphQL, check -out the https://neo4j.com/docs/graphql-manual/current/[official -documentation] to get started. +https://neo4j.com/docs/graphql-manual/current/[Neo4j GraphQL] is a library that auto-generates GraphQL APIs from type definitions, creating complete CRUD operations, relationship traversals, and optimized Cypher queries without writing resolvers. +If you are new to Neo4j GraphQL, check out the https://neo4j.com/docs/graphql-manual/current/[official documentation] to get started. -Neo4j GraphQL provides powerful built-in features, including but not -limited to: +Neo4j GraphQL provides powerful built-in features, including but not limited to: * Automatic CRUD operations * Complex relationship traversals @@ -19,32 +14,7 @@ limited to: * Cypher directive for user-defined operations * Custom resolvers -This set of features already provides a significant amount of -flexibility to support various user requirements. However, sometimes -your requirements go beyond what these built-in features can achieve. At -this point, some users may consider abandoning the auto-generated -resolver entirely and writing their own custom logic. - -https://neo4j.com/docs/graphql-manual/current/[Neo4j GraphQL] is a -library that auto-generates GraphQL APIs from type definitions, creating -complete CRUD operations, relationship traversals, and optimized Cypher -queries without writing resolvers. If you’re new to Neo4j GraphQL, check -out the https://neo4j.com/docs/graphql-manual/current/[official -documentation] to get started. - -Neo4j GraphQL provides powerful built-in features, including but not -limited to: - -* Automatic CRUD operations -* Complex relationship traversals -* Optimized Cypher queries -* Built-in filtering and sorting -* Authentication -* Authorization -* Cypher directive for user-defined operations -* Custom resolvers - -This set of features provides a significant amount of flexibility to support various user requirements. +This set of features already provides a significant amount of flexibility to support various user requirements. However, sometimes your requirements go beyond what these built-in features can achieve. At this point, some users may consider abandoning the auto-generated resolver entirely and writing their own custom logic. From 780a12e2a2b1bd6152249ca5956f0aeb30f8298b Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Wed, 30 Jul 2025 14:19:17 +0200 Subject: [PATCH 3/5] page -> guide --- .../security/using-graphql-middleware-with-neo4j-graphql.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc index d68c3316..df163db3 100644 --- a/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc +++ b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc @@ -24,7 +24,7 @@ This approach allows you to retain all the benefits of auto-generation while add == GraphQL middleware -This page makes use of https://github.com/dimatill/graphql-middleware[`graphql-middleware`]. +This guide makes use of https://github.com/dimatill/graphql-middleware[`graphql-middleware`]. GraphQL middleware is a library that provides a way to wrap and extend the behavior of your GraphQL resolvers. It acts as a layer that allows you to apply reusable logic, such as logging, validation, or authentication, across multiple resolvers in a consistent and modular way. From cf04e82adfe4da105fa0fe9b6118c5ff05178d2c Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Wed, 30 Jul 2025 15:49:53 +0200 Subject: [PATCH 4/5] initial md --- ...g-graphql-middleware-with-neo4j-graphql.md | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.md diff --git a/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.md b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.md new file mode 100644 index 00000000..44a97f49 --- /dev/null +++ b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.md @@ -0,0 +1,338 @@ +# Using GraphQL Middleware with Neo4j GraphQL + +[Neo4j GraphQL](https://neo4j.com/docs/graphql-manual/current/) is a library that auto-generates GraphQL APIs from type definitions, creating complete CRUD operations, relationship traversals, and optimized Cypher queries without writing resolvers. If you're new to Neo4j GraphQL, check out the [official documentation](https://neo4j.com/docs/graphql-manual/current/) to get started. + +Neo4j GraphQL provides powerful built-in features, including but not limited to: + +- Automatic CRUD operations +- Complex relationship traversals +- Optimized Cypher queries +- Built-in filtering and sorting +- Authentication +- Authorization +- Cypher directive for user-defined operations +- Custom resolvers + +This set of features already provides a significant amount of flexibility to support various user requirements. However, sometimes your requirements go beyond what these built-in features can achieve. +At this point, some users may consider abandoning the auto-generated resolver entirely and writing their own custom logic. + +**Plot twist:** It's not. You can wrap your auto-generated Neo4j GraphQL resolver with custom logic that intercepts specific GraphQL operations. This approach allows you to retain all the benefits of auto-generation while adding exactly the custom behavior you need. + +## GraphQL Middleware + +In this guide, we'll use [`graphql-middleware`](https://github.com/dimatill/graphql-middleware). +GraphQL middleware is a library that provides a way to wrap and extend the behavior of your GraphQL resolvers. It acts as a layer that allows you to apply reusable logic, such as logging, validation, or authentication, across multiple resolvers in a consistent and modular way. + +Let's see it in action. + +## Example: Logging Every Request + +Let's begin with this Neo4j GraphQL setup: + +```typescript +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { applyMiddleware } from "graphql-middleware"; +import * as neo4j from "neo4j-driver"; +import { Neo4jGraphQL } from "@neo4j/graphql"; + +const typeDefs = /* GraphQL */ ` + type User @node { + id: ID! @id + name: String! + email: String! + posts: [Post!]! @relationship(type: "AUTHORED", direction: OUT) + } + + type Post @node { + id: ID! + title: String! + content: String! + author: [User!]! @relationship(type: "AUTHORED", direction: IN) + } + + type Query { + me: User @cypher(statement: "MATCH (u:User {id: $userId}) RETURN u", columnName: "u") + } +`; + +const driver = neo4j.driver("bolt://localhost:7687", neo4j.auth.basic("neo4j", "password")); + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, +}); + +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +const { url } = await startStandaloneServer(server, { + listen: { port: 4000 }, +}); +console.log(`πŸš€ Server ready at ${url}`); +``` + +Now, let's add logging to every single operation without touching the generated schema: + +```typescript +import { applyMiddleware } from "graphql-middleware"; + +/* ...existing code... */ + +const logMiddleware = async (resolve, root, args, context, info) => { + const start = Date.now(); + console.log(`πŸš€ ${info.fieldName} started`); + + try { + const result = await resolve(root, args, context, info); + console.log(`βœ… ${info.fieldName} completed in ${Date.now() - start}ms`); + return result; + } catch (error) { + console.log(`πŸ’₯ ${info.fieldName} failed`); + throw error; + } +}; + +// Wrap your executable schema +const schemaWithLogging = applyMiddleware(await neoSchema.getSchema(), { + Query: logMiddleware, + Mutation: logMiddleware, +}); + +const server = new ApolloServer({ schema: schemaWithLogging }); +``` + +**That's it. Every query and mutation is now logged.** Your auto-generated resolver is unchanged, but you've added custom behavior. + +Query the users: + +```graphql +{ + users { + name + } +} +``` + +And you'll see in your server: + +``` +πŸš€ users started +βœ… users completed in 23ms +``` + +### Email Validation Before Database Writes + +Let's say you want to enforce specific business rules before data is written to the database. For example, ensuring that email addresses provided during user creation are valid. By using middleware, you can intercept and validate the input before it reaches the Neo4j GraphQL resolver. + +Let's add a middleware that validates the email input in the `createUsers` operation. +A validation error will be thrown before it reaches the Neo4j GraphQL resolver, and the GraphQL client will receive an error message: "Invalid email addresses detected". + +```typescript +/* ...existing code... */ + +const validateEmails = async (resolve, root, args, context, info) => { + // Only check createUsers mutations + if (info.fieldName === "createUsers") { + // Note: This is a simplistic and intentionally flawed email validation example, but good for demonstration purposes. + const invalidEmails = args.input.filter((user) => !user.email.includes("@")); + if (invalidEmails.length > 0) { + throw new Error("Invalid email addresses detected"); + } + } + + return resolve(root, args, context, info); +}; + +const schema = applyMiddleware( + await neoSchema.getSchema(), + { + Query: logMiddleware, + Mutation: logMiddleware, + }, + { + Mutation: validateEmails, + } +); +``` + +Try to create a user with an email "not-an-email" + +```graphql +mutation createUsers { + createUsers(input: [{ email: "not-an-email.com", name: "simone" }]) { + users { + email + } + } +} +``` + +## Working with Neo4j GraphQL + +Until now, most of what we've learned is applicable even outside Neo4j GraphQL as well, but there's an important concept to understand when writing middleware for Neo4j GraphQL resolvers. + +Here's the key difference from how traditional GraphQL resolvers are usually built: + +**Traditional GraphQL**: Each field resolver executes independently, potentially causing multiple database calls +**Neo4j GraphQL**: The root field resolver (like `users` or `createUsers`) analyzes the entire query tree and executes one optimized Cypher query + +One of Neo4j GraphQL's benefits is solving the **[N+1 problem](https://www.google.com/search?q=graphql+n%2B1)** by analyzing the entire GraphQL operation (via the `info` object) and generating optimized Cypher queries that fetch all requested data in a single database round-trip. + +This means that when you query: + +```graphql +{ + users { + name + email + posts { + title + content + } + } +} +``` + +Neo4j GraphQL doesn't execute separate resolvers for `name`, `email`, `posts`, `title`, and `content`. Instead, the `users` field resolver generates and executes a single Cypher query that returns all the data at once. The nested field resolvers simply return the already-fetched data from memory. + +**This is why timing matters for middleware** - by the time individual field resolvers execute, the database has already been queried and the data is sitting in the resolver's result. + +## The Key Insight: Timing Matters + +Let's take the `logMiddleware` middleware that we wrote before: + +```typescript +const logMiddleware = async (resolve, root, args, context, info) => { + const start = Date.now(); + console.log(`πŸš€ ${info.fieldName} started`); + + try { + const result = await resolve(root, args, context, info); + console.log(`βœ… ${info.fieldName} completed in ${Date.now() - start}ms`); + return result; + } catch (error) { + console.log(`πŸ’₯ ${info.fieldName} failed: ${error.message}`); + throw error; + } +}; +``` + +Now let's apply the `logMiddleware` middleware to Queries and the User's name: + +```typescript +const schema = applyMiddleware( + schema, + { + Query: logMiddleware, // wraps all the Queries and it's executed before the database round-trip + }, + { + User: { + name: logMiddleware, // wraps only the User's name field resolver and it's executed after the database roundtrip + }, + } +); +``` + +If we now run this query: + +```graphql +query { + users { + name + } +} +``` + +We can expect the following: + +``` +πŸš€ users started +... Neo4j resolver generates and executes Cypher ... +βœ… users completed in 48ms +πŸš€ name started +βœ… name completed in 0ms +``` + +Note how the name resolution happens after the round-trip to the database. + +Here's what you need to know about when your middleware runs: + +**Query/Mutation level middleware** = runs **BEFORE** and **AFTER** the Neo4j GraphQL autogenerated resolvers +**Type and Field level middleware** = runs only **AFTER** the Neo4j GraphQL autogenerated resolvers + +## Stack Multiple Middleware + +It's possible to apply multiple middleware for the same field. +For instance, we can apply diverse middleware to the same `users` resolver: + +```typescript +const schema = applyMiddleware( + schema, + { + Query: { + users: async (resolve, root, args, context, info) => { + console.log("A started"); + await resolve(root, args, context, info); + console.log("A completed"); + }, + }, + }, + { + Query: { + users: async (resolve, root, args, context, info) => { + console.log("B started"); + await resolve(root, args, context, info); + console.log("B completed"); + }, + }, + }, + { + Query: { + users: async (resolve, root, args, context, info) => { + console.log("C started"); + await resolve(root, args, context, info); + console.log("C completed"); + }, + }, + } +); +``` + +The order in which middleware is applied is important, as they execute in sequence. Each middleware wraps the next one, creating a chain of execution from outermost to innermost. + +When you run the query: + +```graphql +query { + users { + name + } +} +``` + +Output: + +``` +A started +B started +C started +... Neo4j GraphQL user resolver ... +C completed +B completed +A completed +``` + +The users' resolver is wrapped in three layers of middleware. + +## Conclusion + +GraphQL middleware with Neo4j GraphQL gives you the best of both worlds: the power of auto-generated schemas and the flexibility to inject custom logic exactly where you need it. + +When you need custom logic, graphql-middleware lets you keep the rapid development benefits of Neo4j GraphQL while adding exactly the custom behavior you need. + +The GraphQL ecosystem evolves rapidly. [The Guild](https://the-guild.dev/) has developed [Envelop](https://envelop.dev/) with its own [graphql-middleware plugin](https://www.npmjs.com/package/@envelop/graphql-middleware). + +We chose `graphql-middleware` for this guide because it's server-agnostic and delivers the clearest path to understanding middleware with Neo4j GraphQL. +If you need a more comprehensive plugin ecosystem, we recommend exploring Envelop! \ No newline at end of file From 0556863be47c8d5f048b44f36282411737faa351 Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Wed, 30 Jul 2025 15:59:15 +0200 Subject: [PATCH 5/5] delta --- ...graphql-middleware-with-neo4j-graphql.adoc | 18 ++-- ...g-graphql-middleware-with-neo4j-graphql.md | 99 +++++++++++-------- 2 files changed, 67 insertions(+), 50 deletions(-) diff --git a/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc index df163db3..05d9629b 100644 --- a/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc +++ b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.adoc @@ -110,8 +110,8 @@ const schemaWithLogging = applyMiddleware(await neoSchema.getSchema(), { const server = new ApolloServer({ schema: schemaWithLogging }); ---- -*That’s it. Every query and mutation is now logged.* Your auto-generated -resolver is unchanged, but you’ve added custom behavior. +Each query and mutation is now logged. +Your auto-generated resolver is unchanged, but you have added custom behavior. Query the users: @@ -126,10 +126,11 @@ Query the users: You should see in your server: -.... +[source,bash] +---- πŸš€ users started βœ… users completed in 23ms -.... +---- === Email validation before database writes @@ -270,13 +271,14 @@ query { You should see: -.... +[source,bash] +---- πŸš€ users started ... Neo4j resolver generates and executes Cypher ... βœ… users completed in 48ms πŸš€ name started βœ… name completed in 0ms -.... +---- Note how the name resolution happens after the round-trip to the database. @@ -286,7 +288,7 @@ Note the following difference: * Type and field level middleware runs only after the Neo4j GraphQL autogenerated resolvers. -== Stack multiple middleware +== Stack multiple middlewares It's possible to apply multiple pieces of middleware for the same field. For instance, you can apply diverse middleware to the same `users` resolver: @@ -339,7 +341,7 @@ query { } ---- -Schematic output: +Output: [source,bash] ---- diff --git a/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.md b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.md index 44a97f49..617c5775 100644 --- a/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.md +++ b/modules/ROOT/pages/security/using-graphql-middleware-with-neo4j-graphql.md @@ -1,6 +1,7 @@ # Using GraphQL Middleware with Neo4j GraphQL -[Neo4j GraphQL](https://neo4j.com/docs/graphql-manual/current/) is a library that auto-generates GraphQL APIs from type definitions, creating complete CRUD operations, relationship traversals, and optimized Cypher queries without writing resolvers. If you're new to Neo4j GraphQL, check out the [official documentation](https://neo4j.com/docs/graphql-manual/current/) to get started. +[Neo4j GraphQL](https://neo4j.com/docs/graphql-manual/current/)is a library that auto-generates GraphQL APIs from type definitions, creating complete CRUD operations, relationship traversals, and optimized Cypher queries without writing resolvers. +If you are new to Neo4j GraphQL, check out the [official documentation](https://neo4j.com/docs/graphql-manual/current/) to get started. Neo4j GraphQL provides powerful built-in features, including but not limited to: @@ -13,21 +14,24 @@ Neo4j GraphQL provides powerful built-in features, including but not limited to: - Cypher directive for user-defined operations - Custom resolvers -This set of features already provides a significant amount of flexibility to support various user requirements. However, sometimes your requirements go beyond what these built-in features can achieve. +This set of features already provides a significant amount of flexibility to support various user requirements. +However, sometimes your requirements go beyond what these built-in features can achieve. At this point, some users may consider abandoning the auto-generated resolver entirely and writing their own custom logic. -**Plot twist:** It's not. You can wrap your auto-generated Neo4j GraphQL resolver with custom logic that intercepts specific GraphQL operations. This approach allows you to retain all the benefits of auto-generation while adding exactly the custom behavior you need. +However, you can wrap your auto-generated Neo4j GraphQL resolver with custom logic that intercepts specific GraphQL operations. +This approach allows you to retain all the benefits of auto-generation while adding the custom behavior you need. -## GraphQL Middleware -In this guide, we'll use [`graphql-middleware`](https://github.com/dimatill/graphql-middleware). -GraphQL middleware is a library that provides a way to wrap and extend the behavior of your GraphQL resolvers. It acts as a layer that allows you to apply reusable logic, such as logging, validation, or authentication, across multiple resolvers in a consistent and modular way. +## GraphQL Middleware -Let's see it in action. +This guide makes use of [`graphql-middleware`](https://github.com/dimatill/graphql-middleware). +GraphQL middleware is a library that provides a way to wrap and extend the behavior of your GraphQL resolvers. +It acts as a layer that allows you to apply reusable logic, such as logging, validation, or authentication, across multiple resolvers in a consistent and modular +way. -## Example: Logging Every Request +### Logging every request -Let's begin with this Neo4j GraphQL setup: +Consider this Neo4j GraphQL setup: ```typescript import { ApolloServer } from "@apollo/server"; @@ -103,7 +107,8 @@ const schemaWithLogging = applyMiddleware(await neoSchema.getSchema(), { const server = new ApolloServer({ schema: schemaWithLogging }); ``` -**That's it. Every query and mutation is now logged.** Your auto-generated resolver is unchanged, but you've added custom behavior. +Each query and mutation is now logged. +Your auto-generated resolver is unchanged, but you have added custom behavior. Query the users: @@ -115,19 +120,22 @@ Query the users: } ``` -And you'll see in your server: +You should see in your server: ``` πŸš€ users started βœ… users completed in 23ms ``` -### Email Validation Before Database Writes -Let's say you want to enforce specific business rules before data is written to the database. For example, ensuring that email addresses provided during user creation are valid. By using middleware, you can intercept and validate the input before it reaches the Neo4j GraphQL resolver. +### Email validation before database writes + +You can use middleware to enforce specific business rules before data is written to the database. +For example, you can ensure that email addresses provided during user creation are valid. +By using middleware, you can intercept and validate the input before it reaches the Neo4j GraphQL resolver. -Let's add a middleware that validates the email input in the `createUsers` operation. -A validation error will be thrown before it reaches the Neo4j GraphQL resolver, and the GraphQL client will receive an error message: "Invalid email addresses detected". +Add a middleware that validates the email input in the `createUsers` operation. +A validation error will be thrown before it reaches the Neo4j GraphQL resolver, and the GraphQL client will receive the error message "Invalid email addresses detected". ```typescript /* ...existing code... */ @@ -157,7 +165,7 @@ const schema = applyMiddleware( ); ``` -Try to create a user with an email "not-an-email" +Try to create a user with the email "not-an-email": ```graphql mutation createUsers { @@ -171,16 +179,17 @@ mutation createUsers { ## Working with Neo4j GraphQL -Until now, most of what we've learned is applicable even outside Neo4j GraphQL as well, but there's an important concept to understand when writing middleware for Neo4j GraphQL resolvers. +Most of the above is applicable even outside Neo4j GraphQL, but there is an important concept when writing middleware for Neo4j GraphQL resolvers. -Here's the key difference from how traditional GraphQL resolvers are usually built: +Here's the key difference from how traditional GraphQL resolvers are +usually built: -**Traditional GraphQL**: Each field resolver executes independently, potentially causing multiple database calls -**Neo4j GraphQL**: The root field resolver (like `users` or `createUsers`) analyzes the entire query tree and executes one optimized Cypher query +In *traditional GraphQL*, each field resolver executes independently, potentially causing multiple database calls. +By contrast, in *Neo4j GraphQL* the root field resolver (like `users` or `createUsers`) analyzes the entire query tree and executes one optimized Cypher query. -One of Neo4j GraphQL's benefits is solving the **[N+1 problem](https://www.google.com/search?q=graphql+n%2B1)** by analyzing the entire GraphQL operation (via the `info` object) and generating optimized Cypher queries that fetch all requested data in a single database round-trip. +The N+1 problem is solved in Neo4j GraphQL by analyzing the entire GraphQL operation (via the `info` object) and generating optimized Cypher queries that fetch all requested data in a single database round-trip. -This means that when you query: +Consider this query: ```graphql { @@ -195,13 +204,16 @@ This means that when you query: } ``` -Neo4j GraphQL doesn't execute separate resolvers for `name`, `email`, `posts`, `title`, and `content`. Instead, the `users` field resolver generates and executes a single Cypher query that returns all the data at once. The nested field resolvers simply return the already-fetched data from memory. +Neo4j GraphQL doesn't execute separate resolvers for `name`, `email`, `posts`, `title`, and `content`. +Instead, the `users` field resolver generates and executes a single Cypher query that returns all the data at once. +The nested field resolvers simply return the already fetched data from memory. -**This is why timing matters for middleware** - by the time individual field resolvers execute, the database has already been queried and the data is sitting in the resolver's result. -## The Key Insight: Timing Matters +### Timing matters -Let's take the `logMiddleware` middleware that we wrote before: +Timing matters for middleware - by the time the individual field resolvers execute, the database has already been queried and the data is available in the resolver's result. + +Consider the `logMiddleware` from above: ```typescript const logMiddleware = async (resolve, root, args, context, info) => { @@ -219,7 +231,7 @@ const logMiddleware = async (resolve, root, args, context, info) => { }; ``` -Now let's apply the `logMiddleware` middleware to Queries and the User's name: +Apply the `logMiddleware` to queries and the user's name: ```typescript const schema = applyMiddleware( @@ -235,7 +247,7 @@ const schema = applyMiddleware( ); ``` -If we now run this query: +Run this query: ```graphql query { @@ -245,7 +257,7 @@ query { } ``` -We can expect the following: +You should see: ``` πŸš€ users started @@ -257,15 +269,15 @@ We can expect the following: Note how the name resolution happens after the round-trip to the database. -Here's what you need to know about when your middleware runs: +Note the following difference: -**Query/Mutation level middleware** = runs **BEFORE** and **AFTER** the Neo4j GraphQL autogenerated resolvers -**Type and Field level middleware** = runs only **AFTER** the Neo4j GraphQL autogenerated resolvers +* Query and mutation level middleware runs before and after the Neo4j GraphQL autogenerated resolvers. +* Type and field level middleware runs only after the Neo4j GraphQL autogenerated resolvers. -## Stack Multiple Middleware +## Stack multiple middlewares -It's possible to apply multiple middleware for the same field. -For instance, we can apply diverse middleware to the same `users` resolver: +It's possible to apply multiple pieces of middleware for the same field. +For instance, you can apply diverse middleware to the same `users` resolver: ```typescript const schema = applyMiddleware( @@ -300,9 +312,10 @@ const schema = applyMiddleware( ); ``` -The order in which middleware is applied is important, as they execute in sequence. Each middleware wraps the next one, creating a chain of execution from outermost to innermost. +The order in which middleware is applied is important, as they execute one after the other. +Each middleware wraps the next one, creating a chain of execution from outermost to innermost. -When you run the query: +Run this query: ```graphql query { @@ -324,15 +337,17 @@ B completed A completed ``` -The users' resolver is wrapped in three layers of middleware. +The user's resolver is wrapped in three layers of middleware. + ## Conclusion GraphQL middleware with Neo4j GraphQL gives you the best of both worlds: the power of auto-generated schemas and the flexibility to inject custom logic exactly where you need it. -When you need custom logic, graphql-middleware lets you keep the rapid development benefits of Neo4j GraphQL while adding exactly the custom behavior you need. +When you need custom logic, graphql-middleware lets you keep the rapid development benefits of Neo4j GraphQL while adding the custom behavior you need. -The GraphQL ecosystem evolves rapidly. [The Guild](https://the-guild.dev/) has developed [Envelop](https://envelop.dev/) with its own [graphql-middleware plugin](https://www.npmjs.com/package/@envelop/graphql-middleware). +The GraphQL ecosystem evolves rapidly. +[The Guild](https://the-guild.dev/) has developed [Envelop](https://envelop.dev/) with its own [graphql-middleware plugin](https://www.npmjs.com/package/@envelop/graphql-middleware). -We chose `graphql-middleware` for this guide because it's server-agnostic and delivers the clearest path to understanding middleware with Neo4j GraphQL. -If you need a more comprehensive plugin ecosystem, we recommend exploring Envelop! \ No newline at end of file +This guide uses `graphql-middleware` because it's server-agnostic and delivers the clearest path to understanding middleware with Neo4j GraphQL. +If you need a more comprehensive plugin ecosystem, we recommend exploring envelop. \ No newline at end of file