./ct_report/coverage/mongoose_graphql_directive.COVER.html

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 4067 #{<<"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 29682 #{operation_name := OpName} = Ctx,
46 29682 F = fun(#op{schema = RootObject, selection_set = Set} = Op) ->
47 29682 {RootObject2, Ctx2} = handle_object_directives(RootObject, Ctx),
48 29682 Ctx3 =
49 Ctx2#{parent => RootObject2,
50 definitions => Definitions,
51 path => [op_name(OpName)]},
52 29682 #{set := ResSet, parent := RootObject3} = process_selection_set(Set, Ctx3),
53 29680 Op#op{schema = RootObject3, selection_set = ResSet}
54 end,
55 29682 Definitions2 = lists:map(fun(D) -> if_req_operation(D, OpName, F) end, Definitions),
56 29680 #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 904239 Fun = fun(S, #{set := AccSet, parent := AccParent}) ->
63 874594 Ctx2 = Ctx#{parent := AccParent},
64 874594 #{schema := Schema, parent := Parent} = process_selection(S, Ctx2),
65 874592 #{set => [Schema | AccSet], parent => Parent}
66 end,
67 904239 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 873752 case Schema of
78 #schema_field{ty = FieldType, directives = Directives} ->
79 873717 Ctx2 = append_path(Ctx, name(Id)),
80 %% Process field type (object) directives
81 873717 {FieldType2, FieldTypeCtx} = handle_object_directives(unwrap_type(FieldType), Ctx2),
82 %% Process field directives
83 873715 ObjField =
84 handle_directives(Directives,
85 get_object_field(Id, Ctx2),
86 set_field_args(FieldArgs, Ctx2)),
87 %% Process field type fields
88 873715 #{set := ResSet, parent := FieldType3} =
89 process_selection_set(Set, set_parent_object(FieldType2, FieldTypeCtx)),
90 873715 FieldType4 = wrap_new_type(FieldType, FieldType3),
91 873715 #{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 35 #{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 842 {ObjectType2, ObjectCtx} = handle_object_directives(ObjectType, Ctx),
102 842 #{set := ResSet, parent := ObjectType3} =
103 process_selection_set(Set, set_parent_object(ObjectType2, ObjectCtx)),
104 842 #{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 904241 Fun = fun(D, {Obj, ObjCtx}) -> handle_object_directive(D, Obj, ObjCtx) end,
118 904241 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 193859 Directives;
125 get_directives(#union_type{directives = Directives}) ->
126 304 Directives;
127 get_directives(_) ->
128 710078 [].
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 2470 handle_object_directive(D#directive{id = Name}, Field, Ctx);
134 handle_object_directive(#directive{id = Name} = D, Field, Ctx) ->
135 2470 Module = maps:get(Name, dir_handlers()),
136 2470 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 873715 Fun = fun(D, FieldSchema) -> handle_directive(D, FieldSchema, Ctx) end,
141 873715 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 1597 handle_directive(D#directive{id = Name}, Field, Ctx);
146 handle_directive(#directive{id = Name} = D, Field, Ctx) ->
147 1597 Module = maps:get(Name, dir_handlers()),
148 1597 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 29682 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 873715 maps:from_list([prepare_arg(ArgName, Type, Params) || {ArgName, Type} <- FieldArgs]).
169
170 prepare_arg(ArgName, #{value := #var{id = Name}}, Vars) ->
171 30682 {ArgName, maps:get(name(Name), Vars, null)};
172 prepare_arg(ArgName, #{value := Val}, _) ->
173 28979 {ArgName, Val}.
174
175 %% GraphQL type helpers
176
177 unwrap_type({non_null, {list, T}}) ->
178 190 T;
179 unwrap_type({non_null, T}) ->
180 586818 T;
181 unwrap_type({list, T}) ->
182 149167 T;
183 unwrap_type(T) ->
184 1011257 T.
185
186 wrap_new_type({non_null, {list, _}}, T) ->
187 190 {non_null, {list, T}};
188 wrap_new_type({list, _}, T) ->
189 149167 {list, T};
190 wrap_new_type({non_null, _}, T) ->
191 284319 {non_null, T};
192 wrap_new_type(_, T) ->
193 440039 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 302499 {non_null, Fun(T)};
199 update_wrapped_type({list, T}, Fun) ->
200
:-(
{list, Fun(T)};
201 update_wrapped_type(T, Fun) ->
202 571216 Fun(T).
203
204 %% Context helpers
205
206 append_path(#{path := Path} = Ctx, FieldName) ->
207 873717 Ctx#{path => [FieldName | Path]}.
208
209 set_field_args(FieldArgs, #{params := Params} = Ctx) ->
210 873715 Ctx#{field_args => field_args_to_map(FieldArgs, Params)}.
211
212 set_parent_object(Parent, Ctx) ->
213 874557 Ctx#{parent => Parent}.
214
215 get_object_field(Id, #{parent := Parent}) ->
216 873715 Fields =
217 case unwrap_type(Parent) of
218 #object_type{fields = Fs} ->
219 873715 Fs;
220 #interface_type{fields = Fs} ->
221
:-(
Fs
222 end,
223 873715 maps:get(name(Id), Fields).
224
225 update_object_field(Id, ObjField, #{parent := ParentType}) ->
226 873715 Fun = fun (#object_type{} = Obj) ->
227 873715 Fields = maps:put(name(Id), ObjField, Obj#object_type.fields),
228 873715 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 873715 update_wrapped_type(ParentType, Fun).
Line Hits Source