Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit 57fa2c3

Browse files
committed
Validate a variable type
Fixed Int type to be between -2^31 and 2^31-1 instead of -2^32 and 2^32-1. Forbid implicit fraction part removing from Int immediate value. Fixed Long type to forbid variable value of 'number' Lua type that cannot preciselly represent integral value (allow only ones inside -2^53 and 2^53 exclusivelly). Related to #159. Related to #160.
1 parent d2c2dce commit 57fa2c3

12 files changed

+494
-36
lines changed

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ default:
1010
lint:
1111
luacheck graphql/*.lua \
1212
graphql/core/execute.lua \
13+
graphql/core/validate_variables.lua \
1314
graphql/convert_schema/*.lua \
1415
graphql/server/*.lua \
1516
test/bench/*.lua \

graphql/core/execute.lua

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ local path = (...):gsub('%.[^%.]+$', '')
22
local util = require(path .. '.util')
33
local introspection = require(path .. '.introspection')
44
local query_util = require(path .. '.query_util')
5+
local validate_variables = require(path .. '.validate_variables')
56

67
local function defaultResolver(object, arguments, info)
78
return object[info.fieldASTs[1].name.value]
@@ -145,5 +146,7 @@ return function(schema, tree, rootValue, variables, operationName, opts)
145146
error('Unsupported operation "' .. context.operation.operation .. '"')
146147
end
147148

149+
validate_variables.validate_variables(context)
150+
148151
return evaluateSelections(rootType, rootValue, context.operation.selectionSet.selections, context)
149152
end

graphql/core/query_util.lua

+8-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ function query_util.buildContext(schema, tree, rootValue, variables, operationNa
111111
rootValue = rootValue,
112112
variables = variables,
113113
operation = nil,
114-
fragmentMap = {}
114+
fragmentMap = {},
115+
variableTypes = {},
115116
}
116117

117118
for _, definition in ipairs(tree.definitions) do
@@ -136,6 +137,12 @@ function query_util.buildContext(schema, tree, rootValue, variables, operationNa
136137
end
137138
end
138139

140+
-- Save variableTypes for the operation.
141+
for _, definition in ipairs(context.operation.variableDefinitions or {}) do
142+
context.variableTypes[definition.variable.name.value] =
143+
query_util.typeFromAST(definition.type, context.schema)
144+
end
145+
139146
return context
140147
end
141148

graphql/core/types.lua

+81-17
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local ffi = require('ffi')
12
local path = (...):gsub('%.[^%.]+$', '')
23
local util = require(path .. '.util')
34

@@ -55,7 +56,8 @@ function types.scalar(config)
5556
description = config.description,
5657
serialize = config.serialize,
5758
parseValue = config.parseValue,
58-
parseLiteral = config.parseLiteral
59+
parseLiteral = config.parseLiteral,
60+
isValueOfTheType = config.isValueOfTheType,
5961
}
6062

6163
instance.nonNull = types.nonNull(instance)
@@ -253,6 +255,7 @@ function types.inputUnion(config)
253255
error('Literal parsing is implemented in util.coerceValue; ' ..
254256
'we should not go here')
255257
end,
258+
resolveType = config.resolveType,
256259
resolveNodeType = config.resolveNodeType,
257260
}
258261

@@ -261,14 +264,58 @@ function types.inputUnion(config)
261264
return instance
262265
end
263266

264-
local coerceInt = function(value)
265-
value = tonumber(value)
267+
-- Based on the code from tarantool/checks.
268+
local function isInt(value)
269+
if type(value) == 'number' then
270+
return value >= -2^31 and value < 2^31 and math.floor(value) == value
271+
end
266272

267-
if not value then return end
273+
if type(value) == 'cdata' then
274+
if ffi.istype('int64_t', value) then
275+
return value >= -2^31 and value < 2^31
276+
elseif ffi.istype('uint64_t', value) then
277+
return value < 2^31
278+
end
279+
end
280+
281+
return false
282+
end
268283

269-
if value == value and value < 2 ^ 32 and value >= -2 ^ 32 then
270-
return value < 0 and math.ceil(value) or math.floor(value)
284+
-- The code from tarantool/checks.
285+
local function isLong(value)
286+
if type(value) == 'number' then
287+
-- Double floating point format has 52 fraction bits. If we want to keep
288+
-- integer precision, the number must be less than 2^53.
289+
return value > -2^53 and value < 2^53 and math.floor(value) == value
271290
end
291+
292+
if type(value) == 'cdata' then
293+
if ffi.istype('int64_t', value) then
294+
return true
295+
elseif ffi.istype('uint64_t', value) then
296+
return value < 2^63
297+
end
298+
end
299+
300+
return false
301+
end
302+
303+
local function coerceInt(value)
304+
local value = tonumber(value)
305+
306+
if value == nil then return end
307+
if not isInt(value) then return end
308+
309+
return value
310+
end
311+
312+
local function coerceLong(value)
313+
local value = tonumber64(value)
314+
315+
if value == nil then return end
316+
if not isLong(value) then return end
317+
318+
return value
272319
end
273320

274321
types.int = types.scalar({
@@ -280,20 +327,22 @@ types.int = types.scalar({
280327
if node.kind == 'int' then
281328
return coerceInt(node.value)
282329
end
283-
end
330+
end,
331+
isValueOfTheType = isInt,
284332
})
285333

286334
types.long = types.scalar({
287335
name = 'Long',
288-
description = 'Long is non-bounded integral type',
289-
serialize = function(value) return tonumber(value) end,
290-
parseValue = function(value) return tonumber(value) end,
336+
description = "The `Long` scalar type represents non-fractional signed whole numeric values. Long can represent values between -(2^63) and 2^63 - 1.",
337+
serialize = coerceLong,
338+
parseValue = coerceLong,
291339
parseLiteral = function(node)
292340
-- 'int' is name of the immediate value type
293341
if node.kind == 'int' then
294-
return tonumber(node.value)
342+
return coerceLong(node.value)
295343
end
296-
end
344+
end,
345+
isValueOfTheType = isLong,
297346
})
298347

299348
types.float = types.scalar({
@@ -304,7 +353,10 @@ types.float = types.scalar({
304353
if node.kind == 'float' or node.kind == 'int' then
305354
return tonumber(node.value)
306355
end
307-
end
356+
end,
357+
isValueOfTheType = function(value)
358+
return type(value) == 'number'
359+
end,
308360
})
309361

310362
types.double = types.scalar({
@@ -316,7 +368,10 @@ types.double = types.scalar({
316368
if node.kind == 'float' or node.kind == 'int' then
317369
return tonumber(node.value)
318370
end
319-
end
371+
end,
372+
isValueOfTheType = function(value)
373+
return type(value) == 'number'
374+
end,
320375
})
321376

322377
types.string = types.scalar({
@@ -328,7 +383,10 @@ types.string = types.scalar({
328383
if node.kind == 'string' then
329384
return node.value
330385
end
331-
end
386+
end,
387+
isValueOfTheType = function(value)
388+
return type(value) == 'string'
389+
end,
332390
})
333391

334392
local function toboolean(x)
@@ -346,7 +404,10 @@ types.boolean = types.scalar({
346404
else
347405
return nil
348406
end
349-
end
407+
end,
408+
isValueOfTheType = function(value)
409+
return type(value) == 'boolean'
410+
end,
350411
})
351412

352413
types.id = types.scalar({
@@ -355,7 +416,10 @@ types.id = types.scalar({
355416
parseValue = tostring,
356417
parseLiteral = function(node)
357418
return node.kind == 'string' or node.kind == 'int' and node.value or nil
358-
end
419+
end,
420+
isValueOfTheType = function(value)
421+
error('NIY')
422+
end,
359423
})
360424

361425
function types.directive(config)

graphql/core/validate_variables.lua

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
local types = require('graphql.core.types')
2+
local graphql_utils = require('graphql.utils')
3+
4+
local check = graphql_utils.check
5+
6+
local validate_variables = {}
7+
8+
-- Traverse type more or less likewise util.coerceValue do.
9+
local function checkVariableValue(variableName, value, variableType)
10+
check(variableName, 'variableName', 'string')
11+
check(variableType, 'variableType', 'table')
12+
13+
local isNonNull = variableType.__type == 'NonNull'
14+
15+
if isNonNull then
16+
variableType = types.nullable(variableType)
17+
assert(value ~= nil,
18+
('Variable "%s" expected to be non-null'):format(variableName))
19+
end
20+
21+
local isList = variableType.__type == 'List'
22+
local isScalar = variableType.__type == 'Scalar'
23+
local isInputObject = variableType.__type == 'InputObject'
24+
local isInputMap = isScalar and variableType.subtype == 'InputMap'
25+
local isInputUnion = isScalar and variableType.subtype == 'InputUnion'
26+
27+
-- Nullable variable type + null value case: value can be nil only when
28+
-- isNonNull is false.
29+
if value == nil then return end
30+
31+
if isList then
32+
assert(type(value) == 'table',
33+
('Variable "%s" for a List must be a Lua table, got %s')
34+
:format(variableName, type(value)))
35+
assert(graphql_utils.is_array(value),
36+
('Variable "%s" for a List must be an array, got map')
37+
:format(variableName))
38+
assert(variableType.ofType ~= nil, 'variableType.ofType must not be nil')
39+
for i, item in ipairs(value) do
40+
local itemName = variableName .. '[' .. tostring(i) .. ']'
41+
checkVariableValue(itemName, item, variableType.ofType)
42+
end
43+
return
44+
end
45+
46+
if isInputObject then
47+
assert(type(value) == 'table',
48+
('Variable "%s" for the InputObject "%s" must be a Lua table, ' ..
49+
'got %s'):format(variableName, variableType.name, type(value)))
50+
51+
-- check all fields: as from value as well as from schema
52+
local fieldNameSet = {}
53+
for fieldName, _ in pairs(value) do
54+
fieldNameSet[fieldName] = true
55+
end
56+
for fieldName, _ in pairs(variableType.fields) do
57+
fieldNameSet[fieldName] = true
58+
end
59+
60+
for fieldName, _ in pairs(fieldNameSet) do
61+
local fieldValue = value[fieldName]
62+
assert(type(fieldName) == 'string',
63+
('Field key of the variable "%s" for the InputObject "%s" ' ..
64+
'must be a string, got %s'):format(variableName, variableType.name,
65+
type(fieldName)))
66+
assert(type(variableType.fields[fieldName]) ~= 'nil',
67+
('Unknown field "%s" of the variable "%s" for the ' ..
68+
'InputObject "%s"'):format(fieldName, variableName, variableType.name))
69+
70+
local childType = variableType.fields[fieldName].kind
71+
local childName = variableName .. '.' .. fieldName
72+
checkVariableValue(childName, fieldValue, childType)
73+
end
74+
75+
return
76+
end
77+
78+
if isInputMap then
79+
assert(type(value) == 'table',
80+
('Variable "%s" for the InputMap "%s" must be a Lua table, got %s')
81+
:format(variableName, variableType.name, type(value)))
82+
83+
for fieldName, fieldValue in pairs(value) do
84+
assert(type(fieldName) == 'string',
85+
('Field key of the variable "%s" for the InputMap "%s" must be a ' ..
86+
'string, got %s'):format(variableName, variableType.name,
87+
type(fieldName)))
88+
local childType = variableType.values
89+
local childName = variableName .. '.' .. fieldName
90+
checkVariableValue(childName, fieldValue, childType)
91+
end
92+
93+
return
94+
end
95+
96+
-- XXX: Enum
97+
98+
if isInputUnion then
99+
local childType = variableType.resolveType(value)
100+
local childName = variableName .. '.' .. childType.name
101+
checkVariableValue(childName, value, childType)
102+
return
103+
end
104+
105+
if isScalar then
106+
check(variableType.isValueOfTheType, 'isValueOfTheType', 'function')
107+
assert(variableType.isValueOfTheType(value),
108+
('Wrong variable "%s" for the Scalar "%s"'):format(
109+
variableName, variableType.name))
110+
return
111+
end
112+
113+
error(('Unknown type of the variable "%s"'):format(variableName))
114+
end
115+
116+
function validate_variables.validate_variables(context)
117+
-- check that all variable values have corresponding variable declaration
118+
for variableName, _ in pairs(context.variables or {}) do
119+
assert(context.variableTypes[variableName] ~= nil,
120+
('There is no declaration for the variable "%s"'):format(variableName))
121+
end
122+
123+
-- check that variable values have correct type
124+
for variableName, variableType in pairs(context.variableTypes) do
125+
local value = (context.variables or {})[variableName]
126+
checkVariableValue(variableName, value, variableType)
127+
end
128+
end
129+
130+
return validate_variables

0 commit comments

Comments
 (0)