Skip to content

Type validation from graphql.0 #7

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

Closed
wants to merge 4 commits into from
Closed
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
103 changes: 62 additions & 41 deletions graphql/execute.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,8 @@ local path = (...):gsub('%.[^%.]+$', '')
local types = require(path .. '.types')
local util = require(path .. '.util')
local introspection = require(path .. '.introspection')

local function typeFromAST(node, schema)
local innerType
if node.kind == 'listType' then
innerType = typeFromAST(node.type)
return innerType and types.list(innerType)
elseif node.kind == 'nonNullType' then
innerType = typeFromAST(node.type)
return innerType and types.nonNull(innerType)
else
assert(node.kind == 'namedType', 'Variable must be a named type')
return schema:getType(node.name.value)
end
end
local query_util = require(path .. '.query_util')
local validate_variables = require(path .. '.validate_variables')

local function getFieldResponseKey(field)
return field.alias and field.alias.name.value or field.name.value
Expand Down Expand Up @@ -49,7 +37,7 @@ end
local function doesFragmentApply(fragment, type, context)
if not fragment.typeCondition then return true end

local innerType = typeFromAST(fragment.typeCondition, context.schema)
local innerType = query_util.typeFromAST(fragment.typeCondition, context.schema)

if innerType == type then
return true
Expand Down Expand Up @@ -82,38 +70,69 @@ local function defaultResolver(object, arguments, info)
return object[info.fieldASTs[1].name.value]
end

local function buildContext(schema, tree, rootValue, variables, operationName)
local context = {
schema = schema,
rootValue = rootValue,
variables = variables,
operation = nil,
fragmentMap = {}
}
local function getOperation(tree, operationName)
local operation

for _, definition in ipairs(tree.definitions) do
if definition.kind == 'operation' then
if not operationName and context.operation then
error('Operation name must be specified if more than one operation exists.')
end
for _, definition in ipairs(tree.definitions) do
if definition.kind == 'operation' then
if not operationName and operation then
error('Operation name must be specified if more than one operation exists.')
end

if not operationName or definition.name.value == operationName then
context.operation = definition
end
elseif definition.kind == 'fragmentDefinition' then
context.fragmentMap[definition.name.value] = definition
if not operationName or definition.name.value == operationName then
operation = definition
end
end
end
end

if not context.operation then
if operationName then
error('Unknown operation "' .. operationName .. '"')
else
error('Must provide an operation')
if not operation then
if operationName then
error('Unknown operation "' .. operationName .. '"')
else
error('Must provide an operation')
end
end
end

return context
return operation
end

local function getFragmentDefinitions(tree)
local fragmentMap = {}

for _, definition in ipairs(tree.definitions) do
if definition.kind == 'fragmentDefinition' then
fragmentMap[definition.name.value] = definition
end
end

return fragmentMap
end

-- Extract variableTypes from the operation.
local function getVariableTypes(schema, operation)
local variableTypes = {}

for _, definition in ipairs(operation.variableDefinitions or {}) do
variableTypes[definition.variable.name.value] =
query_util.typeFromAST(definition.type, schema)
end

return variableTypes
end

local function buildContext(schema, tree, rootValue, variables, operationName)
local operation = getOperation(tree, operationName)
local fragmentMap = getFragmentDefinitions(tree)
local variableTypes = getVariableTypes(schema, operation)
return {
schema = schema,
rootValue = rootValue,
variables = variables,
operation = operation,
fragmentMap = fragmentMap,
variableTypes = variableTypes,
request_cache = {},
}
end

local function collectFields(objectType, selections, visitedFragments, result, context)
Expand Down Expand Up @@ -247,5 +266,7 @@ return function(schema, tree, rootValue, variables, operationName)
error('Unsupported operation "' .. context.operation.operation .. '"')
end

validate_variables.validate_variables(context)

return evaluateSelections(rootType, rootValue, context.operation.selectionSet.selections, context)
end
20 changes: 20 additions & 0 deletions graphql/query_util.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
local path = (...):gsub('%.[^%.]+$', '')
local types = require(path .. '.types')

local function typeFromAST(node, schema)
local innerType
if node.kind == 'listType' then
innerType = typeFromAST(node.type, schema)
return innerType and types.list(innerType)
elseif node.kind == 'nonNullType' then
innerType = typeFromAST(node.type, schema)
return innerType and types.nonNull(innerType)
else
assert(node.kind == 'namedType', 'Variable must be a named type')
return schema:getType(node.name.value)
end
end

return {
typeFromAST = typeFromAST,
}
20 changes: 3 additions & 17 deletions graphql/rules.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
local path = (...):gsub('%.[^%.]+$', '')
local types = require(path .. '.types')
local util = require(path .. '.util')
local schema = require(path .. '.schema')
local query_util = require(path .. '.query_util')
local introspection = require(path .. '.introspection')

local function getParentField(context, name, count)
Expand Down Expand Up @@ -475,22 +475,8 @@ function rules.variableUsageAllowed(node, context)
local variableName = argument.value.name.value
local variableDefinition = variableMap[variableName]
local hasDefault = variableDefinition.defaultValue ~= nil

