From 620d70b0d2f2c1af2861b576c9563e13fc464acd Mon Sep 17 00:00:00 2001 From: Simone Sanfratello Date: Wed, 6 Mar 2024 15:29:01 +0100 Subject: [PATCH] fix: query arguments --- lib/query-builder.js | 50 +++++++++++++++- lib/query-lookup.js | 9 ++- lib/utils.js | 127 +++++++++++++++++++++++++++++++++++------ test/fixtures/books.js | 26 ++++++++- test/query.test.js | 5 ++ 5 files changed, 191 insertions(+), 26 deletions(-) diff --git a/lib/query-builder.js b/lib/query-builder.js index b845db5..7f3d52d 100644 --- a/lib/query-builder.js +++ b/lib/query-builder.js @@ -228,8 +228,56 @@ function deferredKeys (keys, typeName, fieldName) { }) } +// to avoid data transformation, both cases are covered in different functions +// args can be either from client query, which are from gql nodes structure +// or can be built by composer subquery // TODO filter same values -function buildQueryArgs (v, root = true) { +function buildQueryArgs (args, root = true) { + if (args === undefined || args === null) { return '' } + + // args from client query + if (args.node) { + return buildNodeQueryArgs(args, root) + } + // composer built args + return buildPlainQueryArgs(args, root) +} + +function buildNodeQueryArgs (args, root = true) { + if (args === undefined || args === null) { return '' } + + if (args.type === 'ListValue') { + const queryArgs = [] + for (let i = 0; i < args.value.length; i++) { + const arg = buildNodeQueryArgs(args.value[i], false) + if (arg === '') { continue } + queryArgs.push(arg) + } + return `[${queryArgs.join(', ')}]` + } + + if (args.type === 'ObjectValue') { + const keys = Object.keys(args.value) + const queryArgs = [] + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + const value = buildNodeQueryArgs(args.value[key], false) + if (value === '') { continue } + queryArgs.push(`${key}: ${value}`) + } + + if (root) { + return queryArgs?.length > 0 ? `(${queryArgs.join(',')})` : '' + } + + return `{ ${queryArgs.join(', ')} }` + } + + // TODO test: quotes + return args.type !== 'StringValue' ? args.value.toString() : `"${args.value}"` +} + +function buildPlainQueryArgs (v, root = true) { if (v === undefined || v === null) { return '' } if (Array.isArray(v)) { diff --git a/lib/query-lookup.js b/lib/query-lookup.js index 01aacca..debfa00 100644 --- a/lib/query-lookup.js +++ b/lib/query-lookup.js @@ -2,7 +2,7 @@ const { createFieldId } = require('./fields') const { createQueryNode, createQuery, createDeferredQuery, addDeferredQueryField } = require('./query-builder') -const { mergeMaps, collectArgs, pathJoin } = require('./utils') +const { mergeMaps, collectNodeArgs, collectPlainArgs, pathJoin } = require('./utils') /** * !important: the "lookup" functions in there: @@ -57,7 +57,7 @@ function collectQueries ({ operation: context.info ? context.info.operation.operation : '', resolver: resolver ?? { name: resolverName }, selection: [], - args + args: collectPlainArgs(args, queryFieldNode.arguments, context.info) }) }) const querySelection = queryFieldNode @@ -93,7 +93,7 @@ function collectQueries ({ // TODO createResolver fn resolver: resolver ?? { name: resolverName }, selection: [], - args + args: collectPlainArgs(args, queryFieldNode.arguments, context.info) }) }) @@ -261,7 +261,6 @@ function collectNestedQueries ({ alias }) { context.logger.debug({ fieldId, path }, 'query lookup, nested') - const args = collectArgs(querySelection.arguments, context.info) const a = alias ? Object.values(alias)[0] : null return collectQueries({ @@ -271,7 +270,7 @@ function collectNestedQueries ({ parent: queryNode, path, fieldId, - args, + args: collectNodeArgs(querySelection.arguments, context.info), types, fields, aliases, diff --git a/lib/utils.js b/lib/utils.js index ab7d8e6..d0e14b7 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -76,35 +76,123 @@ function unwrapFieldTypeName (field) { return field.type.name || field.type.ofType.name || field.type.ofType.ofType.name } -function collectArgs (nodeArguments, info) { +function collectPlainArgs (args, nodeArguments, info) { + if (args === undefined || args === null) { return } + + if (nodeArguments?.kind === 'Variable') { + return collectVariableArg(nodeArguments, info) + } + + if (Array.isArray(args)) { + const queryArgs = [] + for (let i = 0; i < args.length; i++) { + const arg = collectPlainArgs(args[i], nodeArguments[i], info) + if (!arg) { continue } + queryArgs.push(arg) + } + + return { value: queryArgs, type: 'ListValue', node: nodeArguments } + } + + if (typeof args === 'object') { + const keys = Object.keys(args) + const queryArgs = {} + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + const node = selectArgNode(nodeArguments, key) + const value = collectPlainArgs(args[key], node, info) + if (!value) { continue } + queryArgs[key] = value + } + + return { value: queryArgs, type: 'ObjectValue', node: nodeArguments } + } + + return { value: args, type: nodeArguments.kind, node: nodeArguments } +} + +function selectArgNode (nodeArguments, key) { + let value + if (!Array.isArray(nodeArguments)) { + if (nodeArguments.kind === 'Variable') { + return nodeArguments + } + + if (nodeArguments.kind === 'ObjectValue') { + for (const f of nodeArguments.fields) { + if (f.name.value === key) { + value = f.value + break + } + } + } else { + value = nodeArguments + } + } + + if (!value) { + for (const n of nodeArguments) { + if (n.name.value === key) { + value = n.value + break + } + } + } + + if (!value) { return } + + if (value.kind === 'ObjectValue') { + return value.fields + } + + if (value.kind === 'ListValue') { + return value.values + } + + return value +} + +function collectNodeArgs (nodeArguments, info) { if (!nodeArguments || nodeArguments.length < 1) { return {} } const args = {} for (let i = 0; i < nodeArguments.length; i++) { - const a = nodeArguments[i] - const name = a.name.value - if (a.value.kind !== 'Variable') { - args[name] = a.value.value + const node = nodeArguments[i] + const name = node.name.value + if (node.value.kind !== 'Variable') { + args[name] = node.value.value continue } - const varName = a.value.name.value - const varValue = info.variableValues[varName] - if (typeof varValue === 'object') { - // TODO check this - const object = {} - const keys = Object.keys(varValue) - for (let j = 0; j < keys.length; j++) { - object[keys[j]] = varValue[keys[j]] - } - args[name] = object - continue - } - args[name] = varValue + args[name] = collectVariableArg(node.value, info) } return args } +function collectVariableArg (node, info) { + const varName = node.name.value + const varValue = info.variableValues[varName] + if (typeof varValue === 'object') { + const value = {} + const keys = Object.keys(varValue) + for (let j = 0; j < keys.length; j++) { + const v = varValue[keys[j]] + value[keys[j]] = { value: v, type: mapJsToGqlType(v) } + } + return { value, type: 'ObjectValue' } + } + return { value: varValue, type: mapJsToGqlType(varValue) } +} + +const _mapJsToGqlType = { + string: 'StringValue' +} + +function mapJsToGqlType (value) { + console.log('mapJsToGqlType', value, typeof value) + return _mapJsToGqlType[typeof value] +} + function schemaTypeName (types, subgraphName, entityName, fieldName) { const t = types[entityName][subgraphName].fields.get(fieldName).src.type const notNull = t.kind === 'NON_NULL' ? '!' : '' @@ -120,7 +208,8 @@ module.exports = { mergeMaps, pathJoin, - collectArgs, + collectNodeArgs, + collectPlainArgs, unwrapFieldTypeName, schemaTypeName } diff --git a/test/fixtures/books.js b/test/fixtures/books.js index 2cfc0eb..3c6c7de 100644 --- a/test/fixtures/books.js +++ b/test/fixtures/books.js @@ -11,10 +11,24 @@ const schema = ` genre: BookGenre } + enum BookField { + id, title, genre + } + + enum OrderDirection { + ASD, DESC + } + + input BookOrderField { + field: BookField + direction: OrderDirection + } + type Query { getBook(id: ID!): Book getBookTitle(id: ID!): String getBooksByIds(ids: [ID]!): [Book]! + getBooks(limit: Int, orderBy: [BookOrderField]): [Book] } ` const data = { library: null } @@ -38,7 +52,10 @@ reset() const resolvers = { Query: { - getBook (_, { id }) { + getBook (_, { id, genre }) { + if (genre) { + return data.library[id]?.genre === genre ? data.library[id] : null + } return data.library[id] }, getBookTitle (_, { id }) { @@ -48,6 +65,13 @@ const resolvers = { return ids .map((id) => { return data.library[id] }) .filter(b => !!b) + }, + getBooks (_, { limit, orderBy }) { + const books = structuredClone(Object.values(data.library)) + for (const order of orderBy) { + books.sort((a, b) => order.direction === 'DESC' ? (a[order.field] > b[order.field] ? 1 : -1) : (b[order.field] > a[order.field] ? 1 : -1)) + } + return books.slice(0, limit) } } } diff --git a/test/query.test.js b/test/query.test.js index 04a172a..28b8999 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -214,6 +214,11 @@ test('should run a query that has null results', async (t) => { test('query capabilities', async t => { const capabilities = [ + { + name: 'should run a query with different types in arguments', + query: 'query { getBooks(limit: 1, orderBy: [{ field: genre, direction: DESC }]) { title } }', + result: { getBooks: [{ title: 'A Book About Things That Never Happened' }] } + }, { name: 'should run a query with a literal argument', query: 'query { getBook(id: 1) { id genre } }',