-
Notifications
You must be signed in to change notification settings - Fork 0
Handle aliases #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Handle aliases #24
Conversation
There was a problem hiding this 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 |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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 }
}
}
src/utils/build-query-field-info.ts
Outdated
|
||
function traverseSelections(selections: readonly SelectionNode[], path: string = ''): void { | ||
for (const selection of selections) { | ||
if (selection.kind === 'Field') { |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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.
src/utils/build-query-field-info.ts
Outdated
if (selection.kind === 'Field') { | ||
const field = selection as FieldNode; | ||
const actualFieldName = field.name.value; | ||
const aliasName = field.alias?.value || actualFieldName; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
// 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; | ||
} |
There was a problem hiding this comment.
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 }
}
}
b1009c4
to
cc4beb1
Compare
… union/interface type detection
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:
buildQueryFieldInfo returns a Map:
The key is the path (how you'd access the field in the fixture), and the value contains:
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:
And the Map from
buildQueryFieldInfo
(from the previous example),convertFixtureToQuery
:"discount.classes"
), looks it up in the Map"discountClasses"
) and any arguments from the Map entrydiscountClasses
andmetafield(namespace: "$app:test", key: "config")
classes
→discountClasses
Returns:
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.