local function typeFromAST(variable)
local innerType
if variable.kind == 'listType' then
innerType = typeFromAST(variable.type)
return innerType and types.list(innerType)
elseif variable.kind == 'nonNullType' then
innerType = typeFromAST(variable.type)
return innerType and types.nonNull(innerType)
else
assert(variable.kind == 'namedType', 'Variable must be a named type')
return context.schema:getType(variable.name.value)
end
end

local variableType = typeFromAST(variableDefinition.type)
local variableType = query_util.typeFromAST(variableDefinition.type,
context.schema)

if hasDefault and variableType.__type ~= 'NonNull' then
variableType = types.nonNull(variableType)
Expand Down
104 changes: 72 additions & 32 deletions graphql/types.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
local ffi = require('ffi')
local path = (...):gsub('%.[^%.]+$', '')
local util = require(path .. '.util')

local types = {}

local function initFields(kind, fields)
assert(type(fields) == 'table', 'fields table must be provided')

local result = {}

for fieldName, field in pairs(fields) do
field = field.__type and { kind = field } or field
result[fieldName] = {
name = fieldName,
kind = field.kind,
description = field.description,
deprecationReason = field.deprecationReason,
arguments = field.arguments or {},
resolve = kind == 'Object' and field.resolve or nil
}
end

return result
end

function types.nonNull(kind)
assert(kind, 'Must provide a type')

Expand All @@ -21,6 +42,15 @@ function types.list(kind)
}
end

function types.nullable(kind)
assert(type(kind) == 'table', 'kind must be a table, got ' .. type(kind))

if kind.__type ~= 'NonNull' then return kind end

assert(kind.ofType ~= nil, 'kind.ofType must not be nil')
return types.nullable(kind.ofType)
end

function types.scalar(config)
assert(type(config.name) == 'string', 'type name must be provided as a string')
assert(type(config.serialize) == 'function', 'serialize must be a function')
Expand All @@ -37,7 +67,8 @@ function types.scalar(config)
description = config.description,
serialize = config.serialize,
parseValue = config.parseValue,
parseLiteral = config.parseLiteral
parseLiteral = config.parseLiteral,
isValueOfTheType = config.isValueOfTheType,
}

instance.nonNull = types.nonNull(instance)
Expand Down Expand Up @@ -99,26 +130,6 @@ function types.interface(config)
return instance
end

function initFields(kind, fields)
assert(type(fields) == 'table', 'fields table must be provided')

local result = {}

for fieldName, field in pairs(fields) do
field = field.__type and { kind = field } or field
result[fieldName] = {
name = fieldName,
kind = field.kind,
description = field.description,
deprecationReason = field.deprecationReason,
arguments = field.arguments or {},
resolve = kind == 'Object' and field.resolve or nil
}
end

return result
end

function types.enum(config)
assert(type(config.name) == 'string', 'type name must be provided as a string')
assert(type(config.values) == 'table', 'values table must be provided')
Expand Down Expand Up @@ -189,14 +200,30 @@ function types.inputObject(config)
return instance
end

local coerceInt = function(value)
value = tonumber(value)

if not value then return end
-- Based on the code from tarantool/checks.
local function isInt(value)
if type(value) == 'number' then
return value >= -2^31 and value < 2^31 and math.floor(value) == value
end

if value == value and value < 2 ^ 32 and value >= -2 ^ 32 then
return value < 0 and math.ceil(value) or math.floor(value)
if type(value) == 'cdata' then
if ffi.istype('int64_t', value) then
return value >= -2^31 and value < 2^31
elseif ffi.istype('uint64_t', value) then
return value < 2^31
end
end

return false
end

local function coerceInt(value)
local value = tonumber(value)

if value == nil then return end
if not isInt(value) then return end

return value
end

types.int = types.scalar({
Expand All @@ -208,7 +235,8 @@ types.int = types.scalar({
if node.kind == 'int' then
return coerceInt(node.value)
end
end
end,
isValueOfTheType = isInt,
})

types.float = types.scalar({
Expand All @@ -219,7 +247,10 @@ types.float = types.scalar({
if node.kind == 'float' or node.kind == 'int' then
return tonumber(node.value)
end
end
end,
isValueOfTheType = function(value)
return type(value) == 'number'
end,
})

types.string = types.scalar({
Expand All @@ -231,7 +262,10 @@ types.string = types.scalar({
if node.kind == 'string' then
return node.value
end
end
end,
isValueOfTheType = function(value)
return type(value) == 'string'
end,
})

local function toboolean(x)
Expand All @@ -249,7 +283,10 @@ types.boolean = types.scalar({
else
return nil
end
end
end,
isValueOfTheType = function(value)
return type(value) == 'boolean'
end,
})

types.id = types.scalar({
Expand All @@ -258,7 +295,10 @@ types.id = types.scalar({
parseValue = tostring,
parseLiteral = function(node)
return node.kind == 'string' or node.kind == 'int' and node.value or nil
end
end,
isValueOfTheType = function(value)
error('Not yet implemented')
end,
})

function types.directive(config)
Expand Down
Loading