Skip to content

Conversation

@nozzlegear
Copy link
Owner

Once merged, this PR will add a new GraphQueryBuilder base class, along with the generated query builders of all Shopify GraphQL types, union types, queries and mutations.

This will resolve #1137 and #1132.

…nto VisitedTypes

This extends the GraphQL parser to properly handle query and mutation
operations as distinct types from regular object types.

- Add QueryOrMutation and OperationArgument types to Domain.fs
- Modify VisitedTypes to include QueryOrMutation case
- Implement visitOperationDefinitions function in Visitor.fs
- Update VisitObjectTypeDefinitionAsync to handle QueryRoot and Mutation objects

type: feature
scope: graphql-parser
Sanitize parameter names against list of potential dotnet keywords
…ration

This commit adds infrastructure for building GraphQL queries via the services. It
also restructures the parser to properly handle union types, return types and
query/mutation operations.

Changes:
- Add core infrastructure classes (Query, QueryOptions, QueryStringBuilder, RequiredArgument)
  for programmatic GraphQL query construction
- Create AstNodeMapper module to separate AST node mapping logic from visitor pattern
- Refactor union type handling to recursively map union case nodes as VisitedTypes
- Introduce ReturnType discriminated union to distinguish between field types and
  visited graph types in query/mutation return values
- Extend IParsedContext with document access and node lookup capabilities
- Add support for mapping QueryRoot and Mutation operations to QueryOrMutation types
- Update Writer to generate service classes from query/mutation operations

type: feature
scope: graphql-parser
…aint

The existing GraphQueryBuilder.AddUnion methods can't be used because
they constrain `TQuery` to be an inheritor of `T`. However, due to the
way GraphQL unions work, and due to C#'s lack of native unions, this
isn't feasible – ShopifySharp's GraphQL union cases do not inherit from
their union type parent. Instead, there's an internal union type mapper
for every union case that the union case is deserialized into.

Long story short, the constraint didn't work with GraphQL union types
because no union case inherits from its union type.

type: feature
scope: graphql
@nozzlegear nozzlegear force-pushed the full-graphql-services branch 2 times, most recently from 2d968df to e6d1b0b Compare November 17, 2025 20:02
@nozzlegear nozzlegear force-pushed the full-graphql-services branch from 531d84c to 23484ac Compare November 30, 2025 03:18
@clement911
Copy link
Contributor

Hi @nozzlegear,
Sounds like you have been working hard on this.
I'm curious what it will look like in the end from the user's perspective.

@nozzlegear
Copy link
Owner Author

nozzlegear commented Dec 3, 2025

Hey @clement911, hope you're doing well!

My goal with this is to add a strongly-typed fluent API for all of the generated GraphQL types, queries and mutations. Each class will have methods like AddFieldFoo, AddUnionBar and AddArgumentBat, and then we can call .Build() on them to convert them to a GraphQL query string which can be passed to the GraphService.

Here's what it looks like in practice:

var shopQueryBuilder = new ShopQueryBuilder();
var graphQuery = shopQueryBuilder.AddFieldName()
  .AddFieldContactEmail()
  .AddFieldCurrencyCode()
  .AddFieldUrl()
  .Build();

Which will output this GraphQL string:

shop {
  name
  contactEmail
  currencyCode
  url
}

A more complicated example with arguments, unions and nested objects might look something like this:

var ordersQuery = new OrdersQueryBuilder();
ordersQuery.AddArgumentFirst(50)
  .AddFieldPageInfo(i =>
    {
      i.AddFieldStartCursor()
        .AddFieldEndCursor()
        .AddFieldHasPreviousPage()
        .AddFieldHasNextPage();
      return i;
    })
  .AddFieldNodes(n =>
    {
      n.AddFieldId()
        .AddFieldName()
        .AddUnionPurchasingEntity((PurchasingCompany u) =>
        {
          u.AddFieldCompany(c =>
          {
            c.AddFieldName();
            return c;
          });
          return u;
        })
        .AddUnionPurchasingEntity((Customer c) =>
        {
          c.AddFieldFirstName()
            .AddFieldLastName();
          return c;
        })
      return n;
    })
  .Build();

