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

Commit ecdb29d

Browse files
committed
Integrate expressions into GraphQL
Fixes #13.
1 parent 8a070d2 commit ecdb29d

11 files changed

+753
-348
lines changed

graphql/accessor_general.lua

+36-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ local avro_helpers = require('graphql.avro_helpers')
1414
local db_schema_helpers = require('graphql.db_schema_helpers')
1515
local error_codes = require('graphql.error_codes')
1616
local statistics = require('graphql.statistics')
17+
local expressions = require('graphql.expressions')
1718

1819
local check = utils.check
1920
local e = error_codes
@@ -762,16 +763,16 @@ local function validate_collections(collections, schemas)
762763
end
763764
end
764765

765-
--- Whether an object match set of PCRE.
766-
---
767-
--- @tparam table obj an object to check
766+
--- Whether an object match set of PCREs.
768767
---
769768
--- @tparam table pcre map with PCRE as values; names are correspond to field
770769
--- names of the `obj` to match
771770
---
771+
--- @tparam table obj an object to check
772+
---
772773
--- @treturn boolean `res` whether the `obj` object match `pcre` set of
773774
--- regexps.
774-
local function match_using_re(obj, pcre)
775+
local function match_using_re(pcre, obj)
775776
if pcre == nil then return true end
776777

777778
assert(rex ~= nil, 'we should not pass over :compile() ' ..
@@ -784,7 +785,7 @@ local function match_using_re(obj, pcre)
784785
return false
785786
end
786787
if type(re) == 'table' then
787-
local match = match_using_re(obj[field_name], re)
788+
local match = match_using_re(re, obj[field_name])
788789
if not match then return false end
789790
elseif not utils.regexp(re, obj[field_name]) then
790791
return false
@@ -794,6 +795,24 @@ local function match_using_re(obj, pcre)
794795
return true
795796
end
796797

798+
--- Whether an object match an expression.
799+
---
800+
--- @param expr (table or string) compiled or raw expression
801+
---
802+
--- @tparam table obj an object to check
803+
---
804+
--- @tparam[opt] table variables variables values from the request
805+
---
806+
--- @treturn boolean `res` whether the `obj` object match `expr` expression
807+
local function match_using_expr(expr, obj, variables)
808+
if expr == nil then return true end
809+
810+
check(expr, 'expression', 'table')
811+
local res = expr:execute(obj, variables)
812+
check(res, 'expression result', 'boolean')
813+
return res
814+
end
815+
797816
--- Check whether we meet deadline time.
798817
---
799818
--- The functions raises an exception in the case.
@@ -834,6 +853,7 @@ end
834853
--- * `pivot_filter` (table, set of fields to match the objected pointed by
835854
--- `offset` arqument of the GraphQL query),
836855
--- * `resolveField` (function) for subrequests, see @{impl.new}.
856+
--- * XXX: describe other fields.
837857
---
838858
--- @return nil
839859
---
@@ -864,7 +884,9 @@ local function process_tuple(self, state, tuple, opts)
864884

865885
local collection_name = opts.collection_name
866886
local pcre = opts.pcre
887+
local expr = opts.expr
867888
local resolveField = opts.resolveField
889+
local variables = qcontext.variables
868890

869891
-- convert tuple -> object
870892
local obj = opts.unflatten_tuple(self, collection_name, tuple,
@@ -895,7 +917,7 @@ local function process_tuple(self, state, tuple, opts)
895917

896918
-- filter out non-matching objects
897919
local match = utils.is_subtable(obj, truncated_filter) and
898-
match_using_re(obj, pcre)
920+
match_using_re(pcre, obj) and match_using_expr(expr, obj, variables)
899921
if do_filter then
900922
if not match then return true end
901923
else
@@ -1035,6 +1057,13 @@ local function prepare_select_internal(self, collection_name, from, filter,
10351057
qcontext = qcontext
10361058
}
10371059

1060+
-- compile an expression argument if provided and did not compiled yet
1061+
local expr = args.filter
1062+
check(expr, 'expression', 'table', 'string', 'nil')
1063+
if type(expr) == 'string' then
1064+
expr = expressions.new(expr)
1065+
end
1066+
10381067
-- read only process_tuple options
10391068
local select_opts = {
10401069
limit = args.limit,
@@ -1046,6 +1075,7 @@ local function prepare_select_internal(self, collection_name, from, filter,
10461075
use_tomap = self.collection_use_tomap[collection_name] or false,
10471076
default_unflatten_tuple = default_unflatten_tuple,
10481077
pcre = args.pcre,
1078+
expr = expr,
10491079
resolveField = extra.resolveField,
10501080
is_hidden = extra.is_hidden,
10511081
}

graphql/core/rules.lua

+7
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,11 @@ function rules.variableDefaultValuesHaveCorrectType(node, context)
415415
end
416416

417417
function rules.variablesAreUsed(node, context)
418+
local operationName = node.name and node.name.value or ''
419+
if context.skipVariableUseCheck[operationName] then
420+
return
421+
end
422+
418423
if node.variableDefinitions then
419424
for _, definition in ipairs(node.variableDefinitions) do
420425
local variableName = definition.variable.name.value
@@ -605,3 +610,5 @@ end
605610
-- }}}
606611

