Packages

Code First GraphQL library for Gleam

Current section

Files

Jump to
mochi src mochi@middleware.erl
Raw

src/mochi@middleware.erl

-module(mochi@middleware).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/mochi/middleware.gleam").
-export([new_resolution/3, set_value/2, set_error/2, put_private/3, get_private/2, has_value/1, has_error/1, new_pipeline/0, add_middleware/2, middleware/2, with_priority/2, with_filter/2, execute_with_middleware/5, to_executor_fn/1, logging_middleware/1, auth_middleware/2, rate_limit_middleware/3, caching_middleware/3, timing_middleware/2, transform_middleware/1, validation_middleware/1, error_wrapper_middleware/1]).
-export_type([resolver_context/0, resolution/0, middleware_def/0, field_filter/0, middleware_pipeline/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type resolver_context() :: {resolver_context,
binary(),
binary(),
list(binary()),
mochi@schema:resolver_info()}.
-type resolution() :: {resolution,
gleam@option:option(gleam@dynamic:dynamic_()),
gleam@option:option(binary()),
binary(),
binary(),
resolver_context(),
gleam@dict:dict(binary(), gleam@dynamic:dynamic_())}.
-type middleware_def() :: {middleware_def,
binary(),
integer(),
gleam@option:option(field_filter()),
fun((resolution(), fun((resolution()) -> resolution())) -> resolution())}.
-type field_filter() :: {specific_fields, list({binary(), binary()})} |
{type_fields, list(binary())} |
{named_fields, list(binary())} |
all_fields.
-opaque middleware_pipeline() :: {middleware_pipeline, list(middleware_def())}.
-file("src/mochi/middleware.gleam", 89).
?DOC(" Create a new resolution state for a field\n").
-spec new_resolution(binary(), binary(), resolver_context()) -> resolution().
new_resolution(Field_name, Parent_type, Context) ->
{resolution, none, none, Field_name, Parent_type, Context, maps:new()}.
-file("src/mochi/middleware.gleam", 105).
?DOC(" Set the resolved value\n").
-spec set_value(resolution(), gleam@dynamic:dynamic_()) -> resolution().
set_value(Resolution, Value) ->
{resolution,
{some, Value},
none,
erlang:element(4, Resolution),
erlang:element(5, Resolution),
erlang:element(6, Resolution),
erlang:element(7, Resolution)}.
-file("src/mochi/middleware.gleam", 110).
?DOC(" Set an error on the resolution\n").
-spec set_error(resolution(), binary()) -> resolution().
set_error(Resolution, Error) ->
{resolution,
none,
{some, Error},
erlang:element(4, Resolution),
erlang:element(5, Resolution),
erlang:element(6, Resolution),
erlang:element(7, Resolution)}.
-file("src/mochi/middleware.gleam", 115).
?DOC(" Add private data to the resolution\n").
-spec put_private(resolution(), binary(), gleam@dynamic:dynamic_()) -> resolution().
put_private(Resolution, Key, Value) ->
{resolution,
erlang:element(2, Resolution),
erlang:element(3, Resolution),
erlang:element(4, Resolution),
erlang:element(5, Resolution),
erlang:element(6, Resolution),
gleam@dict:insert(erlang:element(7, Resolution), Key, Value)}.
-file("src/mochi/middleware.gleam", 124).
?DOC(" Get private data from the resolution\n").
-spec get_private(resolution(), binary()) -> gleam@option:option(gleam@dynamic:dynamic_()).
get_private(Resolution, Key) ->
case gleam_stdlib:map_get(erlang:element(7, Resolution), Key) of
{ok, V} ->
{some, V};
{error, _} ->
none
end.
-file("src/mochi/middleware.gleam", 132).
?DOC(" Check if resolution has a value\n").
-spec has_value(resolution()) -> boolean().
has_value(Resolution) ->
gleam@option:is_some(erlang:element(2, Resolution)).
-file("src/mochi/middleware.gleam", 137).
?DOC(" Check if resolution has an error\n").
-spec has_error(resolution()) -> boolean().
has_error(Resolution) ->
gleam@option:is_some(erlang:element(3, Resolution)).
-file("src/mochi/middleware.gleam", 146).
?DOC(" Create a new empty middleware pipeline\n").
-spec new_pipeline() -> middleware_pipeline().
new_pipeline() ->
{middleware_pipeline, []}.
-file("src/mochi/middleware.gleam", 151).
?DOC(" Add middleware to the pipeline\n").
-spec add_middleware(middleware_pipeline(), middleware_def()) -> middleware_pipeline().
add_middleware(Pipeline, Mw) ->
New_list = begin
_pipe = [Mw | erlang:element(2, Pipeline)],
gleam@list:sort(
_pipe,
fun(A, B) ->
gleam@int:compare(erlang:element(3, A), erlang:element(3, B))
end
)
end,
{middleware_pipeline, New_list}.
-file("src/mochi/middleware.gleam", 162).
?DOC(" Create a basic middleware definition\n").
-spec middleware(
binary(),
fun((resolution(), fun((resolution()) -> resolution())) -> resolution())
) -> middleware_def().
middleware(Name, Mw_fn) ->
{middleware_def, Name, 100, none, Mw_fn}.
-file("src/mochi/middleware.gleam", 172).
?DOC(" Set the priority of a middleware (lower runs first)\n").
-spec with_priority(middleware_def(), integer()) -> middleware_def().
with_priority(Mw, Priority) ->
{middleware_def,
erlang:element(2, Mw),
Priority,
erlang:element(4, Mw),
erlang:element(5, Mw)}.
-file("src/mochi/middleware.gleam", 177).
?DOC(" Set a field filter on middleware\n").
-spec with_filter(middleware_def(), field_filter()) -> middleware_def().
with_filter(Mw, Filter) ->
{middleware_def,
erlang:element(2, Mw),
erlang:element(3, Mw),
{some, Filter},
erlang:element(5, Mw)}.
-file("src/mochi/middleware.gleam", 182).
?DOC(" Check if a middleware applies to a given field\n").
-spec middleware_applies(middleware_def(), binary(), binary()) -> boolean().
middleware_applies(Mw, Parent_type, Field_name) ->
case erlang:element(4, Mw) of
none ->
true;
{some, all_fields} ->
true;
{some, {specific_fields, Pairs}} ->
gleam@list:any(
Pairs,
fun(Pair) ->
(erlang:element(1, Pair) =:= Parent_type) andalso (erlang:element(
2,
Pair
)
=:= Field_name)
end
);
{some, {type_fields, Types}} ->
gleam@list:contains(Types, Parent_type);
{some, {named_fields, Names}} ->
gleam@list:contains(Names, Field_name)
end.
-file("src/mochi/middleware.gleam", 267).
?DOC(" Execute the middleware chain recursively\n").
-spec execute_chain(
list(middleware_def()),
resolution(),
fun((resolution()) -> resolution())
) -> resolution().
execute_chain(Middleware, Resolution, Next) ->
case Middleware of
[] ->
Next(Resolution);
[Mw | Rest] ->
Continue = fun(Res) -> execute_chain(Rest, Res, Next) end,
(erlang:element(5, Mw))(Resolution, Continue)
end.
-file("src/mochi/middleware.gleam", 222).
?DOC(" Execute a resolver with the middleware pipeline\n").
-spec execute_with_middleware(
middleware_pipeline(),
binary(),
mochi@schema:field_definition(),
mochi@schema:resolver_info(),
fun((mochi@schema:resolver_info()) -> {ok, gleam@dynamic:dynamic_()} |
{error, binary()})
) -> {ok, gleam@dynamic:dynamic_()} | {error, binary()}.
execute_with_middleware(Pipeline, Parent_type, Field_def, Info, Resolver) ->
Context = {resolver_context,
erlang:element(2, Field_def),
Parent_type,
[],
Info},
Resolution = new_resolution(
erlang:element(2, Field_def),
Parent_type,
Context
),
Applicable_middleware = gleam@list:filter(
erlang:element(2, Pipeline),
fun(Mw) ->
middleware_applies(Mw, Parent_type, erlang:element(2, Field_def))
end
),
Final_resolution = execute_chain(
Applicable_middleware,
Resolution,
fun(Res) -> case Resolver(Info) of
{ok, Value} ->
set_value(Res, Value);
{error, Msg} ->
set_error(Res, Msg)
end end
),
case erlang:element(3, Final_resolution) of
{some, Err} ->
{error, Err};
none ->
case erlang:element(2, Final_resolution) of
{some, Value@1} ->
{ok, Value@1};
none ->
{ok, gleam_stdlib:identity(nil)}
end
end.
-file("src/mochi/middleware.gleam", 215).
?DOC(
" Convert a MiddlewarePipeline to a schema.MiddlewareFn\n"
"\n"
" This is used to wire a middleware pipeline into the execution context\n"
" without creating a circular dependency between schema and middleware.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let pipeline = middleware.new_pipeline()\n"
" |> middleware.add_middleware(middleware.logging_middleware(io.println))\n"
"\n"
" let ctx = schema.execution_context(user_ctx)\n"
" |> schema.with_middleware(middleware.to_executor_fn(pipeline))\n"
" ```\n"
).
-spec to_executor_fn(middleware_pipeline()) -> fun((binary(), mochi@schema:field_definition(), mochi@schema:resolver_info(), fun((mochi@schema:resolver_info()) -> {ok,
gleam@dynamic:dynamic_()} |
{error, binary()})) -> {ok, gleam@dynamic:dynamic_()} | {error, binary()}).
to_executor_fn(Pipeline) ->
fun(Parent_type, Field_def, Info, Resolver) ->
execute_with_middleware(
Pipeline,
Parent_type,
Field_def,
Info,
Resolver
)
end.
-file("src/mochi/middleware.gleam", 287).
?DOC(" Logging middleware that logs field resolution\n").
-spec logging_middleware(fun((binary()) -> nil)) -> middleware_def().
logging_middleware(Log_fn) ->
middleware(
<<"logging"/utf8>>,
fun(Resolution, Next) ->
Log_fn(
<<<<<<"Resolving "/utf8,
(erlang:element(5, Resolution))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(4, Resolution))/binary>>
),
Result = Next(Resolution),
case erlang:element(3, Result) of
{some, Err} ->
Log_fn(
<<<<<<<<<<"Error resolving "/utf8,
(erlang:element(5, Resolution))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(4, Resolution))/binary>>/binary,
": "/utf8>>/binary,
Err/binary>>
);
none ->
Log_fn(
<<<<<<"Resolved "/utf8,
(erlang:element(5, Resolution))/binary>>/binary,
"."/utf8>>/binary,
(erlang:element(4, Resolution))/binary>>
)
end,
Result
end
).
-file("src/mochi/middleware.gleam", 317).
?DOC(
" Authorization middleware that checks for a specific role in context.\n"
"\n"
" `role_extractor` receives a decoder-friendly view of the user context.\n"
" The middleware decodes via the supplied decoder, then asks the\n"
" extractor for the role string.\n"
).
-spec auth_middleware(
fun((mochi@schema:user_context()) -> gleam@option:option(binary())),
binary()
) -> middleware_def().
auth_middleware(Role_extractor, Required_role) ->
_pipe = middleware(
<<"auth"/utf8>>,
fun(Resolution, Next) ->
case Role_extractor(
erlang:element(
2,
erlang:element(
5,
erlang:element(5, erlang:element(6, Resolution))
)
)
) of
{some, Role} when Role =:= Required_role ->
Next(Resolution);
{some, _} ->
set_error(
Resolution,
<<<<"Forbidden: requires role '"/utf8,
Required_role/binary>>/binary,
"'"/utf8>>
);
none ->
set_error(
Resolution,
<<"Unauthorized: authentication required"/utf8>>
)
end
end
),
with_priority(_pipe, 10).
-file("src/mochi/middleware.gleam", 337).
?DOC(
" Rate limiting middleware (simple in-memory version)\n"
" Note: For production, use external state (Redis, etc.)\n"
).
-spec rate_limit_middleware(
fun((resolution()) -> binary()),
integer(),
fun((binary(), integer()) -> boolean())
) -> middleware_def().
rate_limit_middleware(Key_extractor, Max_requests, Checker) ->
_pipe = middleware(
<<"rate_limit"/utf8>>,
fun(Resolution, Next) ->
Key = Key_extractor(Resolution),
case Checker(Key, Max_requests) of
true ->
Next(Resolution);
false ->
set_error(Resolution, <<"Rate limit exceeded"/utf8>>)
end
end
),
with_priority(_pipe, 5).
-file("src/mochi/middleware.gleam", 353).
?DOC(" Caching middleware that caches field results\n").
-spec caching_middleware(
fun((resolution()) -> binary()),
fun((binary()) -> gleam@option:option(gleam@dynamic:dynamic_())),
fun((binary(), gleam@dynamic:dynamic_()) -> nil)
) -> middleware_def().
caching_middleware(Cache_key, Get_cached, Set_cached) ->
_pipe = middleware(
<<"cache"/utf8>>,
fun(Resolution, Next) ->
Key = Cache_key(Resolution),
case Get_cached(Key) of
{some, Cached} ->
set_value(Resolution, Cached);
none ->
Result = Next(Resolution),
case erlang:element(2, Result) of
{some, Value} ->
Set_cached(Key, Value),
Result;
none ->
Result
end
end
end
),
with_priority(_pipe, 20).
-file("src/mochi/middleware.gleam", 378).
?DOC(" Timing middleware that records field resolution time\n").
-spec timing_middleware(
fun(() -> integer()),
fun((binary(), binary(), integer()) -> nil)
) -> middleware_def().
timing_middleware(Get_time, Record_timing) ->
_pipe = middleware(
<<"timing"/utf8>>,
fun(Resolution, Next) ->
Start = Get_time(),
Result = Next(Resolution),
Duration = Get_time() - Start,
Record_timing(
erlang:element(5, Resolution),
erlang:element(4, Resolution),
Duration
),
Result
end
),
with_priority(_pipe, 1).
-file("src/mochi/middleware.gleam", 393).
?DOC(" Transform middleware that transforms the resolved value\n").
-spec transform_middleware(
fun((gleam@dynamic:dynamic_()) -> gleam@dynamic:dynamic_())
) -> middleware_def().
transform_middleware(Transform) ->
_pipe = middleware(
<<"transform"/utf8>>,
fun(Resolution, Next) ->
Result = Next(Resolution),
case erlang:element(2, Result) of
{some, Value} ->
set_value(Result, Transform(Value));
none ->
Result
end
end
),
with_priority(_pipe, 200).
-file("src/mochi/middleware.gleam", 405).
?DOC(" Validation middleware that validates arguments\n").
-spec validation_middleware(
fun((mochi@schema:resolver_info()) -> {ok, nil} | {error, binary()})
) -> middleware_def().
validation_middleware(Validator) ->
_pipe = middleware(
<<"validation"/utf8>>,
fun(Resolution, Next) ->
case Validator(erlang:element(5, erlang:element(6, Resolution))) of
{ok, _} ->
Next(Resolution);
{error, Msg} ->
set_error(
Resolution,
<<"Validation error: "/utf8, Msg/binary>>
)
end
end
),
with_priority(_pipe, 15).
-file("src/mochi/middleware.gleam", 418).
?DOC(" Error wrapping middleware that transforms errors\n").
-spec error_wrapper_middleware(fun((binary()) -> binary())) -> middleware_def().
error_wrapper_middleware(Wrapper) ->
_pipe = middleware(
<<"error_wrapper"/utf8>>,
fun(Resolution, Next) ->
Result = Next(Resolution),
case erlang:element(3, Result) of
{some, Err} ->
set_error(Result, Wrapper(Err));
none ->
Result
end
end
),
with_priority(_pipe, 250).