From 24a7f6cfbfce74a947d671d35738d18d154478de Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 14 Oct 2026 17:24:59 +0000 Subject: [PATCH] fix: pass object and list arguments of a nested field to the subgraph The arguments of a nested field are collected from the query AST, where an object or a list literal has no value of its own, so an argument such as tags(where: { id: { eq: "1" } }) was dropped from the subquery and the subgraph returned every tag. Collect each literal with valueFromASTUntyped, which also resolves the variables inside it. Fixes #69 Signed-off-by: Mikael Karon Assisted-by: Claude Code:claude-opus-5-5 --- lib/utils.js | 5 +- test/nested-args.test.js | 147 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 test/nested-args.test.js diff --git a/lib/utils.js b/lib/utils.js index f4f18b1..7bb8d41 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,5 +1,7 @@ 'use strict' +const { valueFromASTUntyped } = require('graphql') + function defaultOnError (error) { throw error } @@ -167,7 +169,8 @@ function collectNodeArgs (nodeArguments, info, types) { const node = nodeArguments[i] const name = node.name.value if (node.value.kind !== 'Variable') { - args[name] = node.value.value + // object and list literals have no .value, and may hold variables + args[name] = valueFromASTUntyped(node.value, info?.variableValues) continue } args[name] = collectVariableArg(node.value, info, types) diff --git a/test/nested-args.test.js b/test/nested-args.test.js new file mode 100644 index 0000000..e7b3dda --- /dev/null +++ b/test/nested-args.test.js @@ -0,0 +1,147 @@ +'use strict' + +const assert = require('node:assert') +const { test } = require('node:test') + +const { + createComposerService, + createGraphqlServices, + graphqlRequest +} = require('./helper') +const { compose } = require('../lib') + +const artists = [ + { id: '101', name: 'Nolan' }, + { id: '102', name: 'Benigni' }, + { id: '201', name: 'Pavarotti' } +] + +const tags = [ + { id: '1', artistId: '101', tag: 'visual', score: 3 }, + { id: '2', artistId: '102', tag: 'visual', score: 1 }, + { id: '3', artistId: '101', tag: 'sound', score: 2 } +] + +const schema = ` + enum OrderDirection { ASC DESC } + + input IdCondition { eq: ID, in: [ID] } + input TagsWhere { id: IdCondition, tag: String } + input TagsOrderBy { field: String!, direction: OrderDirection! } + + type Tag { id: ID! tag: String! } + type Artist { id: ID! name: String! tags(where: TagsWhere, orderBy: [TagsOrderBy], limit: Int): [Tag] } + + type Query { artists: [Artist] } +` + +function matches (tag, where) { + if (!where) { return true } + if (where.id?.eq !== undefined && tag.id !== where.id.eq) { return false } + if (where.id?.in !== undefined && !where.id.in.includes(tag.id)) { return false } + if (where.tag !== undefined && tag.tag !== where.tag) { return false } + return true +} + +const resolvers = { + Query: { + artists: () => artists + }, + Artist: { + tags: (artist, { where, orderBy, limit }) => { + let result = tags.filter(t => t.artistId === artist.id && matches(t, where)) + for (const { field, direction } of orderBy ?? []) { + const sign = direction === 'ASC' ? 1 : -1 + result = result.toSorted((a, b) => sign * (a[field] - b[field])) + } + return limit === undefined ? result : result.slice(0, limit) + } + } +} + +async function setup (t) { + const services = await createGraphqlServices(t, [{ + name: 'artists-subgraph', + mercurius: { schema, resolvers }, + listen: true + }]) + + const options = { + subgraphs: services.map(service => ({ + name: service.name, + server: { host: service.host } + })) + } + + const { service } = await createComposerService(t, { compose, options }) + return service +} + +test('should pass an object argument of a nested field to the subgraph', async (t) => { + const service = await setup(t) + + const query = '{ artists { id tags(where: { id: { eq: "1" } }) { id } } }' + const result = await graphqlRequest(service, query) + + assert.deepStrictEqual(result, { + artists: [ + { id: '101', tags: [{ id: '1' }] }, + { id: '102', tags: [] }, + { id: '201', tags: [] } + ] + }) +}) + +test('should pass list, enum and int arguments of a nested field to the subgraph', async (t) => { + const service = await setup(t) + + const query = `{ + artists { + id + tags(where: { id: { in: ["1", "2", "3"] } }, orderBy: [{ field: "score", direction: ASC }], limit: 1) { id } + } + }` + const result = await graphqlRequest(service, query) + + assert.deepStrictEqual(result, { + artists: [ + { id: '101', tags: [{ id: '3' }] }, + { id: '102', tags: [{ id: '2' }] }, + { id: '201', tags: [] } + ] + }) +}) + +test('should pass variables inside an object argument of a nested field to the subgraph', async (t) => { + const service = await setup(t) + + const query = `query ($id: ID, $tag: String) { + artists { id tags(where: { id: { eq: $id }, tag: $tag }) { id } } + }` + const result = await graphqlRequest(service, query, { id: '3', tag: 'sound' }) + + assert.deepStrictEqual(result, { + artists: [ + { id: '101', tags: [{ id: '3' }] }, + { id: '102', tags: [] }, + { id: '201', tags: [] } + ] + }) +}) + +test('should pass an object variable as argument of a nested field to the subgraph', async (t) => { + const service = await setup(t) + + const query = `query ($where: TagsWhere) { + artists { id tags(where: $where) { id } } + }` + const result = await graphqlRequest(service, query, { where: { tag: 'visual' } }) + + assert.deepStrictEqual(result, { + artists: [ + { id: '101', tags: [{ id: '1' }] }, + { id: '102', tags: [{ id: '2' }] }, + { id: '201', tags: [] } + ] + }) +})