Skip to content

Commit

Permalink
fix: query arguments (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
simone-sanfratello authored Mar 7, 2024
1 parent 25270d5 commit a0236ba
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 26 deletions.
50 changes: 49 additions & 1 deletion lib/query-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
9 changes: 4 additions & 5 deletions lib/query-lookup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -93,7 +93,7 @@ function collectQueries ({
// TODO createResolver fn
resolver: resolver ?? { name: resolverName },
selection: [],
args
args: collectPlainArgs(args, queryFieldNode.arguments, context.info)
})
})

Expand Down Expand Up @@ -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({
Expand All @@ -271,7 +270,7 @@ function collectNestedQueries ({
parent: queryNode,
path,
fieldId,
args,
args: collectNodeArgs(querySelection.arguments, context.info),
types,
fields,
aliases,
Expand Down
126 changes: 107 additions & 19 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,35 +76,122 @@ 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) {
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' ? '!' : ''
Expand All @@ -120,7 +207,8 @@ module.exports = {
mergeMaps,
pathJoin,

collectArgs,
collectNodeArgs,
collectPlainArgs,
unwrapFieldTypeName,
schemaTypeName
}
26 changes: 25 additions & 1 deletion test/fixtures/books.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 }) {
Expand All @@ -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)
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions test/query.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }',
Expand Down

0 comments on commit a0236ba

Please sign in to comment.