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, |
13 |
|
execute/3]). |
14 |
|
|
15 |
|
-ignore_xref([create_endpoint/3]). |
16 |
|
|
17 |
|
-type request() :: #{document := binary(), |
18 |
|
operation_name := binary() | undefined, |
19 |
|
vars := map(), |
20 |
|
authorized := boolean(), |
21 |
|
ctx := map()}. |
22 |
|
-type context() :: map(). |
23 |
|
-type object() :: term(). |
24 |
|
-type field() :: binary(). |
25 |
|
-type args() :: map(). |
26 |
|
|
27 |
|
-type result() :: {ok, term()} | {error, term()}. |
28 |
|
-callback execute(Ctx :: context(), Obj :: object(), Field :: field(), Args :: args()) -> |
29 |
|
result(). |
30 |
|
|
31 |
|
-export_type([request/0, context/0, object/0, field/0, args/0]). |
32 |
|
|
33 |
|
-define(USER_EP_NAME, user_schema_ep). |
34 |
|
-define(ADMIN_EP_NAME, admin_schema_ep). |
35 |
|
|
36 |
|
%% @doc Create and initialize endpoints for user and admin. |
37 |
|
-spec init() -> ok. |
38 |
|
init() -> |
39 |
76 |
create_endpoint(?USER_EP_NAME, user_mapping_rules(), schema_global_patterns("user")), |
40 |
76 |
create_endpoint(?ADMIN_EP_NAME, admin_mapping_rules(), schema_global_patterns("admin")), |
41 |
76 |
ok. |
42 |
|
|
43 |
|
%% @doc Get endpoint_context for passed endpoint name. |
44 |
|
-spec get_endpoint(atom()) -> graphql:endpoint_context(). |
45 |
|
get_endpoint(admin) -> |
46 |
230 |
graphql_schema:get_endpoint_ctx(?ADMIN_EP_NAME); |
47 |
|
get_endpoint(domain_admin) -> |
48 |
2 |
graphql_schema:get_endpoint_ctx(?ADMIN_EP_NAME); |
49 |
|
get_endpoint(user) -> |
50 |
154 |
graphql_schema:get_endpoint_ctx(?USER_EP_NAME); |
51 |
|
get_endpoint(Name) -> |
52 |
:-( |
graphql_schema:get_endpoint_ctx(Name). |
53 |
|
|
54 |
|
%% @doc Create a new endpoint and load schema. |
55 |
|
-spec create_endpoint(atom(), map(), [file:filename_all()]) -> gen:start_ret(). |
56 |
|
create_endpoint(Name, Mapping, Patterns) -> |
57 |
152 |
Res = graphql_schema:start_link(Name), |
58 |
152 |
Ep = graphql_schema:get_endpoint_ctx(Name), |
59 |
152 |
{ok, SchemaData} = load_multiple_file_schema(Patterns), |
60 |
152 |
ok = graphql:load_schema(Ep, Mapping, SchemaData), |
61 |
152 |
ok = graphql:validate_schema(Ep), |
62 |
152 |
Res. |
63 |
|
|
64 |
|
%% @doc Execute request on a given endpoint. |
65 |
|
-spec execute(graphql:endpoint_context(), request()) -> |
66 |
|
{ok, map()} | {error, term()}. |
67 |
|
execute(Ep, #{document := Doc, |
68 |
|
operation_name := OpName, |
69 |
|
authorized := AuthStatus, |
70 |
|
vars := Vars, |
71 |
|
ctx := Ctx}) -> |
72 |
386 |
try |
73 |
386 |
{ok, Ast} = graphql_parse(Doc), |
74 |
385 |
{ok, #{ast := Ast2, |
75 |
|
fun_env := FunEnv}} = graphql:type_check(Ep, Ast), |
76 |
385 |
ok = graphql:validate(Ast2), |
77 |
385 |
Coerced = graphql:type_check_params(Ep, FunEnv, OpName, Vars), |
78 |
377 |
Ctx2 = Ctx#{params => Coerced, |
79 |
|
operation_name => OpName, |
80 |
|
authorized => AuthStatus, |
81 |
|
error_module => mongoose_graphql_errors}, |
82 |
377 |
ok = mongoose_graphql_permissions:check_permissions(Ctx2, Ast2), |
83 |
377 |
{ok, graphql:execute(Ep, Ctx2, Ast2)} |
84 |
|
catch |
85 |
|
throw:{error, Err} -> |
86 |
9 |
{error, Err}; |
87 |
|
Class:Reason:Stacktrace -> |
88 |
:-( |
Err = #{what => graphql_internal_crash, |
89 |
|
class => Class, reason => Reason, |
90 |
|
stacktrace => Stacktrace}, |
91 |
:-( |
?LOG_ERROR(Err), |
92 |
:-( |
{error, internal_crash} |
93 |
|
end. |
94 |
|
|
95 |
|
%% @doc Execute selected operation on a given endpoint with authorization. |
96 |
|
-spec execute(graphql:endpoint_context(), undefined | binary(), binary()) -> |
97 |
|
{ok, map()} | {error, term()}. |
98 |
|
execute(Ep, OpName, Doc) -> |
99 |
2 |
Req = #{document => Doc, |
100 |
|
operation_name => OpName, |
101 |
|
vars => #{}, |
102 |
|
authorized => true, |
103 |
|
ctx => #{}}, |
104 |
2 |
execute(Ep, Req). |
105 |
|
|
106 |
|
% Internal |
107 |
|
|
108 |
|
-spec schema_global_patterns(file:name_all()) -> [file:filename_all()]. |
109 |
|
schema_global_patterns(SchemaDir) -> |
110 |
152 |
[schema_pattern(SchemaDir), schema_pattern("global")]. |
111 |
|
|
112 |
|
-spec schema_pattern(file:name_all()) -> file:filename_all(). |
113 |
|
schema_pattern(DirName) -> |
114 |
304 |
schema_pattern(DirName, "*.gql"). |
115 |
|
|
116 |
|
-spec schema_pattern(file:name_all(), file:name_all()) -> file:filename_all(). |
117 |
|
schema_pattern(DirName, Pattern) -> |
118 |
304 |
filename:join([code:priv_dir(mongooseim), "graphql/schemas", DirName, Pattern]). |
119 |
|
|
120 |
|
graphql_parse(Doc) -> |
121 |
386 |
case graphql:parse(Doc) of |
122 |
|
{ok, _} = Ok -> |
123 |
385 |
Ok; |
124 |
|
{error, Err} -> |
125 |
1 |
graphql_err:abort([], parse, Err) |
126 |
|
end. |
127 |
|
|
128 |
|
admin_mapping_rules() -> |
129 |
76 |
#{objects => #{ |
130 |
|
'AdminQuery' => mongoose_graphql_admin_query, |
131 |
|
'AdminAuthInfo' => mongoose_graphql_admin_auth_info, |
132 |
|
'DomainAdminQuery' => mongoose_graphql_domain_admin_query, |
133 |
|
'AdminMutation' => mongoose_graphql_admin_mutation, |
134 |
|
'DomainAdminMutation' => mongoose_graphql_domain_admin_mutation, |
135 |
|
'SessionAdminMutation' => mongoose_graphql_session_admin_mutation, |
136 |
|
'SessionAdminQuery' => mongoose_graphql_session_admin_query, |
137 |
|
'StanzaAdminMutation' => mongoose_graphql_stanza_admin_mutation, |
138 |
|
'StanzaAdminQuery' => mongoose_graphql_stanza_admin_query, |
139 |
|
'AccountAdminQuery' => mongoose_graphql_account_admin_query, |
140 |
|
'AccountAdminMutation' => mongoose_graphql_account_admin_mutation, |
141 |
|
'MUCAdminMutation' => mongoose_graphql_muc_admin_mutation, |
142 |
|
'MUCAdminQuery' => mongoose_graphql_muc_admin_query, |
143 |
|
'MUCLightAdminMutation' => mongoose_graphql_muc_light_admin_mutation, |
144 |
|
'MUCLightAdminQuery' => mongoose_graphql_muc_light_admin_query, |
145 |
|
'RosterAdminQuery' => mongoose_graphql_roster_admin_query, |
146 |
|
'RosterAdminMutation' => mongoose_graphql_roster_admin_mutation, |
147 |
|
'Domain' => mongoose_graphql_domain, |
148 |
|
default => mongoose_graphql_default}, |
149 |
|
interfaces => #{default => mongoose_graphql_default}, |
150 |
|
scalars => #{default => mongoose_graphql_scalar}, |
151 |
|
enums => #{default => mongoose_graphql_enum}}. |
152 |
|
|
153 |
|
user_mapping_rules() -> |
154 |
76 |
#{objects => #{ |
155 |
|
'UserQuery' => mongoose_graphql_user_query, |
156 |
|
'UserMutation' => mongoose_graphql_user_mutation, |
157 |
|
'AccountUserQuery' => mongoose_graphql_account_user_query, |
158 |
|
'AccountUserMutation' => mongoose_graphql_account_user_mutation, |
159 |
|
'MUCUserMutation' => mongoose_graphql_muc_user_mutation, |
160 |
|
'MUCUserQuery' => mongoose_graphql_muc_user_query, |
161 |
|
'MUCLightUserMutation' => mongoose_graphql_muc_light_user_mutation, |
162 |
|
'MUCLightUserQuery' => mongoose_graphql_muc_light_user_query, |
163 |
|
'RosterUserQuery' => mongoose_graphql_roster_user_query, |
164 |
|
'RosterUserMutation' => mongoose_graphql_roster_user_mutation, |
165 |
|
'SessionUserQuery' => mongoose_graphql_session_user_query, |
166 |
|
'StanzaUserMutation' => mongoose_graphql_stanza_user_mutation, |
167 |
|
'StanzaUserQuery' => mongoose_graphql_stanza_user_query, |
168 |
|
'UserAuthInfo' => mongoose_graphql_user_auth_info, |
169 |
|
default => mongoose_graphql_default}, |
170 |
|
interfaces => #{default => mongoose_graphql_default}, |
171 |
|
scalars => #{default => mongoose_graphql_scalar}, |
172 |
|
enums => #{default => mongoose_graphql_enum}}. |
173 |
|
|
174 |
|
load_multiple_file_schema(Patterns) -> |
175 |
152 |
Paths = lists:flatmap(fun(P) -> filelib:wildcard(P) end, Patterns), |
176 |
152 |
try |
177 |
152 |
SchemaData = [read_schema_file(P) || P <- Paths], |
178 |
152 |
{ok, lists:flatten(SchemaData)} |
179 |
|
catch |
180 |
|
throw:{error, Reason, Path} -> |
181 |
:-( |
?LOG_ERROR(#{what => graphql_cannot_load_schema, |
182 |
:-( |
reason => Reason, path => Path}), |
183 |
:-( |
{error, cannot_load} |
184 |
|
end. |
185 |
|
|
186 |
|
read_schema_file(Path) -> |
187 |
2660 |
case file:read_file(Path) of |
188 |
|
{ok, Data} -> |
189 |
2660 |
binary_to_list(Data); |
190 |
|
{error, Reason} -> |
191 |
:-( |
throw({error, Reason, Path}) |
192 |
|
end. |