Which should create this GraphQL string:

orders (first: 50) {
  pageInfo {
    startCursor
    endCursor
    hasPreviousPage
    hasNextPage
  }
  nodes {
    id
    name
    ... on PurchasingCompany {
      company {
        name
      }
    }
    ... on Customer {
      firstName
      lastName
    }
  }
}

I'm planning on adding a special case to the GraphService that will let us pass these querybuilders directly to the service, where it will handle the serialization of the graphql string/parameterization of arguments, etc.

I'm also not sold on the names of the AddFieldFoo, AddUnionBar, AddArgumentBaz methods yet, so they're still open to change and I'm open to suggestions there. I was considering something like builder.Fields.AddFoo(), builder.Arguments.AddBar() and builder.Unions.AddBaz() instead.

Let me know what you think!

@clement911
Copy link
Contributor

Oh I see.
That's quite ambitious. I see how it will be useful to build queries dynamically in a type safe way 👍

Extract query builder writing, utility functions, and reserved keywords
into separate modules.

type: refactor
scope: graphql-parser
… document

This commit extends the parser to generate QueryBuilder classes for all GraphQL
classes, interfaces, union types, queries and mutations. This refactoring
improves code modularity and lays the groundwork for more comprehensive code
generation.

- Move service writing logic from Writer.fs to QueryBuilderWriter.fs
- Extend VisitedTypes union to include Operation case
- Add tryMap function to AstNodeMapper for graceful type mapping

type: feature
scope: graphql-parser
The ParserContext constructor now requires a GraphQL document parameter.
Also updated union type assertions to use the Cases property instead of Types.

type: refactor
scope: tests
Allows Connection, Edge, PageInfo, and Node types to be used with GraphQueryBuilder.
These are core GraphQL/Relay specification types that need query builders generated
for them. Without implementing IGraphQLObject, they don't satisfy the generic type
constraint on GraphQueryBuilder<T>, causing compilation errors.

type: fix
scope: graphql
This fixes the issue where type names were not being reported as interfaces due to
a mismatch between how interfaces were registered vs how they were looked up:
  - Registration: NamedType.Interface "IDisplayableError" (with "I" prefix)
  - Lookup: NamedType.Interface "DisplayableError" (without "I" prefix, from schema)

This caused isNamedType to return false, preventing `mapFieldTypeToString` from adding
the "I" prefix to field types.

type: fix
scope: parser, generated-types
The Visitor no longer processes QueryRoot and Mutation types itself. Instead,
the QueryBuilderWriter now checks for the QueryRoot and Mutation types
specifically when iterating through all definitions in the GraphDocument.
Once found, it iterates over all of the field definitions on the two types
and maps them to operation QueryBuilders.

type: refactor
scope: parser
This commit adds a new `--builders-dir` option to the CLI, and renames `--output`
to `--types-dir`. The generated QueryBuilders are now written to the given
`--builders-dir` directory, separating them them from the generated GraphQL
classes/interfaces/union types.

This commit also extracts the various filesystem functions (i.e.
writeFileToPath, etc) to a new `FileSystem` module.

type: feature
scope: parser
This adds support for writing the `AddValue()` method to all Operation
query builders for operations that return a field type instead of a visited
type.

type: feature
scope: parser, sourcegen
type: refactor
scope: dependencies
Some operations and types have the QueryRoot itself as one of their field
types, so it needs to be represented as a class.

type: fix
scope: sourcegen, parser
The query builder argument method names were accidentally being
sanitized against a list of C# and CLR keywords, which led to the
argument names turning into things like "AddArgument @metaspace". They
don't need to be sanitized at all, instead I meant to use the `toCasing`
function.

type: fix
scope: sourcegen, parser
The type name string here was accidentally suffixed with "QueryBuilder",
so the querybuilder for every type would think that the "foo" type was
actually named "fooQueryBuilder".

type: fix
scope: sourcegen, parser
This script standardizes the default settings and output directories for
the types and querybuilders generated by
ShopifySharp.GraphQL.Parser.CLI.

type: feature
scope: dev, sourcegen
… a CLR return type

