Skip to content
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

fix: query arguments #48

Merged
merged 1 commit into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading