# GraphQL

Course: [API](https://www.devtutorials.ai/learn/building-apis) · Section: Beyond REST
Source: https://www.devtutorials.ai/learn/building-apis/beyond-rest/graphql

> Let the client ask for exactly the fields it needs, in one request, with a GraphQL query.

**GraphQL** is an API style where the client sends a single query describing exactly the data it wants, and the server returns exactly that data. Instead of calling multiple endpoints and receiving fixed response shapes, the client chooses the fields it needs for a particular screen. This helps reduce extra network requests and avoids sending data that is never used.

## Watch the Home Screen Make Four Trips

The Brew & Bytes home screen needs:

- Customer name
- Loyalty points
- Recent order totals
- Featured drink

With a traditional REST API, those values come from multiple endpoints. Press **Run**.

Results:

- Four separate HTTP requests are sent.
- Four network trips are required.
- The screen uses only a few values from each response.

The responses also contain additional fields:

```text
email
phone
address
imageUrl
calories
placedAt
```

The screen never uses them. Sending data that the client does not need is called **over-fetching**.

**In simple words:** REST returns fixed responses. GraphQL allows the client to request exactly the fields it needs.

## TL;DR

- GraphQL usually exposes a single endpoint: **POST /graphql**

- Queries specify exactly which fields should be returned.
- Responses match the shape of the query.
- A **schema** defines available types and fields.
- A **resolver** provides the value for a field.
- **Queries** read data.
- **Mutations** change data.
- GraphQL can suffer from the **N+1 problem**.
- Use **batching** (DataLoader) to solve N+1.
- GraphQL errors often return: **200 OK** with an `errors` collection.
- Apply query depth and cost limits to prevent abuse.

## A Real-World Example

Imagine a waiter serving a table. A poor experience looks like this:

1. Ask for coffee.
2. Wait.
3. Ask for water.
4. Wait.
5. Ask for dessert.
6. Wait.

Many trips are required. A better waiter collects the entire order at once and brings back exactly what everyone requested. GraphQL works similarly. The client describes all required data in one request, and the server returns everything together.

## What You Will Learn

In this lesson, you will learn:

1. What over-fetching is.
2. How GraphQL queries work.
3. What schemas and resolvers do.
4. How mutations modify data.
5. How batching solves N+1 query problems.
6. How GraphQL reports errors.
7. Why query limits are important.

## One Query, One Trip

The Brew & Bytes home screen can be loaded with a single GraphQL query:

```graphql
{
  customer(id: 7) {
    name
    points
    orders {
      id
      total
    }
  }
  featured {
    name
    price
  }
}
```

### Query Components

Field with argument:

```graphql
customer(id: 7)
```

Selection set:

```graphql
{
  name
  points
}
```

Nested selection set:

```graphql
orders {
  id
  total
}
```

Benefits:

- One network request.
- One response.
- Only requested fields are returned.

The response shape mirrors the query structure:

```json
{
  "customer": {
    "name": "Asha Rao",
    "points": 120,
    "orders": [...]
  },
  "featured": {
    "name": "Cold Coffee",
    "price": 180
  }
}
```

Notice what does **not** appear:

```text
email
phone
imageUrl
calories
```

because they were never requested.

Real GraphQL servers are built with frameworks such as: Hot Chocolate (.NET), GraphQL Yoga (TypeScript), Apollo Server (TypeScript). The examples in this lesson use a tiny educational GraphQL engine to demonstrate the concepts.

## Schema and Resolvers

A GraphQL server exposes a **schema**. The schema defines:

- Available types
- Fields
- Arguments
- Mutations

Example:

```graphql
type Query {
  customer(id: Int!): Customer
  featured: MenuItem
}

type Customer {
  id: Int!
  name: String!
  points: Int!
  orders: [Order!]!
}
```

### Understanding the Symbols

Required integer:

```graphql
Int!
```

Required string:

```graphql
String!
```

Required list of orders:

```graphql
[Order!]!
```

### What Is a Resolver?

Each field has a small function called a resolver.

Example:

```text
customer → Load customer
name     → Return customer name
orders   → Return customer orders
points   → Return loyalty points
```

Resolvers execute only for fields that appear in the query.

If a client requests:

```graphql
{
  customer(id: 7) {
    name
  }
}
```

then the server does not need to compute:

```text
points
orders
email
```

because they were never requested.

## The N+1 Problem

GraphQL makes nested queries easy, but it can unintentionally create excessive database activity.

Consider:

```graphql
{
  orders {
    id
    items {
      name
    }
  }
}
```

Execution:

1. Load all orders.
2. Load items for order 1.
3. Load items for order 2.
4. Load items for order 3.

Database queries: 1 + N

This is called the **N+1 problem**.

### Without Batching

```http
X-Db-Queries: 4
```

### With Batching

```http
X-Db-Queries: 2
```

Execution:

1. Load all orders.
2. Load all items in a single query.

The response remains identical.

The database work becomes dramatically smaller.

Use a DataLoader whenever a resolver appears inside a collection. Popular GraphQL frameworks include built-in support for batching and DataLoader patterns.

## Change Data with Mutations

Queries retrieve data. Mutations change data. Example mutation:

```graphql
mutation {
  placeOrder(
    item: "Latte"
    quantity: 2
  ) {
    id
    total
  }
}
```

The mutation:

1. Creates a new order.
2. Calculates the total.
3. Saves the order.
4. Returns requested fields.

Response:

```json
{
  "placeOrder": {
    "id": 103,
    "total": 440
  }
}
```

The client chooses which fields to receive.

For example:

```graphql
{
  id
}
```

or:

```graphql
{
  id
  total
}
```

or:

```graphql
{
  id
  total
  item
}
```

## Errors and Depth Limits

GraphQL handles errors differently from REST. A request may return: **200 OK** while still containing errors.

### Invalid Field

Request:

```graphql
{
  customer(id: 7) {
    email
  }
}
```

Response:

```json
{
  "errors": [
    {
      "message": "Cannot query field \"email\"..."
    }
  ]
}
```

The HTTP status remains:

```http
200 OK
```

### Important Rule

Always check:

```json
"errors"
```

not just the HTTP status code.

A GraphQL response with status 200 is not automatically successful. Check the `errors` property on every response.

## Why Depth Limits Matter

Objects can reference each other.

Example:

```graphql
Customer
 └─ Orders
     └─ Customer
         └─ Orders
             └─ Customer
```

A malicious user could create extremely deep queries:

```graphql
customer {
  orders {
    customer {
      orders {
        customer {
          orders {
            ...
          }
        }
      }
    }
  }
}
```

These queries consume significant resources. A depth limit prevents this. Example rule:

```text
Maximum depth: 3
```

Allowed:

```graphql
customer {
  orders {
    id
  }
}
```

Rejected:

```graphql
customer {
  orders {
    customer {
      orders {
        id
      }
    }
  }
}
```

Many production GraphQL servers also implement:

- Cost limits
- Complexity analysis
- Query size limits

## REST vs GraphQL

| Feature | REST | GraphQL |
|---|---|---|
| Endpoints | Multiple endpoints | Usually one endpoint |
| Response Shape | Defined by server | Defined by client |
| Over-fetching | Common | Rare |
| Under-fetching | Possible | Rare |
| Multiple Requests | Often needed | Usually fewer |
| Caching | Simple HTTP caching | More complex |
| Learning Curve | Lower | Higher |

### When GraphQL Works Well

GraphQL is often a good choice when:

- Mobile apps need small responses.
- Different screens need different fields.
- Data comes from many sources.
- A page requires several related resources.

### When REST Works Well

REST is often better when:

- APIs are public.
- HTTP caching is important.
- Resources map naturally to URLs.
- Simplicity matters more than flexibility.

## Where You See This

- GitHub GraphQL API retrieves repositories, issues, pull requests, and users with a single query.
- Shopify provides GraphQL APIs for products, customers, and orders.
- Mobile applications use GraphQL to reduce network traffic.
- Many .NET teams use Hot Chocolate.
- Many TypeScript teams use GraphQL Yoga or Apollo Server.

## Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| `Cannot query field "x"` | The field is not defined in the schema | Add the field or correct the spelling |
| Empty screen with no visible error | The application ignores GraphQL errors | Always inspect the `errors` collection |
| High database load on list pages | N+1 query problem | Use batching and DataLoader |
| Slow server under heavy load | Deep or expensive queries | Add depth and complexity limits |
| Unexpected null values | Resolver failures | Handle errors and validate resolver logic |
| Large responses return slowly | Too many fields are requested | Request only the fields actually needed |

## Exercises

## What to Remember

- GraphQL has one endpoint. The client sends a query and gets back exactly the fields that it asked for.
- The reply has the same shape as the query.
- Each field has a **resolver**. **Queries** read data, and **mutations** change data.
- Fields inside lists cause **N+1** queries. Batch them with a DataLoader.
- Errors come back in an `errors` list, often with status `200`. Always check it.
- Limit the depth of queries. In real projects, use Hot Chocolate, GraphQL Yoga or Apollo, not a home-made engine.

**Next:** gRPC. You will see how services talk to each other fast, with a strict contract and binary messages.

var app = builder.Build();

// The app home screen shows: the name, recent order totals, the featured drink and points.
// With REST, that is four endpoints, so four trips, and each reply has fields the screen never uses.
app.MapGet("/customers/7", () => Results.Ok(new
{
    id = 7, name = "Asha Rao", email = "asha@example.com", phone = "+91 98450 12345",
    address = "12 MG Road, Bengaluru", joined = "2023-04-12", marketingOptIn = true
}));

app.MapGet("/customers/7/orders", () => Results.Ok(new[]
{
    new { id = 101, customerId = 7, item = "Latte", quantity = 2, price = 220, total = 440, status = "delivered", placedAt = "2026-10-01T09:30:00Z" },
    new { id = 103, customerId = 7, item = "Blueberry Muffin", quantity = 1, price = 150, total = 150, status = "delivered", placedAt = "2026-10-03T08:10:00Z" }
}));

app.MapGet("/menu/featured", () => Results.Ok(new
{
    id = 4, name = "Cold Coffee", price = 180, description = "Chilled coffee with milk and ice",
    calories = 210, allergens = new[] { "milk" }, imageUrl = "/img/cold-coffee.jpg"
}));

app.MapGet("/customers/7/points", () => Results.Ok(new { customerId = 7, points = 120, tier = "Silver", expires = "2027-03-31" }));

app.Run();`;

const app = new Hono();

// The app home screen shows: the name, recent order totals, the featured drink and points.
// With REST, that is four endpoints, so four trips, and each reply has fields the screen never uses.
app.get("/customers/7", (c) =>
  c.json({
    id: 7, name: "Asha Rao", email: "asha@example.com", phone: "+91 98450 12345",
    address: "12 MG Road, Bengaluru", joined: "2023-04-12", marketingOptIn: true,
  }, 200));

app.get("/customers/7/orders", (c) =>
  c.json([
    { id: 101, customerId: 7, item: "Latte", quantity: 2, price: 220, total: 440, status: "delivered", placedAt: "2026-10-01T09:30:00Z" },
    { id: 103, customerId: 7, item: "Blueberry Muffin", quantity: 1, price: 150, total: 150, status: "delivered", placedAt: "2026-10-03T08:10:00Z" },
  ], 200));

app.get("/menu/featured", (c) =>
  c.json({
    id: 4, name: "Cold Coffee", price: 180, description: "Chilled coffee with milk and ice",
    calories: 210, allergens: ["milk"], imageUrl: "/img/cold-coffee.jpg",
  }, 200));

app.get("/customers/7/points", (c) => c.json({ customerId: 7, points: 120, tier: "Silver", expires: "2027-03-31" }, 200));

###
GET /customers/7/orders

###
GET /menu/featured

###
GET /customers/7/points`;

using System.Text.Json;
using System.Text.RegularExpressions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var customers = new Dictionary<int, CustomerRow> { [7] = new(7, "Asha Rao", 120), [8] = new(8, "Ravi", 40) };
var orders = new List<OrderRow> { new(101, 7, "Latte", 440), new(102, 8, "Masala Chai", 120), new(103, 7, "Blueberry Muffin", 150) };

// Each type is a set of resolvers: one small function per field.
Obj Customer(CustomerRow c) => new("Customer", new()
{
    ["id"] = _ => c.Id, ["name"] = _ => c.Name, ["points"] = _ => c.Points, 
    ["orders"] = _ => orders.Where(o => o.CustomerId == c.Id).Select(Order).ToList()
});
Obj Order(OrderRow o) => new("Order", new() { ["id"] = _ => o.Id, ["item"] = _ => o.Item, ["total"] = _ => o.Total });

var root = new Obj("Query", new()
{
    ["customer"] = a => Customer(customers[(int)a["id"]]),
    ["featured"] = _ => new Obj("MenuItem", new() { ["name"] = _ => "Cold Coffee", ["price"] = _ => 180 })
});

// One endpoint. The client sends a query, and gets back exactly what it asked for.
app.MapPost("/graphql", (GqlRequest req) => Results.Text(Gql.Execute(req.Query, root), "application/json"));

app.Run();

record GqlRequest(string Query);
record CustomerRow(int Id, string Name, int Points);
record OrderRow(int Id, int CustomerId, string Item, int Total);

// ---------- Mini GraphQL engine: for learning only. Real projects use Hot Chocolate. ----------
record Field(string Name, Dictionary<string, object> Args, List<Field>? Sub);
record Obj(string Type, Dictionary<string, Func<Dictionary<string, object>, object?>> Fields);
class GqlError : Exception { public GqlError(string m) : base(m) { } }

static class Gql
{
    static readonly JsonSerializerOptions Opts = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

    // Run a query and return the JSON text: {"data":...} or {"errors":[...]}.
    public static string Execute(string query, Obj root, Obj? mutation = null, int maxDepth = 99)
    {
        object result;
        try
        {
            var (isMutation, sel) = Parse(query);
            if (Depth(sel) > maxDepth) throw new GqlError($"Query is too deep: depth {Depth(sel)}, max {maxDepth}.");
            result = new { data = Run(isMutation ? mutation! : root, sel) };
        }
        catch (GqlError e) { result = new { errors = new[] { new { message = e.Message } } }; }
        return JsonSerializer.Serialize(result, Opts);
    }

    static int Depth(List<Field>? sel) => sel == null ? 0 : 1 + sel.Max(f => Depth(f.Sub));

    // Split the text into tokens: strings, numbers, names and { } ( ) :
    static (bool, List<Field>) Parse(string q)
    {
        var t = Regex.Matches(q, "\\"[^\\"]*\\"|-?\\\\d+|\\\\w+|[{}():]").Select(m => m.Value).ToList();
        int i = 0; bool isMutation = t[0] == "mutation";
        if (t[0] == "query" || isMutation) i = t[1] == "{" ? 1 : 2;
        return (isMutation, ReadSet(t, ref i));
    }

    // A selection set: { field(arg: value) { ... } field ... }
    static List<Field> ReadSet(List<string> t, ref int i)
    {
        var fields = new List<Field>(); i++;
        while (t[i] != "}")
        {
            string name = t[i++]; var args = new Dictionary<string, object>();
            if (t[i] == "(")
            {
                for (i++; t[i] != ")"; i += 3)
                    args[t[i]] = t[i + 2].StartsWith("\\"") ? t[i + 2].Trim('"') : int.Parse(t[i + 2]);
                i++;
            }
            fields.Add(new Field(name, args, t[i] == "{" ? ReadSet(t, ref i) : null));
        }
        i++; return fields;
    }

    // Call the resolver for each selected field, in the order asked.
    static Dictionary<string, object?> Run(Obj obj, List<Field> sel)
    {
        var result = new Dictionary<string, object?>();
        foreach (var f in sel)
        {
            if (!obj.Fields.TryGetValue(f.Name, out var resolve))
                throw new GqlError($"Cannot query field \\"{f.Name}\\" on type \\"{obj.Type}\\".");
            result[f.Name] = resolve(f.Args) switch
            {
                Obj child => Run(child, f.Sub!),
                IEnumerable<Obj> list => list.Select(x => Run(x, f.Sub!)).ToList(),
                var value => value
            };
        }
        return result;
    }
}`;

const customers = new Map([[7, { id: 7, name: "Asha Rao", points: 120 }], [8, { id: 8, name: "Ravi", points: 40 }]]);
const orders = [
  { id: 101, customerId: 7, item: "Latte", total: 440 },
  { id: 102, customerId: 8, item: "Masala Chai", total: 120 },
  { id: 103, customerId: 7, item: "Blueberry Muffin", total: 150 },
];

// Each type is a set of resolvers: one small function per field.
const customer = (c: { id: number; name: string; points: number }): Obj => ({
  type: "Customer",
  fields: {
    id: () => c.id, name: () => c.name, points: () => c.points, 
    orders: () => orders.filter((o) => o.customerId === c.id).map(order),
  },
});
const order = (o: { id: number; item: string; total: number }): Obj => ({
  type: "Order",
  fields: { id: () => o.id, item: () => o.item, total: () => o.total },
});

const root: Obj = {
  type: "Query",
  fields: {
    customer: (a) => customer(customers.get(a.id)!),
    featured: () => ({ type: "MenuItem", fields: { name: () => "Cold Coffee", price: () => 180 } }),
  },
};

const app = new Hono();

// One endpoint. The client sends a query, and gets back exactly what it asked for.
app.post("/graphql", async (c) => {
  const body = await c.req.json<{ query: string }>();
  return c.json(execute(body.query, root), 200);
});

// ---------- Mini GraphQL engine: for learning only. Real projects use GraphQL Yoga or Apollo. ----------
type Field = { name: string; args: Record<string, any>; sub: Field[] | null };
type Obj = { type: string; fields: Record<string, (args: Record<string, any>) => any> };
class GqlError extends Error {}

// Run a query and return {data: ...} or {errors: [...]}.
function execute(query: string, root: Obj, mutation: Obj | null = null, maxDepth = 99): object {
  try {
    const [isMutation, sel] = parse(query);
    if (depth(sel) > maxDepth) throw new GqlError(\`Query is too deep: depth \${depth(sel)}, max \${maxDepth}.\`);
    return { data: run(isMutation ? mutation! : root, sel) };
  } catch (e) {
    if (e instanceof GqlError) return { errors: [{ message: e.message }] };
    throw e;
  }
}

function depth(sel: Field[] | null): number {
  return sel === null ? 0 : 1 + Math.max(...sel.map((f) => depth(f.sub)));
}

// Split the text into tokens: strings, numbers, names and { } ( ) :
function parse(q: string): [boolean, Field[]] {
  const t = q.match(/"[^"]*"|-?\\d+|\\w+|[{}():]/g)!;
  const pos = { i: 0 };
  const isMutation = t[0] === "mutation";
  if (t[0] === "query" || isMutation) pos.i = t[1] === "{" ? 1 : 2;
  return [isMutation, readSet(t, pos)];
}

// A selection set: { field(arg: value) { ... } field ... }
function readSet(t: string[], pos: { i: number }): Field[] {
  const fields: Field[] = [];
  pos.i++;
  while (t[pos.i] !== "}") {
    const name = t[pos.i++];
    const args: Record<string, any> = {};
    if (t[pos.i] === "(") {
      for (pos.i++; t[pos.i] !== ")"; pos.i += 3) {
        const v = t[pos.i + 2];
        args[t[pos.i]] = v.startsWith('"') ? v.slice(1, -1) : parseInt(v);
      }
      pos.i++;
    }
    fields.push({ name, args, sub: t[pos.i] === "{" ? readSet(t, pos) : null });
  }
  pos.i++;
  return fields;
}

// Call the resolver for each selected field, in the order asked.
function run(obj: Obj, sel: Field[]): Record<string, any> {
  const result: Record<string, any> = {};
  for (const f of sel) {
    const resolve = Object.hasOwn(obj.fields, f.name) ? obj.fields[f.name] : undefined;
    if (!resolve) throw new GqlError(\`Cannot query field "\${f.name}" on type "\${obj.type}".\`);
    const value = resolve(f.args);
    if (Array.isArray(value)) result[f.name] = value.map((x) => run(x, f.sub!));
    else if (value && typeof value === "object" && "fields" in value) result[f.name] = run(value, f.sub!);
    else result[f.name] = value;
  }
  return result;
}`;

Content-Type: application/json

{"query":"{ customer(id: 7) { name points orders { id total } } featured { name price } }"}`;

using System.Text.Json;
using System.Text.RegularExpressions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var orders = new List<OrderRow> { new(101, 7, 440), new(102, 8, 120), new(103, 7, 330) };
var items = new List<ItemRow> { new(101, "Latte"), new(102, "Masala Chai"), new(103, "Blueberry Muffin"), new(103, "Cold Coffee") };

// Pretend database: every Load call is one database query, and we count them.
int queries = 0;
List<OrderRow> LoadOrders() { queries++; return orders; }
List<ItemRow> LoadItems(List<int> ids) { queries++; return items.Where(x => ids.Contains(x.OrderId)).ToList(); }
Obj Item(ItemRow r) => new("Item", new() { ["name"] = _ => r.Name });

// Naive: each order loads its own items. 1 query for the orders + 1 per order = N+1.
Obj NaiveOrder(OrderRow o) => new("Order", new()
{
    ["id"] = _ => o.Id,
    ["items"] = _ => LoadItems(new List<int> { o.Id }).Select(Item).ToList()
});
var naiveRoot = new Obj("Query", new() { ["orders"] = _ => LoadOrders().Select(NaiveOrder).ToList() });

// Batched (DataLoader style): collect every order id first, then load all items in ONE query.
var pending = new List<int>();
List<ItemRow>? batch = null;
Obj BatchedOrder(OrderRow o)
{
    pending.Add(o.Id);
    return new("Order", new()
    {
        ["id"] = _ => o.Id,
        ["items"] = _ => (batch ??= LoadItems(pending)).Where(x => x.OrderId == o.Id).Select(Item).ToList()
    });
}
var batchedRoot = new Obj("Query", new() { ["orders"] = _ => LoadOrders().Select(BatchedOrder).ToList() });

IResult Answer(HttpContext ctx, string query, Obj root)
{
    queries = 0; pending.Clear(); batch = null;
    string json = Gql.Execute(query, root);
    ctx.Response.Headers["X-Db-Queries"] = queries.ToString();
    return Results.Text(json, "application/json");
}
app.MapPost("/graphql", (GqlRequest req, HttpContext ctx) => Answer(ctx, req.Query, naiveRoot));
app.MapPost("/graphql-batched", (GqlRequest req, HttpContext ctx) => Answer(ctx, req.Query, batchedRoot));

app.Run();

record GqlRequest(string Query);
record OrderRow(int Id, int CustomerId, int Total);
record ItemRow(int OrderId, string Name);

// ---------- Mini GraphQL engine: for learning only. Real projects use Hot Chocolate. ----------
record Field(string Name, Dictionary<string, object> Args, List<Field>? Sub);
record Obj(string Type, Dictionary<string, Func<Dictionary<string, object>, object?>> Fields);
class GqlError : Exception { public GqlError(string m) : base(m) { } }

static class Gql
{
    static readonly JsonSerializerOptions Opts = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

    // Run a query and return the JSON text: {"data":...} or {"errors":[...]}.
    public static string Execute(string query, Obj root, Obj? mutation = null, int maxDepth = 99)
    {
        object result;
        try
        {
            var (isMutation, sel) = Parse(query);
            if (Depth(sel) > maxDepth) throw new GqlError($"Query is too deep: depth {Depth(sel)}, max {maxDepth}.");
            result = new { data = Run(isMutation ? mutation! : root, sel) };
        }
        catch (GqlError e) { result = new { errors = new[] { new { message = e.Message } } }; }
        return JsonSerializer.Serialize(result, Opts);
    }

    static int Depth(List<Field>? sel) => sel == null ? 0 : 1 + sel.Max(f => Depth(f.Sub));

    // Split the text into tokens: strings, numbers, names and { } ( ) :
    static (bool, List<Field>) Parse(string q)
    {
        var t = Regex.Matches(q, "\\"[^\\"]*\\"|-?\\\\d+|\\\\w+|[{}():]").Select(m => m.Value).ToList();
        int i = 0; bool isMutation = t[0] == "mutation";
        if (t[0] == "query" || isMutation) i = t[1] == "{" ? 1 : 2;
        return (isMutation, ReadSet(t, ref i));
    }

    // A selection set: { field(arg: value) { ... } field ... }
    static List<Field> ReadSet(List<string> t, ref int i)
    {
        var fields = new List<Field>(); i++;
        while (t[i] != "}")
        {
            string name = t[i++]; var args = new Dictionary<string, object>();
            if (t[i] == "(")
            {
                for (i++; t[i] != ")"; i += 3)
                    args[t[i]] = t[i + 2].StartsWith("\\"") ? t[i + 2].Trim('"') : int.Parse(t[i + 2]);
                i++;
            }
            fields.Add(new Field(name, args, t[i] == "{" ? ReadSet(t, ref i) : null));
        }
        i++; return fields;
    }

    // Call the resolver for each selected field, in the order asked.
    static Dictionary<string, object?> Run(Obj obj, List<Field> sel)
    {
        var result = new Dictionary<string, object?>();
        foreach (var f in sel)
        {
            if (!obj.Fields.TryGetValue(f.Name, out var resolve))
                throw new GqlError($"Cannot query field \\"{f.Name}\\" on type \\"{obj.Type}\\".");
            result[f.Name] = resolve(f.Args) switch
            {
                Obj child => Run(child, f.Sub!),
                IEnumerable<Obj> list => list.Select(x => Run(x, f.Sub!)).ToList(),
                var value => value
            };
        }
        return result;
    }
}`;

type OrderRow = { id: number; customerId: number; total: number };
type ItemRow = { orderId: number; name: string };
const orders: OrderRow[] = [{ id: 101, customerId: 7, total: 440 }, { id: 102, customerId: 8, total: 120 }, { id: 103, customerId: 7, total: 330 }];
const items: ItemRow[] = [{ orderId: 101, name: "Latte" }, { orderId: 102, name: "Masala Chai" }, { orderId: 103, name: "Blueberry Muffin" }, { orderId: 103, name: "Cold Coffee" }];

// Pretend database: every load call is one database query, and we count them.
let queries = 0;
const loadOrders = () => { queries++; return orders; };
const loadItems = (ids: number[]) => { queries++; return items.filter((x) => ids.includes(x.orderId)); };
const item = (r: ItemRow): Obj => ({ type: "Item", fields: { name: () => r.name } });

// Naive: each order loads its own items. 1 query for the orders + 1 per order = N+1.
const naiveOrder = (o: OrderRow): Obj => ({
  type: "Order",
  fields: { id: () => o.id, items: () => loadItems([o.id]).map(item) },
});
const naiveRoot: Obj = { type: "Query", fields: { orders: () => loadOrders().map(naiveOrder) } };

// Batched (DataLoader style): collect every order id first, then load all items in ONE query.
let pending: number[] = [];
let batch: ItemRow[] | null = null;
const batchedOrder = (o: OrderRow): Obj => {
  pending.push(o.id);
  return {
    type: "Order",
    fields: { id: () => o.id, items: () => (batch ??= loadItems(pending)).filter((x) => x.orderId === o.id).map(item) },
  };
};
const batchedRoot: Obj = { type: "Query", fields: { orders: () => loadOrders().map(batchedOrder) } };

const app = new Hono();

const answer = async (c: any, root: Obj) => {
  const body = await c.req.json();
  queries = 0; pending = []; batch = null;
  const result = execute(body.query, root);
  c.header("X-Db-Queries", String(queries));
  return c.json(result, 200);
};
app.post("/graphql", (c) => answer(c, naiveRoot));
app.post("/graphql-batched", (c) => answer(c, batchedRoot));

// ---------- Mini GraphQL engine: for learning only. Real projects use GraphQL Yoga or Apollo. ----------
type Field = { name: string; args: Record<string, any>; sub: Field[] | null };
type Obj = { type: string; fields: Record<string, (args: Record<string, any>) => any> };
class GqlError extends Error {}

// Run a query and return {data: ...} or {errors: [...]}.
function execute(query: string, root: Obj, mutation: Obj | null = null, maxDepth = 99): object {
  try {
    const [isMutation, sel] = parse(query);
    if (depth(sel) > maxDepth) throw new GqlError(\`Query is too deep: depth \${depth(sel)}, max \${maxDepth}.\`);
    return { data: run(isMutation ? mutation! : root, sel) };
  } catch (e) {
    if (e instanceof GqlError) return { errors: [{ message: e.message }] };
    throw e;
  }
}

function depth(sel: Field[] | null): number {
  return sel === null ? 0 : 1 + Math.max(...sel.map((f) => depth(f.sub)));
}

// Split the text into tokens: strings, numbers, names and { } ( ) :
function parse(q: string): [boolean, Field[]] {
  const t = q.match(/"[^"]*"|-?\\d+|\\w+|[{}():]/g)!;
  const pos = { i: 0 };
  const isMutation = t[0] === "mutation";
  if (t[0] === "query" || isMutation) pos.i = t[1] === "{" ? 1 : 2;
  return [isMutation, readSet(t, pos)];
}

// A selection set: { field(arg: value) { ... } field ... }
function readSet(t: string[], pos: { i: number }): Field[] {
  const fields: Field[] = [];
  pos.i++;
  while (t[pos.i] !== "}") {
    const name = t[pos.i++];
    const args: Record<string, any> = {};
    if (t[pos.i] === "(") {
      for (pos.i++; t[pos.i] !== ")"; pos.i += 3) {
        const v = t[pos.i + 2];
        args[t[pos.i]] = v.startsWith('"') ? v.slice(1, -1) : parseInt(v);
      }
      pos.i++;
    }
    fields.push({ name, args, sub: t[pos.i] === "{" ? readSet(t, pos) : null });
  }
  pos.i++;
  return fields;
}

// Call the resolver for each selected field, in the order asked.
function run(obj: Obj, sel: Field[]): Record<string, any> {
  const result: Record<string, any> = {};
  for (const f of sel) {
    const resolve = Object.hasOwn(obj.fields, f.name) ? obj.fields[f.name] : undefined;
    if (!resolve) throw new GqlError(\`Cannot query field "\${f.name}" on type "\${obj.type}".\`);
    const value = resolve(f.args);
    if (Array.isArray(value)) result[f.name] = value.map((x) => run(x, f.sub!));
    else if (value && typeof value === "object" && "fields" in value) result[f.name] = run(value, f.sub!);
    else result[f.name] = value;
  }
  return result;
}`;

Content-Type: application/json

{"query":"{ orders { id items { name } } }"}

###
POST /graphql-batched
Content-Type: application/json

{"query":"{ orders { id items { name } } }"}`;

using System.Text.Json;
using System.Text.RegularExpressions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var menu = new Dictionary<string, int> { ["Latte"] = 220, ["Masala Chai"] = 120, ["Blueberry Muffin"] = 150, ["Cold Coffee"] = 180 };
var orders = new List<OrderRow> { new(101, "Latte", 2, 440), new(102, "Masala Chai", 1, 120) };

Obj Order(OrderRow o) => new("Order", new()
{
    ["id"] = _ => o.Id, ["item"] = _ => o.Item, ["quantity"] = _ => o.Quantity, ["total"] = _ => o.Total
});

// Query = read. Mutation = change. Both are just fields with resolvers.
var root = new Obj("Query", new() { ["orders"] = _ => orders.Select(Order).ToList() });
var mutation = new Obj("Mutation", new()
{
    ["placeOrder"] = a =>
    {
        string item = (string)a["item"];
        int quantity = (int)a["quantity"];
        var order = new OrderRow(orders.Count + 101, item, quantity, menu[item] * quantity);
        orders.Add(order);
        return Order(order); // The client picks which fields of the new order it wants back.
    }
});

app.MapPost("/graphql", (GqlRequest req) => Results.Text(Gql.Execute(req.Query, root, mutation), "application/json"));

app.Run();

record GqlRequest(string Query);
record OrderRow(int Id, string Item, int Quantity, int Total);

// ---------- Mini GraphQL engine: for learning only. Real projects use Hot Chocolate. ----------
record Field(string Name, Dictionary<string, object> Args, List<Field>? Sub);
record Obj(string Type, Dictionary<string, Func<Dictionary<string, object>, object?>> Fields);
class GqlError : Exception { public GqlError(string m) : base(m) { } }

static class Gql
{
    static readonly JsonSerializerOptions Opts = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

    // Run a query and return the JSON text: {"data":...} or {"errors":[...]}.
    public static string Execute(string query, Obj root, Obj? mutation = null, int maxDepth = 99)
    {
        object result;
        try
        {
            var (isMutation, sel) = Parse(query);
            if (Depth(sel) > maxDepth) throw new GqlError($"Query is too deep: depth {Depth(sel)}, max {maxDepth}.");
            result = new { data = Run(isMutation ? mutation! : root, sel) };
        }
        catch (GqlError e) { result = new { errors = new[] { new { message = e.Message } } }; }
        return JsonSerializer.Serialize(result, Opts);
    }

    static int Depth(List<Field>? sel) => sel == null ? 0 : 1 + sel.Max(f => Depth(f.Sub));

    // Split the text into tokens: strings, numbers, names and { } ( ) :
    static (bool, List<Field>) Parse(string q)
    {
        var t = Regex.Matches(q, "\\"[^\\"]*\\"|-?\\\\d+|\\\\w+|[{}():]").Select(m => m.Value).ToList();
        int i = 0; bool isMutation = t[0] == "mutation";
        if (t[0] == "query" || isMutation) i = t[1] == "{" ? 1 : 2;
        return (isMutation, ReadSet(t, ref i));
    }

    // A selection set: { field(arg: value) { ... } field ... }
    static List<Field> ReadSet(List<string> t, ref int i)
    {
        var fields = new List<Field>(); i++;
        while (t[i] != "}")
        {
            string name = t[i++]; var args = new Dictionary<string, object>();
            if (t[i] == "(")
            {
                for (i++; t[i] != ")"; i += 3)
                    args[t[i]] = t[i + 2].StartsWith("\\"") ? t[i + 2].Trim('"') : int.Parse(t[i + 2]);
                i++;
            }
            fields.Add(new Field(name, args, t[i] == "{" ? ReadSet(t, ref i) : null));
        }
        i++; return fields;
    }

    // Call the resolver for each selected field, in the order asked.
    static Dictionary<string, object?> Run(Obj obj, List<Field> sel)
    {
        var result = new Dictionary<string, object?>();
        foreach (var f in sel)
        {
            if (!obj.Fields.TryGetValue(f.Name, out var resolve))
                throw new GqlError($"Cannot query field \\"{f.Name}\\" on type \\"{obj.Type}\\".");
            result[f.Name] = resolve(f.Args) switch
            {
                Obj child => Run(child, f.Sub!),
                IEnumerable<Obj> list => list.Select(x => Run(x, f.Sub!)).ToList(),
                var value => value
            };
        }
        return result;
    }
}`;

const menu = new Map([["Latte", 220], ["Masala Chai", 120], ["Blueberry Muffin", 150], ["Cold Coffee", 180]]);
type OrderRow = { id: number; item: string; quantity: number; total: number };
const orders: OrderRow[] = [{ id: 101, item: "Latte", quantity: 2, total: 440 }, { id: 102, item: "Masala Chai", quantity: 1, total: 120 }];

const order = (o: OrderRow): Obj => ({
  type: "Order",
  fields: { id: () => o.id, item: () => o.item, quantity: () => o.quantity, total: () => o.total },
});

// Query = read. Mutation = change. Both are just fields with resolvers.
const root: Obj = { type: "Query", fields: { orders: () => orders.map(order) } };
const mutation: Obj = {
  type: "Mutation",
  fields: {
    placeOrder: (a) => {
      const row: OrderRow = { id: orders.length + 101, item: a.item, quantity: a.quantity, total: menu.get(a.item)! * a.quantity };
      orders.push(row);
      return order(row); // The client picks which fields of the new order it wants back.
    },
  },
};

const app = new Hono();

app.post("/graphql", async (c) => {
  const body = await c.req.json<{ query: string }>();
  return c.json(execute(body.query, root, mutation), 200);
});

// ---------- Mini GraphQL engine: for learning only. Real projects use GraphQL Yoga or Apollo. ----------
type Field = { name: string; args: Record<string, any>; sub: Field[] | null };
type Obj = { type: string; fields: Record<string, (args: Record<string, any>) => any> };
class GqlError extends Error {}

// Run a query and return {data: ...} or {errors: [...]}.
function execute(query: string, root: Obj, mutation: Obj | null = null, maxDepth = 99): object {
  try {
    const [isMutation, sel] = parse(query);
    if (depth(sel) > maxDepth) throw new GqlError(\`Query is too deep: depth \${depth(sel)}, max \${maxDepth}.\`);
    return { data: run(isMutation ? mutation! : root, sel) };
  } catch (e) {
    if (e instanceof GqlError) return { errors: [{ message: e.message }] };
    throw e;
  }
}

function depth(sel: Field[] | null): number {
  return sel === null ? 0 : 1 + Math.max(...sel.map((f) => depth(f.sub)));
}

// Split the text into tokens: strings, numbers, names and { } ( ) :
function parse(q: string): [boolean, Field[]] {
  const t = q.match(/"[^"]*"|-?\\d+|\\w+|[{}():]/g)!;
  const pos = { i: 0 };
  const isMutation = t[0] === "mutation";
  if (t[0] === "query" || isMutation) pos.i = t[1] === "{" ? 1 : 2;
  return [isMutation, readSet(t, pos)];
}

// A selection set: { field(arg: value) { ... } field ... }
function readSet(t: string[], pos: { i: number }): Field[] {
  const fields: Field[] = [];
  pos.i++;
  while (t[pos.i] !== "}") {
    const name = t[pos.i++];
    const args: Record<string, any> = {};
    if (t[pos.i] === "(") {
      for (pos.i++; t[pos.i] !== ")"; pos.i += 3) {
        const v = t[pos.i + 2];
        args[t[pos.i]] = v.startsWith('"') ? v.slice(1, -1) : parseInt(v);
      }
      pos.i++;
    }
    fields.push({ name, args, sub: t[pos.i] === "{" ? readSet(t, pos) : null });
  }
  pos.i++;
  return fields;
}

// Call the resolver for each selected field, in the order asked.
function run(obj: Obj, sel: Field[]): Record<string, any> {
  const result: Record<string, any> = {};
  for (const f of sel) {
    const resolve = Object.hasOwn(obj.fields, f.name) ? obj.fields[f.name] : undefined;
    if (!resolve) throw new GqlError(\`Cannot query field "\${f.name}" on type "\${obj.type}".\`);
    const value = resolve(f.args);
    if (Array.isArray(value)) result[f.name] = value.map((x) => run(x, f.sub!));
    else if (value && typeof value === "object" && "fields" in value) result[f.name] = run(value, f.sub!);
    else result[f.name] = value;
  }
  return result;
}`;

Content-Type: application/json

{"query":"mutation { placeOrder(item: \\"Latte\\", quantity: 2) { id total } }"}

###
POST /graphql
Content-Type: application/json

{"query":"query { orders { id item total } }"}`;

using System.Text.Json;
using System.Text.RegularExpressions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var orders = new List<OrderRow> { new(101, 7, 440), new(103, 7, 150) };

// Order points back to its Customer, so a query can go round and round forever.
Obj Customer(int id) => new("Customer", new()
{
    ["id"] = _ => id, ["name"] = _ => "Asha Rao",
    ["orders"] = _ => orders.Where(o => o.CustomerId == id).Select(Order).ToList()
});
Obj Order(OrderRow o) => new("Order", new()
{
    ["id"] = _ => o.Id, ["total"] = _ => o.Total, ["customer"] = _ => Customer(o.CustomerId)
});
var root = new Obj("Query", new() { ["customer"] = a => Customer((int)a["id"]) });

// Reject any query nested deeper than 3 levels, before running a single resolver.
app.MapPost("/graphql", (GqlRequest req) => Results.Text(Gql.Execute(req.Query, root, maxDepth: 3), "application/json"));

app.Run();

record GqlRequest(string Query);
record OrderRow(int Id, int CustomerId, int Total);

// ---------- Mini GraphQL engine: for learning only. Real projects use Hot Chocolate. ----------
record Field(string Name, Dictionary<string, object> Args, List<Field>? Sub);
record Obj(string Type, Dictionary<string, Func<Dictionary<string, object>, object?>> Fields);
class GqlError : Exception { public GqlError(string m) : base(m) { } }

static class Gql
{
    static readonly JsonSerializerOptions Opts = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

    // Run a query and return the JSON text: {"data":...} or {"errors":[...]}.
    public static string Execute(string query, Obj root, Obj? mutation = null, int maxDepth = 99)
    {
        object result;
        try
        {
            var (isMutation, sel) = Parse(query);
            if (Depth(sel) > maxDepth) throw new GqlError($"Query is too deep: depth {Depth(sel)}, max {maxDepth}.");
            result = new { data = Run(isMutation ? mutation! : root, sel) };
        }
        catch (GqlError e) { result = new { errors = new[] { new { message = e.Message } } }; }
        return JsonSerializer.Serialize(result, Opts);
    }

    static int Depth(List<Field>? sel) => sel == null ? 0 : 1 + sel.Max(f => Depth(f.Sub));

    // Split the text into tokens: strings, numbers, names and { } ( ) :
    static (bool, List<Field>) Parse(string q)
    {
        var t = Regex.Matches(q, "\\"[^\\"]*\\"|-?\\\\d+|\\\\w+|[{}():]").Select(m => m.Value).ToList();
        int i = 0; bool isMutation = t[0] == "mutation";
        if (t[0] == "query" || isMutation) i = t[1] == "{" ? 1 : 2;
        return (isMutation, ReadSet(t, ref i));
    }

    // A selection set: { field(arg: value) { ... } field ... }
    static List<Field> ReadSet(List<string> t, ref int i)
    {
        var fields = new List<Field>(); i++;
        while (t[i] != "}")
        {
            string name = t[i++]; var args = new Dictionary<string, object>();
            if (t[i] == "(")
            {
                for (i++; t[i] != ")"; i += 3)
                    args[t[i]] = t[i + 2].StartsWith("\\"") ? t[i + 2].Trim('"') : int.Parse(t[i + 2]);
                i++;
            }
            fields.Add(new Field(name, args, t[i] == "{" ? ReadSet(t, ref i) : null));
        }
        i++; return fields;
    }

    // Call the resolver for each selected field, in the order asked.
    static Dictionary<string, object?> Run(Obj obj, List<Field> sel)
    {
        var result = new Dictionary<string, object?>();
        foreach (var f in sel)
        {
            if (!obj.Fields.TryGetValue(f.Name, out var resolve))
                throw new GqlError($"Cannot query field \\"{f.Name}\\" on type \\"{obj.Type}\\".");
            result[f.Name] = resolve(f.Args) switch
            {
                Obj child => Run(child, f.Sub!),
                IEnumerable<Obj> list => list.Select(x => Run(x, f.Sub!)).ToList(),
                var value => value
            };
        }
        return result;
    }
}`;

type OrderRow = { id: number; customerId: number; total: number };
const orders: OrderRow[] = [{ id: 101, customerId: 7, total: 440 }, { id: 103, customerId: 7, total: 150 }];

// Order points back to its Customer, so a query can go round and round forever.
const customer = (id: number): Obj => ({
  type: "Customer",
  fields: { id: () => id, name: () => "Asha Rao", orders: () => orders.filter((o) => o.customerId === id).map(order) },
});
const order = (o: OrderRow): Obj => ({
  type: "Order",
  fields: { id: () => o.id, total: () => o.total, customer: () => customer(o.customerId) },
});
const root: Obj = { type: "Query", fields: { customer: (a) => customer(a.id) } };

const app = new Hono();

// Reject any query nested deeper than 3 levels, before running a single resolver.
app.post("/graphql", async (c) => {
  const body = await c.req.json<{ query: string }>();
  return c.json(execute(body.query, root, null, 3), 200);
});

// ---------- Mini GraphQL engine: for learning only. Real projects use GraphQL Yoga or Apollo. ----------
type Field = { name: string; args: Record<string, any>; sub: Field[] | null };
type Obj = { type: string; fields: Record<string, (args: Record<string, any>) => any> };
class GqlError extends Error {}

// Run a query and return {data: ...} or {errors: [...]}.
function execute(query: string, root: Obj, mutation: Obj | null = null, maxDepth = 99): object {
  try {
    const [isMutation, sel] = parse(query);
    if (depth(sel) > maxDepth) throw new GqlError(\`Query is too deep: depth \${depth(sel)}, max \${maxDepth}.\`);
    return { data: run(isMutation ? mutation! : root, sel) };
  } catch (e) {
    if (e instanceof GqlError) return { errors: [{ message: e.message }] };
    throw e;
  }
}

function depth(sel: Field[] | null): number {
  return sel === null ? 0 : 1 + Math.max(...sel.map((f) => depth(f.sub)));
}

// Split the text into tokens: strings, numbers, names and { } ( ) :
function parse(q: string): [boolean, Field[]] {
  const t = q.match(/"[^"]*"|-?\\d+|\\w+|[{}():]/g)!;
  const pos = { i: 0 };
  const isMutation = t[0] === "mutation";
  if (t[0] === "query" || isMutation) pos.i = t[1] === "{" ? 1 : 2;
  return [isMutation, readSet(t, pos)];
}

// A selection set: { field(arg: value) { ... } field ... }
function readSet(t: string[], pos: { i: number }): Field[] {
  const fields: Field[] = [];
  pos.i++;
  while (t[pos.i] !== "}") {
    const name = t[pos.i++];
    const args: Record<string, any> = {};
    if (t[pos.i] === "(") {
      for (pos.i++; t[pos.i] !== ")"; pos.i += 3) {
        const v = t[pos.i + 2];
        args[t[pos.i]] = v.startsWith('"') ? v.slice(1, -1) : parseInt(v);
      }
      pos.i++;
    }
    fields.push({ name, args, sub: t[pos.i] === "{" ? readSet(t, pos) : null });
  }
  pos.i++;
  return fields;
}

// Call the resolver for each selected field, in the order asked.
function run(obj: Obj, sel: Field[]): Record<string, any> {
  const result: Record<string, any> = {};
  for (const f of sel) {
    const resolve = Object.hasOwn(obj.fields, f.name) ? obj.fields[f.name] : undefined;
    if (!resolve) throw new GqlError(\`Cannot query field "\${f.name}" on type "\${obj.type}".\`);
    const value = resolve(f.args);
    if (Array.isArray(value)) result[f.name] = value.map((x) => run(x, f.sub!));
    else if (value && typeof value === "object" && "fields" in value) result[f.name] = run(value, f.sub!);
    else result[f.name] = value;
  }
  return result;
}`;

Content-Type: application/json

{"query":"{ customer(id: 7) { name email } }"}

###
POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { orders { customer { orders { id } } } } }"}

###
POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { name orders { id total } } }"}`;

  {
    prompt: "Print only the selected fields of the order, in the order they were asked for. This is the core of GraphQL.",
    hint: "Start with an empty dictionary (or object). Loop over selection and copy each field from order.",
    explain: "GraphQL does exactly this at every level: it walks the selection and copies only those fields. The output order follows the query, not the data.",
    csharp: { starter: `using System.Text.Json;

// The core of GraphQL: the client lists the fields it wants, and gets only those.
var order = new Dictionary<string, object>
{
    ["id"] = 101, ["item"] = "Latte", ["quantity"] = 2, ["price"] = 220, ["total"] = 440, ["status"] = "delivered"
};
string[] selection = { "total", "id" };

// TODO: build a new dictionary with only the fields in "selection", in the same order.
// Right now it prints the whole order.
var result = order;

Console.WriteLine(JsonSerializer.Serialize(result));`, solution: `using System.Text.Json;

// The core of GraphQL: the client lists the fields it wants, and gets only those.
var order = new Dictionary<string, object>
{
    ["id"] = 101, ["item"] = "Latte", ["quantity"] = 2, ["price"] = 220, ["total"] = 440, ["status"] = "delivered"
};
string[] selection = { "total", "id" };

// Copy only the selected fields, in the order the client asked for them.
var result = new Dictionary<string, object>();
foreach (string field in selection)
    result[field] = order[field];

Console.WriteLine(JsonSerializer.Serialize(result));`, mode: "console" },
    react: { starter: `// The core of GraphQL: the client lists the fields it wants, and gets only those.
const order: Record<string, unknown> = { id: 101, item: "Latte", quantity: 2, price: 220, total: 440, status: "delivered" };
const selection = ["total", "id"];

// TODO: build a new object with only the fields in "selection", in the same order.
// Right now it prints the whole order.
const result = order;

console.log(JSON.stringify(result));`, solution: `// The core of GraphQL: the client lists the fields it wants, and gets only those.
const order: Record<string, unknown> = { id: 101, item: "Latte", quantity: 2, price: 220, total: 440, status: "delivered" };
const selection = ["total", "id"];

// Copy only the selected fields, in the order the client asked for them.
const result: Record<string, unknown> = {};
for (const field of selection) result[field] = order[field];

console.log(JSON.stringify(result));`, mode: "console" }
  },
  {
    prompt: "The home screen also shows loyalty points. Add a points field to the Customer type so the query works.",
    hint: "Add one resolver next to name that returns the customer's points.",
    explain: "A field exists in GraphQL only if the type has a resolver for it. Without it, the engine answers with a Cannot query field error. One line adds it, and old queries keep working.",
    csharp: { starter: `using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.RegularExpressions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// TODO: the home screen also shows points. Add a "points" field to the Customer type.
// Hint: the data already has it (CustomerRow.Points). Add one resolver next to "name".
var customers = new Dictionary<int, CustomerRow> { [7] = new(7, "Asha Rao", 120), [8] = new(8, "Ravi", 40) };
var orders = new List<OrderRow> { new(101, 7, "Latte", 440), new(102, 8, "Masala Chai", 120), new(103, 7, "Blueberry Muffin", 150) };

// Each type is a set of resolvers: one small function per field.
Obj Customer(CustomerRow c) => new("Customer", new()
{
    ["id"] = _ => c.Id, ["name"] = _ => c.Name,
    ["orders"] = _ => orders.Where(o => o.CustomerId == c.Id).Select(Order).ToList()
});
Obj Order(OrderRow o) => new("Order", new() { ["id"] = _ => o.Id, ["item"] = _ => o.Item, ["total"] = _ => o.Total });

var root = new Obj("Query", new()
{
    ["customer"] = a => Customer(customers[(int)a["id"]]),
    ["featured"] = _ => new Obj("MenuItem", new() { ["name"] = _ => "Cold Coffee", ["price"] = _ => 180 })
});

// One endpoint. The client sends a query, and gets back exactly what it asked for.
app.MapPost("/graphql", (GqlRequest req) => Results.Text(Gql.Execute(req.Query, root), "application/json"));

app.Run();

record GqlRequest(string Query);
record CustomerRow(int Id, string Name, int Points);
record OrderRow(int Id, int CustomerId, string Item, int Total);

// ---------- Mini GraphQL engine: for learning only. Real projects use Hot Chocolate. ----------
record Field(string Name, Dictionary<string, object> Args, List<Field>? Sub);
record Obj(string Type, Dictionary<string, Func<Dictionary<string, object>, object?>> Fields);
class GqlError : Exception { public GqlError(string m) : base(m) { } }

static class Gql
{
    static readonly JsonSerializerOptions Opts = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

    // Run a query and return the JSON text: {"data":...} or {"errors":[...]}.
    public static string Execute(string query, Obj root, Obj? mutation = null, int maxDepth = 99)
    {
        object result;
        try
        {
            var (isMutation, sel) = Parse(query);
            if (Depth(sel) > maxDepth) throw new GqlError($"Query is too deep: depth {Depth(sel)}, max {maxDepth}.");
            result = new { data = Run(isMutation ? mutation! : root, sel) };
        }
        catch (GqlError e) { result = new { errors = new[] { new { message = e.Message } } }; }
        return JsonSerializer.Serialize(result, Opts);
    }

    static int Depth(List<Field>? sel) => sel == null ? 0 : 1 + sel.Max(f => Depth(f.Sub));

    // Split the text into tokens: strings, numbers, names and { } ( ) :
    static (bool, List<Field>) Parse(string q)
    {
        var t = Regex.Matches(q, "\\"[^\\"]*\\"|-?\\\\d+|\\\\w+|[{}():]").Select(m => m.Value).ToList();
        int i = 0; bool isMutation = t[0] == "mutation";
        if (t[0] == "query" || isMutation) i = t[1] == "{" ? 1 : 2;
        return (isMutation, ReadSet(t, ref i));
    }

    // A selection set: { field(arg: value) { ... } field ... }
    static List<Field> ReadSet(List<string> t, ref int i)
    {
        var fields = new List<Field>(); i++;
        while (t[i] != "}")
        {
            string name = t[i++]; var args = new Dictionary<string, object>();
            if (t[i] == "(")
            {
                for (i++; t[i] != ")"; i += 3)
                    args[t[i]] = t[i + 2].StartsWith("\\"") ? t[i + 2].Trim('"') : int.Parse(t[i + 2]);
                i++;
            }
            fields.Add(new Field(name, args, t[i] == "{" ? ReadSet(t, ref i) : null));
        }
        i++; return fields;
    }

    // Call the resolver for each selected field, in the order asked.
    static Dictionary<string, object?> Run(Obj obj, List<Field> sel)
    {
        var result = new Dictionary<string, object?>();
        foreach (var f in sel)
        {
            if (!obj.Fields.TryGetValue(f.Name, out var resolve))
                throw new GqlError($"Cannot query field \\"{f.Name}\\" on type \\"{obj.Type}\\".");
            result[f.Name] = resolve(f.Args) switch
            {
                Obj child => Run(child, f.Sub!),
                IEnumerable<Obj> list => list.Select(x => Run(x, f.Sub!)).ToList(),
                var value => value
            };
        }
        return result;
    }
}`, solution: `using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.RegularExpressions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var customers = new Dictionary<int, CustomerRow> { [7] = new(7, "Asha Rao", 120), [8] = new(8, "Ravi", 40) };
var orders = new List<OrderRow> { new(101, 7, "Latte", 440), new(102, 8, "Masala Chai", 120), new(103, 7, "Blueberry Muffin", 150) };

// Each type is a set of resolvers: one small function per field.
Obj Customer(CustomerRow c) => new("Customer", new()
{
    ["id"] = _ => c.Id, ["name"] = _ => c.Name, ["points"] = _ => c.Points, 
    ["orders"] = _ => orders.Where(o => o.CustomerId == c.Id).Select(Order).ToList()
});
Obj Order(OrderRow o) => new("Order", new() { ["id"] = _ => o.Id, ["item"] = _ => o.Item, ["total"] = _ => o.Total });

var root = new Obj("Query", new()
{
    ["customer"] = a => Customer(customers[(int)a["id"]]),
    ["featured"] = _ => new Obj("MenuItem", new() { ["name"] = _ => "Cold Coffee", ["price"] = _ => 180 })
});

// One endpoint. The client sends a query, and gets back exactly what it asked for.
app.MapPost("/graphql", (GqlRequest req) => Results.Text(Gql.Execute(req.Query, root), "application/json"));

app.Run();

record GqlRequest(string Query);
record CustomerRow(int Id, string Name, int Points);
record OrderRow(int Id, int CustomerId, string Item, int Total);

// ---------- Mini GraphQL engine: for learning only. Real projects use Hot Chocolate. ----------
record Field(string Name, Dictionary<string, object> Args, List<Field>? Sub);
record Obj(string Type, Dictionary<string, Func<Dictionary<string, object>, object?>> Fields);
class GqlError : Exception { public GqlError(string m) : base(m) { } }

static class Gql
{
    static readonly JsonSerializerOptions Opts = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

    // Run a query and return the JSON text: {"data":...} or {"errors":[...]}.
    public static string Execute(string query, Obj root, Obj? mutation = null, int maxDepth = 99)
    {
        object result;
        try
        {
            var (isMutation, sel) = Parse(query);
            if (Depth(sel) > maxDepth) throw new GqlError($"Query is too deep: depth {Depth(sel)}, max {maxDepth}.");
            result = new { data = Run(isMutation ? mutation! : root, sel) };
        }
        catch (GqlError e) { result = new { errors = new[] { new { message = e.Message } } }; }
        return JsonSerializer.Serialize(result, Opts);
    }

    static int Depth(List<Field>? sel) => sel == null ? 0 : 1 + sel.Max(f => Depth(f.Sub));

    // Split the text into tokens: strings, numbers, names and { } ( ) :
    static (bool, List<Field>) Parse(string q)
    {
        var t = Regex.Matches(q, "\\"[^\\"]*\\"|-?\\\\d+|\\\\w+|[{}():]").Select(m => m.Value).ToList();
        int i = 0; bool isMutation = t[0] == "mutation";
        if (t[0] == "query" || isMutation) i = t[1] == "{" ? 1 : 2;
        return (isMutation, ReadSet(t, ref i));
    }

    // A selection set: { field(arg: value) { ... } field ... }
    static List<Field> ReadSet(List<string> t, ref int i)
    {
        var fields = new List<Field>(); i++;
        while (t[i] != "}")
        {
            string name = t[i++]; var args = new Dictionary<string, object>();
            if (t[i] == "(")
            {
                for (i++; t[i] != ")"; i += 3)
                    args[t[i]] = t[i + 2].StartsWith("\\"") ? t[i + 2].Trim('"') : int.Parse(t[i + 2]);
                i++;
            }
            fields.Add(new Field(name, args, t[i] == "{" ? ReadSet(t, ref i) : null));
        }
        i++; return fields;
    }

    // Call the resolver for each selected field, in the order asked.
    static Dictionary<string, object?> Run(Obj obj, List<Field> sel)
    {
        var result = new Dictionary<string, object?>();
        foreach (var f in sel)
        {
            if (!obj.Fields.TryGetValue(f.Name, out var resolve))
                throw new GqlError($"Cannot query field \\"{f.Name}\\" on type \\"{obj.Type}\\".");
            result[f.Name] = resolve(f.Args) switch
            {
                Obj child => Run(child, f.Sub!),
                IEnumerable<Obj> list => list.Select(x => Run(x, f.Sub!)).ToList(),
                var value => value
            };
        }
        return result;
    }
}`, mode: "aspnet", requests: `POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { name points } }"}` },
    react: { starter: `import { Hono } from "hono";

// TODO: the home screen also shows points. Add a "points" field to the Customer type.
// Hint: the data already has it (c.points). Add one resolver next to "name".
const customers = new Map([[7, { id: 7, name: "Asha Rao", points: 120 }], [8, { id: 8, name: "Ravi", points: 40 }]]);
const orders = [
  { id: 101, customerId: 7, item: "Latte", total: 440 },
  { id: 102, customerId: 8, item: "Masala Chai", total: 120 },
  { id: 103, customerId: 7, item: "Blueberry Muffin", total: 150 },
];

// Each type is a set of resolvers: one small function per field.
const customer = (c: { id: number; name: string; points: number }): Obj => ({
  type: "Customer",
  fields: {
    id: () => c.id, name: () => c.name,
    orders: () => orders.filter((o) => o.customerId === c.id).map(order),
  },
});
const order = (o: { id: number; item: string; total: number }): Obj => ({
  type: "Order",
  fields: { id: () => o.id, item: () => o.item, total: () => o.total },
});

const root: Obj = {
  type: "Query",
  fields: {
    customer: (a) => customer(customers.get(a.id)!),
    featured: () => ({ type: "MenuItem", fields: { name: () => "Cold Coffee", price: () => 180 } }),
  },
};

const app = new Hono();

// One endpoint. The client sends a query, and gets back exactly what it asked for.
app.post("/graphql", async (c) => {
  const body = await c.req.json<{ query: string }>();
  return c.json(execute(body.query, root), 200);
});

// ---------- Mini GraphQL engine: for learning only. Real projects use GraphQL Yoga or Apollo. ----------
type Field = { name: string; args: Record<string, any>; sub: Field[] | null };
type Obj = { type: string; fields: Record<string, (args: Record<string, any>) => any> };
class GqlError extends Error {}

// Run a query and return {data: ...} or {errors: [...]}.
function execute(query: string, root: Obj, mutation: Obj | null = null, maxDepth = 99): object {
  try {
    const [isMutation, sel] = parse(query);
    if (depth(sel) > maxDepth) throw new GqlError(\`Query is too deep: depth \${depth(sel)}, max \${maxDepth}.\`);
    return { data: run(isMutation ? mutation! : root, sel) };
  } catch (e) {
    if (e instanceof GqlError) return { errors: [{ message: e.message }] };
    throw e;
  }
}

function depth(sel: Field[] | null): number {
  return sel === null ? 0 : 1 + Math.max(...sel.map((f) => depth(f.sub)));
}

// Split the text into tokens: strings, numbers, names and { } ( ) :
function parse(q: string): [boolean, Field[]] {
  const t = q.match(/"[^"]*"|-?\\d+|\\w+|[{}():]/g)!;
  const pos = { i: 0 };
  const isMutation = t[0] === "mutation";
  if (t[0] === "query" || isMutation) pos.i = t[1] === "{" ? 1 : 2;
  return [isMutation, readSet(t, pos)];
}

// A selection set: { field(arg: value) { ... } field ... }
function readSet(t: string[], pos: { i: number }): Field[] {
  const fields: Field[] = [];
  pos.i++;
  while (t[pos.i] !== "}") {
    const name = t[pos.i++];
    const args: Record<string, any> = {};
    if (t[pos.i] === "(") {
      for (pos.i++; t[pos.i] !== ")"; pos.i += 3) {
        const v = t[pos.i + 2];
        args[t[pos.i]] = v.startsWith('"') ? v.slice(1, -1) : parseInt(v);
      }
      pos.i++;
    }
    fields.push({ name, args, sub: t[pos.i] === "{" ? readSet(t, pos) : null });
  }
  pos.i++;
  return fields;
}

// Call the resolver for each selected field, in the order asked.
function run(obj: Obj, sel: Field[]): Record<string, any> {
  const result: Record<string, any> = {};
  for (const f of sel) {
    const resolve = Object.hasOwn(obj.fields, f.name) ? obj.fields[f.name] : undefined;
    if (!resolve) throw new GqlError(\`Cannot query field "\${f.name}" on type "\${obj.type}".\`);
    const value = resolve(f.args);
    if (Array.isArray(value)) result[f.name] = value.map((x) => run(x, f.sub!));
    else if (value && typeof value === "object" && "fields" in value) result[f.name] = run(value, f.sub!);
    else result[f.name] = value;
  }
  return result;
}`, solution: `import { Hono } from "hono";

const customers = new Map([[7, { id: 7, name: "Asha Rao", points: 120 }], [8, { id: 8, name: "Ravi", points: 40 }]]);
const orders = [
  { id: 101, customerId: 7, item: "Latte", total: 440 },
  { id: 102, customerId: 8, item: "Masala Chai", total: 120 },
  { id: 103, customerId: 7, item: "Blueberry Muffin", total: 150 },
];

// Each type is a set of resolvers: one small function per field.
const customer = (c: { id: number; name: string; points: number }): Obj => ({
  type: "Customer",
  fields: {
    id: () => c.id, name: () => c.name, points: () => c.points, 
    orders: () => orders.filter((o) => o.customerId === c.id).map(order),
  },
});
const order = (o: { id: number; item: string; total: number }): Obj => ({
  type: "Order",
  fields: { id: () => o.id, item: () => o.item, total: () => o.total },
});

const root: Obj = {
  type: "Query",
  fields: {
    customer: (a) => customer(customers.get(a.id)!),
    featured: () => ({ type: "MenuItem", fields: { name: () => "Cold Coffee", price: () => 180 } }),
  },
};

const app = new Hono();

// One endpoint. The client sends a query, and gets back exactly what it asked for.
app.post("/graphql", async (c) => {
  const body = await c.req.json<{ query: string }>();
  return c.json(execute(body.query, root), 200);
});

// ---------- Mini GraphQL engine: for learning only. Real projects use GraphQL Yoga or Apollo. ----------
type Field = { name: string; args: Record<string, any>; sub: Field[] | null };
type Obj = { type: string; fields: Record<string, (args: Record<string, any>) => any> };
class GqlError extends Error {}

// Run a query and return {data: ...} or {errors: [...]}.
function execute(query: string, root: Obj, mutation: Obj | null = null, maxDepth = 99): object {
  try {
    const [isMutation, sel] = parse(query);
    if (depth(sel) > maxDepth) throw new GqlError(\`Query is too deep: depth \${depth(sel)}, max \${maxDepth}.\`);
    return { data: run(isMutation ? mutation! : root, sel) };
  } catch (e) {
    if (e instanceof GqlError) return { errors: [{ message: e.message }] };
    throw e;
  }
}

function depth(sel: Field[] | null): number {
  return sel === null ? 0 : 1 + Math.max(...sel.map((f) => depth(f.sub)));
}

// Split the text into tokens: strings, numbers, names and { } ( ) :
function parse(q: string): [boolean, Field[]] {
  const t = q.match(/"[^"]*"|-?\\d+|\\w+|[{}():]/g)!;
  const pos = { i: 0 };
  const isMutation = t[0] === "mutation";
  if (t[0] === "query" || isMutation) pos.i = t[1] === "{" ? 1 : 2;
  return [isMutation, readSet(t, pos)];
}

// A selection set: { field(arg: value) { ... } field ... }
function readSet(t: string[], pos: { i: number }): Field[] {
  const fields: Field[] = [];
  pos.i++;
  while (t[pos.i] !== "}") {
    const name = t[pos.i++];
    const args: Record<string, any> = {};
    if (t[pos.i] === "(") {
      for (pos.i++; t[pos.i] !== ")"; pos.i += 3) {
        const v = t[pos.i + 2];
        args[t[pos.i]] = v.startsWith('"') ? v.slice(1, -1) : parseInt(v);
      }
      pos.i++;
    }
    fields.push({ name, args, sub: t[pos.i] === "{" ? readSet(t, pos) : null });
  }
  pos.i++;
  return fields;
}

// Call the resolver for each selected field, in the order asked.
function run(obj: Obj, sel: Field[]): Record<string, any> {
  const result: Record<string, any> = {};
  for (const f of sel) {
    const resolve = Object.hasOwn(obj.fields, f.name) ? obj.fields[f.name] : undefined;
    if (!resolve) throw new GqlError(\`Cannot query field "\${f.name}" on type "\${obj.type}".\`);
    const value = resolve(f.args);
    if (Array.isArray(value)) result[f.name] = value.map((x) => run(x, f.sub!));
    else if (value && typeof value === "object" && "fields" in value) result[f.name] = run(value, f.sub!);
    else result[f.name] = value;
  }
  return result;
}`, mode: "server", requests: `POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { name points } }"}` },
    checks: `POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 8) { points name } }"}

###
POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { id points orders { item } } }"}`
  },
  {
    prompt: "A client can nest customer, orders, customer, orders... without end. Add a depth limit of 3.",
    hint: "The engine's Execute function already takes a maxDepth argument. Pass 3.",
    explain: "The depth is checked before any resolver runs, so a deep query costs almost nothing. Real servers add depth and cost limits for the same reason.",
    csharp: { starter: `using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.RegularExpressions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var orders = new List<OrderRow> { new(101, 7, 440), new(103, 7, 150) };

// Order points back to its Customer, so a query can go round and round forever.
Obj Customer(int id) => new("Customer", new()
{
    ["id"] = _ => id, ["name"] = _ => "Asha Rao",
    ["orders"] = _ => orders.Where(o => o.CustomerId == id).Select(Order).ToList()
});
Obj Order(OrderRow o) => new("Order", new()
{
    ["id"] = _ => o.Id, ["total"] = _ => o.Total, ["customer"] = _ => Customer(o.CustomerId)
});
var root = new Obj("Query", new() { ["customer"] = a => Customer((int)a["id"]) });

// TODO: a client can nest customer > orders > customer > orders ... and make the server work hard.
// Pass a depth limit of 3 to the engine (it has a maxDepth parameter), so deep queries get an error.
app.MapPost("/graphql", (GqlRequest req) => Results.Text(Gql.Execute(req.Query, root), "application/json"));

app.Run();

record GqlRequest(string Query);
record OrderRow(int Id, int CustomerId, int Total);

// ---------- Mini GraphQL engine: for learning only. Real projects use Hot Chocolate. ----------
record Field(string Name, Dictionary<string, object> Args, List<Field>? Sub);
record Obj(string Type, Dictionary<string, Func<Dictionary<string, object>, object?>> Fields);
class GqlError : Exception { public GqlError(string m) : base(m) { } }

static class Gql
{
    static readonly JsonSerializerOptions Opts = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

    // Run a query and return the JSON text: {"data":...} or {"errors":[...]}.
    public static string Execute(string query, Obj root, Obj? mutation = null, int maxDepth = 99)
    {
        object result;
        try
        {
            var (isMutation, sel) = Parse(query);
            if (Depth(sel) > maxDepth) throw new GqlError($"Query is too deep: depth {Depth(sel)}, max {maxDepth}.");
            result = new { data = Run(isMutation ? mutation! : root, sel) };
        }
        catch (GqlError e) { result = new { errors = new[] { new { message = e.Message } } }; }
        return JsonSerializer.Serialize(result, Opts);
    }

    static int Depth(List<Field>? sel) => sel == null ? 0 : 1 + sel.Max(f => Depth(f.Sub));

    // Split the text into tokens: strings, numbers, names and { } ( ) :
    static (bool, List<Field>) Parse(string q)
    {
        var t = Regex.Matches(q, "\\"[^\\"]*\\"|-?\\\\d+|\\\\w+|[{}():]").Select(m => m.Value).ToList();
        int i = 0; bool isMutation = t[0] == "mutation";
        if (t[0] == "query" || isMutation) i = t[1] == "{" ? 1 : 2;
        return (isMutation, ReadSet(t, ref i));
    }

    // A selection set: { field(arg: value) { ... } field ... }
    static List<Field> ReadSet(List<string> t, ref int i)
    {
        var fields = new List<Field>(); i++;
        while (t[i] != "}")
        {
            string name = t[i++]; var args = new Dictionary<string, object>();
            if (t[i] == "(")
            {
                for (i++; t[i] != ")"; i += 3)
                    args[t[i]] = t[i + 2].StartsWith("\\"") ? t[i + 2].Trim('"') : int.Parse(t[i + 2]);
                i++;
            }
            fields.Add(new Field(name, args, t[i] == "{" ? ReadSet(t, ref i) : null));
        }
        i++; return fields;
    }

    // Call the resolver for each selected field, in the order asked.
    static Dictionary<string, object?> Run(Obj obj, List<Field> sel)
    {
        var result = new Dictionary<string, object?>();
        foreach (var f in sel)
        {
            if (!obj.Fields.TryGetValue(f.Name, out var resolve))
                throw new GqlError($"Cannot query field \\"{f.Name}\\" on type \\"{obj.Type}\\".");
            result[f.Name] = resolve(f.Args) switch
            {
                Obj child => Run(child, f.Sub!),
                IEnumerable<Obj> list => list.Select(x => Run(x, f.Sub!)).ToList(),
                var value => value
            };
        }
        return result;
    }
}`, solution: `using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.RegularExpressions;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var orders = new List<OrderRow> { new(101, 7, 440), new(103, 7, 150) };

// Order points back to its Customer, so a query can go round and round forever.
Obj Customer(int id) => new("Customer", new()
{
    ["id"] = _ => id, ["name"] = _ => "Asha Rao",
    ["orders"] = _ => orders.Where(o => o.CustomerId == id).Select(Order).ToList()
});
Obj Order(OrderRow o) => new("Order", new()
{
    ["id"] = _ => o.Id, ["total"] = _ => o.Total, ["customer"] = _ => Customer(o.CustomerId)
});
var root = new Obj("Query", new() { ["customer"] = a => Customer((int)a["id"]) });

// Reject any query nested deeper than 3 levels, before running a single resolver.
app.MapPost("/graphql", (GqlRequest req) => Results.Text(Gql.Execute(req.Query, root, maxDepth: 3), "application/json"));

app.Run();

record GqlRequest(string Query);
record OrderRow(int Id, int CustomerId, int Total);

// ---------- Mini GraphQL engine: for learning only. Real projects use Hot Chocolate. ----------
record Field(string Name, Dictionary<string, object> Args, List<Field>? Sub);
record Obj(string Type, Dictionary<string, Func<Dictionary<string, object>, object?>> Fields);
class GqlError : Exception { public GqlError(string m) : base(m) { } }

static class Gql
{
    static readonly JsonSerializerOptions Opts = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

    // Run a query and return the JSON text: {"data":...} or {"errors":[...]}.
    public static string Execute(string query, Obj root, Obj? mutation = null, int maxDepth = 99)
    {
        object result;
        try
        {
            var (isMutation, sel) = Parse(query);
            if (Depth(sel) > maxDepth) throw new GqlError($"Query is too deep: depth {Depth(sel)}, max {maxDepth}.");
            result = new { data = Run(isMutation ? mutation! : root, sel) };
        }
        catch (GqlError e) { result = new { errors = new[] { new { message = e.Message } } }; }
        return JsonSerializer.Serialize(result, Opts);
    }

    static int Depth(List<Field>? sel) => sel == null ? 0 : 1 + sel.Max(f => Depth(f.Sub));

    // Split the text into tokens: strings, numbers, names and { } ( ) :
    static (bool, List<Field>) Parse(string q)
    {
        var t = Regex.Matches(q, "\\"[^\\"]*\\"|-?\\\\d+|\\\\w+|[{}():]").Select(m => m.Value).ToList();
        int i = 0; bool isMutation = t[0] == "mutation";
        if (t[0] == "query" || isMutation) i = t[1] == "{" ? 1 : 2;
        return (isMutation, ReadSet(t, ref i));
    }

    // A selection set: { field(arg: value) { ... } field ... }
    static List<Field> ReadSet(List<string> t, ref int i)
    {
        var fields = new List<Field>(); i++;
        while (t[i] != "}")
        {
            string name = t[i++]; var args = new Dictionary<string, object>();
            if (t[i] == "(")
            {
                for (i++; t[i] != ")"; i += 3)
                    args[t[i]] = t[i + 2].StartsWith("\\"") ? t[i + 2].Trim('"') : int.Parse(t[i + 2]);
                i++;
            }
            fields.Add(new Field(name, args, t[i] == "{" ? ReadSet(t, ref i) : null));
        }
        i++; return fields;
    }

    // Call the resolver for each selected field, in the order asked.
    static Dictionary<string, object?> Run(Obj obj, List<Field> sel)
    {
        var result = new Dictionary<string, object?>();
        foreach (var f in sel)
        {
            if (!obj.Fields.TryGetValue(f.Name, out var resolve))
                throw new GqlError($"Cannot query field \\"{f.Name}\\" on type \\"{obj.Type}\\".");
            result[f.Name] = resolve(f.Args) switch
            {
                Obj child => Run(child, f.Sub!),
                IEnumerable<Obj> list => list.Select(x => Run(x, f.Sub!)).ToList(),
                var value => value
            };
        }
        return result;
    }
}`, mode: "aspnet", requests: `POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { orders { customer { orders { customer { name } } } } } }"}

###
POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { name orders { id total } } }"}` },
    react: { starter: `import { Hono } from "hono";

type OrderRow = { id: number; customerId: number; total: number };
const orders: OrderRow[] = [{ id: 101, customerId: 7, total: 440 }, { id: 103, customerId: 7, total: 150 }];

// Order points back to its Customer, so a query can go round and round forever.
const customer = (id: number): Obj => ({
  type: "Customer",
  fields: { id: () => id, name: () => "Asha Rao", orders: () => orders.filter((o) => o.customerId === id).map(order) },
});
const order = (o: OrderRow): Obj => ({
  type: "Order",
  fields: { id: () => o.id, total: () => o.total, customer: () => customer(o.customerId) },
});
const root: Obj = { type: "Query", fields: { customer: (a) => customer(a.id) } };

const app = new Hono();

// TODO: a client can nest customer > orders > customer > orders ... and make the server work hard.
// Pass a depth limit of 3 to the engine (it has a maxDepth parameter), so deep queries get an error.
app.post("/graphql", async (c) => {
  const body = await c.req.json<{ query: string }>();
  return c.json(execute(body.query, root), 200);
});

// ---------- Mini GraphQL engine: for learning only. Real projects use GraphQL Yoga or Apollo. ----------
type Field = { name: string; args: Record<string, any>; sub: Field[] | null };
type Obj = { type: string; fields: Record<string, (args: Record<string, any>) => any> };
class GqlError extends Error {}

// Run a query and return {data: ...} or {errors: [...]}.
function execute(query: string, root: Obj, mutation: Obj | null = null, maxDepth = 99): object {
  try {
    const [isMutation, sel] = parse(query);
    if (depth(sel) > maxDepth) throw new GqlError(\`Query is too deep: depth \${depth(sel)}, max \${maxDepth}.\`);
    return { data: run(isMutation ? mutation! : root, sel) };
  } catch (e) {
    if (e instanceof GqlError) return { errors: [{ message: e.message }] };
    throw e;
  }
}

function depth(sel: Field[] | null): number {
  return sel === null ? 0 : 1 + Math.max(...sel.map((f) => depth(f.sub)));
}

// Split the text into tokens: strings, numbers, names and { } ( ) :
function parse(q: string): [boolean, Field[]] {
  const t = q.match(/"[^"]*"|-?\\d+|\\w+|[{}():]/g)!;
  const pos = { i: 0 };
  const isMutation = t[0] === "mutation";
  if (t[0] === "query" || isMutation) pos.i = t[1] === "{" ? 1 : 2;
  return [isMutation, readSet(t, pos)];
}

// A selection set: { field(arg: value) { ... } field ... }
function readSet(t: string[], pos: { i: number }): Field[] {
  const fields: Field[] = [];
  pos.i++;
  while (t[pos.i] !== "}") {
    const name = t[pos.i++];
    const args: Record<string, any> = {};
    if (t[pos.i] === "(") {
      for (pos.i++; t[pos.i] !== ")"; pos.i += 3) {
        const v = t[pos.i + 2];
        args[t[pos.i]] = v.startsWith('"') ? v.slice(1, -1) : parseInt(v);
      }
      pos.i++;
    }
    fields.push({ name, args, sub: t[pos.i] === "{" ? readSet(t, pos) : null });
  }
  pos.i++;
  return fields;
}

// Call the resolver for each selected field, in the order asked.
function run(obj: Obj, sel: Field[]): Record<string, any> {
  const result: Record<string, any> = {};
  for (const f of sel) {
    const resolve = Object.hasOwn(obj.fields, f.name) ? obj.fields[f.name] : undefined;
    if (!resolve) throw new GqlError(\`Cannot query field "\${f.name}" on type "\${obj.type}".\`);
    const value = resolve(f.args);
    if (Array.isArray(value)) result[f.name] = value.map((x) => run(x, f.sub!));
    else if (value && typeof value === "object" && "fields" in value) result[f.name] = run(value, f.sub!);
    else result[f.name] = value;
  }
  return result;
}`, solution: `import { Hono } from "hono";

type OrderRow = { id: number; customerId: number; total: number };
const orders: OrderRow[] = [{ id: 101, customerId: 7, total: 440 }, { id: 103, customerId: 7, total: 150 }];

// Order points back to its Customer, so a query can go round and round forever.
const customer = (id: number): Obj => ({
  type: "Customer",
  fields: { id: () => id, name: () => "Asha Rao", orders: () => orders.filter((o) => o.customerId === id).map(order) },
});
const order = (o: OrderRow): Obj => ({
  type: "Order",
  fields: { id: () => o.id, total: () => o.total, customer: () => customer(o.customerId) },
});
const root: Obj = { type: "Query", fields: { customer: (a) => customer(a.id) } };

const app = new Hono();

// Reject any query nested deeper than 3 levels, before running a single resolver.
app.post("/graphql", async (c) => {
  const body = await c.req.json<{ query: string }>();
  return c.json(execute(body.query, root, null, 3), 200);
});

// ---------- Mini GraphQL engine: for learning only. Real projects use GraphQL Yoga or Apollo. ----------
type Field = { name: string; args: Record<string, any>; sub: Field[] | null };
type Obj = { type: string; fields: Record<string, (args: Record<string, any>) => any> };
class GqlError extends Error {}

// Run a query and return {data: ...} or {errors: [...]}.
function execute(query: string, root: Obj, mutation: Obj | null = null, maxDepth = 99): object {
  try {
    const [isMutation, sel] = parse(query);
    if (depth(sel) > maxDepth) throw new GqlError(\`Query is too deep: depth \${depth(sel)}, max \${maxDepth}.\`);
    return { data: run(isMutation ? mutation! : root, sel) };
  } catch (e) {
    if (e instanceof GqlError) return { errors: [{ message: e.message }] };
    throw e;
  }
}

function depth(sel: Field[] | null): number {
  return sel === null ? 0 : 1 + Math.max(...sel.map((f) => depth(f.sub)));
}

// Split the text into tokens: strings, numbers, names and { } ( ) :
function parse(q: string): [boolean, Field[]] {
  const t = q.match(/"[^"]*"|-?\\d+|\\w+|[{}():]/g)!;
  const pos = { i: 0 };
  const isMutation = t[0] === "mutation";
  if (t[0] === "query" || isMutation) pos.i = t[1] === "{" ? 1 : 2;
  return [isMutation, readSet(t, pos)];
}

// A selection set: { field(arg: value) { ... } field ... }
function readSet(t: string[], pos: { i: number }): Field[] {
  const fields: Field[] = [];
  pos.i++;
  while (t[pos.i] !== "}") {
    const name = t[pos.i++];
    const args: Record<string, any> = {};
    if (t[pos.i] === "(") {
      for (pos.i++; t[pos.i] !== ")"; pos.i += 3) {
        const v = t[pos.i + 2];
        args[t[pos.i]] = v.startsWith('"') ? v.slice(1, -1) : parseInt(v);
      }
      pos.i++;
    }
    fields.push({ name, args, sub: t[pos.i] === "{" ? readSet(t, pos) : null });
  }
  pos.i++;
  return fields;
}

// Call the resolver for each selected field, in the order asked.
function run(obj: Obj, sel: Field[]): Record<string, any> {
  const result: Record<string, any> = {};
  for (const f of sel) {
    const resolve = Object.hasOwn(obj.fields, f.name) ? obj.fields[f.name] : undefined;
    if (!resolve) throw new GqlError(\`Cannot query field "\${f.name}" on type "\${obj.type}".\`);
    const value = resolve(f.args);
    if (Array.isArray(value)) result[f.name] = value.map((x) => run(x, f.sub!));
    else if (value && typeof value === "object" && "fields" in value) result[f.name] = run(value, f.sub!);
    else result[f.name] = value;
  }
  return result;
}`, mode: "server", requests: `POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { orders { customer { orders { customer { name } } } } } }"}

###
POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { name orders { id total } } }"}` },
    checks: `POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { orders { customer { name } } } }"}

###
POST /graphql
Content-Type: application/json

{"query":"{ customer(id: 7) { orders { total } } }"}`
  }
];
