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

Commit 10ffd0a

Browse files
authored
Merge pull request #180 from tarantool/gh-134-error-codes
Enhance validation and introduce error codes
2 parents 13489d6 + 5510e30 commit 10ffd0a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1945
-854
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ default:
99
.PHONY: lint
1010
lint:
1111
luacheck graphql/*.lua \
12+
graphql/core/execute.lua \
13+
graphql/core/rules.lua \
14+
graphql/core/validate_variables.lua \
1215
graphql/convert_schema/*.lua \
1316
graphql/server/*.lua \
1417
test/bench/*.lua \

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,23 +141,23 @@ be consistent.
141141
Mutations are disabled in the resharding state of a shard cluster.
142142

143143
There are three types of modifications: insert, update and delete. Several
144-
modifications are allowed in an one GraphQL request, but they will be processed
145-
in non-transactional way.
144+
modifications are allowed in one GraphQL request, but they will be processed in
145+
non-transactional way.
146146

147147
In the case of shard accessor the following constraints can guarantee that data
148-
will be changed in atomic way or, in other words, in an one shard request (but
148+
will be changed in atomic way or, in other words, in one shard request (but
149149
foregoing and upcoming selects can see other data):
150150

151151
* One insert / update / delete argument over the entire GraphQL request.
152152
* For update / delete: either the argument is for 1:1 connection or `limit: 1`
153-
is used for a collection (a topmost field) or 1:N connection (a nested
153+
is used for a collection (a upmost field) or 1:N connection (a nested
154154
field).
155155
* No update of a first field of a **tuple** (shard key is calculated by it). It
156156
is the first field of upmost record in the schema for a collection in case
157157
when there are no service fields. If there are service fields, the first
158158
field of a tuple cannot be changed by a mutation GraphQL request.
159159

160-
Data can be changed between shard requests which are part of the one GraphQL
160+
Data can be changed between shard requests which are part of one GraphQL
161161
request, so the result can observe inconsistent state. We'll don't show all
162162
possible cases, but give an idea what going on in the following paragraph.
163163

@@ -229,7 +229,7 @@ Consider the following details:
229229
#### Update
230230

231231
Example with an update statement passed from a variable. Note that here we
232-
update an object given by a connection (inside an one of nested fields of a
232+
update an object given by a connection (inside one of nested fields of a
233233
request):
234234

235235
```
@@ -300,9 +300,10 @@ Consider the following details:
300300
`update` argument, then connected objects are selected.
301301
* The `limit` and `offset` arguments applied before update, so a user can use
302302
`limit: 1` to update only first match.
303-
* Objects traversed in deep-first up-first order as it written in a mutation
304-
request. So an `update` argument potentially changes those fields that are
305-
follows the updated object in this order.
303+
* Objects are traversed in pre-order depth-first way, object's fields are
304+
traversed in an order as they are written in a mutation request. So an
305+
`update` argument potentially changes those fields that are follows the
306+
updated object in this order.
306307
* Filters by connected objects are performed before update. Resulting connected
307308
objects given after the update (it is matter when a field(s) of the parent
308309
objects by whose the connection is made is subject to change).
@@ -336,7 +337,7 @@ Consider the following details:
336337
* The `delete` argument is forbidden with `insert` or `update` arguments.
337338
* The `delete` argument is forbidden in `query` requests.
338339
* The same fields traversal order and 'select -> change -> select connected'
339-
order of operations for an one field are applied likewise for the `update`
340+
order of operations for one field are applied likewise for the `update`
340341
argument.
341342
* The `limit` argument can be used to define how many objects are subject to
342343
deletion and `offset` can help with adjusting start point of multi-object

graphql/accessor_general.lua

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ local bit = require('bit')
1313
local rex, is_pcre2 = utils.optional_require_rex()
1414
local avro_helpers = require('graphql.avro_helpers')
1515
local db_schema_helpers = require('graphql.db_schema_helpers')
16+
local error_codes = require('graphql.error_codes')
1617

1718
local check = utils.check
19+
local e = error_codes
1820

1921
-- XXX: consider using [1] when it will be mature enough;
2022
-- look into [2] for the status.
@@ -856,12 +858,17 @@ local function process_tuple(self, state, tuple, opts)
856858
local resulting_object_cnt_max = opts.resulting_object_cnt_max
857859
local fetched_object_cnt_max = opts.fetched_object_cnt_max
858860
qstats.fetched_object_cnt = qstats.fetched_object_cnt + 1
859-
assert(qstats.fetched_object_cnt <= fetched_object_cnt_max,
860-
('fetched object count[%d] exceeds limit[%d] ' ..
861-
'(`fetched_object_cnt_max` in accessor)'):format(
862-
qstats.fetched_object_cnt, fetched_object_cnt_max))
863-
assert(qcontext.deadline_clock > clock.monotonic64(),
864-
'query execution timeout exceeded, use `timeout_ms` to increase it')
861+
if qstats.fetched_object_cnt > fetched_object_cnt_max then
862+
error(e.fetched_objects_limit_exceeded(
863+
('fetched objects count (%d) exceeds fetched_object_cnt_max ' ..
864+
'limit (%d)'):format(qstats.fetched_object_cnt,
865+
fetched_object_cnt_max)))
866+
end
867+
if clock.monotonic64() > qcontext.deadline_clock then
868+
error(e.timeout_exceeded((
869+
'query execution timeout exceeded timeout_ms limit (%s ms)'):format(
870+
tostring(self.settings.timeout_ms))))
871+
end
865872
local collection_name = opts.collection_name
866873
local pcre = opts.pcre
867874
local resolveField = opts.resolveField
@@ -878,6 +885,12 @@ local function process_tuple(self, state, tuple, opts)
878885
return true -- skip pivot item too
879886
end
880887

888+
-- Don't count subrequest resulting objects (needed for filtering) into
889+
-- count of object we show to an user as a result.
890+
-- XXX: It is better to have an option to control whether selected objects
891+
-- will be counted as resulting ones.
892+
local saved_resulting_object_cnt = qstats.resulting_object_cnt
893+
881894
-- make subrequests if needed
882895
for k, v in pairs(filter) do
883896
if obj[k] == nil then
@@ -891,6 +904,8 @@ local function process_tuple(self, state, tuple, opts)
891904
end
892905
end
893906

907+
qstats.resulting_object_cnt = saved_resulting_object_cnt
908+
894909
-- filter out non-matching objects
895910
local match = utils.is_subtable(obj, filter) and
896911
match_using_re(obj, pcre)
@@ -905,10 +920,12 @@ local function process_tuple(self, state, tuple, opts)
905920
state.objs[#state.objs + 1] = obj
906921
state.count = state.count + 1
907922
qstats.resulting_object_cnt = qstats.resulting_object_cnt + 1
908-
assert(qstats.resulting_object_cnt <= resulting_object_cnt_max,
909-
('returning object count[%d] exceeds limit[%d] ' ..
910-
'(`resulting_object_cnt_max` in accessor)'):format(
911-
qstats.resulting_object_cnt, resulting_object_cnt_max))
923+
if qstats.resulting_object_cnt > resulting_object_cnt_max then
924+
error(e.resulting_objects_limit_exceeded(
925+
('resulting objects count (%d) exceeds resulting_object_cnt_max ' ..
926+
'limit (%d)'):format(qstats.resulting_object_cnt,
927+
resulting_object_cnt_max)))
928+
end
912929
if limit ~= nil and state.count >= limit then
913930
return false
914931
end
@@ -1060,6 +1077,13 @@ local function select_internal(self, collection_name, from, filter, args, extra)
10601077
-- fullscan
10611078
local primary_index = self.funcs.get_primary_index(self,
10621079
collection_name)
1080+
1081+
-- count full scan select request
1082+
extra.qcontext.statistics.select_requests_cnt =
1083+
extra.qcontext.statistics.select_requests_cnt + 1
1084+
extra.qcontext.statistics.full_scan_select_requests_cnt =
1085+
extra.qcontext.statistics.full_scan_select_requests_cnt + 1
1086+
10631087
for _, tuple in primary_index:pairs() do
10641088
assert(pivot == nil,
10651089
'offset for top-level objects must use a primary index')
@@ -1102,6 +1126,12 @@ local function select_internal(self, collection_name, from, filter, args, extra)
11021126

11031127
local tuple_count = 0
11041128

1129+
-- count index select request
1130+
extra.qcontext.statistics.select_requests_cnt =
1131+
extra.qcontext.statistics.select_requests_cnt + 1
1132+
extra.qcontext.statistics.index_select_requests_cnt =
1133+
extra.qcontext.statistics.index_select_requests_cnt + 1
1134+
11051135
for _, tuple in index:pairs(index_value, iterator_opts) do
11061136
tuple_count = tuple_count + 1
11071137
-- check full match constraint
@@ -1287,7 +1317,10 @@ local function init_qcontext(accessor, qcontext)
12871317
local settings = accessor.settings
12881318
qcontext.statistics = {
12891319
resulting_object_cnt = 0,
1290-
fetched_object_cnt = 0
1320+
fetched_object_cnt = 0,
1321+
select_requests_cnt = 0,
1322+
full_scan_select_requests_cnt = 0,
1323+
index_select_requests_cnt = 0,
12911324
}
12921325
qcontext.deadline_clock = clock.monotonic64() +
12931326
settings.timeout_ms * 1000 * 1000

graphql/convert_schema/union.lua

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ local yaml = require('yaml')
22
local core_types = require('graphql.core.types')
33
local avro_helpers = require('graphql.avro_helpers')
44
local helpers = require('graphql.convert_schema.helpers')
5-
5+
local error_codes = require('graphql.error_codes')
66
local utils = require('graphql.utils')
7+
78
local check = utils.check
9+
local e = error_codes
810

911
local union = {}
1012

@@ -98,8 +100,10 @@ local function create_union_types(avro_schema, opts)
98100
if type == 'null' then
99101
is_nullable = true
100102
else
101-
local variant_type = convert(type, {context = context})
102103
local box_field_name = type.name or avro_helpers.avro_type(type)
104+
table.insert(context.path, box_field_name)
105+
local variant_type = convert(type, {context = context})
106+
table.remove(context.path, #context.path)
103107
union_types[#union_types + 1] = box_type(variant_type,
104108
box_field_name, {
105109
gen_argument = gen_argument,
@@ -301,25 +305,34 @@ function union.convert(avro_schema, opts)
301305
types = union_types,
302306
name = helpers.full_name(union_name, context),
303307
resolveType = function(result)
308+
if type(result) ~= 'table' then
309+
error(e.wrong_value('union value must be a map with one ' ..
310+
'field, got ' .. type(result)))
311+
end
312+
if next(result) == nil or next(result, next(result)) ~= nil then
313+
error(e.wrong_value('union value must have only one field'))
314+
end
304315
for determinant, type in pairs(determinant_to_type) do
305316
if result[determinant] ~= nil then
306317
return type
307318
end
308319
end
309-
error(('result object has no determinant field matching ' ..
310-
'determinants for this union\nresult object:\n%s' ..
311-
'determinants:\n%s'):format(yaml.encode(result),
312-
yaml.encode(determinant_to_type)))
320+
local field_name = tostring(next(result))
321+
error(e.wrong_value(('unexpected union value field: %s'):format(
322+
field_name)))
313323
end,
314324
resolveNodeType = function(node)
315-
assert(#node.values == 1,
316-
('box object with more then one field: %d'):format(
317-
#node.values))
325+
if #node.values ~= 1 then
326+
error(e.wrong_value('box object with more then one field: %d')
327+
:format(#node.values))
328+
end
318329
local determinant = node.values[1].name
319330
check(determinant, 'determinant', 'string')
320331
local res = determinant_to_type[determinant]
321-
assert(determinant ~= nil,
322-
('the union has no "%s" field'):format(determinant))
332+
if res == nil then
333+
error(e.wrong_value('the union has no "%s" field'):format(
334+
determinant))
335+
end
323336
return res
324337
end,
325338
})

graphql/core/execute.lua

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
local path = (...):gsub('%.[^%.]+$', '')
2-
local types = require(path .. '.types')
32
local util = require(path .. '.util')
43
local introspection = require(path .. '.introspection')
54
local query_util = require(path .. '.query_util')
5+
local validate_variables = require(path .. '.validate_variables')
66

77
local function defaultResolver(object, arguments, info)
88
return object[info.fieldASTs[1].name.value]
99
end
1010

1111
local evaluateSelections
1212

13-
-- @param[opt] resolvedType a type to be used instead of one returned by
14-
-- `fieldType.resolveType(result)` in case when the `fieldType` is Interface or
15-
-- Union; that is needed to increase flexibility of an union type resolving
16-
-- (e.g. resolving by a parent object instead of a current object) via
17-
-- returning it from the `fieldType.resolve` function, which called before
18-
-- `resolvedType` and may need to determine the type itself for its needs
19-
local function completeValue(fieldType, result, subSelections, context, resolvedType)
13+
-- @tparam[opt] table opts the following options:
14+
--
15+
-- * fieldName (string; optional)
16+
--
17+
-- * resolvedType (table; optional) resolvedType a type to be used instead of
18+
-- one returned by `fieldType.resolveType(result)` in case when the
19+
-- `fieldType` is Interface or Union; that is needed to increase flexibility
20+
-- of an union type resolving (e.g. resolving by a parent object instead of a
21+
-- current object) via returning it from the `fieldType.resolve` function,
22+
-- which called before `resolvedType` and may need to determine the type
23+
-- itself for its needs
24+
local function completeValue(fieldType, result, subSelections, context, opts)
25+
local opts = opts or {}
26+
local resolvedType = opts.resolvedType
27+
local fieldName = opts.fieldName or '???'
2028
local fieldTypeName = fieldType.__type
2129

2230
if fieldTypeName == 'NonNull' then
@@ -65,13 +73,13 @@ local function completeValue(fieldType, result, subSelections, context, resolved
6573
return evaluateSelections(objectType, result, subSelections, context)
6674
end
6775

68-
error('Unknown type "' .. fieldTypeName .. '" for field "' .. field.name .. '"')
76+
error('Unknown type "' .. fieldTypeName .. '" for field "' .. fieldName .. '"')
6977
end
7078

7179
local function getFieldEntry(objectType, object, fields, context)
7280
local firstField = fields[1]
7381
local fieldName = firstField.name.value
74-
local responseKey = query_util.getFieldResponseKey(firstField)
82+
-- local responseKey = query_util.getFieldResponseKey(firstField)
7583
local fieldType = introspection.fieldMap[fieldName] or objectType.fields[fieldName]
7684

7785
if fieldType == nil then
@@ -85,7 +93,8 @@ local function getFieldEntry(objectType, object, fields, context)
8593

8694
local arguments = util.map(fieldType.arguments or {}, function(argument, name)
8795
local supplied = argumentMap[name] and argumentMap[name].value
88-
supplied = supplied and util.coerceValue(supplied, argument, context.variables)
96+
supplied = util.coerceValue(supplied, argument, context.variables,
97+
{strict_non_null = true})
8998
if supplied ~= nil then
9099
return supplied
91100
else
@@ -110,7 +119,8 @@ local function getFieldEntry(objectType, object, fields, context)
110119
local resolvedObject, resolvedType = (fieldType.resolve or defaultResolver)(object, arguments, info)
111120
local subSelections = query_util.mergeSelectionSets(fields)
112121

113-
return completeValue(fieldType.kind, resolvedObject, subSelections, context, resolvedType)
122+
return completeValue(fieldType.kind, resolvedObject, subSelections, context,
123+
{resolvedType = resolvedType})
114124
end
115125

116126
evaluateSelections = function(objectType, object, selections, context)
@@ -125,16 +135,19 @@ evaluateSelections = function(objectType, object, selections, context)
125135
return result
126136
end
127137

128-
return function(schema, tree, rootValue, variables, operationName)
138+
return function(schema, tree, rootValue, variables, operationName, opts)
139+
local opts = opts or {}
129140
local context = query_util.buildContext(schema, tree, rootValue, variables, operationName)
130141
-- The field is passed to resolve function within info attribute.
131142
-- Can be used to store any data within one query.
132-
context.qcontext = {}
143+
context.qcontext = opts.qcontext or {}
133144
local rootType = schema[context.operation.operation]
134145

135146
if not rootType then
136147
error('Unsupported operation "' .. context.operation.operation .. '"')
137148
end
138149

150+
validate_variables.validate_variables(context)
151+
139152
return evaluateSelections(rootType, rootValue, context.operation.selectionSet.selections, context)
140153
end

graphql/core/query_util.lua

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ local query_util = {}
77
function query_util.typeFromAST(node, schema)
88
local innerType
99
if node.kind == 'listType' then
10-
innerType = query_util.typeFromAST(node.type)
10+
innerType = query_util.typeFromAST(node.type, schema)
1111
return innerType and types.list(innerType)
1212
elseif node.kind == 'nonNullType' then
13-
innerType = query_util.typeFromAST(node.type)
13+
innerType = query_util.typeFromAST(node.type, schema)
1414
return innerType and types.nonNull(innerType)
1515
else
1616
assert(node.kind == 'namedType', 'Variable must be a named type')
@@ -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

0 commit comments

Comments
 (0)