Skip to content

Commit fb4b3a3

Browse files
authored
Fix GraphQL values parsing (#1199)
Motivation We want to create our own scalar type with JSON parsing. Due to the fact that the `parseValue` function isn't called, it's impossible to make the correct use of the variables with JSON type. In particular this test fails: ```lua local resp = server:graphql({ query = 'query($field: Json) { test_json_type(field: $field) }', variables = {field = '{"test": 123}'}} ) t.assert_equals(resp.data.test_json_type, '{"test":123}') ``` Changes - The `parseValue` function is called during the variables parsing step. - In `parseValue` and `parseLiteral` one can use `box.NULL` as a return value, returning `nil` is considered as a parsing error. - The `serialize` function is called for `nil` scalar values too. - Parsing of primitive scalar variables is fixed, `parseValue` is eliminated there. - The `parseLiteral` function is mandatory for scalar types Links https://medium.com/@alizhdanov/lets-understand-graphql-scalars-3b2b016feb4a https://atheros.ai/blog/how-to-design-graphql-custom-scalars Close #1198
1 parent f654c4a commit fb4b3a3

File tree

6 files changed

+286
-124
lines changed

6 files changed

+286
-124
lines changed

cartridge/graphql.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ local function funcall_wrap(fun_name, operation, field_name)
3737

3838
local res, err = funcall.call(fun_name, ...)
3939

40-
if res == nil then
40+
if err ~= nil then
4141
error(err, 0)
4242
end
4343

cartridge/graphql/execute.lua

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ local function completeValue(fieldType, result, subSelections, context, opts)
189189
return completedResult
190190
end
191191

192+
if fieldTypeName == 'Scalar' or fieldTypeName == 'Enum' then
193+
return fieldType.serialize(result)
194+
end
195+
192196
if result == nil then
193197
return nil
194198
end
@@ -212,10 +216,6 @@ local function completeValue(fieldType, result, subSelections, context, opts)
212216
return values
213217
end
214218

215-
if fieldTypeName == 'Scalar' or fieldTypeName == 'Enum' then
216-
return fieldType.serialize(result)
217-
end
218-
219219
if fieldTypeName == 'Object' then
220220
if type(result) ~= 'table' then
221221
local message = ('Expected %q to be a "map", got %q'):format(fieldName, type(result))
@@ -260,14 +260,10 @@ local function getFieldEntry(objectType, object, fields, context)
260260

261261
local arguments = util.map(fieldType.arguments or {}, function(argument, name)
262262
local supplied = argumentMap[name] and argumentMap[name].value
263-
264-
supplied = util.coerceValue(supplied, argument, context.variables,
265-
{strict_non_null = true})
266-
if type(supplied) ~= 'nil' then
267-
return supplied
268-
end
269-
270-
return defaultValues[name]
263+
return util.coerceValue(supplied, argument, context.variables, {
264+
strict_non_null = true,
265+
defaultValues = defaultValues,
266+
})
271267
end)
272268

273269
--[[

cartridge/graphql/types.lua

Lines changed: 79 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,9 @@ function types.scalar(config)
8787
assert(type(config.name) == 'string', 'type name must be provided as a string')
8888
assert(type(config.serialize) == 'function', 'serialize must be a function')
8989
assert(type(config.isValueOfTheType) == 'function', 'isValueOfTheType must be a function')
90-
if config.parseValue or config.parseLiteral then
91-
assert(
92-
type(config.parseValue) == 'function' and type(config.parseLiteral) == 'function',
93-
'must provide both parseValue and parseLiteral to scalar type'
94-
)
90+
assert(type(config.parseLiteral) == 'function', 'parseLiteral must be a function')
91+
if config.parseValue then
92+
assert(type(config.parseValue) == 'function', 'parseValue must be a function')
9593
end
9694

9795
local instance = {
@@ -260,6 +258,26 @@ local function isInt(value)
260258
return false
261259
end
262260

261+
local function coerceInt(value)
262+
if value ~= nil then
263+
value = tonumber(value)
264+
if not isInt(value) then return end
265+
end
266+
267+
return value
268+
end
269+
270+
types.int = types.scalar({
271+
name = 'Int',
272+
description = "The `Int` scalar type represents non-fractional signed whole numeric values. " ..
273+
"Int can represent values from -(2^31) to 2^31 - 1, inclusive.",
274+
serialize = coerceInt,
275+
parseLiteral = function(node)
276+
return coerceInt(node.value)
277+
end,
278+
isValueOfTheType = isInt,
279+
})
280+
263281
-- The code from tarantool/checks.
264282
local function isLong(value)
265283
if type(value) == 'number' then
@@ -279,101 +297,97 @@ local function isLong(value)
279297
return false
280298
end
281299

282-
local function coerceInt(value)
283-
local value = tonumber(value)
284-
285-
if value == nil then return end
286-
if not isInt(value) then return end
287-
288-
return value
289-
end
290-
291300
local function coerceLong(value)
292-
local value = tonumber64(value)
293-
294-
if value == nil then return end
295-
if not isLong(value) then return end
301+
if value ~= nil then
302+
value = tonumber64(value)
303+
if not isLong(value) then return end
304+
end
296305

297306
return value
298307
end
299308

300-
types.int = types.scalar({
301-
name = 'Int',
302-
description = "The `Int` scalar type represents non-fractional signed whole numeric values. " ..
303-
"Int can represent values from -(2^31) to 2^31 - 1, inclusive.",
304-
serialize = coerceInt,
305-
parseValue = coerceInt,
306-
parseLiteral = function(node)
307-
if node.kind == 'int' then
308-
return coerceInt(node.value)
309-
end
310-
end,
311-
isValueOfTheType = isInt,
312-
})
313-
314309
types.long = types.scalar({
315310
name = 'Long',
316311
description = "The `Long` scalar type represents non-fractional signed whole numeric values. " ..
317312
"Long can represent values from -(2^52) to 2^52 - 1, inclusive.",
318313
serialize = coerceLong,
319-
parseValue = coerceLong,
320314
parseLiteral = function(node)
321-
if node.kind == 'long' or node.kind == 'int' then
322-
return coerceLong(node.value)
323-
end
315+
return coerceLong(node.value)
324316
end,
325317
isValueOfTheType = isLong,
326318
})
327319

320+
local function isFloat(value)
321+
return type(value) == 'number'
322+
end
323+
324+
local function coerceFloat(value)
325+
if value ~= nil then
326+
value = tonumber(value)
327+
if not isFloat(value) then return end
328+
end
329+
330+
return value
331+
end
332+
328333
types.float = types.scalar({
329334
name = 'Float',
330-
serialize = tonumber,
331-
parseValue = tonumber,
335+
serialize = coerceFloat,
332336
parseLiteral = function(node)
333-
if node.kind == 'float' or node.kind == 'int' then
334-
return tonumber(node.value)
335-
end
336-
end,
337-
isValueOfTheType = function(value)
338-
return type(value) == 'number'
337+
return coerceFloat(node.value)
339338
end,
339+
isValueOfTheType = isFloat,
340340
})
341341

342+
local function isString(value)
343+
return type(value) == 'string'
344+
end
345+
346+
local function coerceString(value)
347+
if value ~= nil then
348+
value = tostring(value)
349+
if not isString(value) then return end
350+
end
351+
352+
return value
353+
end
354+
342355
types.string = types.scalar({
343356
name = 'String',
344357
description = "The `String` scalar type represents textual data, represented as UTF-8 character sequences. " ..
345358
"The String type is most often used by GraphQL to represent free-form human-readable text.",
346-
serialize = tostring,
347-
parseValue = tostring,
359+
serialize = coerceString,
348360
parseLiteral = function(node)
349-
if node.kind == 'string' then
350-
return node.value
351-
end
352-
end,
353-
isValueOfTheType = function(value)
354-
return type(value) == 'string'
361+
return coerceString(node.value)
355362
end,
363+
isValueOfTheType = isString,
356364
})
357365

358366
local function toboolean(x)
359367
return (x and x ~= 'false') and true or false
360368
end
361369

370+
local function isBoolean(value)
371+
return type(value) == 'boolean'
372+
end
373+
374+
local function coerceBoolean(value)
375+
if value ~= nil then
376+
value = toboolean(value)
377+
if not isBoolean(value) then return end
378+
end
379+
380+
return value
381+
end
382+
362383
types.boolean = types.scalar({
363384
name = 'Boolean',
364385
description = "The `Boolean` scalar type represents `true` or `false`.",
365-
serialize = toboolean,
366-
parseValue = toboolean,
386+
serialize = coerceBoolean,
367387
parseLiteral = function(node)
368-
if node.kind == 'boolean' then
369-
return toboolean(node.value)
370-
else
371-
return nil
372-
end
373-
end,
374-
isValueOfTheType = function(value)
375-
return type(value) == 'boolean'
388+
return coerceBoolean(node.value)
376389
end,
390+
isValueOfTheType = isBoolean,
377391
})
378392

379393
--[[
@@ -384,14 +398,11 @@ however, defining it as an ID signifies that it is not intended to be human‐re
384398
--]]
385399
types.id = types.scalar({
386400
name = 'ID',
387-
serialize = tostring,
388-
parseValue = tostring,
401+
serialize = coerceString,
389402
parseLiteral = function(node)
390-
return node.kind == 'string' or node.kind == 'int' and node.value or nil
391-
end,
392-
isValueOfTheType = function(value)
393-
return type(value) == 'string'
403+
return coerceString(node.value)
394404
end,
405+
isValueOfTheType = isString,
395406
})
396407

397408
function types.directive(config)

cartridge/graphql/util.lua

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ local function coerceValue(node, schemaType, variables, opts)
6666
variables = variables or {}
6767
opts = opts or {}
6868
local strict_non_null = opts.strict_non_null or false
69+
local defaultValues = opts.defaultValues or {}
6970

7071
if schemaType.__type == 'NonNull' then
7172
local res = coerceValue(node, schemaType.ofType, variables, opts)
@@ -86,7 +87,20 @@ local function coerceValue(node, schemaType, variables, opts)
8687
end
8788

8889
if node.kind == 'variable' then
89-
return variables[node.name.value]
90+
local value = variables[node.name.value]
91+
local defaultValue = defaultValues[node.name.value]
92+
if type(value) == 'nil' and type(defaultValue) ~= 'nil' then
93+
-- default value was parsed by parseLiteral
94+
value = defaultValue
95+
elseif schemaType.parseValue ~= nil then
96+
value = schemaType.parseValue(value)
97+
if strict_non_null and type(value) == 'nil' then
98+
error(('Could not coerce variable "%s" with value "%s" to type "%s"'):format(
99+
node.name.value, variables[node.name.value], schemaType.name
100+
))
101+
end
102+
end
103+
return value
90104
end
91105

92106
if schemaType.__type == 'List' then
@@ -144,12 +158,11 @@ local function coerceValue(node, schemaType, variables, opts)
144158
end
145159

146160
if schemaType.__type == 'Scalar' then
147-
if schemaType.parseLiteral(node) == nil then
148-
error(('Could not coerce "%s" to "%s"'):format(
149-
node.value or node.kind, schemaType.name))
161+
local value = schemaType.parseLiteral(node)
162+
if strict_non_null and type(value) == 'nil' then
163+
error(('Could not coerce value "%s" to type "%s"'):format(node.value or node.kind, schemaType.name))
150164
end
151-
152-
return schemaType.parseLiteral(node)
165+
return value
153166
end
154167
end
155168

0 commit comments

Comments
 (0)