This is a more explicit method name which should make it more obvious
what the method is going to do.

type: refactor
scope: sourcegen, parser
This commit makes the QueryOptions property used by the `Query` class
publicly accessible, allowing us to access and pass down options from
parent query to subquery generically (i.e. without casting `IQuery<T>`
to `Query<T>`).

type: refactor
scope: graphql
This commit adds a new constructor that accepts an IQuery<T> instance
directly, letting query builders be initialized with pre-configured query
objects.

type: feature
scope: graphql, sourcegen, graphquerybuilder
Continuing the type naming standardization in commit 068f488, this refactors
ArgumentsBuilderWriter and UnionsBuilderWriter to use the qualifiedPascalTypeName
helper function for consistent type name generation. The function ensures all
generated type names follow the same naming/formatting pattern.

type: refactor
scope: graphql, sourcegen, graphquerybuilder
This fixes an issue where one of Shopify's GraphQL types had a field
named `Query`, which was causing the compiler to throw an error due to
the property and the field having an identical name.

type: refactor
scope: graphql, sourcegen, graphquerybuilder
This commit refactors the QueryBuilderWriter to generate constructors that accept a name
parameter and create the Query instance internally using the new IQuery
constructor. This allows the generated builders to once again create
subqueries that use the field's property name (i.e.
`_query.AddField(new Query("fieldName"))`.

Also fixes typo in Unions property initialization (was "Unios");

type: refactor
scope: graphql, sourcegen, graphquerybuilder
This lets users instantiate a query builder without needing to look up
the operation name or type name first:

Old:
`var shopBuilder = new ShopQueryBuilder("shop");`
New:
`var shopBuilder = new ShopQueryBuilder();`

type: feature
scope: graphql
…query instantiation

Changed query builder field methods from Func<T,T> to Action<T> pattern, requiring
explicit instantiation and invocation of nested query builders. Updated internal field
naming from property-style to private field convention (_query).

- Use Action<T> instead of Func<T,T> for field builder methods
- Instantiate nested query builders explicitly before invoking build action
- Change Query property to readonly _query field for consistency
- Remove redundant namespace qualification in generic type references
- Fix yield! usage in async pipeline operations

type: refactor
scope: graphql
type: chore
scope: dependencies
This change refactors how the union builders work and how the unions are
used in the builder API. Instead of having a `.Unions` builder on the
root query builder, unions are instead moved back into the `.Fields`
builder. In addition, the builder property is now named based on the GraphQL
field (matching the rest of the fields builders)

```cs
public sealed class FooFieldsBuilder
{
    public FooFieldsBuilder Id() {};

    public FooFieldsBuilder Bar(Action<BarUnionCasesBuilder> build) {};
}

public sealed class BarUnionCasesBuilder
{
    public BarUnionCasesBuilder OnBaz(Action<BazQueryBuilder> build){};

    public BarUnionCasesBuilder OnBat(Action<BatQueryBuilder> build){};
}
```

This should make the API slightly less verbose (no need to call
.Unions when invoking a union), but more importantly it fixes a bug
where union builders could be generated with duplicate method signatures
when multiple fields shared the same union type.

type: refactor
scope: graphql, sourcegen, querybuilders
type: refactor
scope: dev
This allows each argument builder to have common `.AddArgument(s)` methods without
generating them on each class.

type: refactor
scope: graphql-parser
Fields builders now inherit from FieldsBuilderBase instead of managing their own
query field. Also updated the  union case field handling to use an `AddUnionCase`
method which merges union case queries into the parent select list.

type: refactor
scope: graphql-parser
Fixed the UnionsBuilderWriter to look up and pass the actual union type
instead of the parent type when creating UnionCasesBuilderWriter instances.
Previously it was passing the parent type to the cases writer, which
caused the cases writer to write the wrong IQuery generic type and thus
cause compilation errors.

type: fix
scope: graphql-parser
Fixed `TryFindGraphObjectType` comparison by using the Name property directly
instead of converting the entire VisitedTypes object to string.

type: fix
scope: graphql-parser
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add GraphQL service classes with pre-generated methods matching Shopify's queries and mutations

3 participants