├── .github └── workflows │ ├── publish.yml │ └── test_on_push.yaml ├── .gitignore ├── .luacheckrc ├── .luacov ├── LICENSE ├── Makefile ├── README.md ├── graphql-scm-1.rockspec ├── graphql ├── execute.lua ├── init.lua ├── introspection.lua ├── parse.lua ├── query_util.lua ├── rules.lua ├── schema.lua ├── types.lua ├── util.lua ├── validate.lua ├── validate_variables.lua └── version.lua ├── test ├── helpers.lua ├── integration │ ├── codegen │ │ └── fuzzing_nullability │ │ │ ├── README.md │ │ │ ├── generate.js │ │ │ ├── package-lock.json │ │ │ └── package.json │ ├── fuzzing_nullability_test.lua │ ├── graphql_test.lua │ └── introspection.lua └── unit │ └── graphql_test.lua └── tmp └── .keep /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | tags: ['*'] 7 | 8 | jobs: 9 | version-check: 10 | # We need this job to run only on push with tag. 11 | if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }} 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - name: Check module version 15 | uses: tarantool/actions/check-module-version@master 16 | with: 17 | module-name: 'graphql' 18 | 19 | publish-rockspec-scm-1: 20 | if: github.ref == 'refs/heads/master' 21 | runs-on: ubuntu-20.04 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: tarantool/rocks.tarantool.org/github-action@master 25 | with: 26 | auth: ${{ secrets.ROCKS_AUTH }} 27 | files: graphql-scm-1.rockspec 28 | 29 | publish-rockspec-tag: 30 | if: startsWith(github.ref, 'refs/tags/') 31 | needs: version-check 32 | runs-on: ubuntu-20.04 33 | steps: 34 | - uses: actions/checkout@v3 35 | - uses: tarantool/setup-tarantool@v2 36 | with: 37 | tarantool-version: '2.10' 38 | 39 | - run: echo "TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV 40 | - run: tarantoolctl rocks new_version --tag $TAG 41 | - run: tarantoolctl rocks make graphql-$TAG-1.rockspec 42 | - run: tarantoolctl rocks pack graphql $TAG 43 | 44 | - uses: tarantool/rocks.tarantool.org/github-action@master 45 | with: 46 | auth: ${{ secrets.ROCKS_AUTH }} 47 | files: | 48 | graphql-${{ env.TAG }}-1.rockspec 49 | graphql-${{ env.TAG }}-1.all.rock 50 | -------------------------------------------------------------------------------- /.github/workflows/test_on_push.yaml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | run-tests-ce: 9 | if: | 10 | github.event_name == 'push' || 11 | github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login != 'tarantool' 12 | strategy: 13 | matrix: 14 | tarantool-version: ["1.10", "2.10"] 15 | fail-fast: false 16 | runs-on: [ubuntu-20.04] 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: tarantool/setup-tarantool@v2 20 | with: 21 | tarantool-version: ${{ matrix.tarantool-version }} 22 | 23 | - name: Install dependencies 24 | run: make .rocks 25 | 26 | - name: Run linter 27 | run: make lint 28 | 29 | - name: Run tests 30 | run: make test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .~* 2 | *~ 3 | .tarantool.cookie 4 | .rocks 5 | .cache 6 | .doctrees 7 | __pycache__ 8 | /dev 9 | /tmp/* 10 | !/tmp/.keep 11 | doc 12 | release 13 | release-doc 14 | .idea 15 | Dockerfile.test 16 | Makefile.test 17 | CMakeFiles 18 | CMakeCache.txt 19 | cmake_install.cmake 20 | CTestTestfile.cmake 21 | build.luarocks 22 | build.rst 23 | coverage_result.txt 24 | .DS_Store 25 | .vscode 26 | luacov.*.out* 27 | **/node_modules 28 | /package-lock.json 29 | *.mo 30 | .history 31 | .vscode 32 | *.rock 33 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | redefined = false 2 | include_files = { 3 | '*.lua', 4 | 'test/**/*.lua', 5 | 'graphql/**/*.lua', 6 | '*.rockspec', 7 | '.luacheckrc', 8 | } 9 | exclude_files = { 10 | '.rocks', 11 | 'test/integration/fuzzing_nullability_test.lua', 12 | } 13 | new_read_globals = { 14 | box = { fields = { 15 | session = { fields = { 16 | storage = {read_only = false, other_fields = true} 17 | }} 18 | }} 19 | } 20 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | statsfile = 'tmp/luacov.stats.out' 2 | reportfile = 'tmp/luacov.report.out' 3 | exclude = { 4 | '/test/', 5 | '/tmp/', 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Bjorn Swenson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | .PHONY: .rocks 4 | .rocks: graphql-scm-1.rockspec Makefile 5 | tarantoolctl rocks make 6 | tarantoolctl rocks install luatest 0.5.7 7 | tarantoolctl rocks install luacov 0.13.0 8 | tarantoolctl rocks install luacheck 0.26.0 9 | 10 | .PHONY: lint 11 | lint: 12 | if [ ! -d ".rocks" ]; then make .rocks; fi 13 | .rocks/bin/luacheck . 14 | 15 | .PHONY: test 16 | test: 17 | if [ ! -d ".rocks" ]; then make .rocks; fi 18 | rm -f tmp/luacov* 19 | .rocks/bin/luatest --verbose --coverage --shuffle group 20 | .rocks/bin/luacov . && grep -A999 '^Summary' tmp/luacov.report.out 21 | 22 | .PHONY: clean 23 | clean: 24 | rm -rf .rocks 25 | 26 | .PHONY: build 27 | build: 28 | if [ ! -d ".rocks" ]; then make .rocks; fi 29 | tarantoolctl rocks pack graphql scm-1 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lua implementation of GraphQL for Tarantool 2 | =========================================== 3 | 4 | Lua implementation of GraphQL for Tarantool. 5 | It is based on [graphql-lua](https://github.com/bjornbytes/graphql-lua). 6 | 7 | 8 | Installation 9 | ------------ 10 | 11 | ```bash 12 | tarantoolctl rocks install graphql 13 | ``` 14 | 15 | Example 16 | --- 17 | 18 | ```lua 19 | local parse = require('graphql.parse') 20 | local schema = require('graphql.schema') 21 | local types = require('graphql.types') 22 | local validate = require('graphql.validate') 23 | local execute = require('graphql.execute') 24 | 25 | -- Parse a query 26 | local ast = parse [[ 27 | query getUser($id: ID) { 28 | person(id: $id) { 29 | firstName 30 | lastName 31 | } 32 | } 33 | ]] 34 | 35 | -- Create a type 36 | local Person = types.object({ 37 | name = 'Person', 38 | fields = { 39 | id = types.id.nonNull, 40 | firstName = types.string.nonNull, 41 | middleName = types.string, 42 | lastName = types.string.nonNull, 43 | age = types.int.nonNull 44 | } 45 | }) 46 | 47 | -- Create a schema 48 | local schema = schema.create({ 49 | query = types.object({ 50 | name = 'Query', 51 | fields = { 52 | person = { 53 | kind = Person, 54 | arguments = { 55 | id = types.id 56 | }, 57 | resolve = function(rootValue, arguments) 58 | if arguments.id ~= 1 then return nil end 59 | 60 | return { 61 | id = 1, 62 | firstName = 'Bob', 63 | lastName = 'Ross', 64 | age = 52 65 | } 66 | end 67 | } 68 | } 69 | }) 70 | }) 71 | 72 | -- Validate a parsed query against a schema 73 | validate(schema, ast) 74 | 75 | -- Execution 76 | local rootValue = {} 77 | local variables = { id = 1 } 78 | local operationName = 'getUser' 79 | 80 | execute(schema, ast, rootValue, variables, operationName) 81 | 82 | --[[ 83 | { 84 | person = { 85 | firstName = 'Bob', 86 | lastName = 'Ross' 87 | } 88 | } 89 | ]] 90 | ``` 91 | 92 | Status 93 | --- 94 | 95 | - [x] Parsing (based on [luagraphqlparser](https://github.com/tarantool/luagraphqlparser)) 96 | - [x] Type system 97 | - [x] Introspection 98 | - [x] Validation 99 | - [x] Execution 100 | - [ ] Asynchronous execution (coroutines) 101 | 102 | Running tests 103 | --- 104 | 105 | ```bash 106 | tarantoolctl rocks make 107 | tarantoolctl rocks install luatest 0.5.2 108 | .rocks/bin/luatest 109 | ``` 110 | 111 | License 112 | --- 113 | 114 | MIT 115 | -------------------------------------------------------------------------------- /graphql-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'graphql' 2 | version = 'scm-1' 3 | 4 | source = { 5 | url = 'git+https://github.com/tarantool/graphql.git' 6 | } 7 | 8 | description = { 9 | summary = 'GraphQL implementation for Tarantool', 10 | homepage = 'https://github.com/tarantool/graphql', 11 | maintainer = 'https://github.com/tarantool', 12 | license = 'MIT' 13 | } 14 | 15 | dependencies = { 16 | 'lua >= 5.1', 17 | 'luagraphqlparser == 0.2.0-1', 18 | } 19 | 20 | build = { 21 | type = 'builtin', 22 | modules = { 23 | ['graphql.init'] = 'graphql/init.lua', 24 | ['graphql.version'] = 'graphql/version.lua', 25 | ['graphql.execute'] = 'graphql/execute.lua', 26 | ['graphql.introspection'] = 'graphql/introspection.lua', 27 | ['graphql.parse'] = 'graphql/parse.lua', 28 | ['graphql.query_util'] = 'graphql/query_util.lua', 29 | ['graphql.rules'] = 'graphql/rules.lua', 30 | ['graphql.schema'] = 'graphql/schema.lua', 31 | ['graphql.types'] = 'graphql/types.lua', 32 | ['graphql.util'] = 'graphql/util.lua', 33 | ['graphql.validate'] = 'graphql/validate.lua', 34 | ['graphql.validate_variables'] = 'graphql/validate_variables.lua', 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /graphql/execute.lua: -------------------------------------------------------------------------------- 1 | local introspection = require('graphql.introspection') 2 | local query_util = require('graphql.query_util') 3 | local types = require('graphql.types') 4 | local util = require('graphql.util') 5 | local validate_variables = require('graphql.validate_variables') 6 | 7 | local function error(...) 8 | return _G.error(..., 0) 9 | end 10 | 11 | local function getFieldResponseKey(field) 12 | return field.alias and field.alias.name.value or field.name.value 13 | end 14 | 15 | local function shouldIncludeNode(selection, context) 16 | if selection.directives then 17 | local function isDirectiveActive(key, _type) 18 | local directive = util.find(selection.directives, function(directive) 19 | return directive.name.value == key 20 | end) 21 | 22 | if not directive then return end 23 | 24 | local ifArgument = util.find(directive.arguments, function(argument) 25 | return argument.name.value == 'if' 26 | end) 27 | 28 | if not ifArgument then return end 29 | 30 | return util.coerceValue(ifArgument.value, _type.arguments['if'], 31 | context.variables, context.defaultValues) 32 | end 33 | 34 | if isDirectiveActive('skip', types.skip) then return false end 35 | if isDirectiveActive('include', types.include) == false then return false end 36 | end 37 | 38 | return true 39 | end 40 | 41 | local function doesFragmentApply(fragment, type, context) 42 | if not fragment.typeCondition then return true end 43 | 44 | local innerType = query_util.typeFromAST(fragment.typeCondition, context.schema) 45 | 46 | if innerType == type then 47 | return true 48 | elseif innerType.__type == 'Interface' then 49 | local implementors = context.schema:getImplementors(innerType.name) 50 | return implementors and implementors[type] 51 | elseif innerType.__type == 'Union' then 52 | return util.find(innerType.types, function(member) 53 | return member == type 54 | end) 55 | end 56 | end 57 | 58 | local function mergeSelectionSets(fields) 59 | local selections = {} 60 | 61 | for i = 1, #fields do 62 | local selectionSet = fields[i].selectionSet 63 | if selectionSet then 64 | for j = 1, #selectionSet.selections do 65 | table.insert(selections, selectionSet.selections[j]) 66 | end 67 | end 68 | end 69 | 70 | return selections 71 | end 72 | 73 | local function defaultResolver(object, _, info) 74 | return object[info.fieldASTs[1].name.value] 75 | end 76 | 77 | local function getOperation(tree, operationName) 78 | local operation 79 | 80 | for _, definition in ipairs(tree.definitions) do 81 | if definition.kind == 'operation' then 82 | if not operationName and operation then 83 | error('Operation name must be specified if more than one operation exists.') 84 | end 85 | 86 | if not operationName or definition.name.value == operationName then 87 | operation = definition 88 | end 89 | end 90 | end 91 | 92 | if not operation then 93 | if operationName then 94 | error('Unknown operation "' .. operationName .. '"') 95 | else 96 | error('Must provide an operation') 97 | end 98 | end 99 | 100 | return operation 101 | end 102 | 103 | local function getFragmentDefinitions(tree) 104 | local fragmentMap = {} 105 | 106 | for _, definition in ipairs(tree.definitions) do 107 | if definition.kind == 'fragmentDefinition' then 108 | fragmentMap[definition.name.value] = definition 109 | end 110 | end 111 | 112 | return fragmentMap 113 | end 114 | 115 | -- Extract variableTypes from the operation. 116 | local function getVariableTypes(schema, operation) 117 | local variableTypes = {} 118 | 119 | for _, definition in ipairs(operation.variableDefinitions or {}) do 120 | variableTypes[definition.variable.name.value] = 121 | query_util.typeFromAST(definition.type, schema) 122 | end 123 | 124 | return variableTypes 125 | end 126 | 127 | local function buildContext(schema, tree, rootValue, variables, operationName) 128 | local operation = getOperation(tree, operationName) 129 | local fragmentMap = getFragmentDefinitions(tree) 130 | local variableTypes = getVariableTypes(schema, operation) 131 | return { 132 | schema = schema, 133 | rootValue = rootValue, 134 | variables = variables, 135 | operation = operation, 136 | fragmentMap = fragmentMap, 137 | variableTypes = variableTypes, 138 | request_cache = {}, 139 | } 140 | end 141 | 142 | local function collectFields(objectType, selections, visitedFragments, result, context) 143 | for _, selection in ipairs(selections) do 144 | if selection.kind == 'field' then 145 | if shouldIncludeNode(selection, context) then 146 | local name = getFieldResponseKey(selection) 147 | table.insert(result, {name = name, selection = selection}) 148 | end 149 | elseif selection.kind == 'inlineFragment' then 150 | if shouldIncludeNode(selection, context) and doesFragmentApply(selection, objectType, context) then 151 | collectFields(objectType, selection.selectionSet.selections, visitedFragments, result, context) 152 | end 153 | elseif selection.kind == 'fragmentSpread' then 154 | local fragmentName = selection.name.value 155 | if shouldIncludeNode(selection, context) and not visitedFragments[fragmentName] then 156 | visitedFragments[fragmentName] = true 157 | local fragment = context.fragmentMap[fragmentName] 158 | if fragment and shouldIncludeNode(fragment, context) and doesFragmentApply(fragment, objectType, context) then 159 | collectFields(objectType, fragment.selectionSet.selections, visitedFragments, result, context) 160 | end 161 | end 162 | end 163 | end 164 | 165 | return result 166 | end 167 | 168 | local evaluateSelections 169 | local serializemap = {__serialize='map'} 170 | 171 | local function completeValue(fieldType, result, subSelections, context, opts) 172 | local fieldName = opts and opts.fieldName or '???' 173 | local fieldTypeName = fieldType.__type 174 | 175 | if fieldTypeName == 'NonNull' then 176 | local innerType = fieldType.ofType 177 | local completedResult = completeValue(innerType, result, subSelections, context, opts) 178 | 179 | if type(completedResult) == 'nil' then 180 | local err = string.format( 181 | 'No value provided for non-null %s %q', 182 | (innerType.name or innerType.__type), 183 | fieldName 184 | ) 185 | error(err) 186 | end 187 | 188 | return completedResult 189 | end 190 | 191 | if fieldTypeName == 'Scalar' or fieldTypeName == 'Enum' then 192 | return fieldType.serialize(result) 193 | end 194 | 195 | if result == nil then 196 | return result 197 | end 198 | 199 | if fieldTypeName == 'List' then 200 | if not util.is_array(result) then 201 | local resultType = type(result) 202 | if resultType == 'table' then 203 | resultType = 'map' 204 | end 205 | local message = ('Expected %q to be an "array", got %q'):format(fieldName, resultType) 206 | error(message) 207 | end 208 | 209 | local innerType = fieldType.ofType 210 | local values = {} 211 | for i, value in pairs(result) do 212 | values[i] = completeValue(innerType, value, subSelections, context) 213 | end 214 | 215 | return values 216 | end 217 | 218 | if fieldTypeName == 'Object' then 219 | if type(result) ~= 'table' then 220 | local message = ('Expected %q to be a "map", got %q'):format(fieldName, type(result)) 221 | error(message) 222 | end 223 | local completed = evaluateSelections(fieldType, result, subSelections, context) 224 | setmetatable(completed, serializemap) 225 | return completed 226 | elseif fieldTypeName == 'Interface' or fieldTypeName == 'Union' then 227 | local objectType = fieldType.resolveType(result) 228 | local completed = evaluateSelections(objectType, result, subSelections, context) 229 | setmetatable(completed, serializemap) 230 | return completed 231 | end 232 | 233 | error('Unknown type "' .. fieldTypeName .. '" for field "' .. fieldName .. '"') 234 | end 235 | 236 | local function getFieldEntry(objectType, object, fields, context) 237 | local firstField = fields[1] 238 | local fieldName = firstField.name.value 239 | local fieldType = introspection.fieldMap[fieldName] or objectType.fields[fieldName] 240 | 241 | if fieldType == nil then 242 | return nil 243 | end 244 | 245 | local argumentMap = {} 246 | for _, argument in ipairs(firstField.arguments or {}) do 247 | argumentMap[argument.name.value] = argument 248 | end 249 | 250 | local variablesDefaultValues = {} 251 | if context.operation.variableDefinitions ~= nil then 252 | for _, value in ipairs(context.operation.variableDefinitions) do 253 | if value.defaultValue ~= nil then 254 | local variableType = query_util.typeFromAST(value.type, context.schema) 255 | variablesDefaultValues[value.variable.name.value] = util.coerceValue(value.defaultValue, variableType) 256 | end 257 | end 258 | end 259 | 260 | local arguments = util.map(fieldType.arguments or {}, function(argument, name) 261 | local supplied = argumentMap[name] and argumentMap[name].value 262 | 263 | -- This line of code provides support to using 264 | -- `arg = { kind = type, description = desc }` 265 | -- type declaration in query input arguments 266 | -- instead of `arg = type` one. 267 | if argument.kind then argument = argument.kind end 268 | 269 | return util.coerceValue(supplied, argument, context.variables, { 270 | strict_non_null = true, 271 | defaultValues = variablesDefaultValues, 272 | }) 273 | end) 274 | 275 | --[[ 276 | Make arguments ordered map using metatable. 277 | This way callback can use positions to access argument values. 278 | For example business logic depends on argument positions to choose 279 | appropriate storage iteration. 280 | ]] 281 | local positions = {} 282 | local pos = 1 283 | for _, argument in ipairs(firstField.arguments or {}) do 284 | if argument and argument.value then 285 | positions[pos] = { 286 | name=argument.name.value, 287 | value=arguments[argument.name.value], 288 | } 289 | pos = pos + 1 290 | end 291 | end 292 | 293 | arguments = setmetatable(arguments, {__index=positions}) 294 | 295 | local directiveMap = {} 296 | for _, directive in ipairs(firstField.directives or {}) do 297 | directiveMap[directive.name.value] = directive 298 | end 299 | 300 | local directives = {} 301 | 302 | if next(directiveMap) ~= nil then 303 | util.map_name(context.schema.directives or {}, function(directive, directive_name) 304 | local supplied_directive = directiveMap[directive_name] 305 | if supplied_directive == nil then 306 | return nil 307 | end 308 | 309 | local directiveArgumentMap = {} 310 | for _, argument in ipairs(supplied_directive.arguments or {}) do 311 | directiveArgumentMap[argument.name.value] = argument 312 | end 313 | 314 | directives[directive_name] = util.map(directive.arguments or {}, function(argument, name) 315 | local supplied = directiveArgumentMap[name] and directiveArgumentMap[name].value 316 | if argument.kind then argument = argument.kind end 317 | return util.coerceValue(supplied, argument, context.variables, { 318 | strict_non_null = true, 319 | defaultValues = variablesDefaultValues, 320 | }) 321 | end) 322 | end) 323 | end 324 | 325 | local info = { 326 | context = context, 327 | fieldName = fieldName, 328 | fieldASTs = fields, 329 | returnType = fieldType.kind, 330 | parentType = objectType, 331 | schema = context.schema, 332 | fragments = context.fragmentMap, 333 | rootValue = context.rootValue, 334 | operation = context.operation, 335 | variableValues = context.variables, 336 | defaultValues = context.defaultValues, 337 | directives = directives, 338 | directivesDefaultValues = context.schema.directivesDefaultValues, 339 | } 340 | 341 | local resolvedObject, err = (fieldType.resolve or defaultResolver)(object, arguments, info) 342 | if resolvedObject == nil and err ~= nil then 343 | error(err) 344 | end 345 | 346 | local subSelections = mergeSelectionSets(fields) 347 | return completeValue(fieldType.kind, resolvedObject, subSelections, context, 348 | {fieldName = fieldName} 349 | ), err 350 | end 351 | 352 | evaluateSelections = function(objectType, object, selections, context) 353 | local result = {} 354 | local err 355 | local fields = collectFields(objectType, selections, {}, {}, context) 356 | local defaultValues 357 | 358 | if context.defaultValues == nil then 359 | if context.schema.defaultValues ~= nil and type(context.schema.defaultValues) == 'table' then 360 | local operationDefaults = context.schema.defaultValues[context.operation.operation] 361 | if operationDefaults ~= nil and type(operationDefaults) == 'table' then 362 | defaultValues = context.schema.defaultValues[context.operation.operation] 363 | end 364 | end 365 | else 366 | defaultValues = context.defaultValues 367 | end 368 | 369 | for _, field in ipairs(fields) do 370 | assert(result[field.name] == nil, 371 | 'two selections into the one field: ' .. field.name) 372 | 373 | if defaultValues ~= nil then 374 | context.defaultValues = defaultValues[field.name] 375 | end 376 | 377 | result[field.name], err = getFieldEntry(objectType, object, {field.selection}, 378 | context) 379 | if err ~= nil then 380 | context.errors = context.errors or {} 381 | table.insert(context.errors, err) 382 | end 383 | if result[field.name] == nil then 384 | result[field.name] = box.NULL 385 | end 386 | end 387 | return result, context.errors 388 | end 389 | 390 | local function execute(schema, tree, rootValue, variables, operationName) 391 | local context = buildContext(schema, tree, rootValue, variables, operationName) 392 | local rootType = schema[context.operation.operation] 393 | 394 | if not rootType then 395 | error('Unsupported operation "' .. context.operation.operation .. '"') 396 | end 397 | 398 | validate_variables.validate_variables(context) 399 | 400 | return evaluateSelections(rootType, rootValue, context.operation.selectionSet.selections, context) 401 | end 402 | 403 | 404 | return { 405 | execute = execute, 406 | } 407 | -------------------------------------------------------------------------------- /graphql/init.lua: -------------------------------------------------------------------------------- 1 | local log = require('log') 2 | 3 | local VERSION = require('graphql.version') 4 | 5 | return setmetatable({ 6 | _VERSION = VERSION, 7 | }, { 8 | __index = function(_, key) 9 | if key == 'VERSION' then 10 | log.warn("require('graphql').VERSION is deprecated, " .. 11 | "use require('graphql')._VERSION instead.") 12 | return VERSION 13 | end 14 | 15 | return nil 16 | end 17 | }) 18 | -------------------------------------------------------------------------------- /graphql/introspection.lua: -------------------------------------------------------------------------------- 1 | local types = require('graphql.types') 2 | local util = require('graphql.util') 3 | 4 | local __Schema, __Directive, __DirectiveLocation, __Type, __Field, __InputValue,__EnumValue, __TypeKind 5 | 6 | local function resolveArgs(field) 7 | local function transformArg(arg, name) 8 | if arg.__type then 9 | return { kind = arg, name = name, description = arg.description } 10 | elseif arg.name then 11 | return arg 12 | else 13 | local result = { name = name } 14 | 15 | for k, v in pairs(arg) do 16 | result[k] = v 17 | end 18 | 19 | return result 20 | end 21 | end 22 | 23 | return util.values(util.map(field.arguments or {}, transformArg)) 24 | end 25 | 26 | __Schema = types.object({ 27 | name = '__Schema', 28 | 29 | description = util.trim [[ 30 | A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types 31 | and directives on the server, as well as the entry points for query and mutation operations. 32 | ]], 33 | 34 | fields = function() 35 | return { 36 | types = { 37 | description = 'A list of all types supported by this server.', 38 | kind = types.nonNull(types.list(types.nonNull(__Type))), 39 | resolve = function(schema) 40 | return util.values(schema:getTypeMap()) 41 | end, 42 | }, 43 | 44 | queryType = { 45 | description = 'The type that query operations will be rooted at.', 46 | kind = __Type.nonNull, 47 | resolve = function(schema) 48 | return schema:getQueryType() 49 | end, 50 | }, 51 | 52 | mutationType = { 53 | description = 'If this server supports mutation, the type that mutation operations will be rooted at.', 54 | kind = __Type, 55 | resolve = function(schema) 56 | return schema:getMutationType() 57 | end, 58 | }, 59 | 60 | subscriptionType = { 61 | description = 'If this server supports mutation, the type that mutation operations will be rooted at.', 62 | kind = __Type, 63 | resolve = function(_) 64 | return nil 65 | end, 66 | }, 67 | 68 | 69 | directives = { 70 | description = 'A list of all directives supported by this server.', 71 | kind = types.nonNull(types.list(types.nonNull(__Directive))), 72 | resolve = function(schema) 73 | return schema.directives 74 | end, 75 | }, 76 | } 77 | end, 78 | }) 79 | 80 | __Directive = types.object({ 81 | name = '__Directive', 82 | 83 | description = util.trim [[ 84 | A Directive provides a way to describe alternate runtime execution and type validation behavior 85 | in a GraphQL document. 86 | 87 | In some cases, you need to provide options to alter GraphQL’s execution 88 | behavior in ways field arguments will not suffice, such as conditionally including or skipping a 89 | field. Directives provide this by describing additional information to the executor. 90 | ]], 91 | 92 | fields = function() 93 | return { 94 | name = types.nonNull(types.string), 95 | 96 | description = types.string, 97 | 98 | locations = { 99 | kind = types.nonNull(types.list(types.nonNull( 100 | __DirectiveLocation 101 | ))), 102 | resolve = function(directive) 103 | local res = {} 104 | 105 | if directive.onQuery then table.insert(res, 'QUERY') end 106 | if directive.onMutation then table.insert(res, 'MUTATION') end 107 | if directive.onField then table.insert(res, 'FIELD') end 108 | if directive.onFragmentDefinition then table.insert(res, 'FRAGMENT_DEFINITION') end 109 | if directive.onFragmentSpread then table.insert(res, 'FRAGMENT_SPREAD') end 110 | if directive.onInlineFragment then table.insert(res, 'INLINE_FRAGMENT') end 111 | if directive.onVariableDefinition then table.insert(res, 'VARIABLE_DEFINITION') end 112 | if directive.onSchema then table.insert(res, 'SCHEMA') end 113 | if directive.onScalar then table.insert(res, 'SCALAR') end 114 | if directive.onObject then table.insert(res, 'OBJECT') end 115 | if directive.onFieldDefinition then table.insert(res, 'FIELD_DEFINITION') end 116 | if directive.onArgumentDefinition then table.insert(res, 'ARGUMENT_DEFINITION') end 117 | if directive.onInterface then table.insert(res, 'INTERFACE') end 118 | if directive.onUnion then table.insert(res, 'UNION') end 119 | if directive.onEnum then table.insert(res, 'ENUM') end 120 | if directive.onEnumValue then table.insert(res, 'ENUM_VALUE') end 121 | if directive.onInputObject then table.insert(res, 'INPUT_OBJECT') end 122 | if directive.onInputFieldDefinition then table.insert(res, 'INPUT_FIELD_DEFINITION') end 123 | 124 | return res 125 | end, 126 | }, 127 | 128 | args = { 129 | kind = types.nonNull(types.list(types.nonNull(__InputValue))), 130 | resolve = resolveArgs, 131 | }, 132 | 133 | isRepeatable = { 134 | kind = types.nonNull(types.boolean), 135 | resolve = function(directive) 136 | return directive.isRepeatable == true 137 | end, 138 | }, 139 | } 140 | end, 141 | }) 142 | 143 | __DirectiveLocation = types.enum({ 144 | name = '__DirectiveLocation', 145 | 146 | description = util.trim [[ 147 | A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation 148 | describes one such possible adjacencies. 149 | ]], 150 | 151 | values = { 152 | QUERY = { 153 | value = 'QUERY', 154 | description = 'Location adjacent to a query operation.', 155 | }, 156 | 157 | MUTATION = { 158 | value = 'MUTATION', 159 | description = 'Location adjacent to a mutation operation.', 160 | }, 161 | 162 | FIELD = { 163 | value = 'FIELD', 164 | description = 'Location adjacent to a field.', 165 | }, 166 | 167 | FRAGMENT_DEFINITION = { 168 | value = 'FRAGMENT_DEFINITION', 169 | description = 'Location adjacent to a fragment definition.', 170 | }, 171 | 172 | FRAGMENT_SPREAD = { 173 | value = 'FRAGMENT_SPREAD', 174 | description = 'Location adjacent to a fragment spread.', 175 | }, 176 | 177 | INLINE_FRAGMENT = { 178 | value = 'INLINE_FRAGMENT', 179 | description = 'Location adjacent to an inline fragment.', 180 | }, 181 | 182 | VARIABLE_DEFINITION = { 183 | value = 'VARIABLE_DEFINITION', 184 | description = 'Location adjacent to a variable definition.', 185 | }, 186 | 187 | SCHEMA = { 188 | value = 'SCHEMA', 189 | description = 'Location adjacent to schema.', 190 | }, 191 | 192 | SCALAR = { 193 | value = 'SCALAR', 194 | description = 'Location adjacent to a scalar.', 195 | }, 196 | 197 | OBJECT = { 198 | value = 'OBJECT', 199 | description = 'Location adjacent to an object.', 200 | }, 201 | 202 | FIELD_DEFINITION = { 203 | value = 'FIELD_DEFINITION', 204 | description = 'Location adjacent to a field definition.', 205 | }, 206 | 207 | ARGUMENT_DEFINITION = { 208 | value = 'ARGUMENT_DEFINITION', 209 | description = 'Location adjacent to an argument definition.', 210 | }, 211 | 212 | INTERFACE = { 213 | value = 'INTERFACE', 214 | description = 'Location adjacent to an interface.', 215 | }, 216 | 217 | UNION = { 218 | value = 'UNION', 219 | description = 'Location adjacent to an union.', 220 | }, 221 | 222 | ENUM = { 223 | value = 'ENUM', 224 | description = 'Location adjacent to an enum.', 225 | }, 226 | 227 | ENUM_VALUE = { 228 | value = 'ENUM_VALUE', 229 | description = 'Location adjacent to an enum value.', 230 | }, 231 | 232 | INPUT_OBJECT = { 233 | value = 'INPUT_OBJECT', 234 | description = 'Location adjacent to an input object.', 235 | }, 236 | 237 | INPUT_FIELD_DEFINITION = { 238 | value = 'INPUT_FIELD_DEFINITION', 239 | description = 'Location adjacent to an input field definition.', 240 | }, 241 | }, 242 | }) 243 | 244 | __Type = types.object({ 245 | name = '__Type', 246 | 247 | description = util.trim [[ 248 | The fundamental unit of any GraphQL Schema is the type. There are 249 | many kinds of types in GraphQL as represented by the `__TypeKind` enum. 250 | 251 | Depending on the kind of a type, certain fields describe 252 | information about that type. Scalar types provide no information 253 | beyond a name and description, while Enum types provide their values. 254 | Object and Interface types provide the fields they describe. Abstract 255 | types, Union and Interface, provide the Object types possible 256 | at runtime. List and NonNull types compose other types. 257 | ]], 258 | 259 | fields = function() 260 | return { 261 | name = types.string, 262 | description = types.string, 263 | 264 | kind = { 265 | kind = __TypeKind.nonNull, 266 | resolve = function(kind) 267 | if kind.__type == 'Scalar' then 268 | return 'SCALAR' 269 | elseif kind.__type == 'Object' then 270 | return 'OBJECT' 271 | elseif kind.__type == 'Interface' then 272 | return 'INTERFACE' 273 | elseif kind.__type == 'Union' then 274 | return 'UNION' 275 | elseif kind.__type == 'Enum' then 276 | return 'ENUM' 277 | elseif kind.__type == 'InputObject' then 278 | return 'INPUT_OBJECT' 279 | elseif kind.__type == 'List' then 280 | return 'LIST' 281 | elseif kind.__type == 'NonNull' then 282 | return 'NON_NULL' 283 | end 284 | 285 | error('Unknown type ' .. kind) 286 | end, 287 | }, 288 | 289 | fields = { 290 | kind = types.list(types.nonNull(__Field)), 291 | arguments = { 292 | includeDeprecated = { 293 | kind = types.boolean, 294 | defaultValue = false, 295 | }, 296 | }, 297 | resolve = function(kind, arguments) 298 | if kind.__type == 'Object' or kind.__type == 'Interface' then 299 | return util.filter(util.values(kind.fields), function(field) 300 | return arguments.includeDeprecated or field.deprecationReason == nil 301 | end) 302 | end 303 | 304 | return nil 305 | end 306 | }, 307 | 308 | interfaces = { 309 | kind = types.list(types.nonNull(__Type)), 310 | resolve = function(kind) 311 | if kind.__type == 'Object' then 312 | return kind.interfaces or {} 313 | end 314 | end, 315 | }, 316 | 317 | possibleTypes = { 318 | kind = types.list(types.nonNull(__Type)), 319 | resolve = function(kind, _, context) 320 | if kind.__type == 'Interface' or kind.__type == 'Union' then 321 | return context.schema:getPossibleTypes(kind) 322 | end 323 | end, 324 | }, 325 | 326 | enumValues = { 327 | kind = types.list(types.nonNull(__EnumValue)), 328 | arguments = { 329 | includeDeprecated = { kind = types.boolean, defaultValue = false }, 330 | }, 331 | resolve = function(kind, arguments) 332 | if kind.__type == 'Enum' then 333 | return util.filter(util.values(kind.values), function(value) 334 | return arguments.includeDeprecated or not value.deprecationReason 335 | end) 336 | end 337 | end, 338 | }, 339 | 340 | inputFields = { 341 | kind = types.list(types.nonNull(__InputValue)), 342 | resolve = function(kind) 343 | if kind.__type == 'InputObject' then 344 | return util.values(kind.fields) 345 | end 346 | end, 347 | }, 348 | 349 | specifiedByURL = { 350 | kind = types.string, 351 | resolve = function(kind) 352 | if kind.__type == 'Scalar' then 353 | return kind.specifiedByURL 354 | end 355 | end, 356 | }, 357 | 358 | ofType = { 359 | kind = __Type, 360 | }, 361 | } 362 | end, 363 | }) 364 | 365 | __Field = types.object({ 366 | name = '__Field', 367 | 368 | description = util.trim [[ 369 | Object and Interface types are described by a list of Fields, each of 370 | which has a name, potentially a list of arguments, and a return type. 371 | ]], 372 | 373 | fields = function() 374 | return { 375 | name = types.string.nonNull, 376 | description = types.string, 377 | 378 | args = { 379 | kind = types.nonNull(types.list(types.nonNull(__InputValue))), 380 | resolve = resolveArgs, 381 | }, 382 | 383 | type = { 384 | kind = __Type.nonNull, 385 | resolve = function(field) 386 | return field.kind 387 | end, 388 | }, 389 | 390 | isDeprecated = { 391 | kind = types.boolean.nonNull, 392 | resolve = function(field) 393 | return field.deprecationReason ~= nil 394 | end, 395 | }, 396 | 397 | deprecationReason = types.string, 398 | } 399 | end, 400 | }) 401 | 402 | __InputValue = types.object({ 403 | name = '__InputValue', 404 | 405 | description = util.trim [[ 406 | Arguments provided to Fields or Directives and the input fields of an 407 | InputObject are represented as Input Values which describe their type 408 | and optionally a default value. 409 | ]], 410 | 411 | fields = function() 412 | return { 413 | name = types.string.nonNull, 414 | description = types.string, 415 | 416 | type = { 417 | kind = types.nonNull(__Type), 418 | resolve = function(field) 419 | return field.kind 420 | end, 421 | }, 422 | 423 | defaultValue = { 424 | kind = types.string, 425 | description = 'A GraphQL-formatted string representing the default value for this input value.', 426 | resolve = function(inputVal) 427 | return inputVal.defaultValue and tostring(inputVal.defaultValue) -- TODO improve serialization a lot 428 | end, 429 | }, 430 | } 431 | end, 432 | }) 433 | 434 | __EnumValue = types.object({ 435 | name = '__EnumValue', 436 | 437 | description = [[ 438 | One possible value for a given Enum. Enum values are unique values, not 439 | a placeholder for a string or numeric value. However an Enum value is 440 | returned in a JSON response as a string. 441 | ]], 442 | 443 | fields = function() 444 | return { 445 | name = types.string.nonNull, 446 | description = types.string, 447 | isDeprecated = { 448 | kind = types.boolean.nonNull, 449 | resolve = function(enumValue) return enumValue.deprecationReason ~= nil end 450 | }, 451 | deprecationReason = types.string, 452 | } 453 | end, 454 | }) 455 | 456 | __TypeKind = types.enum({ 457 | name = '__TypeKind', 458 | description = 'An enum describing what kind of type a given `__Type` is.', 459 | values = { 460 | SCALAR = { 461 | value = 'SCALAR', 462 | description = 'Indicates this type is a scalar.', 463 | }, 464 | 465 | OBJECT = { 466 | value = 'OBJECT', 467 | description = 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', 468 | }, 469 | 470 | INTERFACE = { 471 | value = 'INTERFACE', 472 | description = 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.', 473 | }, 474 | 475 | UNION = { 476 | value = 'UNION', 477 | description = 'Indicates this type is a union. `possibleTypes` is a valid field.', 478 | }, 479 | 480 | ENUM = { 481 | value = 'ENUM', 482 | description = 'Indicates this type is an enum. `enumValues` is a valid field.', 483 | }, 484 | 485 | INPUT_OBJECT = { 486 | value = 'INPUT_OBJECT', 487 | description = 'Indicates this type is an input object. `inputFields` is a valid field.', 488 | }, 489 | 490 | LIST = { 491 | value = 'LIST', 492 | description = 'Indicates this type is a list. `ofType` is a valid field.', 493 | }, 494 | 495 | NON_NULL = { 496 | value = 'NON_NULL', 497 | description = 'Indicates this type is a non-null. `ofType` is a valid field.', 498 | }, 499 | }, 500 | }) 501 | 502 | local Schema = { 503 | name = '__schema', 504 | kind = __Schema.nonNull, 505 | description = 'Access the current type schema of this server.', 506 | arguments = {}, 507 | resolve = function(_, _, info) 508 | return info.schema 509 | end, 510 | } 511 | 512 | local Type = { 513 | name = '__type', 514 | kind = __Type, 515 | description = 'Request the type information of a single type.', 516 | arguments = { 517 | name = types.string.nonNull, 518 | }, 519 | resolve = function(_, arguments, info) 520 | return info.schema:getType(arguments.name) 521 | end, 522 | } 523 | 524 | local TypeName = { 525 | name = '__typename', 526 | kind = types.string.nonNull, 527 | description = 'The name of the current Object type at runtime.', 528 | arguments = {}, 529 | resolve = function(_, _, info) 530 | return info.parentType.name 531 | end, 532 | } 533 | 534 | return { 535 | __Schema = __Schema, 536 | __Directive = __Directive, 537 | __DirectiveLocation = __DirectiveLocation, 538 | __Type = __Type, 539 | __Field = __Field, 540 | __EnumValue = __EnumValue, 541 | __TypeKind = __TypeKind, 542 | Schema = Schema, 543 | Type = Type, 544 | TypeName = TypeName, 545 | fieldMap = { 546 | __schema = Schema, 547 | __type = Type, 548 | __typename = TypeName, 549 | }, 550 | } 551 | -------------------------------------------------------------------------------- /graphql/parse.lua: -------------------------------------------------------------------------------- 1 | local luagraphqlparser = require('luagraphqlparser') 2 | 3 | local function parse(s) 4 | local ast, err = luagraphqlparser.parse(s) 5 | if err ~= nil then 6 | error(err, 0) 7 | end 8 | return ast 9 | end 10 | 11 | return { 12 | parse = parse, 13 | } 14 | -------------------------------------------------------------------------------- /graphql/query_util.lua: -------------------------------------------------------------------------------- 1 | local types = require('graphql.types') 2 | 3 | local function typeFromAST(node, schema) 4 | local innerType 5 | if node.kind == 'listType' then 6 | innerType = typeFromAST(node.type, schema) 7 | return innerType and types.list(innerType) 8 | elseif node.kind == 'nonNullType' then 9 | innerType = typeFromAST(node.type, schema) 10 | return innerType and types.nonNull(innerType) 11 | else 12 | assert(node.kind == 'namedType', 'Variable must be a named type') 13 | return schema:getType(node.name.value) 14 | end 15 | end 16 | 17 | return { 18 | typeFromAST = typeFromAST, 19 | } 20 | -------------------------------------------------------------------------------- /graphql/rules.lua: -------------------------------------------------------------------------------- 1 | local introspection = require('graphql.introspection') 2 | local query_util = require('graphql.query_util') 3 | local types = require('graphql.types') 4 | local util = require('graphql.util') 5 | 6 | local function error(...) 7 | return _G.error(..., 0) 8 | end 9 | 10 | local function getParentField(context, name, count) 11 | if introspection.fieldMap[name] then return introspection.fieldMap[name] end 12 | 13 | count = count or 1 14 | local parent = context.objects[#context.objects - count] 15 | 16 | -- Unwrap lists and non-null types 17 | while parent.ofType do 18 | parent = parent.ofType 19 | end 20 | 21 | return parent.fields[name] 22 | end 23 | 24 | local rules = {} 25 | 26 | function rules.uniqueOperationNames(node, context) 27 | local name = node.name and node.name.value 28 | 29 | if name then 30 | if context.operationNames[name] then 31 | error('Multiple operations exist named "' .. name .. '"') 32 | end 33 | 34 | context.operationNames[name] = true 35 | end 36 | end 37 | 38 | function rules.loneAnonymousOperation(node, context) 39 | local name = node.name and node.name.value 40 | 41 | if context.hasAnonymousOperation or (not name and next(context.operationNames)) then 42 | error('Cannot have more than one operation when using anonymous operations') 43 | end 44 | 45 | if not name then 46 | context.hasAnonymousOperation = true 47 | end 48 | end 49 | 50 | function rules.fieldsDefinedOnType(node, context) 51 | if context.objects[#context.objects] == false then 52 | local parent = context.objects[#context.objects - 1] 53 | while parent.ofType do parent = parent.ofType end 54 | error('Field "' .. node.name.value .. '" is not defined on type "' .. parent.name .. '"') 55 | end 56 | end 57 | 58 | function rules.argumentsDefinedOnType(node, context) 59 | if node.arguments then 60 | local parentField = getParentField(context, node.name.value) 61 | for _, argument in pairs(node.arguments) do 62 | local name = argument.name.value 63 | if not parentField.arguments[name] then 64 | error('Non-existent argument "' .. name .. '"') 65 | end 66 | end 67 | end 68 | end 69 | 70 | function rules.scalarFieldsAreLeaves(node, context) 71 | local field_t = types.bare(context.objects[#context.objects]).__type 72 | if field_t == 'Scalar' and node.selectionSet then 73 | local valueName = node.name.value 74 | error(('Scalar field %q cannot have subselections'):format(valueName)) 75 | end 76 | end 77 | 78 | function rules.compositeFieldsAreNotLeaves(node, context) 79 | local field_t = types.bare(context.objects[#context.objects]).__type 80 | local isCompositeType = field_t == 'Object' or field_t == 'Interface' or 81 | field_t == 'Union' 82 | 83 | if isCompositeType and not node.selectionSet then 84 | local fieldName = node.name.value 85 | error(('Composite field %q must have subselections'):format(fieldName)) 86 | end 87 | end 88 | 89 | function rules.unambiguousSelections(node, context) 90 | local selectionMap = {} 91 | local seen = {} 92 | 93 | local function findConflict(entryA, entryB) 94 | 95 | -- Parent types can't overlap if they're different objects. 96 | -- Interface and union types may overlap. 97 | if entryA.parent ~= entryB.parent and entryA.__type == 'Object' and entryB.__type == 'Object' then 98 | return 99 | end 100 | 101 | -- Error if there are aliases that map two different fields to the same name. 102 | if entryA.field.name.value ~= entryB.field.name.value then 103 | return 'Type name mismatch' 104 | end 105 | 106 | -- Error if there are fields with the same name that have different return types. 107 | if entryA.definition and entryB.definition and entryA.definition ~= entryB.definition then 108 | return 'Return type mismatch' 109 | end 110 | 111 | -- Error if arguments are not identical for two fields with the same name. 112 | local argsA = entryA.field.arguments or {} 113 | local argsB = entryB.field.arguments or {} 114 | 115 | if #argsA ~= #argsB then 116 | return 'Argument mismatch' 117 | end 118 | 119 | local argMap = {} 120 | 121 | for i = 1, #argsA do 122 | argMap[argsA[i].name.value] = argsA[i].value 123 | end 124 | 125 | for i = 1, #argsB do 126 | local name = argsB[i].name.value 127 | if not argMap[name] then 128 | return 'Argument mismatch' 129 | elseif argMap[name].kind ~= argsB[i].value.kind then 130 | return 'Argument mismatch' 131 | elseif argMap[name].value ~= argsB[i].value.value then 132 | return 'Argument mismatch' 133 | end 134 | end 135 | end 136 | 137 | local function validateField(key, entry) 138 | if selectionMap[key] then 139 | for i = 1, #selectionMap[key] do 140 | local conflict = findConflict(selectionMap[key][i], entry) 141 | if conflict then 142 | error(conflict) 143 | end 144 | end 145 | 146 | table.insert(selectionMap[key], entry) 147 | else 148 | selectionMap[key] = { entry } 149 | end 150 | end 151 | 152 | -- Recursively make sure that there are no ambiguous selections with the same name. 153 | local function validateSelectionSet(selectionSet, parentType) 154 | for _, selection in ipairs(selectionSet.selections) do 155 | if selection.kind == 'field' then 156 | if not parentType or not parentType.fields or not parentType.fields[selection.name.value] then return end 157 | 158 | local key = selection.alias and selection.alias.name.value or selection.name.value 159 | local definition = parentType.fields[selection.name.value].kind 160 | 161 | local fieldEntry = { 162 | parent = parentType, 163 | field = selection, 164 | definition = definition, 165 | } 166 | 167 | validateField(key, fieldEntry) 168 | elseif selection.kind == 'inlineFragment' then 169 | local parentType = selection.typeCondition and context.schema:getType( 170 | selection.typeCondition.name.value) or parentType 171 | validateSelectionSet(selection.selectionSet, parentType) 172 | elseif selection.kind == 'fragmentSpread' then 173 | local fragmentDefinition = context.fragmentMap[selection.name.value] 174 | if fragmentDefinition and not seen[fragmentDefinition] then 175 | seen[fragmentDefinition] = true 176 | if fragmentDefinition and fragmentDefinition.typeCondition then 177 | local parentType = context.schema:getType(fragmentDefinition.typeCondition.name.value) 178 | validateSelectionSet(fragmentDefinition.selectionSet, parentType) 179 | end 180 | end 181 | end 182 | end 183 | end 184 | 185 | validateSelectionSet(node, context.objects[#context.objects]) 186 | end 187 | 188 | function rules.uniqueArgumentNames(node, _) 189 | if node.arguments then 190 | local arguments = {} 191 | for _, argument in ipairs(node.arguments) do 192 | local name = argument.name.value 193 | if arguments[name] then 194 | error('Encountered multiple arguments named "' .. name .. '"') 195 | end 196 | arguments[name] = true 197 | end 198 | end 199 | end 200 | 201 | local coerce_opts = { strict_non_null = true, skip_variables = true } 202 | 203 | function rules.argumentsOfCorrectType(node, context) 204 | if node.arguments then 205 | local parentField = getParentField(context, node.name.value) 206 | for _, argument in pairs(node.arguments) do 207 | local name = argument.name.value 208 | local argumentType = parentField.arguments[name] 209 | util.coerceValue(argument.value, argumentType.kind or argumentType, nil, coerce_opts) 210 | end 211 | end 212 | end 213 | 214 | function rules.requiredArgumentsPresent(node, context) 215 | local arguments = node.arguments or {} 216 | local parentField = getParentField(context, node.name.value) 217 | for name, argument in pairs(parentField.arguments) do 218 | if argument.__type == 'NonNull' then 219 | local present = util.find(arguments, function(argument) 220 | return argument.name.value == name 221 | end) 222 | 223 | if not present then 224 | error('Required argument "' .. name .. '" was not supplied.') 225 | end 226 | end 227 | end 228 | end 229 | 230 | function rules.uniqueFragmentNames(node, _) 231 | local fragments = {} 232 | for _, definition in ipairs(node.definitions) do 233 | if definition.kind == 'fragmentDefinition' then 234 | local name = definition.name.value 235 | if fragments[name] then 236 | error('Encountered multiple fragments named "' .. name .. '"') 237 | end 238 | fragments[name] = true 239 | end 240 | end 241 | end 242 | 243 | function rules.fragmentHasValidType(node, context) 244 | if not node.typeCondition then return end 245 | 246 | local name = node.typeCondition.name.value 247 | local kind = context.schema:getType(name) 248 | 249 | if not kind then 250 | error('Fragment refers to non-existent type "' .. name .. '"') 251 | end 252 | 253 | if kind.__type ~= 'Object' and kind.__type ~= 'Interface' and kind.__type ~= 'Union' then 254 | error('Fragment type must be an Object, Interface, or Union, got ' .. kind.__type) 255 | end 256 | end 257 | 258 | function rules.noUnusedFragments(node, context) 259 | for _, definition in ipairs(node.definitions) do 260 | if definition.kind == 'fragmentDefinition' then 261 | local name = definition.name.value 262 | if not context.usedFragments[name] then 263 | error('Fragment "' .. name .. '" was not used.') 264 | end 265 | end 266 | end 267 | end 268 | 269 | function rules.fragmentSpreadTargetDefined(node, context) 270 | if not context.fragmentMap[node.name.value] then 271 | error('Fragment spread refers to non-existent fragment "' .. node.name.value .. '"') 272 | end 273 | end 274 | 275 | function rules.fragmentDefinitionHasNoCycles(node, context) 276 | local seen = { [node.name.value] = true } 277 | 278 | local function detectCycles(selectionSet) 279 | for _, selection in ipairs(selectionSet.selections) do 280 | if selection.kind == 'inlineFragment' then 281 | detectCycles(selection.selectionSet) 282 | elseif selection.kind == 'fragmentSpread' then 283 | if seen[selection.name.value] then 284 | error('Fragment definition has cycles') 285 | end 286 | 287 | seen[selection.name.value] = true 288 | 289 | local fragmentDefinition = context.fragmentMap[selection.name.value] 290 | if fragmentDefinition and fragmentDefinition.typeCondition then 291 | detectCycles(fragmentDefinition.selectionSet) 292 | end 293 | end 294 | end 295 | end 296 | 297 | detectCycles(node.selectionSet) 298 | end 299 | 300 | function rules.fragmentSpreadIsPossible(node, context) 301 | local fragment = node.kind == 'inlineFragment' and node or context.fragmentMap[node.name.value] 302 | 303 | local parentType = context.objects[#context.objects - 1] 304 | while parentType.ofType do parentType = parentType.ofType end 305 | 306 | local fragmentType 307 | if node.kind == 'inlineFragment' then 308 | fragmentType = node.typeCondition and context.schema:getType(node.typeCondition.name.value) or parentType 309 | else 310 | fragmentType = context.schema:getType(fragment.typeCondition.name.value) 311 | end 312 | 313 | -- Some types are not present in the schema. Let other rules handle this. 314 | if not parentType or not fragmentType then return end 315 | 316 | local function getTypes(kind) 317 | if kind.__type == 'Object' then 318 | return { [kind] = kind } 319 | elseif kind.__type == 'Interface' then 320 | return context.schema:getImplementors(kind.name) 321 | elseif kind.__type == 'Union' then 322 | local types = {} 323 | for i = 1, #kind.types do 324 | types[kind.types[i]] = kind.types[i] 325 | end 326 | return types 327 | else 328 | return {} 329 | end 330 | end 331 | 332 | local parentTypes = getTypes(parentType) 333 | local fragmentTypes = getTypes(fragmentType) 334 | 335 | local valid = util.find(parentTypes, function(kind) 336 | local kind = kind 337 | -- Here is the check that type, mentioned in '... on some_type' 338 | -- conditional fragment expression is type of some field of parent object. 339 | -- In case of Union parent object and NonNull wrapped inner types 340 | -- graphql-lua missed unwrapping so we add it here 341 | while kind.__type == 'NonNull' do 342 | kind = kind.ofType 343 | end 344 | return fragmentTypes[kind] 345 | end) 346 | 347 | if not valid then 348 | error('Fragment type condition is not possible for given type') 349 | end 350 | end 351 | 352 | function rules.uniqueInputObjectFields(node, _) 353 | local function validateValue(value) 354 | if value.kind == 'listType' or value.kind == 'nonNullType' then 355 | return validateValue(value.type) 356 | elseif value.kind == 'inputObject' then 357 | local fieldMap = {} 358 | for _, field in ipairs(value.values) do 359 | if fieldMap[field.name] then 360 | error('Multiple input object fields named "' .. field.name .. '"') 361 | end 362 | 363 | fieldMap[field.name] = true 364 | 365 | validateValue(field.value) 366 | end 367 | end 368 | end 369 | 370 | if node.kind == 'inputObject' then 371 | validateValue(node) 372 | else 373 | validateValue(node.value) 374 | end 375 | end 376 | 377 | function rules.directivesAreDefined(node, context) 378 | if not node.directives then return end 379 | 380 | for _, directive in pairs(node.directives) do 381 | if not context.schema:getDirective(directive.name.value) then 382 | error('Unknown directive "' .. directive.name.value .. '"') 383 | end 384 | end 385 | end 386 | 387 | function rules.variablesHaveCorrectType(node, context) 388 | local function validateType(type) 389 | if type.kind == 'listType' or type.kind == 'nonNullType' then 390 | validateType(type.type) 391 | elseif type.kind == 'namedType' then 392 | local schemaType = context.schema:getType(type.name.value) 393 | if not schemaType then 394 | error('Variable specifies unknown type "' .. tostring(type.name.value) .. '"') 395 | elseif schemaType.__type ~= 'Scalar' and schemaType.__type ~= 'Enum' and schemaType.__type ~= 'InputObject' then 396 | error('Variable types must be scalars, enums, or input objects, got "' .. schemaType.__type .. '"') 397 | end 398 | end 399 | end 400 | 401 | if node.variableDefinitions then 402 | for _, definition in ipairs(node.variableDefinitions) do 403 | validateType(definition.type) 404 | end 405 | end 406 | end 407 | 408 | function rules.variableDefaultValuesHaveCorrectType(node, context) 409 | if node.variableDefinitions then 410 | for _, definition in ipairs(node.variableDefinitions) do 411 | if definition.defaultValue then 412 | local variableType = query_util.typeFromAST(definition.type, context.schema) 413 | util.coerceValue(definition.defaultValue, variableType, nil, coerce_opts) 414 | end 415 | end 416 | end 417 | end 418 | 419 | function rules.variablesAreUsed(node, context) 420 | if node.variableDefinitions then 421 | for _, definition in ipairs(node.variableDefinitions) do 422 | local variableName = definition.variable.name.value 423 | if not context.variableReferences[variableName] then 424 | error('Unused variable "' .. variableName .. '"') 425 | end 426 | end 427 | end 428 | end 429 | 430 | function rules.variablesAreDefined(node, context) 431 | if context.variableReferences then 432 | local variableMap = {} 433 | for _, definition in ipairs(node.variableDefinitions or {}) do 434 | variableMap[definition.variable.name.value] = true 435 | end 436 | 437 | for variable in pairs(context.variableReferences) do 438 | if not variableMap[variable] then 439 | error('Unknown variable "' .. variable .. '"') 440 | end 441 | end 442 | end 443 | end 444 | 445 | -- {{{ variableUsageAllowed 446 | 447 | local function collectArguments(referencedNode, context, seen, arguments) 448 | if referencedNode.kind == 'selectionSet' then 449 | for _, selection in ipairs(referencedNode.selections) do 450 | if not seen[selection] then 451 | seen[selection] = true 452 | collectArguments(selection, context, seen, arguments) 453 | end 454 | end 455 | elseif referencedNode.kind == 'field' and referencedNode.arguments then 456 | local fieldName = referencedNode.name.value 457 | arguments[fieldName] = arguments[fieldName] or {} 458 | for _, argument in ipairs(referencedNode.arguments) do 459 | table.insert(arguments[fieldName], argument) 460 | end 461 | elseif referencedNode.kind == 'inlineFragment' then 462 | return collectArguments(referencedNode.selectionSet, context, seen, 463 | arguments) 464 | elseif referencedNode.kind == 'fragmentSpread' then 465 | local fragment = context.fragmentMap[referencedNode.name.value] 466 | return fragment and collectArguments(fragment.selectionSet, context, seen, 467 | arguments) 468 | end 469 | end 470 | 471 | -- http://facebook.github.io/graphql/June2018/#AreTypesCompatible() 472 | local function isTypeSubTypeOf(subType, superType, context) 473 | if subType == superType then return true end 474 | 475 | if superType.__type == 'NonNull' then 476 | if subType.__type == 'NonNull' then 477 | return isTypeSubTypeOf(subType.ofType, superType.ofType, context) 478 | end 479 | 480 | return false 481 | elseif subType.__type == 'NonNull' then 482 | return isTypeSubTypeOf(subType.ofType, superType, context) 483 | end 484 | 485 | if superType.__type == 'List' then 486 | if subType.__type == 'List' then 487 | return isTypeSubTypeOf(subType.ofType, superType.ofType, context) 488 | end 489 | 490 | return false 491 | elseif subType.__type == 'List' then 492 | return false 493 | end 494 | 495 | return false 496 | end 497 | 498 | local function isVariableTypesValid(argument, argumentType, context, 499 | variableMap) 500 | if argument.value.kind == 'variable' then 501 | -- found a variable, check types compatibility 502 | local variableName = argument.value.name.value 503 | local variableDefinition = variableMap[variableName] 504 | 505 | if variableDefinition == nil then 506 | -- The same error as in rules.variablesAreDefined(). 507 | error('Unknown variable "' .. variableName .. '"') 508 | end 509 | 510 | local hasNonNullDefault = (variableDefinition.defaultValue ~= nil) and 511 | (variableDefinition.defaultValue.kind ~= 'null') 512 | 513 | local variableType = query_util.typeFromAST(variableDefinition.type, 514 | context.schema) 515 | 516 | local realVariableTypeName = util.getTypeName(variableType) 517 | if hasNonNullDefault and variableType.__type ~= 'NonNull' then 518 | variableType = types.nonNull(variableType) 519 | end 520 | 521 | -- This line of code provides support to using 522 | -- `arg = { kind = type, description = desc }` 523 | -- type declaration in query input arguments 524 | -- instead of `arg = type` one when passing 525 | -- argument with a variable. 526 | if argumentType.kind ~= nil then argumentType = argumentType.kind end 527 | 528 | if not isTypeSubTypeOf(variableType, argumentType, context) then 529 | return false, ('Variable "%s" type mismatch: the variable type "%s" ' .. 530 | 'is not compatible with the argument type "%s"'):format(variableName, 531 | realVariableTypeName, util.getTypeName(argumentType)) 532 | end 533 | elseif argument.value.kind == 'list' then 534 | -- find variables deeper 535 | local parentType = argumentType 536 | if parentType.__type == 'NonNull' then 537 | parentType = parentType.ofType 538 | end 539 | local childType = parentType.ofType 540 | 541 | for _, child in ipairs(argument.value.values) do 542 | local ok, err = isVariableTypesValid({value = child}, childType, context, 543 | variableMap) 544 | if not ok then return false, err end 545 | end 546 | elseif argument.value.kind == 'inputObject' then 547 | -- find variables deeper 548 | for _, child in ipairs(argument.value.values) do 549 | local isInputObject = argumentType.__type == 'InputObject' 550 | 551 | if isInputObject then 552 | local childArgumentType = argumentType.fields[child.name].kind 553 | local ok, err = isVariableTypesValid(child, childArgumentType, context, 554 | variableMap) 555 | if not ok then return false, err end 556 | end 557 | end 558 | end 559 | return true 560 | end 561 | 562 | function rules.variableUsageAllowed(node, context) 563 | if not context.currentOperation then return end 564 | 565 | local variableMap = {} 566 | local variableDefinitions = context.currentOperation.variableDefinitions 567 | for _, definition in ipairs(variableDefinitions or {}) do 568 | variableMap[definition.variable.name.value] = definition 569 | end 570 | 571 | local arguments 572 | 573 | if node.kind == 'field' then 574 | arguments = { [node.name.value] = node.arguments } 575 | elseif node.kind == 'fragmentSpread' then 576 | local seen = {} 577 | local fragment = context.fragmentMap[node.name.value] 578 | if fragment then 579 | arguments = {} 580 | collectArguments(fragment.selectionSet, context, seen, arguments) 581 | end 582 | end 583 | 584 | if not arguments then return end 585 | 586 | for field in pairs(arguments) do 587 | local parentField = getParentField(context, field) 588 | for i = 1, #arguments[field] do 589 | local argument = arguments[field][i] 590 | local argumentType = parentField.arguments[argument.name.value] 591 | local ok, err = isVariableTypesValid(argument, argumentType, context, 592 | variableMap) 593 | if not ok then 594 | error(err) 595 | end 596 | end 597 | end 598 | end 599 | 600 | -- }}} 601 | 602 | return rules 603 | -------------------------------------------------------------------------------- /graphql/schema.lua: -------------------------------------------------------------------------------- 1 | local introspection = require('graphql.introspection') 2 | local types = require('graphql.types') 3 | 4 | local function error(...) 5 | return _G.error(..., 0) 6 | end 7 | 8 | local schema = {} 9 | schema.__index = schema 10 | 11 | function schema.create(config, name, opts) 12 | assert(type(config.query) == 'table', 'must provide query object') 13 | assert(not config.mutation or type(config.mutation) == 'table', 'mutation must be a table if provided') 14 | 15 | opts = opts or {} 16 | local self = setmetatable({}, schema) 17 | 18 | for k, v in pairs(config) do 19 | self[k] = v 20 | end 21 | 22 | self.directives = self.directives or { 23 | types.include, 24 | types.skip, 25 | types.specifiedBy, 26 | } 27 | 28 | self.typeMap = {} 29 | self.interfaceMap = {} 30 | self.directiveMap = {} 31 | self.name = name 32 | 33 | self:generateTypeMap(self.query) 34 | self:generateTypeMap(self.mutation) 35 | self:generateTypeMap(introspection.__Schema) 36 | self:generateDirectiveMap() 37 | 38 | if opts.defaultValues == true then 39 | self.defaultValues = {} 40 | self.defaultValues.mutation = self:extractDefaults(self.mutation) 41 | self.defaultValues.query = self:extractDefaults(self.query) 42 | end 43 | 44 | if opts.directivesDefaultValues == true then 45 | self.directivesDefaultValues = {} 46 | 47 | for directiveName, directive in pairs(self.directiveMap or {}) do 48 | self.directivesDefaultValues[directiveName] = self:extractDefaults(directive) 49 | end 50 | end 51 | 52 | return self 53 | end 54 | 55 | function schema:extractDefaults(node) 56 | if not node then return end 57 | 58 | local defaultValues 59 | local nodeType = node.__type ~= nil and node or node.kind 60 | 61 | if nodeType.__type == 'NonNull' then 62 | return self:extractDefaults(nodeType.ofType) 63 | end 64 | 65 | if nodeType.__type == 'Enum' then 66 | return node.defaultValue 67 | end 68 | 69 | if nodeType.__type == 'Scalar' then 70 | return node.defaultValue 71 | end 72 | 73 | node.fields = type(node.fields) == 'function' and node.fields() or node.fields 74 | 75 | if nodeType.__type == 'Object' or nodeType.__type == 'InputObject' then 76 | for fieldName, field in pairs(nodeType.fields or {}) do 77 | local fieldDefaultValue = self:extractDefaults(field) 78 | if fieldDefaultValue ~= nil then 79 | defaultValues = defaultValues or {} 80 | defaultValues[fieldName] = fieldDefaultValue 81 | end 82 | 83 | for argumentName, argument in pairs(field.arguments or {}) do 84 | -- BEGIN_HACK: resolve type names to real types 85 | if type(argument) == 'string' then 86 | argument = types.resolve(argument, self.name) 87 | field.arguments[argumentName] = argument 88 | end 89 | 90 | if type(argument.kind) == 'string' then 91 | argument.kind = types.resolve(argument.kind, self.name) 92 | end 93 | -- END_HACK: resolve type names to real types 94 | 95 | local argumentDefaultValue = self:extractDefaults(argument) 96 | if argumentDefaultValue ~= nil then 97 | defaultValues = defaultValues or {} 98 | defaultValues[fieldName] = defaultValues[fieldName] or {} 99 | defaultValues[fieldName][argumentName] = argumentDefaultValue 100 | end 101 | end 102 | end 103 | return defaultValues 104 | end 105 | 106 | if nodeType.__type =='Directive' then 107 | for argumentName, argument in pairs(nodeType.arguments or {}) do 108 | -- BEGIN_HACK: resolve type names to real types 109 | if type(argument) == 'string' then 110 | argument = types.resolve(argument, self.name) 111 | nodeType.arguments[argumentName] = argument 112 | end 113 | 114 | if type(argument.kind) == 'string' then 115 | argument.kind = types.resolve(argument.kind, self.name) 116 | end 117 | -- END_HACK: resolve type names to real types 118 | 119 | local argumentDefaultValue = self:extractDefaults(argument) 120 | if argumentDefaultValue ~= nil then 121 | defaultValues = defaultValues or {} 122 | defaultValues[argumentName] = argumentDefaultValue 123 | end 124 | end 125 | return defaultValues 126 | end 127 | 128 | if nodeType.__type == 'List' then 129 | return self:extractDefaults(nodeType.ofType) 130 | end 131 | end 132 | 133 | function schema:generateTypeMap(node) 134 | if not node or (self.typeMap[node.name] and self.typeMap[node.name] == node) then return end 135 | 136 | if node.__type == 'NonNull' or node.__type == 'List' then 137 | -- HACK: resolve type names to real types 138 | node.ofType = types.resolve(node.ofType, self.name) 139 | return self:generateTypeMap(node.ofType) 140 | end 141 | 142 | if self.typeMap[node.name] and self.typeMap[node.name] ~= node then 143 | error('Encountered multiple types named "' .. node.name .. '"') 144 | end 145 | 146 | node.fields = type(node.fields) == 'function' and node.fields() or node.fields 147 | self.typeMap[node.name] = node 148 | 149 | if node.__type == 'Object' and node.interfaces then 150 | for idx, interface in ipairs(node.interfaces) do 151 | -- BEGIN_HACK: resolve type names to real types 152 | if type(interface) == 'string' then 153 | interface = types.resolve(interface, self.name) 154 | node.interfaces[idx] = interface 155 | end 156 | -- END_HACK: resolve type names to real types 157 | 158 | self:generateTypeMap(interface) 159 | self.interfaceMap[interface.name] = self.interfaceMap[interface.name] or {} 160 | self.interfaceMap[interface.name][node] = node 161 | end 162 | end 163 | 164 | if node.__type == 'Object' or node.__type == 'Interface' or node.__type == 'InputObject' then 165 | for fieldName, field in pairs(node.fields) do 166 | if field.arguments then 167 | for name, argument in pairs(field.arguments) do 168 | -- BEGIN_HACK: resolve type names to real types 169 | if type(argument) == 'string' then 170 | argument = types.resolve(argument, self.name) 171 | field.arguments[name] = argument 172 | end 173 | 174 | if type(argument.kind) == 'string' then 175 | argument.kind = types.resolve(argument.kind, self.name) 176 | end 177 | -- END_HACK: resolve type names to real types 178 | 179 | local argumentType = argument.__type and argument or argument.kind 180 | assert(argumentType, 'Must supply type for argument "' .. name .. '" on "' .. fieldName .. '"') 181 | self:generateTypeMap(argumentType) 182 | end 183 | end 184 | 185 | -- HACK: resolve type names to real types 186 | field.kind = types.resolve(field.kind, self.name) 187 | self:generateTypeMap(field.kind) 188 | end 189 | end 190 | end 191 | 192 | function schema:generateDirectiveMap() 193 | for _, directive in ipairs(self.directives) do 194 | self.directiveMap[directive.name] = directive 195 | if directive.arguments ~= nil then 196 | for name, argument in pairs(directive.arguments) do 197 | 198 | -- BEGIN_HACK: resolve type names to real types 199 | if type(argument) == 'string' then 200 | argument = types.resolve(argument, self.name) 201 | directive.arguments[name] = argument 202 | end 203 | 204 | if type(argument.kind) == 'string' then 205 | argument.kind = types.resolve(argument.kind, self.name) 206 | end 207 | -- END_HACK: resolve type names to real types 208 | 209 | local argumentType = argument.__type and argument or argument.kind 210 | if argumentType == nil then 211 | error('Must supply type for argument "' .. name .. '" on "' .. directive.name .. '"') 212 | end 213 | self:generateTypeMap(argumentType) 214 | end 215 | end 216 | end 217 | end 218 | 219 | function schema:getType(name) 220 | if not name then return end 221 | return self.typeMap[name] 222 | end 223 | 224 | function schema:getImplementors(interface) 225 | local kind = self:getType(interface) 226 | local isInterface = kind and kind.__type == 'Interface' 227 | return self.interfaceMap[interface] or (isInterface and {} or nil) 228 | end 229 | 230 | function schema:getDirective(name) 231 | if not name then return false end 232 | return self.directiveMap[name] 233 | end 234 | 235 | function schema:getQueryType() 236 | return self.query 237 | end 238 | 239 | function schema:getMutationType() 240 | return self.mutation 241 | end 242 | 243 | function schema:getTypeMap() 244 | return self.typeMap 245 | end 246 | 247 | function schema:getPossibleTypes(abstractType) 248 | if abstractType.__type == 'Union' then 249 | return abstractType.types 250 | end 251 | 252 | return self:getImplementors(abstractType) 253 | end 254 | 255 | return schema 256 | -------------------------------------------------------------------------------- /graphql/types.lua: -------------------------------------------------------------------------------- 1 | local ffi = require('ffi') 2 | local util = require('graphql.util') 3 | local format = string.format 4 | 5 | local function error(...) 6 | return _G.error(..., 0) 7 | end 8 | 9 | local types = {} 10 | 11 | local registered_types = {} 12 | local global_schema = '__global__' 13 | function types.get_env(schema_name) 14 | if schema_name == nil then 15 | schema_name = global_schema 16 | end 17 | 18 | registered_types[schema_name] = registered_types[schema_name] or {} 19 | return registered_types[schema_name] 20 | end 21 | 22 | local function initFields(kind, fields) 23 | assert(type(fields) == 'table', 'fields table must be provided') 24 | 25 | local result = {} 26 | 27 | for fieldName, field in pairs(fields) do 28 | field = field.__type and { kind = field } or field 29 | result[fieldName] = { 30 | name = fieldName, 31 | kind = field.kind, 32 | description = field.description, 33 | deprecationReason = field.deprecationReason, 34 | arguments = field.arguments or {}, 35 | resolve = kind == 'Object' and field.resolve or nil, 36 | } 37 | end 38 | 39 | return result 40 | end 41 | 42 | function types.nonNull(kind) 43 | assert(kind, 'Must provide a type') 44 | 45 | return { 46 | __type = 'NonNull', 47 | ofType = kind, 48 | } 49 | end 50 | 51 | function types.list(kind) 52 | assert(kind, 'Must provide a type') 53 | 54 | local instance = { 55 | __type = 'List', 56 | ofType = kind, 57 | } 58 | 59 | instance.nonNull = types.nonNull(instance) 60 | 61 | return instance 62 | end 63 | 64 | function types.nullable(kind) 65 | assert(type(kind) == 'table', 'kind must be a table, got ' .. type(kind)) 66 | 67 | if kind.__type ~= 'NonNull' then return kind end 68 | 69 | assert(kind.ofType ~= nil, 'kind.ofType must not be nil') 70 | return types.nullable(kind.ofType) 71 | end 72 | 73 | function types.bare(kind) 74 | assert(type(kind) == 'table', 'kind must be a table, got ' .. type(kind)) 75 | 76 | if kind.ofType == nil then return kind end 77 | 78 | assert(kind.ofType ~= nil, 'kind.ofType must not be nil') 79 | return types.bare(kind.ofType) 80 | end 81 | 82 | function types.scalar(config) 83 | assert(type(config.name) == 'string', 'type name must be provided as a string') 84 | assert(type(config.serialize) == 'function', 'serialize must be a function') 85 | assert(type(config.isValueOfTheType) == 'function', 'isValueOfTheType must be a function') 86 | assert(type(config.parseLiteral) == 'function', 'parseLiteral must be a function') 87 | if config.parseValue then 88 | assert(type(config.parseValue) == 'function', 'parseValue must be a function') 89 | end 90 | 91 | local instance = { 92 | __type = 'Scalar', 93 | name = config.name, 94 | description = config.description, 95 | serialize = config.serialize, 96 | parseValue = config.parseValue, 97 | parseLiteral = config.parseLiteral, 98 | isValueOfTheType = config.isValueOfTheType, 99 | specifiedByURL = config.specifiedByURL, 100 | } 101 | 102 | instance.nonNull = types.nonNull(instance) 103 | 104 | return instance 105 | end 106 | 107 | function types.object(config) 108 | assert(type(config.name) == 'string', 'type name must be provided as a string') 109 | if config.isTypeOf then 110 | assert(type(config.isTypeOf) == 'function', 'must provide isTypeOf as a function') 111 | end 112 | 113 | local fields 114 | if type(config.fields) == 'function' then 115 | fields = util.compose(util.bind1(initFields, 'Object'), config.fields) 116 | else 117 | fields = initFields('Object', config.fields) 118 | end 119 | 120 | local instance = { 121 | __type = 'Object', 122 | name = config.name, 123 | description = config.description, 124 | isTypeOf = config.isTypeOf, 125 | fields = fields, 126 | interfaces = config.interfaces, 127 | } 128 | 129 | instance.nonNull = types.nonNull(instance) 130 | 131 | types.get_env(config.schema)[config.name] = instance 132 | 133 | return instance 134 | end 135 | 136 | function types.interface(config) 137 | assert(type(config.name) == 'string', 'type name must be provided as a string') 138 | assert(type(config.fields) == 'table', 'fields table must be provided') 139 | if config.resolveType then 140 | assert(type(config.resolveType) == 'function', 'must provide resolveType as a function') 141 | end 142 | 143 | local fields 144 | if type(config.fields) == 'function' then 145 | fields = util.compose(util.bind1(initFields, 'Interface'), config.fields) 146 | else 147 | fields = initFields('Interface', config.fields) 148 | end 149 | 150 | local instance = { 151 | __type = 'Interface', 152 | name = config.name, 153 | description = config.description, 154 | fields = fields, 155 | resolveType = config.resolveType, 156 | } 157 | 158 | instance.nonNull = types.nonNull(instance) 159 | 160 | types.get_env(config.schema)[config.name] = instance 161 | 162 | return instance 163 | end 164 | 165 | function types.enum(config) 166 | assert(type(config.name) == 'string', 'type name must be provided as a string') 167 | assert(type(config.values) == 'table', 'values table must be provided') 168 | 169 | local instance 170 | local values = {} 171 | 172 | for name, entry in pairs(config.values) do 173 | entry = type(entry) == 'table' and entry or { value = entry } 174 | 175 | values[name] = { 176 | name = name, 177 | description = entry.description, 178 | deprecationReason = entry.deprecationReason, 179 | value = entry.value, 180 | } 181 | end 182 | 183 | instance = { 184 | __type = 'Enum', 185 | name = config.name, 186 | description = config.description, 187 | values = values, 188 | serialize = function(name) 189 | return instance.values[name] and instance.values[name].value or name 190 | end, 191 | } 192 | 193 | instance.nonNull = types.nonNull(instance) 194 | 195 | types.get_env(config.schema)[config.name] = instance 196 | 197 | return instance 198 | end 199 | 200 | function types.union(config) 201 | assert(type(config.name) == 'string', 'type name must be provided as a string') 202 | assert(type(config.types) == 'table', 'types table must be provided') 203 | 204 | local instance = { 205 | __type = 'Union', 206 | name = config.name, 207 | types = config.types, 208 | } 209 | 210 | instance.nonNull = types.nonNull(instance) 211 | 212 | types.get_env(config.schema)[config.name] = instance 213 | 214 | return instance 215 | end 216 | 217 | function types.inputObject(config) 218 | assert(type(config.name) == 'string', 'type name must be provided as a string') 219 | 220 | local fields = {} 221 | for fieldName, field in pairs(config.fields) do 222 | field = field.__type and { kind = field } or field 223 | fields[fieldName] = { 224 | name = fieldName, 225 | kind = field.kind, 226 | description = field.description, 227 | defaultValue = field.defaultValue, 228 | } 229 | end 230 | 231 | local instance = { 232 | __type = 'InputObject', 233 | name = config.name, 234 | description = config.description, 235 | fields = fields, 236 | } 237 | 238 | types.get_env(config.schema)[config.name] = instance 239 | 240 | return instance 241 | end 242 | 243 | -- Based on the code from tarantool/checks. 244 | local function isInt(value) 245 | if type(value) == 'number' then 246 | return value >= -2^31 and value < 2^31 and math.floor(value) == value 247 | end 248 | 249 | if type(value) == 'cdata' then 250 | if ffi.istype('int64_t', value) then 251 | return value >= -2^31 and value < 2^31 252 | elseif ffi.istype('uint64_t', value) then 253 | return value < 2^31 254 | end 255 | end 256 | 257 | return false 258 | end 259 | 260 | local function coerceInt(value) 261 | if value ~= nil then 262 | value = tonumber(value) 263 | if not isInt(value) then return end 264 | end 265 | 266 | return value 267 | end 268 | 269 | types.int = types.scalar({ 270 | name = 'Int', 271 | description = "The `Int` scalar type represents non-fractional signed whole numeric values. " .. 272 | "Int can represent values from -(2^31) to 2^31 - 1, inclusive.", 273 | serialize = coerceInt, 274 | parseLiteral = function(node) 275 | return coerceInt(node.value) 276 | end, 277 | isValueOfTheType = isInt, 278 | }) 279 | 280 | -- The code from tarantool/checks. 281 | local function isLong(value) 282 | if type(value) == 'number' then 283 | -- Double floating point format has 52 fraction bits. If we want to keep 284 | -- integer precision, the number must be less than 2^53. 285 | return value > -2^53 and value < 2^53 and math.floor(value) == value 286 | end 287 | 288 | if type(value) == 'cdata' then 289 | return ffi.istype('int64_t', value) or ffi.istype('uint64_t', value) 290 | end 291 | 292 | return false 293 | end 294 | 295 | local function coerceLong(value) 296 | if value ~= nil then 297 | value = tonumber64(value) 298 | end 299 | return value 300 | end 301 | 302 | types.long = types.scalar({ 303 | name = 'Long', 304 | description = "The `Long` scalar type represents non-fractional signed whole numeric values. " .. 305 | "Long can represent values from -(2^52) to 2^52 - 1, inclusive.", 306 | specifiedByURL = 'https://github.com/tarantool/graphql/wiki/Long', 307 | serialize = coerceLong, 308 | parseLiteral = function(node) 309 | return coerceLong(node.value) 310 | end, 311 | isValueOfTheType = isLong, 312 | }) 313 | 314 | -- Return `false` for NaN, Negative Infinity or Positive Infinity. 315 | -- Return `true` otherwise. 316 | local function isfinite(n) 317 | local d = n - n 318 | return n == n and d == d 319 | end 320 | 321 | local function isFloat(value) 322 | -- http://spec.graphql.org/October2021/#sec-Float 323 | -- 324 | -- > Non-finite floating-point internal values (NaN and Infinity) cannot be 325 | -- > coerced to Float and must raise a field error. 326 | if type(value) == 'number' then 327 | return isfinite(value) 328 | end 329 | 330 | if type(value) == 'cdata' then 331 | return ffi.istype('int64_t', value) or ffi.istype('uint64_t', value) 332 | end 333 | 334 | return false 335 | end 336 | 337 | local function coerceFloat(value) 338 | if value ~= nil then 339 | value = tonumber(value) 340 | if not isFloat(value) then return end 341 | end 342 | 343 | return value 344 | end 345 | 346 | types.float = types.scalar({ 347 | name = 'Float', 348 | description = "The `Float` scalar type represents signed double-".. 349 | "precision fractional values as specified by IEEE 754.", 350 | serialize = coerceFloat, 351 | parseLiteral = function(node) 352 | return coerceFloat(node.value) 353 | end, 354 | isValueOfTheType = isFloat, 355 | }) 356 | 357 | local function isString(value) 358 | return type(value) == 'string' 359 | end 360 | 361 | local function coerceString(value) 362 | if value ~= nil then 363 | value = tostring(value) 364 | if not isString(value) then return end 365 | end 366 | 367 | return value 368 | end 369 | 370 | types.string = types.scalar({ 371 | name = 'String', 372 | description = "The `String` scalar type represents textual data, represented as UTF-8 character sequences. " .. 373 | "The String type is most often used by GraphQL to represent free-form human-readable text.", 374 | serialize = coerceString, 375 | parseLiteral = function(node) 376 | return coerceString(node.value) 377 | end, 378 | isValueOfTheType = isString, 379 | }) 380 | 381 | local function toboolean(x) 382 | return (x and x ~= 'false') and true or false 383 | end 384 | 385 | local function isBoolean(value) 386 | return type(value) == 'boolean' 387 | end 388 | 389 | local function coerceBoolean(value) 390 | if value ~= nil then 391 | value = toboolean(value) 392 | if not isBoolean(value) then return end 393 | end 394 | 395 | return value 396 | end 397 | 398 | types.boolean = types.scalar({ 399 | name = 'Boolean', 400 | description = "The `Boolean` scalar type represents `true` or `false`.", 401 | serialize = coerceBoolean, 402 | parseLiteral = function(node) 403 | if node.kind ~= 'boolean' then 404 | error(('Could not coerce value "%s" with type "%s" to type boolean'):format(node.value, node.kind)) 405 | end 406 | return coerceBoolean(node.value) 407 | end, 408 | isValueOfTheType = isBoolean, 409 | }) 410 | 411 | --[[ 412 | The ID scalar type represents a unique identifier, 413 | often used to refetch an object or as the key for a cache. 414 | The ID type is serialized in the same way as a String; 415 | however, defining it as an ID signifies that it is not intended to be human‐readable. 416 | --]] 417 | types.id = types.scalar({ 418 | name = 'ID', 419 | serialize = coerceString, 420 | parseLiteral = function(node) 421 | return coerceString(node.value) 422 | end, 423 | isValueOfTheType = isString, 424 | }) 425 | 426 | function types.directive(config) 427 | assert(type(config.name) == 'string', 'type name must be provided as a string') 428 | 429 | local instance = { 430 | __type = 'Directive', 431 | name = config.name, 432 | description = config.description, 433 | arguments = config.arguments, 434 | onQuery = config.onQuery, 435 | onMutation = config.onMutation, 436 | onField = config.onField, 437 | onFragmentDefinition = config.onFragmentDefinition, 438 | onFragmentSpread = config.onFragmentSpread, 439 | onInlineFragment = config.onInlineFragment, 440 | onVariableDefinition = config.onVariableDefinition, 441 | onSchema = config.onSchema, 442 | onScalar = config.onScalar, 443 | onObject = config.onObject, 444 | onFieldDefinition = config.onFieldDefinition, 445 | onArgumentDefinition = config.onArgumentDefinition, 446 | onInterface = config.onInterface, 447 | onUnion = config.onUnion, 448 | onEnum = config.onEnum, 449 | onEnumValue = config.onEnumValue, 450 | onInputObject = config.onInputObject, 451 | onInputFieldDefinition = config.onInputFieldDefinition, 452 | isRepeatable = config.isRepeatable or false, 453 | } 454 | 455 | return instance 456 | end 457 | 458 | types.include = types.directive({ 459 | name = 'include', 460 | description = 'Directs the executor to include this field or fragment only when the `if` argument is true.', 461 | arguments = { 462 | ['if'] = { kind = types.boolean.nonNull, description = 'Included when true.'}, 463 | }, 464 | onField = true, 465 | onFragmentSpread = true, 466 | onInlineFragment = true, 467 | }) 468 | 469 | types.skip = types.directive({ 470 | name = 'skip', 471 | description = 'Directs the executor to skip this field or fragment when the `if` argument is true.', 472 | arguments = { 473 | ['if'] = { kind = types.boolean.nonNull, description = 'Skipped when true.' }, 474 | }, 475 | onField = true, 476 | onFragmentSpread = true, 477 | onInlineFragment = true, 478 | }) 479 | 480 | types.specifiedBy = types.directive({ 481 | name = 'specifiedBy', 482 | description = 'Custom scalar specification.', 483 | arguments = { 484 | ['url'] = { kind = types.string.nonNull, description = 'Scalar specification URL.', } 485 | }, 486 | onScalar = true, 487 | }) 488 | 489 | types.resolve = function(type_name_or_obj, schema) 490 | if type(type_name_or_obj) == 'table' then 491 | return type_name_or_obj 492 | end 493 | 494 | if type(type_name_or_obj) ~= 'string' then 495 | error('types.resolve() expects type to be string or table') 496 | end 497 | 498 | local type_obj = types.get_env(schema)[type_name_or_obj] 499 | 500 | if type_obj == nil then 501 | error(format("No type found named '%s'", type_name_or_obj)) 502 | end 503 | 504 | return type_obj 505 | end 506 | 507 | return types 508 | -------------------------------------------------------------------------------- /graphql/util.lua: -------------------------------------------------------------------------------- 1 | local ffi = require('ffi') 2 | local yaml = require('yaml').new({encode_use_tostring = true}) 3 | 4 | local function error(...) 5 | return _G.error(..., 0) 6 | end 7 | 8 | local function map(t, fn) 9 | local res = {} 10 | for k, v in pairs(t) do res[k] = fn(v, k) end 11 | return res 12 | end 13 | 14 | local function map_name(t, fn) 15 | local res = {} 16 | for _, v in pairs(t or {}) do 17 | if v.name then 18 | res[v.name] = fn(v, v.name) 19 | end 20 | end 21 | return res 22 | end 23 | 24 | local function find(t, fn) 25 | for k, v in pairs(t) do 26 | if fn(v, k) then return v end 27 | end 28 | end 29 | 30 | local function find_by_name(t, name) 31 | for _, v in pairs(t or {}) do 32 | if v.name == name then return v end 33 | end 34 | return nil 35 | end 36 | 37 | local function filter(t, fn) 38 | local res = {} 39 | for _,v in pairs(t) do 40 | if fn(v) then 41 | table.insert(res, v) 42 | end 43 | end 44 | return res 45 | end 46 | 47 | local function values(t) 48 | local res = {} 49 | for _, value in pairs(t) do 50 | table.insert(res, value) 51 | end 52 | return res 53 | end 54 | 55 | local function compose(f, g) 56 | return function(...) return f(g(...)) end 57 | end 58 | 59 | local function bind1(func, x) 60 | return function(y) 61 | return func(x, y) 62 | end 63 | end 64 | 65 | local function trim(s) 66 | return s:gsub('^%s+', ''):gsub('%s+$', ''):gsub('%s%s+', ' ') 67 | end 68 | 69 | local function getTypeName(t) 70 | if t.name ~= nil then 71 | return t.name 72 | elseif t.__type == 'NonNull' then 73 | return ('NonNull(%s)'):format(getTypeName(t.ofType)) 74 | elseif t.__type == 'List' then 75 | return ('List(%s)'):format(getTypeName(t.ofType)) 76 | end 77 | 78 | local err = ('Internal error: unknown type:\n%s'):format(yaml.encode(t)) 79 | error(err) 80 | end 81 | 82 | local function coerceValue(node, schemaType, variables, opts) 83 | variables = variables or {} 84 | opts = opts or {} 85 | local strict_non_null = opts.strict_non_null or false 86 | local skip_variables = opts.skip_variables or false 87 | local defaultValues = opts.defaultValues or {} 88 | 89 | if node and node.kind == 'variable' and skip_variables then 90 | return nil 91 | end 92 | 93 | if schemaType.__type == 'NonNull' then 94 | local res = coerceValue(node, schemaType.ofType, variables, opts) 95 | if strict_non_null and res == nil then 96 | error(('Expected non-null for "%s", got null'):format(getTypeName(schemaType))) 97 | end 98 | return res 99 | end 100 | 101 | if not node then 102 | return nil 103 | end 104 | 105 | -- handle precompiled values 106 | if node.compiled ~= nil then 107 | return node.compiled 108 | end 109 | 110 | if node.kind == 'variable' then 111 | local value = variables[node.name.value] 112 | local defaultValue = defaultValues[node.name.value] 113 | if type(value) == 'nil' and type(defaultValue) ~= 'nil' then 114 | -- default value was parsed by parseLiteral 115 | value = defaultValue 116 | elseif schemaType.parseValue ~= nil then 117 | value = schemaType.parseValue(value) 118 | if strict_non_null and type(value) == 'nil' then 119 | error(('Could not coerce variable "%s" with value "%s" to type "%s"'):format( 120 | node.name.value, variables[node.name.value], schemaType.name 121 | )) 122 | end 123 | elseif type(value) == 'table' then 124 | if schemaType.__type == 'List' and type(schemaType.ofType) == 'table' and schemaType.ofType.parseValue ~= nil then 125 | for k, v in ipairs(value) do 126 | value[k] = schemaType.ofType.parseValue(v) 127 | end 128 | end 129 | 130 | if schemaType.__type == 'InputObject' then 131 | for k, v in pairs(value) do 132 | if schemaType.fields[k] ~= nil and type(schemaType.fields[k].kind) == 'table' and 133 | schemaType.fields[k].kind.parseValue ~= nil then 134 | value[k] = schemaType.fields[k].kind.parseValue(v) 135 | end 136 | end 137 | end 138 | end 139 | return value 140 | end 141 | 142 | if node.kind == 'null' then 143 | return box.NULL 144 | end 145 | 146 | if schemaType.__type == 'List' then 147 | if node.kind ~= 'list' then 148 | error('Expected a list') 149 | end 150 | 151 | return map(node.values, function(value) 152 | return coerceValue(value, schemaType.ofType, variables, opts) 153 | end) 154 | end 155 | 156 | local isInputObject = schemaType.__type == 'InputObject' 157 | if isInputObject then 158 | if node.kind ~= 'inputObject' then 159 | error('Expected an input object') 160 | end 161 | 162 | -- check all fields: as from value as well as from schema 163 | local fieldNameSet = {} 164 | local fieldValues = {} 165 | for _, field in ipairs(node.values) do 166 | fieldNameSet[field.name] = true 167 | fieldValues[field.name] = field.value 168 | end 169 | for fieldName, _ in pairs(schemaType.fields) do 170 | fieldNameSet[fieldName] = true 171 | end 172 | 173 | local inputObjectValue = {} 174 | for fieldName, _ in pairs(fieldNameSet) do 175 | if not schemaType.fields[fieldName] then 176 | error(('Unknown input object field "%s"'):format(fieldName)) 177 | end 178 | 179 | local childValue = fieldValues[fieldName] 180 | local childType = schemaType.fields[fieldName].kind 181 | inputObjectValue[fieldName] = coerceValue(childValue, childType, 182 | variables, opts) 183 | end 184 | 185 | return inputObjectValue 186 | end 187 | 188 | if schemaType.__type == 'Enum' then 189 | if node.kind ~= 'enum' then 190 | error(('Expected enum value, got %s'):format(node.kind)) 191 | end 192 | 193 | if not schemaType.values[node.value] then 194 | error(('Invalid enum value "%s"'):format(node.value)) 195 | end 196 | 197 | return node.value 198 | end 199 | 200 | if schemaType.__type == 'Scalar' then 201 | local value = schemaType.parseLiteral(node) 202 | if strict_non_null and type(value) == 'nil' then 203 | error(('Could not coerce value "%s" to type "%s"'):format(node.value or node.kind, schemaType.name)) 204 | end 205 | return value 206 | end 207 | end 208 | 209 | --- Check whether passed value has one of listed types. 210 | --- 211 | --- @param obj value to check 212 | --- 213 | --- @tparam string obj_name name of the value to form an error 214 | --- 215 | --- @tparam string type_1 216 | --- @tparam[opt] string type_2 217 | --- @tparam[opt] string type_3 218 | --- 219 | --- @return nothing 220 | local function check(obj, obj_name, type_1, type_2, type_3) 221 | if type(obj) == type_1 or type(obj) == type_2 or type(obj) == type_3 then 222 | return 223 | end 224 | 225 | if type_3 ~= nil then 226 | error(('%s must be a %s or a % or a %s, got %s'):format(obj_name, 227 | type_1, type_2, type_3, type(obj))) 228 | elseif type_2 ~= nil then 229 | error(('%s must be a %s or a %s, got %s'):format(obj_name, type_1, 230 | type_2, type(obj))) 231 | else 232 | error(('%s must be a %s, got %s'):format(obj_name, type_1, type(obj))) 233 | end 234 | end 235 | 236 | local function is_array(t) 237 | if type(t) ~= 'table' then 238 | return false 239 | end 240 | local n = #t 241 | if n > 0 then 242 | for k in next, t, n do 243 | if type(k) ~= 'number' or k < 0 then return false end 244 | end 245 | return true 246 | end 247 | for k in pairs(t) do 248 | if type(k) ~= 'number' or k < 0 then 249 | return false 250 | end 251 | end 252 | return true 253 | end 254 | 255 | -- Copied from tarantool/tap 256 | local function cmpdeeply(got, expected) 257 | if type(expected) == "number" or type(got) == "number" then 258 | if got ~= got and expected ~= expected then 259 | return true -- nan 260 | end 261 | return got == expected 262 | end 263 | 264 | if ffi.istype('bool', got) then got = (got == 1) end 265 | if ffi.istype('bool', expected) then expected = (expected == 1) end 266 | 267 | if type(got) ~= type(expected) then 268 | return false 269 | end 270 | 271 | if type(got) ~= 'table' or type(expected) ~= 'table' then 272 | return got == expected 273 | end 274 | 275 | local visited_keys = {} 276 | 277 | for i, v in pairs(got) do 278 | visited_keys[i] = true 279 | if not cmpdeeply(v, expected[i]) then 280 | return false 281 | end 282 | end 283 | 284 | -- check if expected contains more keys then got 285 | for i in pairs(expected) do 286 | if visited_keys[i] ~= true then 287 | return false 288 | end 289 | end 290 | 291 | return true 292 | end 293 | 294 | return { 295 | map = map, 296 | map_name = map_name, 297 | find = find, 298 | find_by_name = find_by_name, 299 | filter = filter, 300 | values = values, 301 | compose = compose, 302 | bind1 = bind1, 303 | trim = trim, 304 | getTypeName = getTypeName, 305 | coerceValue = coerceValue, 306 | 307 | is_array = is_array, 308 | check = check, 309 | cmpdeeply = cmpdeeply, 310 | } 311 | -------------------------------------------------------------------------------- /graphql/validate.lua: -------------------------------------------------------------------------------- 1 | local introspection = require('graphql.introspection') 2 | local rules = require('graphql.rules') 3 | local util = require('graphql.util') 4 | 5 | local function getParentField(context, name, count) 6 | if introspection.fieldMap[name] then return introspection.fieldMap[name] end 7 | 8 | count = count or 1 9 | local parent = context.objects[#context.objects - count] 10 | 11 | -- Unwrap lists and non-null types 12 | while parent.ofType do 13 | parent = parent.ofType 14 | end 15 | 16 | return parent.fields and parent.fields[name] 17 | end 18 | 19 | local visitors = { 20 | document = { 21 | enter = function(node, context) 22 | for _, definition in ipairs(node.definitions) do 23 | if definition.kind == 'fragmentDefinition' then 24 | context.fragmentMap[definition.name.value] = definition 25 | end 26 | end 27 | end, 28 | 29 | children = function(node, _) 30 | return node.definitions 31 | end, 32 | 33 | rules = { rules.uniqueFragmentNames, exit = { rules.noUnusedFragments } }, 34 | }, 35 | 36 | operation = { 37 | enter = function(node, context) 38 | table.insert(context.objects, context.schema[node.operation]) 39 | context.currentOperation = node 40 | context.variableReferences = {} 41 | end, 42 | 43 | exit = function(_, context) 44 | table.remove(context.objects) 45 | context.currentOperation = nil 46 | context.variableReferences = nil 47 | end, 48 | 49 | children = function(node) 50 | return { node.selectionSet } 51 | end, 52 | 53 | rules = { 54 | rules.uniqueOperationNames, 55 | rules.loneAnonymousOperation, 56 | rules.directivesAreDefined, 57 | rules.variablesHaveCorrectType, 58 | rules.variableDefaultValuesHaveCorrectType, 59 | exit = { 60 | rules.variablesAreUsed, 61 | rules.variablesAreDefined, 62 | }, 63 | }, 64 | }, 65 | 66 | selectionSet = { 67 | children = function(node) 68 | return node.selections 69 | end, 70 | 71 | rules = { rules.unambiguousSelections }, 72 | }, 73 | 74 | field = { 75 | enter = function(node, context) 76 | local name = node.name.value 77 | 78 | if introspection.fieldMap[name] then 79 | table.insert(context.objects, introspection.fieldMap[name].kind) 80 | else 81 | local parentField = getParentField(context, name, 0) 82 | -- false is a special value indicating that the field was not present in the type definition. 83 | table.insert(context.objects, parentField and parentField.kind or false) 84 | end 85 | end, 86 | 87 | exit = function(_, context) 88 | table.remove(context.objects) 89 | end, 90 | 91 | children = function(node) 92 | local children = {} 93 | 94 | if node.arguments then 95 | for i = 1, #node.arguments do 96 | table.insert(children, node.arguments[i]) 97 | end 98 | end 99 | 100 | if node.directives then 101 | for i = 1, #node.directives do 102 | table.insert(children, node.directives[i]) 103 | end 104 | end 105 | 106 | if node.selectionSet then 107 | table.insert(children, node.selectionSet) 108 | end 109 | 110 | return children 111 | end, 112 | 113 | rules = { 114 | rules.fieldsDefinedOnType, 115 | rules.argumentsDefinedOnType, 116 | rules.scalarFieldsAreLeaves, 117 | rules.compositeFieldsAreNotLeaves, 118 | rules.uniqueArgumentNames, 119 | rules.argumentsOfCorrectType, 120 | rules.requiredArgumentsPresent, 121 | rules.directivesAreDefined, 122 | rules.variableUsageAllowed, 123 | }, 124 | }, 125 | 126 | inlineFragment = { 127 | enter = function(node, context) 128 | local kind = false 129 | 130 | if node.typeCondition then 131 | kind = context.schema:getType(node.typeCondition.name.value) or false 132 | end 133 | 134 | table.insert(context.objects, kind) 135 | end, 136 | 137 | exit = function(_, context) 138 | table.remove(context.objects) 139 | end, 140 | 141 | children = function(node, _) 142 | if node.selectionSet then 143 | return {node.selectionSet} 144 | end 145 | end, 146 | 147 | rules = { 148 | rules.fragmentHasValidType, 149 | rules.fragmentSpreadIsPossible, 150 | rules.directivesAreDefined, 151 | }, 152 | }, 153 | 154 | fragmentSpread = { 155 | enter = function(node, context) 156 | context.usedFragments[node.name.value] = true 157 | 158 | local fragment = context.fragmentMap[node.name.value] 159 | 160 | if not fragment then return end 161 | 162 | local fragmentType = context.schema:getType(fragment.typeCondition.name.value) or false 163 | 164 | table.insert(context.objects, fragmentType) 165 | 166 | if context.currentOperation then 167 | local seen = {} 168 | local function collectTransitiveVariables(referencedNode) 169 | if not referencedNode then return end 170 | 171 | if referencedNode.kind == 'selectionSet' then 172 | for _, selection in ipairs(referencedNode.selections) do 173 | if not seen[selection] then 174 | seen[selection] = true 175 | collectTransitiveVariables(selection) 176 | end 177 | end 178 | elseif referencedNode.kind == 'field' then 179 | if referencedNode.arguments then 180 | for _, argument in ipairs(referencedNode.arguments) do 181 | collectTransitiveVariables(argument) 182 | end 183 | end 184 | 185 | if referencedNode.selectionSet then 186 | collectTransitiveVariables(referencedNode.selectionSet) 187 | end 188 | elseif referencedNode.kind == 'argument' then 189 | return collectTransitiveVariables(referencedNode.value) 190 | elseif referencedNode.kind == 'listType' or referencedNode.kind == 'nonNullType' then 191 | return collectTransitiveVariables(referencedNode.type) 192 | elseif referencedNode.kind == 'variable' then 193 | context.variableReferences[referencedNode.name.value] = true 194 | elseif referencedNode.kind == 'inlineFragment' then 195 | return collectTransitiveVariables(referencedNode.selectionSet) 196 | elseif referencedNode.kind == 'fragmentSpread' then 197 | local fragment = context.fragmentMap[referencedNode.name.value] 198 | context.usedFragments[referencedNode.name.value] = true 199 | return fragment and collectTransitiveVariables(fragment.selectionSet) 200 | end 201 | end 202 | 203 | collectTransitiveVariables(fragment.selectionSet) 204 | end 205 | end, 206 | 207 | exit = function(_, context) 208 | table.remove(context.objects) 209 | end, 210 | 211 | rules = { 212 | rules.fragmentSpreadTargetDefined, 213 | rules.fragmentSpreadIsPossible, 214 | rules.directivesAreDefined, 215 | rules.variableUsageAllowed, 216 | }, 217 | }, 218 | 219 | fragmentDefinition = { 220 | enter = function(node, context) 221 | local kind = context.schema:getType(node.typeCondition.name.value) or false 222 | table.insert(context.objects, kind) 223 | end, 224 | 225 | exit = function(_, context) 226 | table.remove(context.objects) 227 | end, 228 | 229 | children = function(node) 230 | local children = {} 231 | 232 | for _, selection in ipairs(node.selectionSet) do 233 | table.insert(children, selection) 234 | end 235 | 236 | return children 237 | end, 238 | 239 | rules = { 240 | rules.fragmentHasValidType, 241 | rules.fragmentDefinitionHasNoCycles, 242 | rules.directivesAreDefined, 243 | }, 244 | }, 245 | 246 | argument = { 247 | enter = function(node, context) 248 | if context.currentOperation then 249 | local value = node.value 250 | while value.kind == 'listType' or value.kind == 'nonNullType' do 251 | value = value.type 252 | end 253 | 254 | if value.kind == 'variable' then 255 | context.variableReferences[value.name.value] = true 256 | end 257 | end 258 | end, 259 | 260 | children = function(node) 261 | return util.map(node.value.values or {}, function(value) 262 | if value.value ~= nil then 263 | return value.value 264 | end 265 | return value 266 | end) 267 | end, 268 | 269 | rules = { rules.uniqueInputObjectFields }, 270 | }, 271 | 272 | inputObject = { 273 | children = function(node) 274 | return util.map(node.values or {}, function(value) 275 | return value.value 276 | end) 277 | end, 278 | 279 | rules = { rules.uniqueInputObjectFields }, 280 | }, 281 | 282 | list = { 283 | children = function(node) 284 | return node.values 285 | end, 286 | }, 287 | 288 | variable = { 289 | enter = function(node, context) 290 | context.variableReferences[node.name.value] = true 291 | end, 292 | }, 293 | 294 | directive = { 295 | children = function(node, _) 296 | return node.arguments 297 | end, 298 | }, 299 | } 300 | 301 | local function validate(schema, tree) 302 | local context = { 303 | schema = schema, 304 | fragmentMap = {}, 305 | operationNames = {}, 306 | hasAnonymousOperation = false, 307 | usedFragments = {}, 308 | objects = {}, 309 | currentOperation = nil, 310 | variableReferences = nil, 311 | } 312 | 313 | local function visit(node) 314 | local visitor = node.kind and visitors[node.kind] 315 | 316 | if not visitor then return end 317 | 318 | if visitor.enter then 319 | visitor.enter(node, context) 320 | end 321 | 322 | if visitor.rules then 323 | for i = 1, #visitor.rules do 324 | visitor.rules[i](node, context) 325 | end 326 | end 327 | 328 | if visitor.children then 329 | local children = visitor.children(node) 330 | if children then 331 | for _, child in ipairs(children) do 332 | visit(child) 333 | end 334 | end 335 | end 336 | 337 | if visitor.rules and visitor.rules.exit then 338 | for i = 1, #visitor.rules.exit do 339 | visitor.rules.exit[i](node, context) 340 | end 341 | end 342 | 343 | if visitor.exit then 344 | visitor.exit(node, context) 345 | end 346 | end 347 | 348 | return visit(tree) 349 | end 350 | 351 | return { 352 | validate = validate, 353 | } 354 | -------------------------------------------------------------------------------- /graphql/validate_variables.lua: -------------------------------------------------------------------------------- 1 | local types = require('graphql.types') 2 | local util = require('graphql.util') 3 | local check = util.check 4 | 5 | local function error(...) 6 | return _G.error(..., 0) 7 | end 8 | 9 | -- Traverse type more or less likewise util.coerceValue do. 10 | local function checkVariableValue(variableName, value, variableType, isNonNullDefaultDefined) 11 | check(variableName, 'variableName', 'string') 12 | check(variableType, 'variableType', 'table') 13 | 14 | local isNonNull = variableType.__type == 'NonNull' 15 | isNonNullDefaultDefined = isNonNullDefaultDefined or false 16 | 17 | if isNonNull then 18 | variableType = types.nullable(variableType) 19 | if (type(value) == 'cdata' and value == nil) or 20 | (type(value) == 'nil' and not isNonNullDefaultDefined) then 21 | error(('Variable %q expected to be non-null'):format(variableName)) 22 | end 23 | end 24 | 25 | local isList = variableType.__type == 'List' 26 | local isScalar = variableType.__type == 'Scalar' 27 | local isInputObject = variableType.__type == 'InputObject' 28 | local isEnum = variableType.__type == 'Enum' 29 | 30 | -- Nullable variable type + null value case: value can be nil only when 31 | -- isNonNull is false. 32 | if value == nil then return end 33 | 34 | if isList then 35 | if type(value) ~= 'table' then 36 | error(('Variable %q for a List must be a Lua ' .. 37 | 'table, got %s'):format(variableName, type(value))) 38 | end 39 | if not util.is_array(value) then 40 | error(('Variable %q for a List must be an array, ' .. 41 | 'got map'):format(variableName)) 42 | end 43 | assert(variableType.ofType ~= nil, 'variableType.ofType must not be nil') 44 | for i, item in ipairs(value) do 45 | local itemName = variableName .. '[' .. tostring(i) .. ']' 46 | checkVariableValue(itemName, item, variableType.ofType) 47 | end 48 | return 49 | end 50 | 51 | if isInputObject then 52 | if type(value) ~= 'table' then 53 | error(('Variable %q for the InputObject %q must ' .. 54 | 'be a Lua table, got %s'):format(variableName, variableType.name, 55 | type(value))) 56 | end 57 | 58 | -- check all fields: as from value as well as from schema 59 | local fieldNameSet = {} 60 | for fieldName, _ in pairs(value) do 61 | fieldNameSet[fieldName] = true 62 | end 63 | for fieldName, _ in pairs(variableType.fields) do 64 | fieldNameSet[fieldName] = true 65 | end 66 | 67 | for fieldName, _ in pairs(fieldNameSet) do 68 | local fieldValue = value[fieldName] 69 | if type(fieldName) ~= 'string' then 70 | error(('Field key of the variable %q for the ' .. 71 | 'InputObject %q must be a string, got %s'):format(variableName, 72 | variableType.name, type(fieldName))) 73 | end 74 | if type(variableType.fields[fieldName]) == 'nil' then 75 | error(('Unknown field %q of the variable %q ' .. 76 | 'for the InputObject %q'):format(fieldName, variableName, 77 | variableType.name)) 78 | end 79 | 80 | local childType = variableType.fields[fieldName].kind 81 | local childName = variableName .. '.' .. fieldName 82 | checkVariableValue(childName, fieldValue, childType) 83 | end 84 | 85 | return 86 | end 87 | 88 | if isEnum then 89 | for _, item in pairs(variableType.values) do 90 | if util.cmpdeeply(item.value, value) then 91 | return 92 | end 93 | end 94 | error(('Wrong variable %q for the Enum "%s" with value %q'):format( 95 | variableName, variableType.name, value)) 96 | end 97 | 98 | if isScalar then 99 | check(variableType.isValueOfTheType, 'isValueOfTheType', 'function') 100 | if not variableType.isValueOfTheType(value) then 101 | error(('Wrong variable %q for the Scalar %q'):format( 102 | variableName, variableType.name)) 103 | end 104 | return 105 | end 106 | 107 | error(('Unknown type of the variable %q'):format(variableName)) 108 | end 109 | 110 | local function validate_variables(context) 111 | -- check that all variable values have corresponding variable declaration 112 | for variableName, _ in pairs(context.variables or {}) do 113 | if context.variableTypes[variableName] == nil then 114 | error(('There is no declaration for the variable %q') 115 | :format(variableName)) 116 | end 117 | end 118 | 119 | -- check that variable values have correct type 120 | for variableName, variableType in pairs(context.variableTypes) do 121 | -- Check if default value presents. 122 | local isNonNullDefaultDefined = false 123 | for _, variableDefinition in ipairs(context.operation.variableDefinitions) do 124 | if variableDefinition.variable.name.value == variableName and 125 | variableDefinition.defaultValue ~= nil then 126 | if (variableDefinition.defaultValue.value ~= nil) or (variableDefinition.defaultValue.values ~= nil) then 127 | isNonNullDefaultDefined = true 128 | end 129 | end 130 | end 131 | 132 | local value = (context.variables or {})[variableName] 133 | checkVariableValue(variableName, value, variableType, isNonNullDefaultDefined) 134 | end 135 | end 136 | 137 | return { 138 | validate_variables = validate_variables, 139 | } 140 | -------------------------------------------------------------------------------- /graphql/version.lua: -------------------------------------------------------------------------------- 1 | -- Сontains the module version. 2 | -- Requires manual update in case of release commit. 3 | 4 | return '0.3.0' 5 | -------------------------------------------------------------------------------- /test/helpers.lua: -------------------------------------------------------------------------------- 1 | local clock = require('clock') 2 | local fiber = require('fiber') 3 | local log = require('log') 4 | 5 | local types = require('graphql.types') 6 | local schema = require('graphql.schema') 7 | local parse = require('graphql.parse') 8 | local validate = require('graphql.validate') 9 | local execute = require('graphql.execute') 10 | 11 | local helpers = {} 12 | 13 | helpers.test_schema_name = 'default' 14 | 15 | function helpers.check_request(query, query_schema, mutation_schema, directives, opts) 16 | opts = opts or {} 17 | local root = { 18 | query = types.object({ 19 | name = 'Query', 20 | fields = query_schema or {}, 21 | }), 22 | mutation = types.object({ 23 | name = 'Mutation', 24 | fields = mutation_schema or {}, 25 | }), 26 | directives = directives, 27 | } 28 | 29 | local compiled_schema = schema.create(root, helpers.test_schema_name, opts) 30 | 31 | local parsed = parse.parse(query) 32 | 33 | validate.validate(compiled_schema, parsed) 34 | 35 | local rootValue = {} 36 | local variables = opts.variables or {} 37 | return execute.execute(compiled_schema, parsed, rootValue, variables) 38 | end 39 | 40 | -- Based on https://github.com/tarantool/crud/blob/5717e87e1f8a6fb852c26181524fafdbc7a472d8/test/helper.lua#L533-L544 41 | function helpers.fflush_main_server_output(server, capture) 42 | -- Sometimes we have a delay here. This hack helps to wait for the end of 43 | -- the output. It shouldn't take much time. 44 | local helper_msg = "metrics fflush message" 45 | if server then 46 | server.net_box:eval([[ 47 | require('log').error(...) 48 | ]], {helper_msg}) 49 | else 50 | log.error(helper_msg) 51 | end 52 | 53 | local max_wait_timeout = 10 54 | local start_time = clock.monotonic() 55 | 56 | local captured = "" 57 | while (not string.find(captured, helper_msg, 1, true)) 58 | and (clock.monotonic() - start_time < max_wait_timeout) do 59 | local captured_part = capture:flush() 60 | captured = captured .. (captured_part.stdout or "") .. (captured_part.stderr or "") 61 | fiber.yield() 62 | end 63 | return captured 64 | end 65 | 66 | return helpers 67 | -------------------------------------------------------------------------------- /test/integration/codegen/fuzzing_nullability/README.md: -------------------------------------------------------------------------------- 1 | To install dependencies, run 2 | ```bash 3 | npm install 4 | ``` 5 | 6 | To generate test file, run 7 | ```bash 8 | node generate.js > ../../fuzzing_nullability_test.lua 9 | ``` 10 | in this directory. 11 | -------------------------------------------------------------------------------- /test/integration/codegen/fuzzing_nullability/generate.js: -------------------------------------------------------------------------------- 1 | const { graphql, buildSchema } = require('graphql'); 2 | 3 | const nil = 'nil' 4 | const box = {NULL: 'box.NULL'} 5 | 6 | const Nullable = 'Nullable' 7 | const NonNullable = 'NonNullable' 8 | 9 | const graphql_types = { 10 | "boolean_true": { 11 | "var_type": 'Boolean', 12 | "value": true, 13 | "default": false, 14 | }, 15 | "boolean_false": { 16 | "var_type": 'Boolean', 17 | "value": false, 18 | "default": true, 19 | }, 20 | "float": { 21 | "var_type": 'Float', 22 | "value": 1.1111111, 23 | "default": 0, 24 | }, 25 | "int": { 26 | "var_type": 'Int', 27 | "value": 2**30, 28 | "default": 0, 29 | }, 30 | "id": { 31 | "var_type": 'ID', 32 | "value": '00000000-0000-0000-0000-000000000000', 33 | "default": '11111111-1111-1111-1111-111111111111', 34 | }, 35 | "enum": { 36 | "graphql_type": ` 37 | enum MyEnum { 38 | a 39 | b 40 | } 41 | `, 42 | "var_type": 'MyEnum', 43 | "value": `b`, 44 | "default": `a`, 45 | }, 46 | } 47 | 48 | const Lua_to_JS_error = [ 49 | { 50 | "regex": /^"Expected value of type \\\"(?[a-zA-Z]+)\!\\\", found null\."$/, 51 | "return": function(groups) { 52 | return `"Expected non-null for \\\"NonNull(${groups.type})\\\", got null"` 53 | } 54 | }, 55 | { 56 | "regex": /^"Expected value of type \\\"\[(?[a-zA-Z]+)\]\!\\\", found null\."$/, 57 | "return": function(groups) { 58 | return `"Expected non-null for \\\"NonNull(List(${groups.type}))\\\", got null"` 59 | } 60 | }, 61 | { 62 | "regex": /^"Expected value of type \\\"\[(?[a-zA-Z]+)\!\]\\\", found null\."$/, 63 | "return": function(groups) { 64 | return `"Expected non-null for \\\"List(NonNull(${groups.type}))\\\", got null"` 65 | } 66 | }, 67 | { 68 | "regex": /^"Expected value of type \\\"\[(?[a-zA-Z]+)\!\]\!\\\", found null\."$/, 69 | "return": function(groups) { 70 | return `"Expected non-null for \\\"NonNull(List(NonNull(${groups.type})))\\\", got null"` 71 | } 72 | }, 73 | { 74 | "regex": /^"Variable \\\"\$var1\\\" of required type \\\"(?[a-zA-Z]+)\!\\\" was not provided\."$/, 75 | "return": function(groups) { 76 | return `"Variable \\\"var1\\\" expected to be non-null"` 77 | } 78 | }, 79 | { 80 | "regex": /^"Variable \\\"\$var1\\\" of non-null type \\\"(?[a-zA-Z]+)\!\\\" must not be null\."$/, 81 | "return": function(groups) { 82 | return `"Variable \\\"var1\\\" expected to be non-null"` 83 | } 84 | }, 85 | { 86 | "regex": /^"Variable \\\"\$var1\\\" of type \\\"(?[a-zA-Z]+)\\\" used in position expecting type \\\"(?[a-zA-Z]+)\!\\\"\."$/, 87 | "return": function(groups) { 88 | return `"Variable \\\"var1\\\" type mismatch: the variable type \\\"${groups.type1}\\\" is not compatible with the argument type \\\"NonNull(${groups.type2})\\\""` 89 | } 90 | }, 91 | { 92 | "regex": /^"Variable \\\"\$var1\\\" got invalid value null at \\\"var1\[0\]\\\"; Expected non-nullable type \\\"(?[a-zA-Z]+)\!\\\" not to be null\."$/, 93 | "return": function(groups) { 94 | return `"Variable \\\"var1[1]\\\" expected to be non-null"` 95 | } 96 | }, 97 | { 98 | "regex": /^"Variable \\\"\$var1\\\" of non-null type \\\"\[(?[a-zA-Z]+)\!\]\!\\\" must not be null\."$/, 99 | "return": function(groups) { 100 | return `"Variable \\\"var1\\\" expected to be non-null"` 101 | } 102 | }, 103 | { 104 | "regex": /^"Variable \\\"\$var1\\\" of required type \\\"\[(?[a-zA-Z]+)\!\]\!\\\" was not provided\."$/, 105 | "return": function(groups) { 106 | return `"Variable \\\"var1\\\" expected to be non-null"` 107 | } 108 | }, 109 | { 110 | "regex": /^"Variable \\\"\$var1\\\" of type \\\"\[(?[a-zA-Z]+)\]\!\\\" used in position expecting type \\\"\[(?[a-zA-Z]+)\!\]\!\\\"\."$/, 111 | "return": function(groups) { 112 | return `"Variable \\\"var1\\\" type mismatch: the variable type \\\"NonNull(List(${groups.type1}))\\\" is not compatible with the argument type \\\"NonNull(List(NonNull(${groups.type2})))\\\""` 113 | } 114 | }, 115 | { 116 | "regex": /^"Variable \\\"\$var1\\\" of type \\\"\[(?[a-zA-Z]+)\!\]\\\" used in position expecting type \\\"\[(?[a-zA-Z]+)\!\]\!\\\"\."$/, 117 | "return": function(groups) { 118 | return `"Variable \\\"var1\\\" type mismatch: the variable type \\\"List(NonNull(${groups.type1}))\\\" is not compatible with the argument type \\\"NonNull(List(NonNull(${groups.type2})))\\\""` 119 | } 120 | }, 121 | { 122 | "regex": /^"Variable \\\"\$var1\\\" of type \\\"\[(?[a-zA-Z]+)\]\\\" used in position expecting type \\\"\[(?[a-zA-Z]+)\!\]\!\\\"\."$/, 123 | "return": function(groups) { 124 | return `"Variable \\\"var1\\\" type mismatch: the variable type \\\"List(${groups.type1})\\\" is not compatible with the argument type \\\"NonNull(List(NonNull(${groups.type2})))\\\""` 125 | } 126 | }, 127 | { 128 | "regex": /^"Variable \\\"\$var1\\\" of non-null type \\\"\[(?[a-zA-Z]+)\]\!\\\" must not be null\."$/, 129 | "return": function(groups) { 130 | return `"Variable \\\"var1\\\" expected to be non-null"` 131 | } 132 | }, 133 | { 134 | "regex": /^"Variable \\\"\$var1\\\" of required type \\\"\[(?[a-zA-Z]+)\]\!\\\" was not provided\."$/, 135 | "return": function(groups) { 136 | return `"Variable \\\"var1\\\" expected to be non-null"` 137 | } 138 | }, 139 | { 140 | "regex": /^"Variable \\\"\$var1\\\" of type \\\"\[(?[a-zA-Z]+)\!\]\\\" used in position expecting type \\\"\[(?[a-zA-Z]+)\]\!\\\"\."$/, 141 | "return": function(groups) { 142 | return `"Variable \\\"var1\\\" type mismatch: the variable type \\\"List(NonNull(${groups.type1}))\\\" is not compatible with the argument type \\\"NonNull(List(${groups.type2}))\\\""` 143 | } 144 | }, 145 | { 146 | "regex": /^"Variable \\\"\$var1\\\" of type \\\"\[(?[a-zA-Z]+)\]\\\" used in position expecting type \\\"\[(?[a-zA-Z]+)\]\!\\\"\."$/, 147 | "return": function(groups) { 148 | return `"Variable \\\"var1\\\" type mismatch: the variable type \\\"List(${groups.type1})\\\" is not compatible with the argument type \\\"NonNull(List(${groups.type2}))\\\""` 149 | } 150 | }, 151 | { 152 | "regex": /^"Variable \\\"\$var1\\\" of type \\\"\[(?[a-zA-Z]+)\]\!\\\" used in position expecting type \\\"\[(?[a-zA-Z]+)\!\]\\\"\."$/, 153 | "return": function(groups) { 154 | return `"Variable \\\"var1\\\" type mismatch: the variable type \\\"NonNull(List(${groups.type1}))\\\" is not compatible with the argument type \\\"List(NonNull(${groups.type2}))\\\""` 155 | } 156 | }, 157 | { 158 | "regex": /^"Argument \\\"arg1\\\" of non-null type \\\"(?[a-zA-Z]+)\!\\\" must not be null\."$/, 159 | "return": function(groups) { 160 | return `"Expected non-null for \\\"NonNull(${groups.type1})\\\", got null"` 161 | } 162 | }, 163 | { 164 | "regex": /^"Argument \\\"arg1\\\" of non-null type \\\"\[(?[a-zA-Z]+)\]\!\\\" must not be null\."$/, 165 | "return": function(groups) { 166 | return `"Expected non-null for \\\"NonNull(List(${groups.type1}))\\\", got null"` 167 | } 168 | }, 169 | { 170 | "regex": /^"Argument \\\"arg1\\\" of non-null type \\\"\[(?[a-zA-Z]+)\!\]\!\\\" must not be null\."$/, 171 | "return": function(groups) { 172 | return `"Expected non-null for \\\"NonNull(List(NonNull(${groups.type1})))\\\", got null"` 173 | } 174 | }, 175 | { 176 | "regex": /^"Variable \\\"\$var1\\\" of type \\\"\[(?[a-zA-Z]+)\]\\\" used in position expecting type \\\"\[(?[a-zA-Z]+)\!\]\\\"\."$/, 177 | "return": function(groups) { 178 | return `"Variable \\\"var1\\\" type mismatch: the variable type \\\"List(${groups.type1})\\\" is not compatible with the argument type \\\"List(NonNull(${groups.type2}))\\\""` 179 | } 180 | }, 181 | ] 182 | 183 | function JS_to_Lua_error_map_func(s) { 184 | let j = 0 185 | for (j = 0; j < Lua_to_JS_error.length; j++) { 186 | let found = s.match(Lua_to_JS_error[j].regex) 187 | 188 | if (found) { 189 | return Lua_to_JS_error[j].return(found.groups) 190 | } 191 | } 192 | 193 | return s 194 | } 195 | 196 | // == Build JS GraphQL objects == 197 | 198 | function get_JS_type_name(type) { 199 | if (type == 'list') { 200 | return 'list' 201 | } 202 | 203 | if (graphql_types[type]) { 204 | return graphql_types[type].var_type 205 | } 206 | 207 | return undefined 208 | } 209 | 210 | function get_JS_type_schema(type, inner_type) { 211 | if (inner_type !== null) { 212 | if (graphql_types[inner_type].graphql_type) { 213 | return graphql_types[inner_type].graphql_type 214 | } 215 | 216 | return '' 217 | } 218 | 219 | if (graphql_types[type]) { 220 | if (graphql_types[type].graphql_type) { 221 | return graphql_types[type].graphql_type 222 | } 223 | return '' 224 | } 225 | 226 | return '' 227 | } 228 | 229 | function get_JS_nullability(nullability) { 230 | if (nullability == NonNullable) { 231 | return `!` 232 | } else { 233 | return `` 234 | } 235 | } 236 | 237 | function get_JS_type(type, nullability, 238 | inner_type, inner_nullability) { 239 | let js_type = get_JS_type_name(type) 240 | let js_nullability = get_JS_nullability(nullability) 241 | let js_inner_type = get_JS_type_name(inner_type) 242 | let js_inner_nullability = get_JS_nullability(inner_nullability) 243 | 244 | if (js_type === 'list') { 245 | return `[${js_inner_type}${js_inner_nullability}]${js_nullability}` 246 | } else { 247 | return `${js_type}${js_nullability}` 248 | } 249 | } 250 | 251 | function get_JS_value(type, inner_type, value, plain_nil_as_null) { 252 | if (Array.isArray(value)) { 253 | if (value[0] === nil) { 254 | return `[]` 255 | } else if (value[0] === box.NULL) { 256 | return `[null]` 257 | } else { 258 | if (inner_type == 'enum') { 259 | return `[${value}]` 260 | } 261 | return JSON.stringify(value) 262 | } 263 | } else { 264 | if (value === nil) { 265 | if (plain_nil_as_null) { 266 | return `null` 267 | } else { 268 | return `` 269 | } 270 | } else if (value === box.NULL) { 271 | return `null` 272 | } else { 273 | if (type == 'enum') { 274 | return value 275 | } 276 | return JSON.stringify(value) 277 | } 278 | } 279 | } 280 | 281 | function get_JS_default_value(type, inner_type, value) { 282 | return get_JS_value(type, inner_type, value, false) 283 | } 284 | 285 | function get_JS_argument_value(type, inner_type, value) { 286 | return get_JS_value(type, inner_type, value, true) 287 | } 288 | 289 | function build_schema(argument_type, argument_nullability, 290 | argument_inner_type, argument_inner_nullability, 291 | argument_value, 292 | variable_type, variable_nullability, 293 | variable_inner_type, variable_inner_nullability, 294 | variable_value, variable_default) { 295 | let argument_str = get_JS_type(argument_type, argument_nullability, 296 | argument_inner_type, argument_inner_nullability) 297 | let additional_schema = get_JS_type_schema(argument_type, argument_inner_type) 298 | 299 | var schema_str = `${additional_schema} 300 | type result { 301 | arg1: ${argument_str} 302 | } 303 | 304 | type Query { 305 | test(arg1: ${argument_str}): result 306 | } 307 | ` 308 | 309 | return schema_str; 310 | }; 311 | 312 | function build_query(argument_type, argument_nullability, 313 | argument_inner_type, argument_inner_nullability, 314 | argument_value, 315 | variable_type, variable_nullability, 316 | variable_inner_type, variable_inner_nullability, 317 | variable_value, variable_default) { 318 | if (variable_type !== null) { 319 | let variable_str = get_JS_type(variable_type, variable_nullability, 320 | variable_inner_type, variable_inner_nullability) 321 | 322 | let default_str = `` 323 | let js_variable_default = get_JS_default_value(variable_type, variable_inner_type, variable_default) 324 | if (js_variable_default !== ``) { 325 | default_str = ` = ${js_variable_default}` 326 | } 327 | 328 | return `query MyQuery($var1: ${variable_str}${default_str}) { test(arg1: $var1) { arg1 } }` 329 | } else { 330 | let js_argument_value = get_JS_argument_value(argument_type, argument_inner_type, argument_value) 331 | return `query MyQuery { test(arg1: ${js_argument_value}) { arg1 } }` 332 | } 333 | }; 334 | 335 | function build_variables(argument_type, argument_nullability, 336 | argument_inner_type, argument_inner_nullability, 337 | argument_value, 338 | variable_type, variable_nullability, 339 | variable_inner_type, variable_inner_nullability, 340 | variable_value, variable_default) { 341 | let variables = []; 342 | 343 | if (Array.isArray(variable_value)) { 344 | if (variable_value[0] == nil) { 345 | return {var1: []} 346 | } else if (variable_value[0] === box.NULL) { 347 | return {var1: [null]} 348 | } else { 349 | return {var1: variable_value} 350 | } 351 | } 352 | 353 | if (variable_value !== nil) { 354 | if (variable_value === box.NULL) { 355 | return {var1: null} 356 | } else { 357 | return {var1: variable_value} 358 | } 359 | } 360 | 361 | return [] 362 | } 363 | 364 | var rootValue = { 365 | test: (args) => { 366 | return args; 367 | }, 368 | }; 369 | 370 | // == Build Lua GraphQL objects == 371 | 372 | var test_header = `-- THIS FILE WAS GENERATED AUTOMATICALLY 373 | -- See generator script at tests/integration/codegen/fuzzing_nullability 374 | -- This test compares library behaviour with reference JavaScript GraphQL 375 | -- implementation. Do not change it manually if the behaviour has changed, 376 | -- please interact with code generation script. 377 | 378 | local json = require('json') 379 | local types = require('graphql.types') 380 | 381 | local t = require('luatest') 382 | local g = t.group('fuzzing_nullability') 383 | 384 | local helpers = require('test.helpers') 385 | 386 | -- constants 387 | local Nullable = true 388 | local NonNullable = false 389 | 390 | -- custom types 391 | local my_enum = types.enum({ 392 | name = 'MyEnum', 393 | values = { 394 | a = { value = 'a' }, 395 | b = { value = 'b' }, 396 | }, 397 | }) 398 | 399 | local graphql_types = { 400 | ['boolean_true'] = { 401 | graphql_type = types.boolean, 402 | var_type = 'Boolean', 403 | value = true, 404 | default = false, 405 | }, 406 | ['boolean_false'] = { 407 | graphql_type = types.boolean, 408 | var_type = 'Boolean', 409 | value = false, 410 | default = true, 411 | }, 412 | ['string'] = { 413 | graphql_type = types.string, 414 | var_type = 'String', 415 | value = 'Test string', 416 | default = 'Default Test string', 417 | }, 418 | ['float'] = { 419 | graphql_type = types.float, 420 | var_type = 'Float', 421 | value = 1.1111111, 422 | default = 0, 423 | }, 424 | ['int'] = { 425 | graphql_type = types.int, 426 | var_type = 'Int', 427 | value = 2^30, 428 | default = 0, 429 | }, 430 | ['id'] = { 431 | graphql_type = types.id, 432 | var_type = 'ID', 433 | value = '00000000-0000-0000-0000-000000000000', 434 | default = '11111111-1111-1111-1111-111111111111', 435 | }, 436 | ['enum'] = { 437 | graphql_type = my_enum, 438 | var_type = 'MyEnum', 439 | value = 'b', 440 | default = 'a', 441 | }, 442 | -- For more types follow https://github.com/tarantool/graphql/issues/63 443 | } 444 | 445 | local function build_schema(argument_type, argument_nullability, 446 | argument_inner_type, argument_inner_nullability, 447 | argument_value, 448 | variable_type, variable_nullability, 449 | variable_inner_type, variable_inner_nullability, 450 | variable_value, variable_default) 451 | local type 452 | if argument_type == 'list' then 453 | if argument_inner_nullability == NonNullable then 454 | type = types.list(types.nonNull(graphql_types[argument_inner_type].graphql_type)) 455 | else 456 | type = types.list(graphql_types[argument_inner_type].graphql_type) 457 | end 458 | if argument_nullability == NonNullable then 459 | type = types.nonNull(type) 460 | end 461 | else 462 | if argument_nullability == NonNullable then 463 | type = types.nonNull(graphql_types[argument_type].graphql_type) 464 | else 465 | type = graphql_types[argument_type].graphql_type 466 | end 467 | end 468 | 469 | return { 470 | ['test'] = { 471 | kind = types.object({ 472 | name = 'result', 473 | fields = {arg1 = type} 474 | }), 475 | arguments = {arg1 = type}, 476 | resolve = function(_, args) 477 | return args 478 | end, 479 | } 480 | } 481 | end 482 | 483 | -- For more test cases follow https://github.com/tarantool/graphql/issues/63` 484 | console.log(test_header) 485 | 486 | function to_Lua(v) { 487 | if (v === null) { 488 | return `nil` 489 | } 490 | 491 | if (v === nil) { 492 | return `${v}` 493 | } 494 | 495 | if (v === box.NULL) { 496 | return `${v}` 497 | } 498 | 499 | if (v === Nullable) { 500 | return `${v}` 501 | } 502 | 503 | if (v === NonNullable) { 504 | return `${v}` 505 | } 506 | 507 | if (Array.isArray(v)) { 508 | if (v[0] === nil) { 509 | return '{}' 510 | } else if (v[0] === box.NULL) { 511 | return '{box.NULL}' 512 | } else { 513 | if (typeof v[0] === 'string' ) { 514 | return `{'${v[0]}'}` 515 | } 516 | 517 | return `{${v}}` 518 | } 519 | } 520 | 521 | if (typeof v === 'string' ) { 522 | return `'${v}'` 523 | } 524 | 525 | return `${v}` 526 | } 527 | 528 | function build_test_case(response, suite_name, i, 529 | argument_type, argument_nullability, 530 | argument_inner_type, argument_inner_nullability, 531 | argument_value, 532 | variable_type, variable_nullability, 533 | variable_inner_type, variable_inner_nullability, 534 | variable_value, variable_default, 535 | query, schema_str) { 536 | let expected_data 537 | 538 | if (response.hasOwnProperty('data')) { 539 | let _expected_data = JSON.stringify(response.data) 540 | expected_data = `'${_expected_data}'` 541 | } else { 542 | expected_data = `nil` 543 | } 544 | 545 | let expected_error 546 | 547 | if (response.hasOwnProperty('errors')) { 548 | let _expected_error = JSON.stringify(response.errors[0].message) 549 | expected_error = JS_to_Lua_error_map_func(`${_expected_error}`) 550 | } else { 551 | expected_error = `nil` 552 | } 553 | 554 | let Lua_argument_type = to_Lua(argument_type) 555 | let Lua_argument_nullability = to_Lua(argument_nullability) 556 | let Lua_argument_inner_type = to_Lua(argument_inner_type) 557 | let Lua_argument_inner_nullability = to_Lua(argument_inner_nullability) 558 | 559 | let Lua_variable_type = to_Lua(variable_type) 560 | let Lua_variable_nullability = to_Lua(variable_nullability) 561 | let Lua_variable_inner_type = to_Lua(variable_inner_type) 562 | let Lua_variable_inner_nullability = to_Lua(variable_inner_nullability) 563 | 564 | 565 | let Lua_variable_default = to_Lua(variable_default) 566 | let Lua_argument_value = to_Lua(argument_value) 567 | let Lua_variable_value = to_Lua(variable_value) 568 | 569 | let type_in_name 570 | if (argument_inner_type !== null) { 571 | type_in_name = argument_inner_type 572 | } else { 573 | type_in_name = argument_type 574 | } 575 | 576 | return ` 577 | g.test_${suite_name}_${type_in_name}_${i} = function(g) 578 | local argument_type = ${Lua_argument_type} 579 | local argument_nullability = ${Lua_argument_nullability} 580 | local argument_inner_type = ${Lua_argument_inner_type} 581 | local argument_inner_nullability = ${Lua_argument_inner_nullability} 582 | local argument_value = ${Lua_argument_value} 583 | local variable_type = ${Lua_variable_type} 584 | local variable_nullability = ${Lua_variable_nullability} 585 | local variable_inner_type = ${Lua_variable_inner_type} 586 | local variable_inner_nullability = ${Lua_variable_inner_nullability} 587 | local variable_default = ${Lua_variable_default} 588 | local variable_value = ${Lua_variable_value} 589 | 590 | local query_schema = build_schema(argument_type, argument_nullability, 591 | argument_inner_type, argument_inner_nullability, 592 | argument_value, 593 | variable_type, variable_nullability, 594 | variable_inner_type, variable_inner_nullability, 595 | variable_value, variable_default) 596 | 597 | -- There is no explicit check that Lua query_schema is the same as JS query_schema. 598 | local reference_schema = [[${schema_str}]] 599 | 600 | local query = '${query}' 601 | 602 | local ok, res = pcall(helpers.check_request, query, query_schema, nil, nil, { variables = { var1 = variable_value }}) 603 | 604 | local result, err 605 | if ok then 606 | result = json.encode(res) 607 | else 608 | err = res 609 | end 610 | 611 | local expected_data_json = ${expected_data} 612 | local expected_error_json = ${expected_error} 613 | 614 | if expected_error_json ~= nil and expected_data_json ~= nil then 615 | t.assert_equals(err, expected_error_json) 616 | t.xfail('See https://github.com/tarantool/graphql/issues/62') 617 | end 618 | 619 | t.assert_equals(result, expected_data_json) 620 | t.assert_equals(err, expected_error_json) 621 | end` 622 | } 623 | 624 | async function build_suite(suite_name, 625 | argument_type, argument_nullabilities, 626 | argument_inner_type, argument_inner_nullabilities, 627 | argument_values, 628 | variable_type, variable_nullabilities, 629 | variable_inner_type, variable_inner_nullabilities, 630 | variable_values, 631 | variable_defaults) { 632 | let i = 0 633 | 634 | if (argument_inner_nullabilities.length == 0) { 635 | // Non-list case 636 | let argument_inner_nullability = null 637 | let variable_inner_nullability = null 638 | 639 | if (variable_type == null) { 640 | // No variables case 641 | let variable_nullability = null 642 | let variable_value = null 643 | let variable_default = null 644 | 645 | argument_nullabilities.forEach( async function (argument_nullability) { 646 | argument_values.forEach( async function (argument_value) { 647 | let schema_str = build_schema(argument_type, argument_nullability, 648 | argument_inner_type, argument_inner_nullability, 649 | argument_value, 650 | variable_type, variable_nullability, 651 | variable_inner_type, variable_inner_nullability, 652 | variable_value, variable_default) 653 | let schema = buildSchema(schema_str) 654 | 655 | let query = build_query(argument_type, argument_nullability, 656 | argument_inner_type, argument_inner_nullability, 657 | argument_value, 658 | variable_type, variable_nullability, 659 | variable_inner_type, variable_inner_nullability, 660 | variable_value, variable_default) 661 | 662 | 663 | await graphql({ 664 | schema, 665 | source: query, 666 | rootValue, 667 | }).then((response) => { 668 | i = i + 1 669 | console.log(build_test_case(response, suite_name, i, 670 | argument_type, argument_nullability, 671 | argument_inner_type, argument_inner_nullability, 672 | argument_value, 673 | variable_type, variable_nullability, 674 | variable_inner_type, variable_inner_nullability, 675 | variable_value, variable_default, 676 | query, schema_str)) 677 | }) 678 | }) 679 | }) 680 | } else { 681 | // Variables case 682 | argument_nullabilities.forEach( async function (argument_nullability) { 683 | variable_nullabilities.forEach( async function (variable_nullability) { 684 | variable_values.forEach( async function (variable_value) { 685 | variable_defaults.forEach( async function (variable_default) { 686 | let argument_value = null 687 | 688 | let schema_str = build_schema(argument_type, argument_nullability, 689 | argument_inner_type, argument_inner_nullability, 690 | argument_value, 691 | variable_type, variable_nullability, 692 | variable_inner_type, variable_inner_nullability, 693 | variable_value, variable_default) 694 | let schema = buildSchema(schema_str) 695 | 696 | let query = build_query(argument_type, argument_nullability, 697 | argument_inner_type, argument_inner_nullability, 698 | argument_value, 699 | variable_type, variable_nullability, 700 | variable_inner_type, variable_inner_nullability, 701 | variable_value, variable_default) 702 | 703 | let variables = build_variables(argument_type, argument_nullability, 704 | argument_inner_type, argument_inner_nullability, 705 | argument_value, 706 | variable_type, variable_nullability, 707 | variable_inner_type, variable_inner_nullability, 708 | variable_value, variable_default) 709 | 710 | 711 | await graphql({ 712 | schema, 713 | source: query, 714 | rootValue, 715 | variableValues: variables 716 | }).then((response) => { 717 | i = i + 1 718 | console.log(build_test_case(response, suite_name, i, 719 | argument_type, argument_nullability, 720 | argument_inner_type, argument_inner_nullability, 721 | argument_value, 722 | variable_type, variable_nullability, 723 | variable_inner_type, variable_inner_nullability, 724 | variable_value, variable_default, 725 | query, schema_str)) 726 | }) 727 | }) 728 | }) 729 | }) 730 | }) 731 | } 732 | 733 | return 734 | } 735 | 736 | // List case 737 | if (variable_type == null) { 738 | argument_nullabilities.forEach( async function (argument_nullability) { 739 | argument_inner_nullabilities.forEach( async function (argument_inner_nullability) { 740 | argument_values.forEach( async function (argument_value) { 741 | let variable_nullability = null 742 | let variable_inner_nullability = null 743 | let variable_value = null 744 | let variable_default = null 745 | 746 | let schema_str = build_schema(argument_type, argument_nullability, 747 | argument_inner_type, argument_inner_nullability, 748 | argument_value, 749 | variable_type, variable_nullability, 750 | variable_inner_type, variable_inner_nullability, 751 | variable_value, variable_default) 752 | let schema = buildSchema(schema_str) 753 | 754 | let query = build_query(argument_type, argument_nullability, 755 | argument_inner_type, argument_inner_nullability, 756 | argument_value, 757 | variable_type, variable_nullability, 758 | variable_inner_type, variable_inner_nullability, 759 | variable_value, variable_default) 760 | 761 | await graphql({ 762 | schema, 763 | source: query, 764 | rootValue, 765 | }).then((response) => { 766 | i = i + 1 767 | console.log(build_test_case(response, suite_name, i, 768 | argument_type, argument_nullability, 769 | argument_inner_type, argument_inner_nullability, 770 | argument_value, 771 | variable_type, variable_nullability, 772 | variable_inner_type, variable_inner_nullability, 773 | variable_value, variable_default, 774 | query, schema_str)) 775 | }) 776 | }) 777 | }) 778 | }) 779 | } else { 780 | argument_nullabilities.forEach( async function (argument_nullability) { 781 | argument_inner_nullabilities.forEach( async function (argument_inner_nullability) { 782 | variable_nullabilities.forEach( async function (variable_nullability) { 783 | variable_inner_nullabilities.forEach( async function (variable_inner_nullability) { 784 | variable_values.forEach( async function (variable_value) { 785 | variable_defaults.forEach( async function (variable_default) { 786 | let argument_value = null 787 | 788 | let schema_str = build_schema(argument_type, argument_nullability, 789 | argument_inner_type, argument_inner_nullability, 790 | argument_value, 791 | variable_type, variable_nullability, 792 | variable_inner_type, variable_inner_nullability, 793 | variable_value, variable_default) 794 | let schema = buildSchema(schema_str) 795 | 796 | let query = build_query(argument_type, argument_nullability, 797 | argument_inner_type, argument_inner_nullability, 798 | argument_value, 799 | variable_type, variable_nullability, 800 | variable_inner_type, variable_inner_nullability, 801 | variable_value, variable_default) 802 | 803 | let variables = build_variables(argument_type, argument_nullability, 804 | argument_inner_type, argument_inner_nullability, 805 | argument_value, 806 | variable_type, variable_nullability, 807 | variable_inner_type, variable_inner_nullability, 808 | variable_value, variable_default) 809 | 810 | await graphql({ 811 | schema, 812 | source: query, 813 | rootValue, 814 | variableValues: variables 815 | }).then((response) => { 816 | i = i + 1 817 | console.log(build_test_case(response, suite_name, i, 818 | argument_type, argument_nullability, 819 | argument_inner_type, argument_inner_nullability, 820 | argument_value, 821 | variable_type, variable_nullability, 822 | variable_inner_type, variable_inner_nullability, 823 | variable_value, variable_default, 824 | query, schema_str)) 825 | }) 826 | }) 827 | }) 828 | }) 829 | }) 830 | }) 831 | }) 832 | } 833 | } 834 | 835 | let type 836 | let type_desc 837 | 838 | // == Non-list argument nullability == 839 | // 840 | // There is no way pass no value to the argument 841 | // since `test(arg1)` is invalid syntax. 842 | // We use `test(arg1: null)` for both nil and box.NULL, 843 | // so the behavior will be the same for them. 844 | 845 | Object.keys(graphql_types).forEach( (type) => { 846 | let type_desc = graphql_types[type] 847 | 848 | build_suite('nonlist_argument_nullability', 849 | type, [Nullable, NonNullable], 850 | null, [], 851 | [nil, box.NULL, type_desc.value], 852 | null, [], 853 | null, [], 854 | [], 855 | []) 856 | }) 857 | 858 | // == List argument nullability == 859 | // 860 | // {nil} is the same as {} in Lua. 861 | 862 | Object.keys(graphql_types).forEach( (type) => { 863 | let type_desc = graphql_types[type] 864 | 865 | build_suite('list_argument_nullability', 866 | 'list', [Nullable, NonNullable], 867 | type, [Nullable, NonNullable], 868 | [nil, box.NULL, [nil], [box.NULL], [type_desc.value]], 869 | null, [], 870 | null, [], 871 | [], 872 | []) 873 | }) 874 | 875 | // == Non-list argument with variable nullability == 876 | 877 | Object.keys(graphql_types).forEach( (type) => { 878 | let type_desc = graphql_types[type] 879 | 880 | build_suite('nonlist_argument_with_variables_nullability', 881 | type, [Nullable, NonNullable], 882 | null, [], 883 | [], 884 | type, [Nullable, NonNullable], 885 | null, [], 886 | [nil, box.NULL, type_desc.value], 887 | [nil, box.NULL, type_desc.default]) 888 | }) 889 | 890 | // == List argument with variable nullability == 891 | // 892 | // {nil} is the same as {} in Lua. 893 | 894 | Object.keys(graphql_types).forEach( (type) => { 895 | let type_desc = graphql_types[type] 896 | 897 | 898 | build_suite('list_argument_with_variables_nullability', 899 | 'list', [Nullable, NonNullable], 900 | type, [Nullable, NonNullable], 901 | [], 902 | 'list', [Nullable, NonNullable], 903 | type, [Nullable, NonNullable], 904 | [nil, box.NULL, [nil], [box.NULL], [type_desc.value]], 905 | [nil, box.NULL, [nil], [box.NULL], [type_desc.default]]) 906 | }) 907 | -------------------------------------------------------------------------------- /test/integration/codegen/fuzzing_nullability/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuzzing_nullability", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "graphql": { 8 | "version": "15.8.0", 9 | "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", 10 | "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/integration/codegen/fuzzing_nullability/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuzzing_nullability", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "generate.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "graphql": "^15.8.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/integration/introspection.lua: -------------------------------------------------------------------------------- 1 | return { 2 | query = [[ 3 | query IntrospectionQuery { 4 | __schema { 5 | queryType { name } 6 | mutationType { name } 7 | subscriptionType { name } 8 | types { 9 | ...FullType 10 | } 11 | directives { 12 | name 13 | description 14 | isRepeatable 15 | locations 16 | args { 17 | ...InputValue 18 | } 19 | } 20 | } 21 | } 22 | 23 | fragment FullType on __Type { 24 | kind 25 | name 26 | description 27 | specifiedByURL 28 | fields(includeDeprecated: true) { 29 | name 30 | description 31 | args { 32 | ...InputValue 33 | } 34 | type { 35 | ...TypeRef 36 | } 37 | isDeprecated 38 | deprecationReason 39 | } 40 | inputFields { 41 | ...InputValue 42 | } 43 | interfaces { 44 | ...TypeRef 45 | } 46 | enumValues(includeDeprecated: true) { 47 | name 48 | description 49 | isDeprecated 50 | deprecationReason 51 | } 52 | possibleTypes { 53 | ...TypeRef 54 | } 55 | } 56 | 57 | fragment InputValue on __InputValue { 58 | name 59 | description 60 | type { ...TypeRef } 61 | defaultValue 62 | } 63 | 64 | fragment TypeRef on __Type { 65 | kind 66 | name 67 | ofType { 68 | kind 69 | name 70 | ofType { 71 | kind 72 | name 73 | ofType { 74 | kind 75 | name 76 | ofType { 77 | kind 78 | name 79 | ofType { 80 | kind 81 | name 82 | ofType { 83 | kind 84 | name 85 | ofType { 86 | kind 87 | name 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | ]], 97 | variables = {} 98 | } 99 | -------------------------------------------------------------------------------- /test/unit/graphql_test.lua: -------------------------------------------------------------------------------- 1 | local t = require('luatest') 2 | local luatest_capture = require('luatest.capture') 3 | local g = t.group('unit') 4 | 5 | local helpers = require('test.helpers') 6 | 7 | local parse = require('graphql.parse').parse 8 | local types = require('graphql.types') 9 | local schema = require('graphql.schema') 10 | local validate = require('graphql.validate').validate 11 | local util = require('graphql.util') 12 | 13 | function g.test_parse_comments() 14 | t.assert_error(parse('{a(b:"#")}').definitions, {}) 15 | end 16 | 17 | function g.test_parse_document() 18 | t.assert_error(parse) 19 | t.assert_error(parse, 'foo') 20 | t.assert_error(parse, 'query') 21 | t.assert_error(parse, 'query{} foo') 22 | end 23 | 24 | function g.test_parse_operation_shorthand() 25 | local operation = parse('{a}').definitions[1] 26 | t.assert_equals(operation.kind, 'operation') 27 | t.assert_equals(operation.name, nil) 28 | t.assert_equals(operation.operation, 'query') 29 | end 30 | 31 | function g.test_parse_operation_operationType() 32 | local operation = parse('query{a}').definitions[1] 33 | t.assert_equals(operation.operation, 'query') 34 | 35 | operation = parse('mutation{a}').definitions[1] 36 | t.assert_equals(operation.operation, 'mutation') 37 | 38 | t.assert_error(parse, 'kneeReplacement{b}') 39 | end 40 | 41 | function g.test_parse_operation_name() 42 | local operation = parse('query{a}').definitions[1] 43 | t.assert_equals(operation.name, nil) 44 | 45 | operation = parse('query queryName{a}').definitions[1] 46 | t.assert_not_equals(operation.name, nil) 47 | t.assert_equals(operation.name.value, 'queryName') 48 | end 49 | 50 | function g.test_parse_operation_variableDefinitions() 51 | t.assert_error(parse, 'query(){b}') 52 | t.assert_error(parse, 'query(x){b}') 53 | 54 | local operation = parse('query name($a:Int,$b:Int){c}').definitions[1] 55 | t.assert_equals(operation.name.value, 'name') 56 | t.assert_not_equals(operation.variableDefinitions, nil) 57 | t.assert_equals(#operation.variableDefinitions, 2) 58 | 59 | operation = parse('query($a:Int,$b:Int){c}').definitions[1] 60 | t.assert_not_equals(operation.variableDefinitions, nil) 61 | t.assert_equals(#operation.variableDefinitions, 2) 62 | end 63 | 64 | function g.test_parse_operation_directives() 65 | local operation = parse('query{a}').definitions[1] 66 | t.assert_equals(operation.directives, nil) 67 | 68 | operation = parse('query @a{b}').definitions[1] 69 | t.assert_not_equals(operation.directives, nil) 70 | 71 | operation = parse('query name @a{b}').definitions[1] 72 | t.assert_not_equals(operation.directives, nil) 73 | 74 | operation = parse('query ($a:Int) @a {b}').definitions[1] 75 | t.assert_not_equals(operation.directives, nil) 76 | 77 | operation = parse('query name ($a:Int) @a {b}').definitions[1] 78 | t.assert_not_equals(operation.directives, nil) 79 | end 80 | 81 | function g.test_parse_fragmentDefinition_fragmentName() 82 | t.assert_error(parse, 'fragment {a}') 83 | t.assert_error(parse, 'fragment on x {a}') 84 | t.assert_error(parse, 'fragment on on x {a}') 85 | 86 | local fragment = parse('fragment x on y { a }').definitions[1] 87 | t.assert_equals(fragment.kind, 'fragmentDefinition') 88 | t.assert_equals(fragment.name.value, 'x') 89 | end 90 | 91 | function g.test_parse_fragmentDefinition_typeCondition() 92 | t.assert_error(parse, 'fragment x {c}') 93 | 94 | local fragment = parse('fragment x on y { a }').definitions[1] 95 | t.assert_equals(fragment.typeCondition.name.value, 'y') 96 | end 97 | 98 | function g.test_parse_fragmentDefinition_selectionSet() 99 | t.assert_error(parse, 'fragment x on y') 100 | 101 | local fragment = parse('fragment x on y { a }').definitions[1] 102 | t.assert_not_equals(fragment.selectionSet, nil) 103 | end 104 | 105 | function g.test_parse_selectionSet() 106 | t.assert_error(parse, '{') 107 | t.assert_error(parse, '}') 108 | 109 | local selectionSet = parse('{a}').definitions[1].selectionSet 110 | t.assert_equals(selectionSet.kind, 'selectionSet') 111 | t.assert_equals(selectionSet.selections, {{kind = "field", name = {kind = "name", value = "a"}}}) 112 | 113 | selectionSet = parse('{a b}').definitions[1].selectionSet 114 | t.assert_equals(#selectionSet.selections, 2) 115 | end 116 | 117 | function g.test_parse_field_name() 118 | t.assert_error(parse, '{$a}') 119 | t.assert_error(parse, '{@a}') 120 | t.assert_error(parse, '{.}') 121 | t.assert_error(parse, '{,}') 122 | 123 | local field = parse('{a}').definitions[1].selectionSet.selections[1] 124 | t.assert_equals(field.kind, 'field') 125 | t.assert_equals(field.name.value, 'a') 126 | end 127 | 128 | function g.test_parse_field_alias() 129 | t.assert_error(parse, '{a:b:}') 130 | t.assert_error(parse, '{a:b:c}') 131 | t.assert_error(parse, '{:a}') 132 | 133 | local field = parse('{a}').definitions[1].selectionSet.selections[1] 134 | t.assert_equals(field.alias, nil) 135 | 136 | field = parse('{a:b}').definitions[1].selectionSet.selections[1] 137 | t.assert_not_equals(field.alias, nil) 138 | t.assert_equals(field.alias.kind, 'alias') 139 | t.assert_equals(field.alias.name.value, 'a') 140 | t.assert_equals(field.name.value, 'b') 141 | end 142 | 143 | function g.test_parse_field_arguments() 144 | t.assert_error(parse, '{a()}') 145 | 146 | local field = parse('{a}').definitions[1].selectionSet.selections[1] 147 | t.assert_equals(field.arguments, nil) 148 | 149 | field = parse('{a(b:false)}').definitions[1].selectionSet.selections[1] 150 | t.assert_not_equals(field.arguments, nil) 151 | end 152 | 153 | function g.test_parse_field_directives() 154 | t.assert_error(parse, '{a@skip(b:false)(c:true)}') 155 | 156 | local field = parse('{a}').definitions[1].selectionSet.selections[1] 157 | t.assert_equals(field.directives, nil) 158 | 159 | field = parse('{a@skip}').definitions[1].selectionSet.selections[1] 160 | t.assert_not_equals(field.directives, nil) 161 | 162 | field = parse('{a(b:1)@skip}').definitions[1].selectionSet.selections[1] 163 | t.assert_not_equals(field.directives, nil) 164 | end 165 | 166 | function g.test_parse_field_selectionSet() 167 | t.assert_error(parse, '{{}}') 168 | 169 | local field = parse('{a}').definitions[1].selectionSet.selections[1] 170 | t.assert_equals(field.selectionSet, nil) 171 | 172 | field = parse('{a { b } }').definitions[1].selectionSet.selections[1] 173 | t.assert_not_equals(field.selectionSet, nil) 174 | 175 | field = parse('{a{a}}').definitions[1].selectionSet.selections[1] 176 | t.assert_not_equals(field.selectionSet, nil) 177 | 178 | field = parse('{a(b:1)@skip{a}}').definitions[1].selectionSet.selections[1] 179 | t.assert_not_equals(field.selectionSet, nil) 180 | end 181 | 182 | function g.test_parse_fragmentSpread_name() 183 | t.assert_error(parse, '{..a}') 184 | t.assert_error(parse, '{...}') 185 | 186 | local fragmentSpread = parse('{...a}').definitions[1].selectionSet.selections[1] 187 | t.assert_equals(fragmentSpread.kind, 'fragmentSpread') 188 | t.assert_equals(fragmentSpread.name.value, 'a') 189 | end 190 | 191 | function g.test_parse_fragmentSpread_directives() 192 | t.assert_error(parse, '{...a@}') 193 | 194 | local fragmentSpread = parse('{...a}').definitions[1].selectionSet.selections[1] 195 | t.assert_equals(fragmentSpread.directives, nil) 196 | 197 | fragmentSpread = parse('{...a@skip}').definitions[1].selectionSet.selections[1] 198 | t.assert_not_equals(fragmentSpread.directives, nil) 199 | end 200 | 201 | function g.test_parse_inlineFragment_typeCondition() 202 | t.assert_error(parse, '{...on{}}') 203 | 204 | local inlineFragment = parse('{...{ a }}').definitions[1].selectionSet.selections[1] 205 | t.assert_equals(inlineFragment.kind, 'inlineFragment') 206 | t.assert_equals(inlineFragment.typeCondition, nil) 207 | 208 | inlineFragment = parse('{...on a{ b }}').definitions[1].selectionSet.selections[1] 209 | t.assert_not_equals(inlineFragment.typeCondition, nil) 210 | t.assert_equals(inlineFragment.typeCondition.name.value, 'a') 211 | end 212 | 213 | function g.test_parse_inlineFragment_directives() 214 | t.assert_error(parse, '{...on a @ {}}') 215 | local inlineFragment = parse('{...{ a }}').definitions[1].selectionSet.selections[1] 216 | t.assert_equals(inlineFragment.directives, nil) 217 | 218 | inlineFragment = parse('{...@skip{ a }}').definitions[1].selectionSet.selections[1] 219 | t.assert_not_equals(inlineFragment.directives, nil) 220 | 221 | inlineFragment = parse('{...on a@skip { a }}').definitions[1].selectionSet.selections[1] 222 | t.assert_not_equals(inlineFragment.directives, nil) 223 | end 224 | 225 | function g.test_parse_inlineFragment_selectionSet() 226 | t.assert_error(parse, '{... on a}') 227 | 228 | local inlineFragment = parse('{...{a}}').definitions[1].selectionSet.selections[1] 229 | t.assert_not_equals(inlineFragment.selectionSet, nil) 230 | 231 | inlineFragment = parse('{... on a{b}}').definitions[1].selectionSet.selections[1] 232 | t.assert_not_equals(inlineFragment.selectionSet, nil) 233 | end 234 | 235 | function g.test_parse_arguments() 236 | t.assert_error(parse, '{a()}') 237 | 238 | local arguments = parse('{a(b:1)}').definitions[1].selectionSet.selections[1].arguments 239 | t.assert_equals(#arguments, 1) 240 | 241 | arguments = parse('{a(b:1 c:1)}').definitions[1].selectionSet.selections[1].arguments 242 | t.assert_equals(#arguments, 2) 243 | end 244 | 245 | function g.test_parse_argument() 246 | t.assert_error(parse, '{a(b)}') 247 | t.assert_error(parse, '{a(@b)}') 248 | t.assert_error(parse, '{a($b)}') 249 | t.assert_error(parse, '{a(b::)}') 250 | t.assert_error(parse, '{a(:1)}') 251 | t.assert_error(parse, '{a(b:)}') 252 | t.assert_error(parse, '{a(:)}') 253 | t.assert_error(parse, '{a(b c)}') 254 | 255 | local argument = parse('{a(b:1)}').definitions[1].selectionSet.selections[1].arguments[1] 256 | t.assert_equals(argument.kind, 'argument') 257 | t.assert_equals(argument.name.value, 'b') 258 | t.assert_equals(argument.value.value, '1') 259 | end 260 | 261 | function g.test_parse_directives() 262 | t.assert_error(parse, '{a@}') 263 | t.assert_error(parse, '{a@@}') 264 | 265 | local directives = parse('{a@b}').definitions[1].selectionSet.selections[1].directives 266 | t.assert_equals(#directives, 1) 267 | 268 | directives = parse('{a@b(c:1)@d}').definitions[1].selectionSet.selections[1].directives 269 | t.assert_equals(#directives, 2) 270 | end 271 | 272 | function g.test_parse_directive() 273 | t.assert_error(parse, '{a@b()}') 274 | 275 | local directive = parse('{a@b}').definitions[1].selectionSet.selections[1].directives[1] 276 | t.assert_equals(directive.kind, 'directive') 277 | t.assert_equals(directive.name.value, 'b') 278 | t.assert_equals(directive.arguments, nil) 279 | 280 | directive = parse('{a@b(c:1)}').definitions[1].selectionSet.selections[1].directives[1] 281 | t.assert_not_equals(directive.arguments, nil) 282 | end 283 | 284 | function g.test_parse_variableDefinitions() 285 | t.assert_error(parse, 'query(){}') 286 | t.assert_error(parse, 'query(a){}') 287 | t.assert_error(parse, 'query(@a){}') 288 | t.assert_error(parse, 'query($a){}') 289 | 290 | local variableDefinitions = parse('query($a:Int){ a }').definitions[1].variableDefinitions 291 | t.assert_equals(#variableDefinitions, 1) 292 | 293 | variableDefinitions = parse('query($a:Int $b:Int){ a }').definitions[1].variableDefinitions 294 | t.assert_equals(#variableDefinitions, 2) 295 | end 296 | 297 | function g.test_parse_variableDefinition_variable() 298 | local variableDefinition = parse('query($a:Int){ b }').definitions[1].variableDefinitions[1] 299 | t.assert_equals(variableDefinition.kind, 'variableDefinition') 300 | t.assert_equals(variableDefinition.variable.name.value, 'a') 301 | end 302 | 303 | function g.test_parse_variableDefinition_type() 304 | t.assert_error(parse, 'query($a){}') 305 | t.assert_error(parse, 'query($a:){}') 306 | t.assert_error(parse, 'query($a Int){}') 307 | 308 | local variableDefinition = parse('query($a:Int){b}').definitions[1].variableDefinitions[1] 309 | t.assert_equals(variableDefinition.type.name.value, 'Int') 310 | end 311 | 312 | function g.test_parse_variableDefinition_defaultValue() 313 | t.assert_error(parse, 'query($a:Int=){}') 314 | 315 | local variableDefinition = parse('query($a:Int){b}').definitions[1].variableDefinitions[1] 316 | t.assert_equals(variableDefinition.defaultValue, nil) 317 | 318 | variableDefinition = parse('query($a:Int=1){c}').definitions[1].variableDefinitions[1] 319 | t.assert_not_equals(variableDefinition.defaultValue, nil) 320 | end 321 | 322 | local function run(input, result, type) 323 | local value = parse('{x(y:' .. input .. ')}').definitions[1].selectionSet.selections[1].arguments[1].value 324 | if type then 325 | t.assert_equals(value.kind, type) 326 | end 327 | if result then 328 | t.assert_equals(value.value, result) 329 | end 330 | return value 331 | end 332 | 333 | function g.test_parse_value_variable() 334 | t.assert_error(parse, '{x(y:$)}') 335 | t.assert_error(parse, '{x(y:$a$)}') 336 | 337 | local value = run('$a') 338 | t.assert_equals(value.kind, 'variable') 339 | t.assert_equals(value.name.value, 'a') 340 | end 341 | 342 | function g.test_parse_value_int() 343 | t.assert_error(parse, '{x(y:01)}') 344 | t.assert_error(parse, '{x(y:-01)}') 345 | t.assert_error(parse, '{x(y:--1)}') 346 | t.assert_error(parse, '{x(y:+0)}') 347 | 348 | run('0', '0', 'int') 349 | run('-0', '-0', 'int') 350 | run('1234', '1234', 'int') 351 | run('-1234', '-1234', 'int') 352 | end 353 | 354 | function g.test_parse_value_float() 355 | t.assert_error(parse, '{x(y:.1)}') 356 | t.assert_error(parse, '{x(y:1.)}') 357 | t.assert_error(parse, '{x(y:1..)}') 358 | t.assert_error(parse, '{x(y:0e1.0)}') 359 | 360 | run('0.0', '0.0', 'float') 361 | run('-0.0', '-0.0', 'float') 362 | run('12.34', '12.34', 'float') 363 | run('1e0', '1e0', 'float') 364 | run('1e3', '1e3', 'float') 365 | run('1.0e3', '1.0e3', 'float') 366 | run('1.0e+3', '1.0e+3', 'float') 367 | run('1.0e-3', '1.0e-3', 'float') 368 | run('1.00e-30', '1.00e-30', 'float') 369 | end 370 | 371 | function g.test_parse_value_boolean() 372 | run('true', 'true', 'boolean') 373 | run('false', 'false', 'boolean') 374 | end 375 | 376 | function g.test_parse_value_string() 377 | t.assert_error(parse, '{x(y:")}') 378 | t.assert_error(parse, '{x(y:\'\')}') 379 | t.assert_error(parse, '{x(y:"\n")}') 380 | 381 | run('"yarn"', 'yarn', 'string') 382 | run('"th\\"read"', 'th"read', 'string') 383 | end 384 | 385 | function g.test_parse_value_enum() 386 | run('a', 'a', 'enum') 387 | end 388 | 389 | function g.test_parse_value_list() 390 | t.assert_error(parse, '{x(y:[)}') 391 | 392 | local value = run('[]') 393 | t.assert_equals(value.values, {}) 394 | 395 | value = run('[a 1]') 396 | t.assert_equals(value, { 397 | kind = 'list', 398 | values = { 399 | { 400 | kind = 'enum', 401 | value = 'a' 402 | }, 403 | { 404 | kind = 'int', 405 | value = '1' 406 | } 407 | } 408 | }) 409 | 410 | value = run('[a [b] c]') 411 | t.assert_equals(value, { 412 | kind = 'list', 413 | values = { 414 | { 415 | kind = 'enum', 416 | value = 'a' 417 | }, 418 | { 419 | kind = 'list', 420 | values = { 421 | { 422 | kind = 'enum', 423 | value = 'b' 424 | } 425 | } 426 | }, 427 | { 428 | kind = 'enum', 429 | value = 'c' 430 | } 431 | } 432 | }) 433 | end 434 | 435 | function g.test_parse_value_object() 436 | t.assert_error(parse, '{x(y:{a})}') 437 | t.assert_error(parse, '{x(y:{a:})}') 438 | t.assert_error(parse, '{x(y:{a::})}') 439 | t.assert_error(parse, '{x(y:{1:1})}') 440 | t.assert_error(parse, '{x(y:{"foo":"bar"})}') 441 | 442 | local value = run('{}') 443 | t.assert_equals(value.kind, 'inputObject') 444 | t.assert_equals(value.values, {}) 445 | 446 | value = run('{a:1}') 447 | t.assert_equals(value.values, { 448 | { 449 | name = 'a', 450 | value = { 451 | kind = 'int', 452 | value = '1' 453 | } 454 | } 455 | }) 456 | 457 | value = run('{a:1 b:2}') 458 | t.assert_equals(#value.values, 2) 459 | end 460 | 461 | function g.test_parse_namedType() 462 | t.assert_error(parse, 'query($a:$b){c}') 463 | 464 | local namedType = parse('query($a:b){ c }').definitions[1].variableDefinitions[1].type 465 | t.assert_equals(namedType.kind, 'namedType') 466 | t.assert_equals(namedType.name.value, 'b') 467 | end 468 | 469 | function g.test_parse_listType() 470 | t.assert_error(parse, 'query($a:[]){ b }') 471 | 472 | local listType = parse('query($a:[b]){ c }').definitions[1].variableDefinitions[1].type 473 | t.assert_equals(listType.kind, 'listType') 474 | t.assert_equals(listType.type.kind, 'namedType') 475 | t.assert_equals(listType.type.name.value, 'b') 476 | 477 | listType = parse('query($a:[[b]]){ c }').definitions[1].variableDefinitions[1].type 478 | t.assert_equals(listType.kind, 'listType') 479 | t.assert_equals(listType.type.kind, 'listType') 480 | end 481 | 482 | function g.test_parse_nonNullType() 483 | t.assert_error(parse, 'query($a:!){ b }') 484 | t.assert_error(parse, 'query($a:b!!){ c }') 485 | 486 | local nonNullType = parse('query($a:b!){ c }').definitions[1].variableDefinitions[1].type 487 | t.assert_equals(nonNullType.kind, 'nonNullType') 488 | t.assert_equals(nonNullType.type.kind, 'namedType') 489 | t.assert_equals(nonNullType.type.name.value, 'b') 490 | 491 | nonNullType = parse('query($a:[b]!) { c }').definitions[1].variableDefinitions[1].type 492 | t.assert_equals(nonNullType.kind, 'nonNullType') 493 | t.assert_equals(nonNullType.type.kind, 'listType') 494 | end 495 | 496 | local dogCommand = types.enum({ 497 | name = 'DogCommand', 498 | values = { 499 | SIT = true, 500 | DOWN = true, 501 | HEEL = true 502 | } 503 | }) 504 | 505 | local pet = types.interface({ 506 | name = 'Pet', 507 | fields = { 508 | name = types.string.nonNull, 509 | nickname = types.int 510 | } 511 | }) 512 | 513 | local dog = types.object({ 514 | name = 'Dog', 515 | interfaces = { pet }, 516 | fields = { 517 | name = types.string, 518 | nickname = types.string, 519 | barkVolume = types.int, 520 | doesKnowCommand = { 521 | kind = types.boolean.nonNull, 522 | arguments = { 523 | dogCommand = dogCommand.nonNull 524 | } 525 | }, 526 | isHouseTrained = { 527 | kind = types.boolean.nonNull, 528 | arguments = { 529 | atOtherHomes = types.boolean 530 | } 531 | }, 532 | complicatedField = { 533 | kind = types.boolean, 534 | arguments = { 535 | complicatedArgument = types.inputObject({ 536 | name = 'complicated', 537 | fields = { 538 | x = types.string, 539 | y = types.integer, 540 | z = types.inputObject({ 541 | name = 'alsoComplicated', 542 | fields = { 543 | x = types.string, 544 | y = types.integer 545 | } 546 | }) 547 | } 548 | }) 549 | } 550 | } 551 | } 552 | }) 553 | 554 | local sentient = types.interface({ 555 | name = 'Sentient', 556 | fields = { 557 | name = types.string.nonNull 558 | } 559 | }) 560 | 561 | local alien = types.object({ 562 | name = 'Alien', 563 | interfaces = sentient, 564 | fields = { 565 | name = types.string.nonNull, 566 | homePlanet = types.string 567 | } 568 | }) 569 | 570 | local human = types.object({ 571 | name = 'Human', 572 | fields = { 573 | name = types.string.nonNull 574 | } 575 | }) 576 | 577 | local cat = types.object({ 578 | name = 'Cat', 579 | fields = { 580 | name = types.string.nonNull, 581 | nickname = types.string, 582 | meowVolume = types.int 583 | } 584 | }) 585 | 586 | local catOrDog = types.union({ 587 | name = 'CatOrDog', 588 | types = { cat, dog } 589 | }) 590 | 591 | local dogOrHuman = types.union({ 592 | name = 'DogOrHuman', 593 | types = { dog, human } 594 | }) 595 | 596 | local humanOrAlien = types.union({ 597 | name = 'HumanOrAlien', 598 | types = { human, alien } 599 | }) 600 | 601 | local query = types.object({ 602 | name = 'Query', 603 | fields = { 604 | dog = { 605 | kind = dog, 606 | args = { 607 | name = { 608 | kind = types.string 609 | } 610 | } 611 | }, 612 | cat = cat, 613 | pet = pet, 614 | sentient = sentient, 615 | catOrDog = catOrDog, 616 | humanOrAlien = humanOrAlien, 617 | dogOrHuman = dogOrHuman, 618 | } 619 | }) 620 | 621 | local schema_instance = schema.create({ query = query }) 622 | local function expectError(message, document) 623 | if not message then 624 | validate(schema_instance, parse(document)) 625 | else 626 | t.assert_error_msg_contains(message, validate, schema_instance, parse(document)) 627 | end 628 | end 629 | 630 | function g.test_rules_uniqueOperationNames() 631 | -- errors if two operations have the same name 632 | expectError('Multiple operations exist named', [[ 633 | query foo { cat { name } } 634 | query foo { cat { name } } 635 | ]]) 636 | 637 | -- passes if all operations have different names 638 | expectError(nil, [[ 639 | query foo { cat { name } } 640 | query bar { cat { name } } 641 | ]]) 642 | end 643 | 644 | function g.test_rules_loneAnonymousOperation() 645 | local message = 'Cannot have more than one operation when' 646 | 647 | -- fails if there is more than one operation and one of them is anonymous' 648 | expectError(message, [[ 649 | query { cat { name } } 650 | query named { cat { name } } 651 | ]]) 652 | 653 | expectError(message, [[ 654 | query named { cat { name } } 655 | query { cat { name } } 656 | ]]) 657 | 658 | expectError(message, [[ 659 | query { cat { name } } 660 | query { cat { name } } 661 | ]]) 662 | 663 | -- passes if there is one anonymous operation 664 | expectError(nil, '{ cat { name } }') 665 | 666 | -- passes if there are two named operations 667 | expectError(nil, [[ 668 | query one { cat { name } } 669 | query two { cat { name } } 670 | ]]) 671 | end 672 | 673 | function g.test_rules_fieldsDefinedOnType() 674 | local message = 'is not defined on type' 675 | 676 | -- fails if a field does not exist on an object type 677 | expectError(message, '{ doggy { name } }') 678 | expectError(message, '{ dog { age } }') 679 | 680 | -- passes if all fields exist on object types 681 | expectError(nil, '{ dog { name } }') 682 | 683 | -- understands aliases 684 | expectError(nil, '{ doggy: dog { name } }') 685 | expectError(message, '{ dog: doggy { name } }') 686 | end 687 | 688 | function g.test_rules_argumentsDefinedOnType() 689 | local message = 'Non-existent argument' 690 | 691 | -- passes if no arguments are supplied 692 | expectError(nil, '{ dog { isHouseTrained } }') 693 | 694 | -- errors if an argument name does not match the schema 695 | expectError(message, [[{ 696 | dog { 697 | doesKnowCommand(doggyCommand: SIT) 698 | } 699 | }]]) 700 | 701 | -- errors if an argument is supplied to a field that takes none 702 | expectError(message, [[{ 703 | dog { 704 | name(truncateToLength: 32) 705 | } 706 | }]]) 707 | 708 | -- passes if all argument names match the schema 709 | expectError(nil, [[{ 710 | dog { 711 | doesKnowCommand(dogCommand: SIT) 712 | } 713 | }]]) 714 | end 715 | 716 | function g.test_rules_scalarFieldsAreLeaves() 717 | local message = 'cannot have subselections' 718 | 719 | -- fails if a scalar field has a subselection 720 | expectError(message, '{ dog { name { firstLetter } } }') 721 | 722 | -- passes if all scalar fields are leaves 723 | expectError(nil, '{ dog { name nickname } }') 724 | end 725 | 726 | function g.test_rules_compositeFieldsAreNotLeaves() 727 | local message = 'must have subselections' 728 | 729 | -- fails if an object is a leaf 730 | expectError(message, '{ dog }') 731 | 732 | -- fails if an interface is a leaf 733 | expectError(message, '{ pet }') 734 | 735 | -- fails if a union is a leaf 736 | expectError(message, '{ catOrDog }') 737 | 738 | -- passes if all composite types have subselections 739 | expectError(nil, '{ dog { name } pet { name } }') 740 | end 741 | 742 | function g.test_rules_unambiguousSelections() 743 | -- fails if two fields with identical response keys have different types 744 | expectError('Type name mismatch', [[{ 745 | dog { 746 | barkVolume 747 | barkVolume: name 748 | } 749 | }]]) 750 | 751 | -- fails if two fields have different argument sets 752 | expectError('Argument mismatch', [[{ 753 | dog { 754 | doesKnowCommand(dogCommand: SIT) 755 | doesKnowCommand(dogCommand: DOWN) 756 | } 757 | }]]) 758 | 759 | -- passes if fields are identical 760 | expectError(nil, [[{ 761 | dog { 762 | doesKnowCommand(dogCommand: SIT) 763 | doesKnowCommand: doesKnowCommand(dogCommand: SIT) 764 | } 765 | }]]) 766 | end 767 | 768 | function g.test_rules_uniqueArgumentNames() 769 | local message = 'Encountered multiple arguments named' 770 | 771 | -- fails if a field has two arguments with the same name 772 | expectError(message, [[{ 773 | dog { 774 | doesKnowCommand(dogCommand: SIT, dogCommand: DOWN) 775 | } 776 | }]]) 777 | end 778 | 779 | function g.test_rules_argumentsOfCorrectType() 780 | -- fails if an argument has an incorrect type 781 | expectError('Expected enum value', [[{ 782 | dog { 783 | doesKnowCommand(dogCommand: 4) 784 | } 785 | }]]) 786 | end 787 | 788 | function g.test_rules_requiredArgumentsPresent() 789 | local message = 'was not supplied' 790 | 791 | -- fails if a non-null argument is not present 792 | expectError(message, [[{ 793 | dog { 794 | doesKnowCommand 795 | } 796 | }]]) 797 | end 798 | 799 | function g.test_rules_uniqueFragmentNames() 800 | local message = 'Encountered multiple fragments named' 801 | 802 | -- fails if there are two fragment definitions with the same name 803 | expectError(message, [[ 804 | query { dog { ...nameFragment } } 805 | fragment nameFragment on Dog { name } 806 | fragment nameFragment on Dog { name } 807 | ]]) 808 | 809 | -- passes if all fragment definitions have different names 810 | expectError(nil, [[ 811 | query { dog { ...one ...two } } 812 | fragment one on Dog { name } 813 | fragment two on Dog { name } 814 | ]]) 815 | end 816 | 817 | function g.test_rules_fragmentHasValidType() 818 | -- fails if a framgent refers to a non-composite type 819 | expectError('Fragment type must be an Object, Interface, or Union', 820 | 'fragment f on DogCommand { name }') 821 | 822 | -- fails if a fragment refers to a non-existent type 823 | expectError('Fragment refers to non-existent type', 'fragment f on Hyena { a }') 824 | 825 | -- passes if a fragment refers to a composite type 826 | expectError(nil, '{ dog { ...f } } fragment f on Dog { name }') 827 | end 828 | 829 | function g.test_rules_noUnusedFragments() 830 | local message = 'was not used' 831 | 832 | -- fails if a fragment is not used 833 | expectError(message, 'fragment f on Dog { name }') 834 | end 835 | 836 | function g.test_rules_fragmentSpreadTargetDefined() 837 | local message = 'Fragment spread refers to non-existent' 838 | 839 | -- fails if the fragment does not exist 840 | expectError(message, '{ dog { ...f } }') 841 | end 842 | 843 | function g.test_rules_fragmentDefinitionHasNoCycles() 844 | local message = 'Fragment definition has cycles' 845 | 846 | -- fails if a fragment spread has cycles 847 | expectError(message, [[ 848 | { dog { ...f } } 849 | fragment f on Dog { ...g } 850 | fragment g on Dog { ...h } 851 | fragment h on Dog { ...f } 852 | ]]) 853 | end 854 | 855 | function g.test_rules_fragmentSpreadIsPossible() 856 | local message = 'Fragment type condition is not possible' 857 | 858 | -- fails if a fragment type condition refers to a different object than the parent object 859 | expectError(message, [[ 860 | { dog { ...f } } 861 | fragment f on Cat { name } 862 | ]]) 863 | 864 | 865 | -- fails if a fragment type condition refers to an interface that the parent object does not implement 866 | expectError(message, [[ 867 | { dog { ...f } } 868 | fragment f on Sentient { name } 869 | ]]) 870 | 871 | 872 | -- fails if a fragment type condition refers to a union that the parent object does not belong to 873 | expectError(message, [[ 874 | { dog { ...f } } 875 | fragment f on HumanOrAlien { name } 876 | ]]) 877 | 878 | end 879 | 880 | function g.test_rules_uniqueInputObjectFields() 881 | local message = 'Multiple input object fields named' 882 | 883 | -- fails if an input object has two fields with the same name 884 | expectError(message, [[ 885 | { 886 | dog { 887 | complicatedField(complicatedArgument: {x: "hi", x: "hi"}) 888 | } 889 | } 890 | ]]) 891 | 892 | 893 | -- passes if an input object has nested fields with the same name 894 | expectError(nil, [[ 895 | { 896 | dog { 897 | complicatedField(complicatedArgument: {x: "hi", z: {x: "hi"}}) 898 | } 899 | } 900 | ]]) 901 | 902 | end 903 | 904 | function g.test_rules_directivesAreDefined() 905 | local message = 'Unknown directive' 906 | 907 | -- fails if a directive does not exist 908 | expectError(message, 'query @someRandomDirective { op }') 909 | 910 | 911 | -- passes if directives exists 912 | expectError(nil, 'query @skip { dog { name } }') 913 | end 914 | 915 | function g.test_types_isValueOfTheType_for_scalars() 916 | local function isString(value) 917 | return type(value) == 'string' 918 | end 919 | 920 | local function coerceString(value) 921 | if value ~= nil then 922 | value = tostring(value) 923 | if not isString(value) then return end 924 | end 925 | 926 | return value 927 | end 928 | 929 | t.assert_error(function() 930 | types.scalar({ 931 | name = 'MyString', 932 | description = 'Custom string type', 933 | serialize = coerceString, 934 | parseValue = coerceString, 935 | parseLiteral = function(node) 936 | return coerceString(node.value) 937 | end, 938 | }) 939 | end) 940 | 941 | local CustomString = types.scalar({ 942 | name = 'MyString', 943 | description = 'Custom string type', 944 | serialize = coerceString, 945 | parseValue = coerceString, 946 | parseLiteral = function(node) 947 | return coerceString(node.value) 948 | end, 949 | isValueOfTheType = isString, 950 | }) 951 | t.assert_equals(CustomString.__type, 'Scalar') 952 | end 953 | 954 | function g.test_types_for_different_schemas() 955 | local object_1 = types.object({ 956 | name = 'Object', 957 | fields = { 958 | long_1 = types.long, 959 | string_1 = types.string, 960 | }, 961 | schema = '1', 962 | }) 963 | 964 | local query_1 = types.object({ 965 | name = 'Query', 966 | fields = { 967 | object = { 968 | kind = object_1, 969 | args = { 970 | name = { 971 | string = types.string 972 | } 973 | } 974 | }, 975 | object_list = types.list('Object'), 976 | }, 977 | schema = '1', 978 | }) 979 | 980 | local object_2 = types.object({ 981 | name = 'Object', 982 | fields = { 983 | long_2 = types.long, 984 | string_2 = types.string, 985 | }, 986 | schema = '2', 987 | }) 988 | 989 | local query_2 = types.object({ 990 | name = 'Query', 991 | fields = { 992 | object = { 993 | kind = object_2, 994 | args = { 995 | name = { 996 | string = types.string 997 | } 998 | } 999 | }, 1000 | object_list = types.list('Object'), 1001 | }, 1002 | schema = '2', 1003 | }) 1004 | 1005 | local schema_1 = schema.create({query = query_1}, '1') 1006 | local schema_2 = schema.create({query = query_2}, '2') 1007 | 1008 | validate(schema_1, parse([[ 1009 | query { object { long_1 string_1 } } 1010 | ]])) 1011 | 1012 | validate(schema_2, parse([[ 1013 | query { object { long_2 string_2 } } 1014 | ]])) 1015 | 1016 | validate(schema_1, parse([[ 1017 | query { object_list { long_1 string_1 } } 1018 | ]])) 1019 | 1020 | validate(schema_2, parse([[ 1021 | query { object_list { long_2 string_2 } } 1022 | ]])) 1023 | 1024 | -- Errors 1025 | t.assert_error_msg_contains('Field "long_2" is not defined on type "Object"', 1026 | validate, schema_1, parse([[query { object { long_2 string_1 } }]])) 1027 | t.assert_error_msg_contains('Field "string_2" is not defined on type "Object"', 1028 | validate, schema_1, parse([[query { object { long_1 string_2 } }]])) 1029 | 1030 | t.assert_error_msg_contains('Field "long_2" is not defined on type "Object"', 1031 | validate, schema_1, parse([[query { object_list { long_2 string_1 } }]])) 1032 | t.assert_error_msg_contains('Field "string_2" is not defined on type "Object"', 1033 | validate, schema_1, parse([[query { object_list { long_1 string_2 } }]])) 1034 | end 1035 | 1036 | function g.test_boolean_coerce() 1037 | local query = types.object({ 1038 | name = 'Query', 1039 | fields = { 1040 | test_boolean = { 1041 | kind = types.boolean.nonNull, 1042 | arguments = { 1043 | value = types.boolean, 1044 | non_null_value = types.boolean.nonNull, 1045 | } 1046 | }, 1047 | } 1048 | }) 1049 | 1050 | local test_schema = schema.create({query = query}) 1051 | 1052 | validate(test_schema, parse([[ { test_boolean(value: true, non_null_value: true) } ]])) 1053 | validate(test_schema, parse([[ { test_boolean(value: false, non_null_value: false) } ]])) 1054 | validate(test_schema, parse([[ { test_boolean(value: null, non_null_value: true) } ]])) 1055 | 1056 | -- Errors 1057 | t.assert_error_msg_contains('Could not coerce value "True" with type "enum" to type boolean', 1058 | validate, test_schema, parse([[ { test_boolean(value: True) } ]])) 1059 | t.assert_error_msg_contains('Could not coerce value "123" with type "int" to type boolean', 1060 | validate, test_schema, parse([[ { test_boolean(value: 123) } ]])) 1061 | t.assert_error_msg_contains('Could not coerce value "value" with type "string" to type boolean', 1062 | validate, test_schema, parse([[ { test_boolean(value: "value") } ]])) 1063 | end 1064 | 1065 | function g.test_util_map_name() 1066 | local res = util.map_name(nil, nil) 1067 | t.assert_equals(res, {}) 1068 | 1069 | res = util.map_name({ { name = 'a' }, { name = 'b' }, }, function(v) return v end) 1070 | t.assert_equals(res, {a = {name = 'a'}, b = {name = 'b'}}) 1071 | 1072 | res = util.map_name({ entry_a = { name = 'a' }, entry_b = { name = 'b' }, }, function(v) return v end) 1073 | t.assert_equals(res, {a = {name = 'a'}, b = {name = 'b'}}) 1074 | end 1075 | 1076 | function g.test_util_find_by_name() 1077 | local res = util.find_by_name({}, 'var') 1078 | t.assert_equals(res, nil) 1079 | 1080 | res = util.find_by_name({ { name = 'avr' } }, 'var') 1081 | t.assert_equals(res, nil) 1082 | 1083 | res = util.find_by_name({ { name = 'avr', value = 1 }, { name = 'var', value = 2 } }, 'var') 1084 | t.assert_equals(res, { name = 'var', value = 2 }) 1085 | 1086 | res = util.find_by_name( 1087 | { 1088 | entry1 = { name = 'avr', value = 1 }, 1089 | entry2 = { name = 'var', value = 2 } 1090 | }, 1091 | 'var') 1092 | t.assert_equals(res, { name = 'var', value = 2 }) 1093 | end 1094 | 1095 | g.test_version = function() 1096 | t.assert_type(require('graphql')._VERSION, 'string') 1097 | end 1098 | 1099 | g.test_deprecated_version = function() 1100 | local capture = luatest_capture:new() 1101 | capture:enable() 1102 | 1103 | t.assert_type(require('graphql').VERSION, 'string') 1104 | local stdout = helpers.fflush_main_server_output(nil, capture) 1105 | capture:disable() 1106 | 1107 | t.assert_str_contains( 1108 | stdout, 1109 | "require('graphql').VERSION is deprecated, " .. 1110 | "use require('graphql')._VERSION instead.") 1111 | end 1112 | 1113 | function g.test_is_array() 1114 | t.assert_equals(util.is_array({[3] = 'a', [1] = 'b', [6] = 'c'}), true) 1115 | t.assert_equals(util.is_array({[3] = 'a', [1] = 'b', [7] = 'c'}), true) 1116 | t.assert_equals(util.is_array({[3] = 'a', nil, [6] = 'c'}), true) 1117 | t.assert_equals(util.is_array({[3] = 'a', nil, [7] = 'c'}), true) 1118 | t.assert_equals(util.is_array({[0] = 'a', [1] = 'b', [6] = 'c'}), true) 1119 | t.assert_equals(util.is_array({[-1] = 'a', [1] = 'b', [6] = 'c'}), false) 1120 | t.assert_equals(util.is_array({[3] = 'a', b = 'b', [6] = 'c'}), false) 1121 | t.assert_equals(util.is_array({}), true) 1122 | t.assert_equals(util.is_array(), false) 1123 | t.assert_equals(util.is_array(''), false) 1124 | t.assert_equals(util.is_array({a = 'a', b = 'b', c = 'c'}), false) 1125 | t.assert_equals(util.is_array({a = 'a', nil, c = 'c'}), false) 1126 | end 1127 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/graphql/e0a19a5845963692a7447b69c5c716c664007582/tmp/.keep --------------------------------------------------------------------------------