607612
return rules
613+
614+
-- vim: set ts=2 sw=2 et:

graphql/core/util.lua

+5
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ function util.coerceValue(node, schemaType, variables, opts)
9292
return nil
9393
end
9494

95+
-- handle precompiled values
96+
if node.compiled ~= nil then
97+
return node.compiled
98+
end
99+
95100
if node.kind == 'variable' then
96101
return variables[node.name.value]
97102
end

graphql/core/validate.lua

+47-21
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ local function getParentField(context, name, count)
1818
return parent.fields[name]
1919
end
2020

21-
local visitors = {
21+
local defaultVisitors = {
2222
-- <document>
2323
--
2424
-- {
@@ -467,7 +467,7 @@ local visitors = {
467467
}
468468
}
469469

470-
return function(schema, tree)
470+
return function(schema, tree, extraVisitors)
471471
local context = {
472472
schema = schema,
473473
fragmentMap = {},
@@ -476,41 +476,67 @@ return function(schema, tree)
476476
usedFragments = {},
477477
objects = {},
478478
currentOperation = nil,
479-
variableReferences = nil
479+
variableReferences = nil,
480+
skipVariableUseCheck = {}, -- operation name -> boolean
480481
}
481482

482483
local function visit(node)
483-
local visitor = node.kind and visitors[node.kind]
484+
local visitors = {}
485+
visitors[#visitors + 1] = node.kind and defaultVisitors[node.kind]
486+
visitors[#visitors + 1] = node.kind and extraVisitors[node.kind]
487+
488+
local enterList = {}
489+
local ruleList = {}
490+
local childrenList = {}
491+
local exitRuleList = {}
492+
local exitList = {}
493+
494+
-- collect visitors methods
495+
for _, visitor in ipairs(visitors) do
496+
enterList[#enterList + 1] = visitor.enter
497+
498+
if visitor.rules then
499+
for _, rule in ipairs(visitor.rules) do
500+
ruleList[#ruleList + 1] = rule
501+
end
502+
end
503+
504+
childrenList[#childrenList + 1] = visitor.children
505+
506+
if visitor.rules and visitor.rules.exit then
507+
for _, exitRule in ipairs(visitor.rules.exit) do
508+
exitRuleList[#exitRuleList + 1] = exitRule
509+
end
510+
end
511+
512+
exitList[#exitList + 1] = visitor.exit
513+
end
484514

485-
if not visitor then return end
515+
-- invoke visitors methods
486516

487-
if visitor.enter then
488-
visitor.enter(node, context)
517+
for _, enter in ipairs(enterList) do
518+
enter(node, context)
489519
end
490520

491-
if visitor.rules then
492-
for i = 1, #visitor.rules do
493-
visitor.rules[i](node, context)
494-
end
521+
for _, rule in ipairs(ruleList) do
522+
rule(node, context)
495523
end
496524

497-
if visitor.children then
498-
local children = visitor.children(node)
499-
if children then
500-
for _, child in ipairs(children) do
525+
for _, children in ipairs(childrenList) do
526+
local childs = children(node)
527+
if childs then
528+
for _, child in ipairs(childs) do
501529
visit(child)
502530
end
503531
end
504532
end
505533

506-
if visitor.rules and visitor.rules.exit then
507-
for i = 1, #visitor.rules.exit do
508-
visitor.rules.exit[i](node, context)
509-
end
534+
for _, exitRule in ipairs(exitRuleList) do
535+
exitRule(node, context)
510536
end
511537

512-
if visitor.exit then
513-
visitor.exit(node, context)
538+
for _, exit in ipairs(exitList) do
539+
exit(node, context)
514540
end
515541
end
516542

graphql/expressions.lua

-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
local lpeg = require('lulpeg')
22
local utils = require('graphql.utils')
33

4-
--- NOTE: Functions that worth moving out into other modules:
5-
--- 1) Regexp implementation (also lies inside of
6-
--- accessor_general.lua)
7-
--- 2) Vararg iterator.
8-
94
--- TODO:
105
--- 1) Validation.
116
--- 2) Big numbers.

graphql/extend_ast.lua

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
local expressions = require('graphql.expressions')
2+
3+
local extend_ast = {}
4+
5+
local function compile_filter_argument(node, context)
6+
assert(node.kind == 'argument')
7+
local argument_node = node
8+
local argument_name = argument_node.name.value
9+
10+
if argument_name ~= 'filter' then return end
11+
12+
-- save compiled expression to <string> node
13+
local value_node = argument_node.value
14+
if value_node.kind == 'variable' then
15+
-- a filter passed as a variable can use any other variables, so we disable
16+
-- this check for the operation with such filter
17+
local operation = context.currentOperation
18+
local operation_name = operation.name and operation.name.value or ''
19+
context.skipVariableUseCheck[operation_name] = true
20+
return
21+
end
22+
assert(value_node.kind == 'string' and type(value_node.value) == 'string',
23+
'"filter" list filtering argument must be a variable or a literal ' ..
24+
'string')
25+
local string_node = value_node
26+
local value = string_node.value
27+
local compiled_expr = expressions.new(value)
28+
string_node.compiled = compiled_expr
29+
30+
-- XXX: don't blindly find the pattern {kind = 'variable', ...}, but either
31+
-- traverse a tree according to node kinds or export used variables info
32+
-- from expressions module
33+
34+
-- mark used variables
35+
local open_set = {compiled_expr}
36+
while true do
37+
local e_node = table.remove(open_set, 1)
38+
if e_node == nil then break end
39+
if type(e_node) == 'table' then
40+
if e_node.kind == 'variable' then
41+
context.variableReferences[e_node.name] = true
42+
else
43+
for _, child_e_node in pairs(e_node) do
44+
table.insert(open_set, child_e_node)
45+
end
46+
end
47+
end
48+
end
49+
end
50+
51+
--- Visitors to pass to validate.lua.
52+
function extend_ast.visitors()
53+
return {
54+
argument = {
55+
rules = {
56+
compile_filter_argument,
57+
}
58+
}
59+
}
60+
end
61+
62+
return extend_ast

graphql/gen_arguments.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ function gen_arguments.list_args(db_schema, collection_name)
348348
return {
349349
{name = 'limit', type = 'int*'},
350350
{name = 'offset', type = offset_type},
351-
-- {name = 'filter', type = ...},
351+
{name = 'filter', type = 'string*'},
352352
pcre_field,
353353
}
354354
end

graphql/impl.lua

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ local simple_config = require('graphql.simple_config')
1414
local config_complement = require('graphql.config_complement')
1515
local server = require('graphql.server.server')
1616
local convert_schema = require('graphql.convert_schema')
17+
local extend_ast = require('graphql.extend_ast')
1718

1819
local utils = require('graphql.utils')
1920
local check = utils.check
@@ -51,6 +52,7 @@ local function gql_execute(qstate, variables, operation_name)
5152

5253
local qcontext = {
5354
query_settings = qstate.query_settings,
55+
variables = variables,
5456
}
5557

5658
local traceback
@@ -187,7 +189,7 @@ local function gql_compile(state, query, opts)
187189
local opts = opts or {}
188190

189191
local ast = parse(query)
190-
validate(state.schema, ast)
192+
validate(state.schema, ast, extend_ast.visitors())
191193

192194
local qstate = {
193195
state = state,

graphql/utils.lua

+3-3
Original file line numberDiff line numberDiff line change
@@ -309,11 +309,11 @@ end
309309

310310
--- Compare pattern with a string using pcre.
311311
---
312-
--- @param pattern matching pattern
312+
--- @tparam table pattern matching pattern
313313
---
314-
--- @param string string to match
314+
--- @tparam string string to match
315315
---
316-
--- @return true or false
316+
--- @treturn boolean true or false
317317
function utils.regexp(pattern, string)
318318
local rex, is_pcre2 = utils.optional_require_rex()
319319
local flags = rex.flags()

0 commit comments

Comments
 (0)