1 |
|
%% @doc Process directives in order to validate or modify the given document. |
2 |
|
%% |
3 |
|
%% GraphQL has directives that allow attaching additional information to the schema, objects, |
4 |
|
%% fields, and more. We decided to use directives to check if the user is allowed to execute |
5 |
|
%% commands and if commands require loaded modules and services. |
6 |
|
%% |
7 |
|
%% The behavior consists of two callbacks. One handles field directives, and the second one |
8 |
|
%% takes object directives. Callbacks can modify the document AST. |
9 |
|
-module(mongoose_graphql_directive). |
10 |
|
|
11 |
|
-export([process_directives/2]). |
12 |
|
|
13 |
|
-include_lib("graphql/src/graphql_schema.hrl"). |
14 |
|
-include_lib("graphql/src/graphql_internal.hrl"). |
15 |
|
-include_lib("graphql/include/graphql.hrl"). |
16 |
|
|
17 |
|
-import(mongoose_graphql_directive_helper, [name/1, op_name/1]). |
18 |
|
|
19 |
|
-type document() :: #document{}. |
20 |
|
-type ctx() :: #{atom() => term()}. |
21 |
|
-type parent() :: term(). |
22 |
|
-type process_set_result() :: #{parent := parent(), set := [selection_set()]}. |
23 |
|
-type process_result() :: #{parent := parent(), schema := selection_set()}. |
24 |
|
-type directive() :: graphql:directive(). |
25 |
|
|
26 |
|
-export_type([ctx/0]). |
27 |
|
|
28 |
|
%% Can modify field or throw an graphql error to stop document execution. |
29 |
|
-callback handle_directive(Dir :: directive(), Field :: schema_field(), Ctx :: map()) -> |
30 |
|
schema_field(). |
31 |
|
%% Can modify object and ctx which is later passed to the field directives handler. |
32 |
|
-callback handle_object_directive(Dir :: directive(), |
33 |
|
Object :: schema_object(), |
34 |
|
Ctx :: ctx()) -> |
35 |
|
{schema_object(), ctx()}. |
36 |
|
|
37 |
|
dir_handlers() -> |
38 |
6323 |
#{<<"use">> => mongoose_graphql_directive_use, |
39 |
|
<<"protected">> => mongoose_graphql_directive_protected}. |
40 |
|
|
41 |
|
%% @doc Traverse given document and process schema directives. The directive handlers can modify |
42 |
|
%% the AST to impact the result or raise a graphql exception. |
43 |
|
-spec process_directives(ctx(), document()) -> document(). |
44 |
|
process_directives(Ctx, #document{definitions = Definitions}) -> |
45 |
36532 |
#{operation_name := OpName} = Ctx, |
46 |
36532 |
F = fun(#op{schema = RootObject, selection_set = Set} = Op) -> |
47 |
36532 |
{RootObject2, Ctx2} = handle_object_directives(RootObject, Ctx), |
48 |
36532 |
Ctx3 = |
49 |
|
Ctx2#{parent => RootObject2, |
50 |
|
definitions => Definitions, |
51 |
|
path => [op_name(OpName)]}, |
52 |
36532 |
#{set := ResSet, parent := RootObject3} = process_selection_set(Set, Ctx3), |
53 |
36348 |
Op#op{schema = RootObject3, selection_set = ResSet} |
54 |
|
end, |
55 |
36532 |
Definitions2 = lists:map(fun(D) -> if_req_operation(D, OpName, F) end, Definitions), |
56 |
36348 |
#document{definitions = Definitions2}. |
57 |
|
|
58 |
|
%% Internal |
59 |
|
|
60 |
|
-spec process_selection_set([selection_set()], ctx()) -> process_set_result(). |
61 |
|
process_selection_set(Set, #{parent := InParent} = Ctx) -> |
62 |
1102112 |
Fun = fun(S, #{set := AccSet, parent := AccParent}) -> |
63 |
1065805 |
Ctx2 = Ctx#{parent := AccParent}, |
64 |
1065805 |
#{schema := Schema, parent := Parent} = process_selection(S, Ctx2), |
65 |
1065439 |
#{set => [Schema | AccSet], parent => Parent} |
66 |
|
end, |
67 |
1102112 |
lists:foldl(Fun, #{set => [], parent => InParent}, Set). |
68 |
|
|
69 |
|
%% Process object field's directives. Return modified parent object and field. |
70 |
|
-spec process_selection(selection_set(), ctx()) -> process_result(). |
71 |
|
%% Process generic fields e.g. `{ categoryA { commandA } }` |
72 |
|
process_selection(F = #field{id = Id, |
73 |
|
selection_set = Set, |
74 |
|
args = FieldArgs, |
75 |
|
schema = Schema}, |
76 |
|
Ctx) -> |
77 |
1064855 |
case Schema of |
78 |
|
#schema_field{ty = FieldType, directives = Directives} -> |
79 |
1064814 |
Ctx2 = append_path(Ctx, name(Id)), |
80 |
|
%% Process field type (object) directives |
81 |
1064814 |
{FieldType2, FieldTypeCtx} = handle_object_directives(unwrap_type(FieldType), Ctx2), |
82 |
|
%% Process field directives |
83 |
1064812 |
ObjField = |
84 |
|
handle_directives(Directives, |
85 |
|
get_object_field(Id, Ctx2), |
86 |
|
set_field_args(FieldArgs, Ctx2)), |
87 |
|
%% Process field type fields |
88 |
1064630 |
#{set := ResSet, parent := FieldType3} = |
89 |
|
process_selection_set(Set, set_parent_object(FieldType2, FieldTypeCtx)), |
90 |
1064448 |
FieldType4 = wrap_new_type(FieldType, FieldType3), |
91 |
1064448 |
#{parent => update_object_field(Id, ObjField, Ctx2), |
92 |
|
schema => |
93 |
|
F#field{selection_set = ResSet, schema = Schema#schema_field{ty = FieldType4}}}; |
94 |
|
_ -> |
95 |
|
% Schema is not a `schema_field()` when a field is an introspection field |
96 |
41 |
#{parent => maps:get(parent, Ctx), schema => F} |
97 |
|
end; |
98 |
|
%% Process inline fragments e.g. `{ category {commandA { ... on Domain { domain }}}}` |
99 |
|
process_selection(F = #frag{selection_set = Set, schema = ObjectType}, Ctx) -> |
100 |
|
% FIXME think if frag is able to have annotations? |
101 |
950 |
{ObjectType2, ObjectCtx} = handle_object_directives(ObjectType, Ctx), |
102 |
950 |
#{set := ResSet, parent := ObjectType3} = |
103 |
|
process_selection_set(Set, set_parent_object(ObjectType2, ObjectCtx)), |
104 |
950 |
#{parent => maps:get(parent, Ctx), % Return unmodified parent object |
105 |
|
schema => F#frag{selection_set = ResSet, schema = ObjectType3}}; |
106 |
|
%% Process fragments e.g. |
107 |
|
%% ``` |
108 |
|
%% fragment DomainParts on Domain { domain enabled } |
109 |
|
%% query { category {commandA { ...DomainParts }}} |
110 |
|
%% ``` |
111 |
|
process_selection(F = #frag_spread{id = Id}, Ctx) -> |
112 |
:-( |
Res = process_selection(get_fragment(Id, Ctx), Ctx), |
113 |
:-( |
Res#{schema := F}. |
114 |
|
|
115 |
|
-spec handle_object_directives(schema_object(), ctx()) -> {schema_object(), ctx()}. |
116 |
|
handle_object_directives(Object, Ctx) -> |
117 |
1102296 |
Fun = fun(D, {Obj, ObjCtx}) -> handle_object_directive(D, Obj, ObjCtx) end, |
118 |
1102296 |
lists:foldl(Fun, {Object, Ctx#{field_args => []}}, get_directives(Object)). |
119 |
|
|
120 |
|
-spec get_directives(schema_object()) -> [directive()]. |
121 |
|
get_directives(#interface_type{directives = Directives}) -> |
122 |
:-( |
Directives; |
123 |
|
get_directives(#object_type{directives = Directives}) -> |
124 |
237029 |
Directives; |
125 |
|
get_directives(#union_type{directives = Directives}) -> |
126 |
354 |
Directives; |
127 |
|
get_directives(_) -> |
128 |
864913 |
[]. |
129 |
|
|
130 |
|
-spec handle_object_directive(directive(), object_type(), ctx()) -> |
131 |
|
{object_type(), ctx()}. |
132 |
|
handle_object_directive(#directive{id = {name, _, Name}} = D, Field, Ctx) -> |
133 |
3822 |
handle_object_directive(D#directive{id = Name}, Field, Ctx); |
134 |
|
handle_object_directive(#directive{id = Name} = D, Field, Ctx) -> |
135 |
3822 |
Module = maps:get(Name, dir_handlers()), |
136 |
3822 |
Module:handle_object_directive(D, Field, Ctx). |
137 |
|
|
138 |
|
-spec handle_directives([directive()], schema_field(), ctx()) -> schema_field(). |
139 |
|
handle_directives(Directives, Field, Ctx) -> |
140 |
1064812 |
Fun = fun(D, FieldSchema) -> handle_directive(D, FieldSchema, Ctx) end, |
141 |
1064812 |
lists:foldl(Fun, Field, Directives). |
142 |
|
|
143 |
|
-spec handle_directive(directive(), schema_field(), ctx()) -> schema_field(). |
144 |
|
handle_directive(#directive{id = {name, _, Name}} = D, Field, Ctx) -> |
145 |
2501 |
handle_directive(D#directive{id = Name}, Field, Ctx); |
146 |
|
handle_directive(#directive{id = Name} = D, Field, Ctx) -> |
147 |
2501 |
Module = maps:get(Name, dir_handlers()), |
148 |
2501 |
Module:handle_directive(D, Field, Ctx). |
149 |
|
|
150 |
|
-spec get_fragment(graphql:name(), ctx()) -> frag(). |
151 |
|
get_fragment(Id, #{definitions := Definitions}) -> |
152 |
:-( |
Name = name(Id), |
153 |
:-( |
Fun = fun (#frag{id = FId}) -> |
154 |
:-( |
name(FId) =:= Name; |
155 |
|
(_) -> |
156 |
:-( |
false |
157 |
|
end, |
158 |
:-( |
hd(lists:filter(Fun, Definitions)). |
159 |
|
|
160 |
|
if_req_operation(#op{id = 'ROOT'} = Op, undefined, Fun) -> |
161 |
36532 |
Fun(Op); |
162 |
|
if_req_operation(#op{id = {name, _, Name}} = Op, Name, Fun) -> |
163 |
:-( |
Fun(Op); |
164 |
|
if_req_operation(Op, _, _) -> |
165 |
:-( |
Op. |
166 |
|
|
167 |
|
field_args_to_map(FieldArgs, Params) -> |
168 |
1064812 |
maps:from_list([prepare_arg(ArgName, Type, Params) || {ArgName, Type} <- FieldArgs]). |
169 |
|
|
170 |
|
prepare_arg(ArgName, #{value := #var{id = Name}}, Vars) -> |
171 |
38326 |
{ArgName, maps:get(name(Name), Vars, null)}; |
172 |
|
prepare_arg(ArgName, #{value := Val}, _) -> |
173 |
35291 |
{ArgName, Val}. |
174 |
|
|
175 |
|
%% GraphQL type helpers |
176 |
|
|
177 |
|
unwrap_type({non_null, {list, T}}) -> |
178 |
235 |
T; |
179 |
|
unwrap_type({non_null, T}) -> |
180 |
714686 |
T; |
181 |
|
unwrap_type({list, T}) -> |
182 |
181556 |
T; |
183 |
|
unwrap_type(T) -> |
184 |
1233149 |
T. |
185 |
|
|
186 |
|
wrap_new_type({non_null, {list, _}}, T) -> |
187 |
232 |
{non_null, {list, T}}; |
188 |
|
wrap_new_type({list, _}, T) -> |
189 |
181517 |
{list, T}; |
190 |
|
wrap_new_type({non_null, _}, T) -> |
191 |
346267 |
{non_null, T}; |
192 |
|
wrap_new_type(_, T) -> |
193 |
536432 |
T. |
194 |
|
|
195 |
|
update_wrapped_type({non_null, {list, T}}, Fun) -> |
196 |
:-( |
{non_null, {list, Fun(T)}}; |
197 |
|
update_wrapped_type({non_null, T}, Fun) -> |
198 |
368419 |
{non_null, Fun(T)}; |
199 |
|
update_wrapped_type({list, T}, Fun) -> |
200 |
:-( |
{list, Fun(T)}; |
201 |
|
update_wrapped_type(T, Fun) -> |
202 |
696029 |
Fun(T). |
203 |
|
|
204 |
|
%% Context helpers |
205 |
|
|
206 |
|
append_path(#{path := Path} = Ctx, FieldName) -> |
207 |
1064814 |
Ctx#{path => [FieldName | Path]}. |
208 |
|
|
209 |
|
set_field_args(FieldArgs, #{params := Params} = Ctx) -> |
210 |
1064812 |
Ctx#{field_args => field_args_to_map(FieldArgs, Params)}. |
211 |
|
|
212 |
|
set_parent_object(Parent, Ctx) -> |
213 |
1065580 |
Ctx#{parent => Parent}. |
214 |
|
|
215 |
|
get_object_field(Id, #{parent := Parent}) -> |
216 |
1064812 |
Fields = |
217 |
|
case unwrap_type(Parent) of |
218 |
|
#object_type{fields = Fs} -> |
219 |
1064812 |
Fs; |
220 |
|
#interface_type{fields = Fs} -> |
221 |
:-( |
Fs |
222 |
|
end, |
223 |
1064812 |
maps:get(name(Id), Fields). |
224 |
|
|
225 |
|
update_object_field(Id, ObjField, #{parent := ParentType}) -> |
226 |
1064448 |
Fun = fun (#object_type{} = Obj) -> |
227 |
1064448 |
Fields = maps:put(name(Id), ObjField, Obj#object_type.fields), |
228 |
1064448 |
Obj#object_type{fields = Fields}; |
229 |
|
(#interface_type{} = Int) -> |
230 |
:-( |
Fields = maps:put(name(Id), ObjField, Int#interface_type.fields), |
231 |
:-( |
Int#interface_type{fields = Fields} |
232 |
|
end, |
233 |
1064448 |
update_wrapped_type(ParentType, Fun). |