1 |
|
%% @doc This module provides main interface to graphql. It initializes schemas |
2 |
|
%% and allows executing queries with permissions checks. |
3 |
|
%% @end |
4 |
|
-module(mongoose_graphql). |
5 |
|
|
6 |
|
-include_lib("kernel/include/logger.hrl"). |
7 |
|
|
8 |
|
%API |
9 |
|
-export([init/0, |
10 |
|
get_endpoint/1, |
11 |
|
create_endpoint/3, |
12 |
|
execute/2, prepare/2, |
13 |
|
execute/3, |
14 |
|
execute_cli/3]). |
15 |
|
|
16 |
|
-ignore_xref([create_endpoint/3]). |
17 |
|
|
18 |
|
-type request() :: #{document := binary(), |
19 |
|
operation_name := binary() | undefined, |
20 |
|
vars := map(), |
21 |
|
authorized := boolean(), |
22 |
|
ctx := map(), |
23 |
|
ast => graphql:ast()}. |
24 |
|
-type context() :: map(). |
25 |
|
-type object() :: term(). |
26 |
|
-type field() :: binary(). |
27 |
|
-type args() :: map(). |
28 |
|
|
29 |
|
-type result() :: {ok, term()} | {ok, term(), Aux :: term()} | {error, term()}. |
30 |
|
-callback execute(Ctx :: context(), Obj :: object(), Field :: field(), Args :: args()) -> |
31 |
|
result(). |
32 |
|
|
33 |
|
-export_type([request/0, context/0, object/0, field/0, args/0]). |
34 |
|
|
35 |
|
%% gen:start_ret() type is not exported from the gen module |
36 |
|
-type gen_start_ret() :: {ok, pid()} | ignore | {error, term()}. |
37 |
|
|
38 |
|
-define(USER_EP_NAME, user_schema_ep). |
39 |
|
-define(ADMIN_EP_NAME, admin_schema_ep). |
40 |
|
|
41 |
|
%% @doc Create and initialize endpoints for user and admin. |
42 |
|
-spec init() -> ok. |
43 |
|
init() -> |
44 |
104 |
create_endpoint(?USER_EP_NAME, user_mapping_rules(), schema_global_patterns("user")), |
45 |
104 |
create_endpoint(?ADMIN_EP_NAME, admin_mapping_rules(), schema_global_patterns("admin")), |
46 |
104 |
ok. |
47 |
|
|
48 |
|
%% @doc Get endpoint_context for passed endpoint name. |
49 |
|
-spec get_endpoint(atom()) -> graphql:endpoint_context(). |
50 |
|
get_endpoint(admin) -> |
51 |
1369 |
graphql_schema:get_endpoint_ctx(?ADMIN_EP_NAME); |
52 |
|
get_endpoint(domain_admin) -> |
53 |
468 |
graphql_schema:get_endpoint_ctx(?ADMIN_EP_NAME); |
54 |
|
get_endpoint(user) -> |
55 |
259 |
graphql_schema:get_endpoint_ctx(?USER_EP_NAME); |
56 |
|
get_endpoint(Name) -> |
57 |
:-( |
graphql_schema:get_endpoint_ctx(Name). |
58 |
|
|
59 |
|
%% @doc Create a new endpoint and load schema. |
60 |
|
-spec create_endpoint(atom(), map(), [file:filename_all()]) -> gen_start_ret(). |
61 |
|
create_endpoint(Name, Mapping, Patterns) -> |
62 |
208 |
Res = graphql_schema:start_link(Name), |
63 |
208 |
Ep = graphql_schema:get_endpoint_ctx(Name), |
64 |
208 |
{ok, SchemaData} = load_multiple_file_schema(Patterns), |
65 |
208 |
ok = graphql:load_schema(Ep, Mapping, SchemaData), |
66 |
208 |
ok = graphql:validate_schema(Ep), |
67 |
208 |
Res. |
68 |
|
|
69 |
|
%% @doc Execute request on a given endpoint. |
70 |
|
-spec execute(graphql:endpoint_context(), request()) -> |
71 |
|
{ok, map()} | {error, term()}. |
72 |
|
execute(Ep, Req = #{ast := _}) -> |
73 |
13 |
execute_graphql(Ep, Req); |
74 |
|
execute(Ep, Req) -> |
75 |
37574 |
case prepare(Ep, Req) of |
76 |
|
{error, _} = Error -> |
77 |
305 |
Error; |
78 |
|
{ok, Req1} -> |
79 |
37269 |
execute_graphql(Ep, Req1) |
80 |
|
end. |
81 |
|
|
82 |
|
-spec prepare(graphql:endpoint_context(), request()) -> {ok, request()} | {error, term()}. |
83 |
|
prepare(Ep, Req) -> |
84 |
37582 |
try {ok, prepare_request(Ep, Req)} |
85 |
|
catch |
86 |
|
throw:{error, Err} -> |
87 |
309 |
{error, Err}; |
88 |
|
Class:Reason:Stacktrace -> |
89 |
:-( |
Err = #{what => graphql_internal_crash, |
90 |
|
class => Class, reason => Reason, |
91 |
|
stacktrace => Stacktrace}, |
92 |
:-( |
?LOG_ERROR(Err), |
93 |
:-( |
{error, internal_crash} |
94 |
|
end. |
95 |
|
|
96 |
|
prepare_request(Ep, #{document := Doc, |
97 |
|
operation_name := OpName, |
98 |
|
authorized := AuthStatus, |
99 |
|
vars := Vars, |
100 |
|
ctx := Ctx} = Request) -> |
101 |
37582 |
{ok, Ast} = graphql_parse(Doc), |
102 |
37580 |
{ok, #{ast := Ast2, |
103 |
|
fun_env := FunEnv}} = graphql:type_check(Ep, Ast), |
104 |
37579 |
ok = graphql:validate(Ast2), |
105 |
37579 |
Vars2 = remove_null_args(Vars), |
106 |
37579 |
Coerced = graphql:type_check_params(Ep, FunEnv, OpName, Vars2), |
107 |
37459 |
Ctx2 = Ctx#{params => Coerced, |
108 |
|
operation_name => OpName, |
109 |
|
authorized => AuthStatus, |
110 |
|
error_module => mongoose_graphql_errors}, |
111 |
37459 |
Ast3 = mongoose_graphql_directive:process_directives(Ctx2, Ast2), |
112 |
37275 |
mongoose_graphql_operations:verify_operations(Ctx2, Ast3), |
113 |
37273 |
AllowedCategories = maps:get(allowed_categories, Ctx2, []), |
114 |
37273 |
Ast4 = mongoose_graphql_check_categories:process_ast(Ast3, AllowedCategories), |
115 |
37273 |
Request#{ast => Ast4, ctx := Ctx2}. |
116 |
|
|
117 |
|
execute_graphql(Ep, #{ast := Ast, ctx := Ctx}) -> |
118 |
37282 |
{ok, graphql:execute(Ep, Ctx, Ast)}. |
119 |
|
|
120 |
|
%% @doc Execute selected operation on a given endpoint with authorization. |
121 |
|
-spec execute(graphql:endpoint_context(), undefined | binary(), binary()) -> |
122 |
|
{ok, map()} | {error, term()}. |
123 |
|
execute(Ep, OpName, Doc) -> |
124 |
558 |
Req = #{document => Doc, |
125 |
|
operation_name => OpName, |
126 |
|
vars => #{}, |
127 |
|
authorized => true, |
128 |
|
ctx => #{}}, |
129 |
558 |
execute(Ep, Req). |
130 |
|
|
131 |
|
-spec execute_cli(graphql:endpoint_context(), undefined | binary(), binary()) -> |
132 |
|
{ok, map()} | {error, term()}. |
133 |
|
execute_cli(Ep, OpName, Doc) -> |
134 |
2 |
Req = #{document => Doc, |
135 |
|
operation_name => OpName, |
136 |
|
vars => #{}, |
137 |
|
authorized => true, |
138 |
|
ctx => #{method => cli}}, |
139 |
2 |
execute(Ep, Req). |
140 |
|
|
141 |
|
% Internal |
142 |
|
|
143 |
|
-spec schema_global_patterns(file:name_all()) -> [file:filename_all()]. |
144 |
|
schema_global_patterns(SchemaDir) -> |
145 |
208 |
[schema_pattern(SchemaDir), schema_pattern("global")]. |
146 |
|
|
147 |
|
-spec schema_pattern(file:name_all()) -> file:filename_all(). |
148 |
|
schema_pattern(DirName) -> |
149 |
416 |
schema_pattern(DirName, "*.gql"). |
150 |
|
|
151 |
|
-spec schema_pattern(file:name_all(), file:name_all()) -> file:filename_all(). |
152 |
|
schema_pattern(DirName, Pattern) -> |
153 |
416 |
filename:join([code:priv_dir(mongooseim), "graphql/schemas", DirName, Pattern]). |
154 |
|
|
155 |
|
graphql_parse(Doc) -> |
156 |
37582 |
case graphql:parse(Doc) of |
157 |
|
{ok, _} = Ok -> |
158 |
37580 |
Ok; |
159 |
|
{error, Err} -> |
160 |
2 |
graphql_err:abort([], parse, Err) |
161 |
|
end. |
162 |
|
|
163 |
|
remove_null_args(Vars) -> |
164 |
37579 |
maps:filter(fun(_Key, Value) -> Value /= null end, Vars). |
165 |
|
|
166 |
|
admin_mapping_rules() -> |
167 |
104 |
#{objects => #{ |
168 |
|
'AdminQuery' => mongoose_graphql_admin_query, |
169 |
|
'AdminMutation' => mongoose_graphql_admin_mutation, |
170 |
|
'AdminSubscription' => mongoose_graphql_admin_subscription, |
171 |
|
'AdminAuthInfo' => mongoose_graphql_admin_auth_info, |
172 |
|
'DomainAdminQuery' => mongoose_graphql_domain_admin_query, |
173 |
|
'GdprAdminQuery' => mongoose_graphql_gdpr_admin_query, |
174 |
|
'DomainAdminMutation' => mongoose_graphql_domain_admin_mutation, |
175 |
|
'InboxAdminMutation' => mongoose_graphql_inbox_admin_mutation, |
176 |
|
'SessionAdminMutation' => mongoose_graphql_session_admin_mutation, |
177 |
|
'SessionAdminQuery' => mongoose_graphql_session_admin_query, |
178 |
|
'StanzaAdminMutation' => mongoose_graphql_stanza_admin_mutation, |
179 |
|
'StatsAdminQuery' => mongoose_graphql_stats_admin_query, |
180 |
|
'TokenAdminMutation' => mongoose_graphql_token_admin_mutation, |
181 |
|
'GlobalStats' => mongoose_graphql_stats_global, |
182 |
|
'DomainStats' => mongoose_graphql_stats_domain, |
183 |
|
'StanzaAdminQuery' => mongoose_graphql_stanza_admin_query, |
184 |
|
'StanzaAdminSubscription' => mongoose_graphql_stanza_admin_subscription, |
185 |
|
'ServerAdminQuery' => mongoose_graphql_server_admin_query, |
186 |
|
'ServerAdminMutation' => mongoose_graphql_server_admin_mutation, |
187 |
|
'LastAdminMutation' => mongoose_graphql_last_admin_mutation, |
188 |
|
'LastAdminQuery' => mongoose_graphql_last_admin_query, |
189 |
|
'AccountAdminQuery' => mongoose_graphql_account_admin_query, |
190 |
|
'AccountAdminMutation' => mongoose_graphql_account_admin_mutation, |
191 |
|
'MUCAdminMutation' => mongoose_graphql_muc_admin_mutation, |
192 |
|
'MUCAdminQuery' => mongoose_graphql_muc_admin_query, |
193 |
|
'MUCLightAdminMutation' => mongoose_graphql_muc_light_admin_mutation, |
194 |
|
'MUCLightAdminQuery' => mongoose_graphql_muc_light_admin_query, |
195 |
|
'MnesiaAdminMutation' => mongoose_graphql_mnesia_admin_mutation, |
196 |
|
'MnesiaAdminQuery' => mongoose_graphql_mnesia_admin_query, |
197 |
|
'CETSAdminQuery' => mongoose_graphql_cets_admin_query, |
198 |
|
'OfflineAdminMutation' => mongoose_graphql_offline_admin_mutation, |
199 |
|
'PrivateAdminMutation' => mongoose_graphql_private_admin_mutation, |
200 |
|
'PrivateAdminQuery' => mongoose_graphql_private_admin_query, |
201 |
|
'RosterAdminQuery' => mongoose_graphql_roster_admin_query, |
202 |
|
'VcardAdminMutation' => mongoose_graphql_vcard_admin_mutation, |
203 |
|
'VcardAdminQuery' => mongoose_graphql_vcard_admin_query, |
204 |
|
'HttpUploadAdminMutation' => mongoose_graphql_http_upload_admin_mutation, |
205 |
|
'RosterAdminMutation' => mongoose_graphql_roster_admin_mutation, |
206 |
|
'Domain' => mongoose_graphql_domain, |
207 |
|
'MetricAdminQuery' => mongoose_graphql_metric_admin_query, |
208 |
|
default => mongoose_graphql_default}, |
209 |
|
interfaces => #{default => mongoose_graphql_default}, |
210 |
|
scalars => #{default => mongoose_graphql_scalar}, |
211 |
|
enums => #{default => mongoose_graphql_enum}, |
212 |
|
unions => #{default => mongoose_graphql_union}}. |
213 |
|
|
214 |
|
user_mapping_rules() -> |
215 |
104 |
#{objects => #{ |
216 |
|
'UserQuery' => mongoose_graphql_user_query, |
217 |
|
'UserMutation' => mongoose_graphql_user_mutation, |
218 |
|
'UserSubscription' => mongoose_graphql_user_subscription, |
219 |
|
'AccountUserQuery' => mongoose_graphql_account_user_query, |
220 |
|
'AccountUserMutation' => mongoose_graphql_account_user_mutation, |
221 |
|
'InboxUserMutation' => mongoose_graphql_inbox_user_mutation, |
222 |
|
'MUCUserMutation' => mongoose_graphql_muc_user_mutation, |
223 |
|
'MUCUserQuery' => mongoose_graphql_muc_user_query, |
224 |
|
'MUCLightUserMutation' => mongoose_graphql_muc_light_user_mutation, |
225 |
|
'MUCLightUserQuery' => mongoose_graphql_muc_light_user_query, |
226 |
|
'PrivateUserMutation' => mongoose_graphql_private_user_mutation, |
227 |
|
'PrivateUserQuery' => mongoose_graphql_private_user_query, |
228 |
|
'RosterUserQuery' => mongoose_graphql_roster_user_query, |
229 |
|
'RosterUserMutation' => mongoose_graphql_roster_user_mutation, |
230 |
|
'VcardUserMutation' => mongoose_graphql_vcard_user_mutation, |
231 |
|
'VcardUserQuery' => mongoose_graphql_vcard_user_query, |
232 |
|
'LastUserMutation' => mongoose_graphql_last_user_mutation, |
233 |
|
'LastUserQuery' => mongoose_graphql_last_user_query, |
234 |
|
'SessionUserQuery' => mongoose_graphql_session_user_query, |
235 |
|
'StanzaUserMutation' => mongoose_graphql_stanza_user_mutation, |
236 |
|
'TokenUserMutation' => mongoose_graphql_token_user_mutation, |
237 |
|
'StanzaUserQuery' => mongoose_graphql_stanza_user_query, |
238 |
|
'StanzaUserSubscription' => mongoose_graphql_stanza_user_subscription, |
239 |
|
'HttpUploadUserMutation' => mongoose_graphql_http_upload_user_mutation, |
240 |
|
'UserAuthInfo' => mongoose_graphql_user_auth_info, |
241 |
|
default => mongoose_graphql_default}, |
242 |
|
interfaces => #{default => mongoose_graphql_default}, |
243 |
|
scalars => #{default => mongoose_graphql_scalar}, |
244 |
|
enums => #{default => mongoose_graphql_enum}, |
245 |
|
unions => #{default => mongoose_graphql_union}}. |
246 |
|
|
247 |
|
load_multiple_file_schema(Patterns) -> |
248 |
208 |
Paths = lists:flatmap(fun(P) -> filelib:wildcard(P) end, Patterns), |
249 |
208 |
try |
250 |
208 |
SchemaData = [read_schema_file(P) || P <- Paths], |
251 |
208 |
{ok, lists:flatten(SchemaData)} |
252 |
|
catch |
253 |
|
throw:{error, Reason, Path} -> |
254 |
:-( |
?LOG_ERROR(#{what => graphql_cannot_load_schema, |
255 |
:-( |
reason => Reason, path => Path}), |
256 |
:-( |
{error, cannot_load} |
257 |
|
end. |
258 |
|
|
259 |
|
read_schema_file(Path) -> |
260 |
6864 |
case file:read_file(Path) of |
261 |
|
{ok, Data} -> |
262 |
6864 |
binary_to_list(Data); |
263 |
|
{error, Reason} -> |
264 |
:-( |
throw({error, Reason, Path}) |
265 |
|
end. |