./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 arg => binary(),
46 atom => term()}.
47
48 %% @doc Check the collected modules and services and swap the field resolver if any of them
49 %% is not loaded. The new field resolver returns the error that some modules or services
50 %% are not loaded.
51 handle_directive(#directive{id = <<"use">>, args = Args}, #schema_field{} = Field, Ctx) ->
52 960 #{modules := Modules, services := Services} = UseCtx = aggregate_use_ctx(Args, Ctx),
53 960 UnloadedServices = filter_unloaded_services(Services),
54 960 UnloadedModules = filter_unloaded_modules(UseCtx, Ctx, Modules),
55 960 case {UnloadedModules, UnloadedServices} of
56 {[], []} ->
57 814 Field;
58 {_, _} ->
59 146 Fun = resolve_not_loaded_fun(UnloadedModules, UnloadedServices),
60 146 Field#schema_field{resolve = Fun}
61 end.
62
63 %% @doc Collect the used modules and services to be checked for each field separately.
64 %% It cannot be checked here because the object directives have no access to the domain sometimes.
65 handle_object_directive(#directive{id = <<"use">>, args = Args}, Object, Ctx) ->
66 992 {Object, Ctx#{use_dir => aggregate_use_ctx(Args, Ctx)}}.
67
68 %% Internal
69
70 -spec get_arg_value(use_ctx(), ctx()) -> jid:jid() | jid:lserver() | mongooseim:host_type().
71 get_arg_value(#{arg := DomainArg}, #{field_args := FieldArgs}) ->
72 861 get_arg(DomainArg, FieldArgs);
73 get_arg_value(_UseCtx, #{user := #jid{lserver = Domain}}) ->
74 41 Domain;
75 get_arg_value(_UseCtx, #{admin := #jid{lserver = Domain}}) ->
76
:-(
Domain.
77
78 -spec aggregate_use_ctx(list(), ctx()) -> use_ctx().
79 aggregate_use_ctx(Args, #{use_dir := #{modules := Modules0, services := Services0}}) ->
80 925 #{modules := Modules, services := Services} = UseCtx = prepare_use_dir_args(Args),
81 925 UseCtx#{modules => Modules0 ++ Modules, services => Services0 ++ Services};
82 aggregate_use_ctx(Args, _Ctx) ->
83 1027 prepare_use_dir_args(Args).
84
85 -spec prepare_use_dir_args([{graphql:name(), term()}]) -> use_ctx().
86 prepare_use_dir_args(Args) ->
87 1952 Default = #{modules => [], services => []},
88 1952 RdyArgs = maps:from_list([{binary_to_existing_atom(name(N)), V} || {N, V} <- Args]),
89 1952 maps:merge(Default, RdyArgs).
90
91 -spec host_type_from_arg(jid:jid() | jid:lserver() | mongooseim:host_type()) ->
92 {ok, mongooseim:host_type()} | {error, not_found}.
93 host_type_from_arg(#jid{lserver = Domain}) ->
94 637 host_type_from_arg(Domain);
95 host_type_from_arg(ArgValue) ->
96 902 case mongoose_domain_api:get_host_type(ArgValue) of
97 {ok, HostType} ->
98 756 {ok, HostType};
99 {error, not_found} ->
100 146 case lists:member(ArgValue, ?ALL_HOST_TYPES) of
101 true ->
102
:-(
{ok, ArgValue};
103 false ->
104 146 {error, not_found}
105 end
106 end.
107
108 -spec filter_unloaded_modules(use_ctx(), ctx(), [binary()]) -> [binary()].
109 filter_unloaded_modules(_UseCtx, _Ctx, []) ->
110 58 [];
111 filter_unloaded_modules(UseCtx, Ctx, Modules) ->
112 902 ArgValue = get_arg_value(UseCtx, Ctx),
113 % Assume that loaded modules can be checked only when host type can be obtained
114 902 case host_type_from_arg(ArgValue) of
115 {ok, HostType} ->
116 756 filter_unloaded_modules(HostType, Modules);
117 {error, not_found} ->
118 146 []
119 end.
120
121 -spec filter_unloaded_modules(host_type(), [binary()]) -> [binary()].
122 filter_unloaded_modules(HostType, Modules) ->
123 756 lists:filter(fun(M) -> not gen_mod:is_loaded(HostType, binary_to_existing_atom(M)) end,
124 Modules).
125
126 -spec filter_unloaded_services([binary()]) -> [binary()].
127 filter_unloaded_services(Services) ->
128 960 lists:filter(fun(S) -> not mongoose_service:is_loaded(binary_to_existing_atom(S)) end,
129 Services).
130
131 -spec resolve_not_loaded_fun([binary()], [binary()]) -> resolver().
132 resolve_not_loaded_fun(Modules, Services) ->
133 146 Msg = <<"Some of required modules or services are not loaded">>,
134 146 Extra = #{not_loaded_modules => Modules, not_loaded_services => Services},
135 146 fun(_, _, _, _) -> mongoose_graphql_helper:make_error(deps_not_loaded, Msg, Extra) end.
Line Hits Source