./ct_report/coverage/mongoose_graphql_directive_use.COVER.html

1 %% @doc The custom directive `@use' specifies which modules or services have to be loaded
2 %% to execute the command. We can annotate both objects and fields. The args from object
3 %% annotation are aggregated and checked for each annotated object's field. Thus annotating
4 %% only a category is not enough because, on the object level, we do not know the host type
5 %% needed to check loaded modules.
6 %%
7 %% In below example <i>command1</i> will be checked for loaded modules, but <i>command2</i>
8 %% will not be because it is not annotated. The admin endpoint does not have a host type in context,
9 %% so we need to specify the `arg'.
10 %% ```
11 %% type Category @use(modules: ["module_a"]){
12 %% command1(domain: String!): String @use(arg: "domain")
13 %% command2: String
14 %%}
15 %%'''
16 %%
17 %% The user's endpoint context contains the authenticated user, so the host type is there,
18 %% and we do not need to specify the `arg'.
19 %% ```
20 %% type Category @use(modules: ["module_a"]){
21 %% command1: String @use
22 %% command2: String
23 %%}
24 %%'''
25
26 -module(mongoose_graphql_directive_use).
27
28 -behaviour(mongoose_graphql_directive).
29
30 -export([handle_directive/3, handle_object_directive/3]).
31
32 -include_lib("graphql/src/graphql_schema.hrl").
33 -include_lib("graphql/include/graphql.hrl").
34 -include_lib("jid/include/jid.hrl").
35
36 -include("mongoose.hrl").
37
38 -import(mongoose_graphql_directive_helper, [name/1, get_arg/2]).
39
40 -type host_type() :: mongooseim:host_type().
41 -type ctx() :: mongoose_graphql_directive:ctx().
42 -type use_ctx() ::
43 #{modules := [binary()],
44 services := [binary()],
45 internal_databases := [binary()],
46 arg => binary(),
47 atom => term()}.
48 -type dependency_type() :: internal_databases | modules | services.
49 -type dependency_name() :: binary().
50
51 %% @doc Check the collected modules and services and swap the field resolver if any of them
52 %% is not loaded. The new field resolver returns the error that some modules or services
53 %% are not loaded.
54 handle_directive(#directive{id = <<"use">>, args = Args}, #schema_field{} = Field, Ctx) ->
55 665 #{modules := Modules, services := Services, internal_databases := DB} =
56 UseCtx = aggregate_use_ctx(Args, Ctx),
57 665 Items = [{modules, filter_unloaded_modules(UseCtx, Ctx, Modules)},
58 {services, filter_unloaded_services(Services)},
59 {internal_databases, filter_unloaded_db(DB)}],
60 665 case lists:filter(fun({_, Names}) -> Names =/= [] end, Items) of
61 [] ->
62 534 Field;
63 NotLoaded ->
64 131 Fun = resolve_not_loaded_fun(NotLoaded),
65 131 Field#schema_field{resolve = Fun}
66 end.
67
68 %% @doc Collect the used modules and services to be checked for each field separately.
69 %% It cannot be checked here because the object directives have no access to the domain sometimes.
70 handle_object_directive(#directive{id = <<"use">>, args = Args}, Object, Ctx) ->
71 660 {Object, Ctx#{use_dir => aggregate_use_ctx(Args, Ctx)}}.
72
73 %% Internal
74
75 -spec get_arg_value(use_ctx(), ctx()) -> jid:jid() | jid:lserver() | mongooseim:host_type().
76 get_arg_value(#{arg := DomainArg}, #{field_args := FieldArgs}) ->
77 578 get_arg(DomainArg, FieldArgs);
78 get_arg_value(_UseCtx, #{user := #jid{lserver = Domain}}) ->
79 31 Domain;
80 get_arg_value(_UseCtx, #{admin := #jid{lserver = Domain}}) ->
81
:-(
Domain.
82
83 -spec aggregate_use_ctx(list(), ctx()) -> use_ctx().
84 aggregate_use_ctx(Args, #{use_dir := #{modules := Modules0, services := Services0,
85 internal_databases := Databases0}}) ->
86 660 #{modules := Modules, services := Services, internal_databases := Databases} =
87 UseCtx = prepare_use_dir_args(Args),
88 660 UpdatedModules = Modules0 ++ Modules,
89 660 UpdatedServices = Services0 ++ Services,
90 660 UpdatedDatabases = Databases0 ++ Databases,
91 660 UseCtx#{modules => UpdatedModules, services => UpdatedServices,
92 internal_databases => UpdatedDatabases};
93 aggregate_use_ctx(Args, _Ctx) ->
94 665 prepare_use_dir_args(Args).
95
96 -spec prepare_use_dir_args([{graphql:name(), term()}]) -> use_ctx().
97 prepare_use_dir_args(Args) ->
98 1325 Default = #{modules => [], services => [], internal_databases => []},
99 1325 RdyArgs = maps:from_list([{binary_to_existing_atom(name(N)), V} || {N, V} <- Args]),
100 1325 maps:merge(Default, RdyArgs).
101
102 -spec host_type_from_arg(jid:jid() | jid:lserver() | mongooseim:host_type()) ->
103 {ok, mongooseim:host_type()} | {error, not_found}.
104 host_type_from_arg(#jid{lserver = Domain}) ->
105 433 host_type_from_arg(Domain);
106 host_type_from_arg(ArgValue) ->
107 609 case mongoose_domain_api:get_host_type(ArgValue) of
108 {ok, HostType} ->
109 504 {ok, HostType};
110 {error, not_found} ->
111 105 case lists:member(ArgValue, ?ALL_HOST_TYPES) of
112 true ->
113
:-(
{ok, ArgValue};
114 false ->
115 105 {error, not_found}
116 end
117 end.
118
119 -spec filter_unloaded_modules(use_ctx(), ctx(), [binary()]) -> [binary()].
120 filter_unloaded_modules(_UseCtx, _Ctx, []) ->
121 56 [];
122 filter_unloaded_modules(UseCtx, Ctx, Modules) ->
123 609 ArgValue = get_arg_value(UseCtx, Ctx),
124 % Assume that loaded modules can be checked only when host type can be obtained
125 609 case host_type_from_arg(ArgValue) of
126 {ok, HostType} ->
127 504 filter_unloaded_modules(HostType, Modules);
128 {error, not_found} ->
129 105 []
130 end.
131
132 -spec filter_unloaded_modules(host_type(), [binary()]) -> [binary()].
133 filter_unloaded_modules(HostType, Modules) ->
134 504 lists:filter(fun(M) -> not gen_mod:is_loaded(HostType, binary_to_existing_atom(M)) end,
135 Modules).
136
137 -spec filter_unloaded_services([binary()]) -> [binary()].
138 filter_unloaded_services(Services) ->
139 665 lists:filter(fun(S) -> not mongoose_service:is_loaded(binary_to_existing_atom(S)) end,
140 Services).
141
142 -spec filter_unloaded_db([binary()]) -> [binary()].
143 filter_unloaded_db(DBs) ->
144 665 lists:filter(fun(DB) -> is_database_unloaded(DB) end, DBs).
145
146 -spec is_database_unloaded(binary()) -> boolean().
147 is_database_unloaded(DB) ->
148 mongoose_config:lookup_opt([internal_databases,
149 56 binary_to_existing_atom(DB)]) == {error, not_found}.
150
151 -spec resolve_not_loaded_fun([{dependency_type(), [dependency_name()]}]) -> resolver().
152 resolve_not_loaded_fun(NotLoaded) ->
153 131 Msg = not_loaded_message(NotLoaded),
154 131 Extra = maps:from_list([{error_key(Type), Names} || {Type, Names} <- NotLoaded]),
155 131 fun(_, _, _, _) -> mongoose_graphql_helper:make_error(deps_not_loaded, Msg, Extra) end.
156
157 -spec not_loaded_message([{dependency_type(), [dependency_name()]}]) -> binary().
158 not_loaded_message(NotLoaded) ->
159 131 MsgPrefix = <<"Some of the required ">>,
160 131 MsgList = string:join([dependency_type_to_string(Item) || {Item, _} <- NotLoaded], " and "),
161 131 MsgBinaryList = list_to_binary(MsgList),
162 131 <<MsgPrefix/binary, MsgBinaryList/binary, " are not loaded">>.
163
164 -spec dependency_type_to_string(dependency_type()) -> [string()].
165 dependency_type_to_string(Type) ->
166 131 string:replace(atom_to_list(Type), "_", " ").
167
168 -spec error_key(dependency_type()) -> atom().
169 error_key(Type) ->
170 131 list_to_atom("not_loaded_" ++ atom_to_list(Type)).
Line Hits Source