Skip to content

Conversation

lopert
Copy link
Collaborator

@lopert lopert commented Oct 7, 2025

Fixes #23

When writing input queries, graphql supports aliasing.
This PR adds some extra handling in convertToFixture to handle that smoothly.

I also ended up reorganizing our test directory.


Breakdown of some of the new and reworked methods.

buildQueryFieldInfo

Extracts field information from a GraphQL query AST by traversing it and building a map of field paths to their metadata.

Example:

Given this query:

query {
  discount {
    classes: discountClasses
    metafield(namespace: "$app:test", key: "config") {
      value
    }
  }
}

buildQueryFieldInfo returns a Map:

{
  "discount" => { fieldName: "discount", arguments: "" },
  "discount.classes" => { fieldName: "discountClasses", arguments: "" },
  "discount.metafield" => { fieldName: "metafield", arguments: '(namespace: "$app:test", key: "config")' },
  "discount.metafield.value" => { fieldName: "value", arguments: "" }
}

The key is the path (how you'd access the field in the fixture), and the value contains:

  • fieldName: The actual field name in the schema
  • arguments: The serialized arguments from the query (empty string if none)

This map is then used by convertFixtureToQuery to know which arguments to include when generating a query from fixture data.


convertFixtureToQuery

Converts fixture data into a GraphQL query string and normalizes the data by using the field information from buildQueryFieldInfo.

Example:

Given this fixture data:

{
  "discount": {
    "classes": ["PRODUCT", "ORDER"],
    "metafield": {
      "value": "{\"enabled\":true}"
    }
  }
}

And the Map from buildQueryFieldInfo (from the previous example), convertFixtureToQuery:

  1. Walks through the fixture data and for each field path (e.g., "discount.classes"), looks it up in the Map
  2. Finds the actual field name ("discountClasses") and any arguments from the Map entry
  3. Builds the query using the actual field names and arguments: discountClasses and metafield(namespace: "$app:test", key: "config")
  4. Transforms the fixture data to use actual field names: classesdiscountClasses

Returns:

{
  query: 'query { discount { discountClasses metafield(namespace: "$app:test", key: "config") { value } } }',
  normalizedData: {
    discount: {
      discountClasses: ["PRODUCT", "ORDER"],
      metafield: {
        value: '{"enabled":true}'
      }
    }
  }
}

The normalized data can then be validated against the schema using the actual field names, while the fixture itself uses the aliased names from the query.

@lopert lopert marked this pull request as ready for review October 7, 2025 19:46
@lopert lopert requested a review from adampetro October 7, 2025 19:46
Copy link

@adampetro adampetro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there is a simpler overall approach that will eliminate a lot of this complexity. Can we not just traverse the input query AST alongside the JSON value and validate as we go?

*
* @param {Object} inputFixtureData - The input fixture data to validate
* @param {GraphQLSchema} originalSchema - The original GraphQL schema with Query root
* @param {DocumentNode} inputQueryAST - Optional input query AST to handle field aliases and arguments

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with this syntax but is there a way to mark the type as optional here?

// Match alias pattern: word followed by colon and whitespace and another word
// But only when followed by whitespace, opening brace, or closing brace
// This avoids matching colons inside strings or arguments
return graphqlString.replace(/\b(\w+):\s*(\w+)(?=[\s{},])/g, '$2');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like it has the potential to miss a number of edge cases. I would much prefer if we could manipulate the AST instead of doing regex operations.

Additionally, how do we expect to handle the case of the same field selected multiple times with aliases? For example:

{
  discountNode {
    metafield1: metafield(key: "my-key1", namespace: "my-namespace") { jsonValue }
    metafield2: metafield(key: "my-key2", namespace: "my-namespace") { jsonValue }
  }
}


function traverseSelections(selections: readonly SelectionNode[], path: string = ''): void {
for (const selection of selections) {
if (selection.kind === 'Field') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we're not handle fragment spreads (inline or named)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. fragment spreads was another thing that we weren't handling, and I was going to tackle in a separate PR. I'll see if I can cover this with the validate-while-traversing approach.

if (selection.kind === 'Field') {
const field = selection as FieldNode;
const actualFieldName = field.name.value;
const aliasName = field.alias?.value || actualFieldName;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it's not an alias name if we fallback to actualFieldName. I would call it responseKey or something like that.

function buildSelectionSet(obj: any): string {
export function convertFixtureToQuery(
fixtureData: Record<string, any>,
queryAST?: DocumentNode

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this optional? Without it there's no way to properly handle aliases, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind this being optional was that if you had a very simply fixture, then in theory you don't need the additional args, since there's no aliases to handle.

That being said, I think it's worth making this required since we always have it available when invoking convertFixtureToQuery.

Comment on lines 43 to 73
// Transform data to use schema field names (removing arguments and resolving aliases)
function transformData(obj: any, path: string = ''): any {
if (obj === null || obj === undefined) {
return obj;
}

if (Array.isArray(obj)) {
return obj.map(item => transformData(item, path));
}

if (typeof obj !== 'object') {
return obj;
}

const transformed: Record<string, any> = {};

for (const [key, value] of Object.entries(obj)) {
// Validate the key doesn't contain arguments
validateFixtureKey(key, path);

const currentPath = path ? `${path}.${key}` : key;

// Get the actual field name (resolve alias if present)
const fieldInfo = queryFieldInfo.get(currentPath);
const actualFieldName = fieldInfo?.fieldName || key;

transformed[actualFieldName] = transformData(value, currentPath);
}

return transformed;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we not lose data if we have multiple aliased selections of the same field? Same example as above:

{
  discountNode {
    metafield1: metafield(key: "my-key1", namespace: "my-namespace") { jsonValue }
    metafield2: metafield(key: "my-key2", namespace: "my-namespace") { jsonValue }
  }
}

@lopert lopert force-pushed the lopert.handle-iq-aliases branch from b1009c4 to cc4beb1 Compare October 9, 2025 18:56
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.

Handle aliasing in input queries and fixture input data

2 participants