From 4d356f0990a1d71ae073f39b5e63b7179498aabe Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 11 Sep 2026 08:59:23 +0200 Subject: [PATCH 1/9] chore: update kura stack to latest and drop pgo kura v2.0.8 -> v2.20.12, kura_postgres v0.4.2 -> v1.1.3 (minato driver), rebar3_kura pinned to v0.16.4, nova 0.14.3 -> 0.16.1 via relock. The unpinned rebar3_kura plugin had started bundling kura 2.20, whose kura_dialect behaviour shadowed the project's kura 2.0.8 at compile time. uuid_erl left with pgo; the suite uses chatli_uuid instead. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011ePHyY6bgheH2WuHz7PUsR --- rebar.config | 7 ++-- rebar.lock | 81 ++++++++++++++++--------------------------- src/chatli.app.src | 1 - test/chatli_SUITE.erl | 2 +- 4 files changed, 33 insertions(+), 58 deletions(-) diff --git a/rebar.config b/rebar.config index 983c26e..eeac0c6 100644 --- a/rebar.config +++ b/rebar.config @@ -16,8 +16,8 @@ {deps, [ nova, mimerl, - {kura, {git, "https://github.com/Taure/kura.git", {tag, "v2.0.8"}}}, - {kura_postgres, {git, "https://github.com/Taure/kura_postgres.git", {tag, "v0.4.2"}}}, + {kura, {git, "https://github.com/Taure/kura.git", {tag, "v2.20.12"}}}, + {kura_postgres, {git, "https://github.com/Taure/kura_postgres.git", {tag, "v1.1.3"}}}, {jwerl, ".*", {git, "https://github.com/Taure/jwerl.git", {ref, "5f856e7987bb65ceec1593c21a01fd9aef87abda"}}} @@ -50,7 +50,7 @@ %% Plugins for rebar3 {project_plugins, [ erlfmt, - rebar3_kura, + {rebar3_kura, {git, "https://github.com/Taure/rebar3_kura.git", {tag, "v0.16.4"}}}, {rebar3_lint, "4.2.2"}, {rebar3_erlydtl_plugin, ".*", {git, "https://github.com/tsloughter/rebar3_erlydtl_plugin.git", {branch, "master"}}} @@ -68,7 +68,6 @@ jhn_stdlib, edoc, xmerl, - uuid, cowboy, erlydtl, cowlib, diff --git a/rebar.lock b/rebar.lock index cb159c8..78007f6 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,9 +1,8 @@ {"1.2.0", -[{<<"backoff">>,{pkg,<<"backoff">>,<<"1.1.6">>},2}, - {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.12.0">>},1}, - {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.13.0">>},2}, +[{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.18.0">>},1}, + {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.19.0">>},2}, {<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},1}, - {<<"jhn_stdlib">>,{pkg,<<"jhn_stdlib">>,<<"5.1.0">>},1}, + {<<"jhn_stdlib">>,{pkg,<<"jhn_stdlib">>,<<"5.11.2">>},1}, {<<"jsx">>,{pkg,<<"jsx">>,<<"2.10.0">>},1}, {<<"jwerl">>, {git,"https://github.com/Taure/jwerl.git", @@ -11,66 +10,44 @@ 0}, {<<"kura">>, {git,"https://github.com/Taure/kura.git", - {ref,"fdb1abc5fdc0cc48c34b7d33fd8e0d6748c4a7d1"}}, + {ref,"2fa6d253af528f3185dd5669efecc018fc9e27cf"}}, 0}, {<<"kura_postgres">>, {git,"https://github.com/Taure/kura_postgres.git", - {ref,"30651b35986bda5fc84d89fc141936cb70b636ab"}}, + {ref,"794fbae00a9fe22ac91770e3e52e612fd7cf61b7"}}, 0}, - {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.3.0">>},0}, - {<<"nova">>,{pkg,<<"nova">>,<<"0.10.2">>},0}, - {<<"opentelemetry_api">>,{pkg,<<"opentelemetry_api">>,<<"1.3.1">>},2}, - {<<"opentelemetry_semantic_conventions">>, - {pkg,<<"opentelemetry_semantic_conventions">>,<<"0.2.0">>}, - 3}, - {<<"pg_types">>,{pkg,<<"pg_types">>,<<"0.4.0">>},2}, - {<<"pgo">>,{pkg,<<"pgo">>,<<"0.20.0">>},1}, - {<<"pmod_transform">>,{pkg,<<"pmod_transform">>,<<"1.1.0">>},1}, - {<<"quickrand">>,{pkg,<<"quickrand">>,<<"2.0.7">>},2}, - {<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},2}, - {<<"routing_tree">>,{pkg,<<"routing_tree">>,<<"1.0.9">>},1}, + {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.5.0">>},0}, + {<<"minato">>,{pkg,<<"minato">>,<<"1.0.2">>},1}, + {<<"nova">>,{pkg,<<"nova">>,<<"0.16.1">>},0}, + {<<"ranch">>,{pkg,<<"ranch">>,<<"2.2.0">>},2}, + {<<"routing_tree">>,{pkg,<<"routing_tree">>,<<"1.0.11">>},1}, {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.4.2">>},1}, - {<<"thoas">>,{pkg,<<"thoas">>,<<"1.2.0">>},1}, - {<<"uuid">>,{pkg,<<"uuid_erl">>,<<"2.0.7">>},1}]}. + {<<"thoas">>,{pkg,<<"thoas">>,<<"1.2.1">>},1}]}. [ {pkg_hash,[ - {<<"backoff">>, <<"83B72ED2108BA1EE8F7D1C22E0B4A00CFE3593A67DBC792799E8CCE9F42F796B">>}, - {<<"cowboy">>, <<"F276D521A1FF88B2B9B4C54D0E753DA6C66DD7BE6C9FCA3D9418B561828A3731">>}, - {<<"cowlib">>, <<"DB8F7505D8332D98EF50A3EF34B34C1AFDDEC7506E4EE4DD4A3A266285D282CA">>}, + {<<"cowboy">>, <<"BFF388EB4D6356CB3F88C26E65B515976BF04B401B805D31550C3E60EBA8FE18">>}, + {<<"cowlib">>, <<"C9D11C9D035472E27A740C9F327786C61ED209269B4BE0260D59D3EC07B8949F">>}, {<<"erlydtl">>, <<"964B2DC84F8C17ACFAA69C59BA129EF26AC45D2BA898C3C6AD9B5BDC8BA13CED">>}, - {<<"jhn_stdlib">>, <<"55338ABE96EEB12222EF50C8506F3204EE5BD287BFF3961886189847FB4E092C">>}, + {<<"jhn_stdlib">>, <<"785074F3CA368EAA8E9AF1592BC19AE9EF1F7AF30B2CD6456A6083173A8F5CCB">>}, {<<"jsx">>, <<"77760560D6AC2B8C51FD4C980E9E19B784016AA70BE354CE746472C33BEB0B1C">>}, - {<<"mimerl">>, <<"D0CD9FC04B9061F82490F6581E0128379830E78535E017F7780F37FEA7545726">>}, - {<<"nova">>, <<"CB5A8245462F01C099AFDEED9A1B4059B5E07D5E9ADBCE0647E2E0F9E340C2B6">>}, - {<<"opentelemetry_api">>, <<"83B4713593F80562D9643C4AB0B6F80F3C5FA4C6D0632C43E11B2CCB6B04DFA7">>}, - {<<"opentelemetry_semantic_conventions">>, <<"B67FE459C2938FCAB341CB0951C44860C62347C005ACE1B50F8402576F241435">>}, - {<<"pg_types">>, <<"3CE365C92903C5BB59C0D56382D842C8C610C1B6F165E20C4B652C96FA7E9C14">>}, - {<<"pgo">>, <<"4F4A1FCB0A4894311BE238195BDAD4DF80312AB091DB87FE5348FAFA4DA75F87">>}, - {<<"pmod_transform">>, <<"A3A4FA607B947C90410BC459DCA27F6B60D25D1588D568280F7276AC8767E106">>}, - {<<"quickrand">>, <<"D2BD76676A446E6A058D678444B7FDA1387B813710D1AF6D6E29BB92186C8820">>}, - {<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>}, - {<<"routing_tree">>, <<"F7B95CF21CAEF1F184948C547780BAB4EC2B2CB0CD75B8CB2BE49F86F0DFD523">>}, + {<<"mimerl">>, <<"F35ACA6F23242339B3666E0AC0702379E362B469D0AEA167F6CC713547E777ED">>}, + {<<"minato">>, <<"962EE20DA49457633BB2B4D01ACF0C8D504BA6407AC8CF93BC075C9F88832043">>}, + {<<"nova">>, <<"E1C9C5B7E4E8B7452200C9D091FF7AE8BC53D5539EB9837472874A6512A1D333">>}, + {<<"ranch">>, <<"25528F82BC8D7C6152C57666CA99EC716510FE0925CB188172F41CE93117B1B0">>}, + {<<"routing_tree">>, <<"72ACEF2095F0EC804F7AFD07EF781DDE5009425A1CA0A28F0706B1DB334A4812">>}, {<<"telemetry">>, <<"A0CB522801DFFB1C49FE6E30561BADFFC7B6D0E180DB1300DF759FAA22062855">>}, - {<<"thoas">>, <<"73B0546BAFF5008E6ED209558FD7B0606B035D96F9F3DE24EF42879100E094D4">>}, - {<<"uuid">>, <<"B2078D2CC814F53AFA52D36C91E08962C7E7373585C623F4C0EA6DFB04B2AF94">>}]}, + {<<"thoas">>, <<"19A25F31177A17E74004D4840F66D791D4298C5738790FA2CC73731EB911F195">>}]}, {pkg_hash_ext,[ - {<<"backoff">>, <<"CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39">>}, - {<<"cowboy">>, <<"8A7ABE6D183372CEB21CAA2709BEC928AB2B72E18A3911AA1771639BEF82651E">>}, - {<<"cowlib">>, <<"E1E1284DC3FC030A64B1AD0D8382AE7E99DA46C3246B815318A4B848873800A4">>}, + {<<"cowboy">>, <<"62D0B26ABCF455054972B0DA242389C69D5982CE5914AFB8C344517F667B9600">>}, + {<<"cowlib">>, <<"6DC66E3135B229193EA4DCB14294E79520C923D391315C9C962EF0B4BEA72356">>}, {<<"erlydtl">>, <<"D80EC044CD8F58809C19D29AC5605BE09E955040911B644505E31E9DD8143431">>}, - {<<"jhn_stdlib">>, <<"760B45C0310FF6E379BC08B0A9E5C2F8C4076223D7E1BDC253D69C7D7055E687">>}, + {<<"jhn_stdlib">>, <<"2329CD16DEE46704AAB6184D09508E59DBA31C4D3255271DBB7D34D115ECA508">>}, {<<"jsx">>, <<"9A83E3704807298016968DB506F9FAD0F027DE37546EB838B3AE1064C3A0AD62">>}, - {<<"mimerl">>, <<"A1E15A50D1887217DE95F0B9B0793E32853F7C258A5CD227650889B38839FE9D">>}, - {<<"nova">>, <<"5C9E6CF451A54CF84B8278F50E49FF17EBB8F8B0D892085742FEFA33DFA2F99D">>}, - {<<"opentelemetry_api">>, <<"9E8A5CC38671E3AC61BE48ABE5F6B3AFDBBB50A1DC08B7950C56F169611505C1">>}, - {<<"opentelemetry_semantic_conventions">>, <<"D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895">>}, - {<<"pg_types">>, <<"B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB">>}, - {<<"pgo">>, <<"2F11E6649CEB38E569EF56B16BE1D04874AE5B11A02867080A2817CE423C683B">>}, - {<<"pmod_transform">>, <<"68C7FF7354C0D837681990D093D367FCC617C32570A1009B8E7ACFF6445499AA">>}, - {<<"quickrand">>, <<"B8ACBF89A224BC217C3070CA8BEBC6EB236DBE7F9767993B274084EA044D35F0">>}, - {<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>}, - {<<"routing_tree">>, <<"FB9A05EA0F3A8B77CC39CED3952B98391C617BA448AF63DDD5908F8557D8C029">>}, + {<<"mimerl">>, <<"DB648CE065BAE14EA84CA8B5DD123F42F49417CEF693541110BF6F9E9BE9ECC4">>}, + {<<"minato">>, <<"387F943683287F06C3570852B79CC2A35D36E2EFC5A3B997587845C4E0286053">>}, + {<<"nova">>, <<"05F87B2F4C05DFAA87A93CFADAEF71B735FE3E5DDEC430596170EF4A9C443804">>}, + {<<"ranch">>, <<"FA0B99A1780C80218A4197A59EA8D3BDAE32FBFF7E88527D7D8A4787EFF4F8E7">>}, + {<<"routing_tree">>, <<"85982C7AC502892C5179CD2A591331003BACD2D2A71723640BA7D23F45408E6E">>}, {<<"telemetry">>, <<"928F6495066506077862C0D1646609EED891A4326BEE3126BA54B60AF61FEBB1">>}, - {<<"thoas">>, <<"540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1">>}, - {<<"uuid">>, <<"4E4C5CA3461DC47C5E157ED42AA3981A053B7A186792AF972A27B14A9489324E">>}]} + {<<"thoas">>, <<"E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A">>}]} ]. diff --git a/src/chatli.app.src b/src/chatli.app.src index 2f65075..144d029 100644 --- a/src/chatli.app.src +++ b/src/chatli.app.src @@ -10,7 +10,6 @@ kernel, stdlib, nova, - pgo, kura, kura_postgres, jwerl, diff --git a/test/chatli_SUITE.erl b/test/chatli_SUITE.erl index 8ee4d4b..ab7609b 100644 --- a/test/chatli_SUITE.erl +++ b/test/chatli_SUITE.erl @@ -88,7 +88,7 @@ init_per_suite(_Config) -> ChatPath, encode(Chat), opts(Token) ), Device = #{<<"name">> => <<"my device">>}, - DeviceId = list_to_binary(uuid:uuid_to_string(uuid:get_v4())), + DeviceId = chatli_uuid:get_v4(), DevicePath = [?BASEPATH, <<"/client/device/">>, DeviceId], #{status := {200, _}} = jhn_shttpc:put(DevicePath, encode(Device), opts(Token)), [ From 069cff6b566c37d188f303dac8f0dae1ed498687 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 11 Sep 2026 08:59:23 +0200 Subject: [PATCH 2/9] build: build image on OTP 28.3.1 minato requires OTP 28; the image was still built on 27.0.1. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011ePHyY6bgheH2WuHz7PUsR --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c7821f9..2c3d0a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage 0 -FROM erlang:27.0.1.0-alpine +FROM erlang:28.3.1.0-alpine # Set working directory RUN mkdir /buildroot From 389212a57f352d2d566c6a6f891d04da998b0665 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 11 Sep 2026 08:59:23 +0200 Subject: [PATCH 3/9] fix: port multipart plugin to the nova 0.16 plugin API pre_request and post_request take Env and PluginState and return the state; the old two-argument callbacks crashed every request with undef. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011ePHyY6bgheH2WuHz7PUsR --- src/plugins/chatli_multipart_plugin.erl | 31 ++++++++++++------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/plugins/chatli_multipart_plugin.erl b/src/plugins/chatli_multipart_plugin.erl index 78f0e65..351595c 100644 --- a/src/plugins/chatli_multipart_plugin.erl +++ b/src/plugins/chatli_multipart_plugin.erl @@ -1,8 +1,8 @@ -module(chatli_multipart_plugin). -export([ - pre_request/2, - post_request/2, + pre_request/4, + post_request/4, plugin_info/0 ]). @@ -11,29 +11,28 @@ %% Pre-request callback %% @end %%-------------------------------------------------------------------- --spec pre_request(Req :: cowboy_req:req(), Options :: map()) -> - {ok, Req0 :: cowboy_req:req()} - | {stop, Req0 :: cowboy_req:req()} - | {error, Reason :: term()}. +-spec pre_request(Req :: cowboy_req:req(), Env :: any(), Options :: map(), State :: any()) -> + {ok, Req0 :: cowboy_req:req(), NewState :: any()}. pre_request( - #{headers := #{<<"content-type">> := <<"multipart/form-data", _/binary>>}} = Req, _Options + #{headers := #{<<"content-type">> := <<"multipart/form-data", _/binary>>}} = Req, + _Env, + _Options, + State ) -> {Req1, FormData} = multipart(Req, []), - {ok, Req1#{multipart_data => FormData}}; -pre_request(Req, _Options) -> - {ok, Req}. + {ok, Req1#{multipart_data => FormData}, State}; +pre_request(Req, _Env, _Options, State) -> + {ok, Req, State}. %%-------------------------------------------------------------------- %% @doc %% Post-request callback %% @end %%-------------------------------------------------------------------- --spec post_request(Req :: cowboy_req:req(), Options :: map()) -> - {ok, Req0 :: cowboy_req:req()} - | {stop, Req0 :: cowboy_req:req()} - | {error, Reason :: term()}. -post_request(Req, _Options) -> - {ok, Req}. +-spec post_request(Req :: cowboy_req:req(), Env :: any(), Options :: map(), State :: any()) -> + {ok, Req0 :: cowboy_req:req(), NewState :: any()}. +post_request(Req, _Env, _Options, State) -> + {ok, Req, State}. %%-------------------------------------------------------------------- %% @doc From 4543ffd71ff44a708da1ff413a9b3112708fd111 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 11 Sep 2026 08:59:23 +0200 Subject: [PATCH 4/9] fix: store message timestamps as bigint Epoch milliseconds overflow int4. pgo truncated the value silently; minato rejects it, which surfaced the bug. Widening is safe for the deployed code, so the migration opts out of the lint via safe/0. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011ePHyY6bgheH2WuHz7PUsR --- .../m20260911065748_alter_message.erl | 21 +++++++++++++++++++ src/schemas/message.erl | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/migrations/m20260911065748_alter_message.erl diff --git a/src/migrations/m20260911065748_alter_message.erl b/src/migrations/m20260911065748_alter_message.erl new file mode 100644 index 0000000..8956411 --- /dev/null +++ b/src/migrations/m20260911065748_alter_message.erl @@ -0,0 +1,21 @@ +-module(m20260911065748_alter_message). +-moduledoc false. +-behaviour(kura_migration). +-include_lib("kura/include/kura.hrl"). +-export([up/0, down/0, safe/0]). + +-spec up() -> [kura_migration:operation()]. +up() -> + [{alter_table, ~"message", [ + {modify_column, timestamp, bigint} + ]}]. + +-spec down() -> [kura_migration:operation()]. +down() -> + [{alter_table, ~"message", [ + {modify_column, timestamp, integer} + ]}]. + +-spec safe() -> [kura_migration:safe_entry()]. +safe() -> + [{modify_column, timestamp}]. diff --git a/src/schemas/message.erl b/src/schemas/message.erl index 33c0397..f88328b 100644 --- a/src/schemas/message.erl +++ b/src/schemas/message.erl @@ -14,6 +14,6 @@ fields() -> [ #kura_field{name = sender, type = uuid, nullable = false}, #kura_field{name = type, type = string}, #kura_field{name = action, type = string}, - #kura_field{name = timestamp, type = integer}, + #kura_field{name = timestamp, type = bigint}, #kura_field{name = sender_info, type = jsonb} ]. From 4042cbfa33a3f6d57cd5233ae503a21390addb0d Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 11 Sep 2026 09:14:10 +0200 Subject: [PATCH 5/9] fix: make the bigint timestamp migration one-way Narrowing back to integer fails on stored millisecond values and, on an empty table, would silently reintroduce the overflow. down/0 is now a no-op. Drops the unused kura.hrl include. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011ePHyY6bgheH2WuHz7PUsR --- src/migrations/m20260911065748_alter_message.erl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/migrations/m20260911065748_alter_message.erl b/src/migrations/m20260911065748_alter_message.erl index 8956411..1e81a2c 100644 --- a/src/migrations/m20260911065748_alter_message.erl +++ b/src/migrations/m20260911065748_alter_message.erl @@ -1,20 +1,19 @@ -module(m20260911065748_alter_message). -moduledoc false. -behaviour(kura_migration). --include_lib("kura/include/kura.hrl"). -export([up/0, down/0, safe/0]). -spec up() -> [kura_migration:operation()]. up() -> - [{alter_table, ~"message", [ - {modify_column, timestamp, bigint} - ]}]. + [ + {alter_table, ~"message", [ + {modify_column, timestamp, bigint} + ]} + ]. -spec down() -> [kura_migration:operation()]. down() -> - [{alter_table, ~"message", [ - {modify_column, timestamp, integer} - ]}]. + []. -spec safe() -> [kura_migration:safe_entry()]. safe() -> From dacc24419afcbeb2bfd4162f57046ff9c87e2079 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 11 Sep 2026 09:14:10 +0200 Subject: [PATCH 6/9] refactor: declare nova_plugin behaviour on the multipart plugin The behaviour attribute would have caught the callback arity change at compile time. plugin_info/0 moves to the nova 0.16 map form with real values, the template edoc blocks go, and the pass-through clauses get eunit coverage. Drops parse_bindings from the local config; nova 0.16 ignores it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011ePHyY6bgheH2WuHz7PUsR --- config/sys.config.local | 1 - src/plugins/chatli_multipart_plugin.erl | 38 +++++++++++-------------- test/chatli_multipart_plugin_tests.erl | 15 ++++++++++ 3 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 test/chatli_multipart_plugin_tests.erl diff --git a/config/sys.config.local b/config/sys.config.local index eb697cb..1c2a14c 100644 --- a/config/sys.config.local +++ b/config/sys.config.local @@ -32,7 +32,6 @@ {pre_request, chatli_multipart_plugin, #{}}, {pre_request, nova_request_plugin, #{ decode_json_body => true, - parse_bindings => true, parse_qs => true }} ]} diff --git a/src/plugins/chatli_multipart_plugin.erl b/src/plugins/chatli_multipart_plugin.erl index 351595c..d8f92e1 100644 --- a/src/plugins/chatli_multipart_plugin.erl +++ b/src/plugins/chatli_multipart_plugin.erl @@ -1,4 +1,5 @@ -module(chatli_multipart_plugin). +-behaviour(nova_plugin). -export([ pre_request/4, @@ -6,11 +7,6 @@ plugin_info/0 ]). -%%-------------------------------------------------------------------- -%% @doc -%% Pre-request callback -%% @end -%%-------------------------------------------------------------------- -spec pre_request(Req :: cowboy_req:req(), Env :: any(), Options :: map(), State :: any()) -> {ok, Req0 :: cowboy_req:req(), NewState :: any()}. pre_request( @@ -24,29 +20,29 @@ pre_request( pre_request(Req, _Env, _Options, State) -> {ok, Req, State}. -%%-------------------------------------------------------------------- -%% @doc -%% Post-request callback -%% @end -%%-------------------------------------------------------------------- -spec post_request(Req :: cowboy_req:req(), Env :: any(), Options :: map(), State :: any()) -> {ok, Req0 :: cowboy_req:req(), NewState :: any()}. post_request(Req, _Env, _Options, State) -> {ok, Req, State}. -%%-------------------------------------------------------------------- -%% @doc -%% nova_plugin callback. Returns information about the plugin. -%% @end -%%-------------------------------------------------------------------- -spec plugin_info() -> - {Title :: binary(), Version :: binary(), Author :: binary(), Description :: binary(), [ - {Key :: atom(), OptionDescription :: atom()} - ]}. + #{ + title := binary(), + version := binary(), + url := binary(), + authors := [binary()], + description := binary(), + options => [{atom(), binary()}] + }. plugin_info() -> - {<<"Plugin name plugin">>, <<"0.0.1">>, <<"User >, <<"Descriptive text">>, - %% Options is specified as {Key, Description} - []}. + #{ + title => ~"chatli multipart plugin", + version => ~"0.1.0", + url => ~"https://github.com/widgrensit/chatli", + authors => [~"Widgrens IT AB"], + description => ~"Parses multipart/form-data bodies into multipart_data on the request", + options => [] + }. multipart(Req0, Acc) -> case cowboy_req:read_part(Req0) of diff --git a/test/chatli_multipart_plugin_tests.erl b/test/chatli_multipart_plugin_tests.erl new file mode 100644 index 0000000..d4b2c0c --- /dev/null +++ b/test/chatli_multipart_plugin_tests.erl @@ -0,0 +1,15 @@ +-module(chatli_multipart_plugin_tests). + +-include_lib("eunit/include/eunit.hrl"). + +passthrough_keeps_request_and_state_test() -> + Req = #{headers => #{~"content-type" => ~"application/json"}}, + ?assertEqual({ok, Req, state}, chatli_multipart_plugin:pre_request(Req, #{}, #{}, state)). + +passthrough_sets_no_multipart_data_test() -> + {ok, Req, _} = chatli_multipart_plugin:pre_request(#{headers => #{}}, #{}, #{}, undefined), + ?assertNot(maps:is_key(multipart_data, Req)). + +post_request_is_identity_test() -> + Req = #{headers => #{}}, + ?assertEqual({ok, Req, state}, chatli_multipart_plugin:post_request(Req, #{}, #{}, state)). From 3c16f0f929625f2b8d589696175dd19884faf789 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 11 Sep 2026 09:14:10 +0200 Subject: [PATCH 7/9] chore: format migrations with erlfmt src/migrations was outside the erlfmt file globs, so generated migrations were never checked. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011ePHyY6bgheH2WuHz7PUsR --- rebar.config | 1 + .../m20260624090455_update_schema.erl | 108 +++++++++--------- 2 files changed, 57 insertions(+), 52 deletions(-) diff --git a/rebar.config b/rebar.config index eeac0c6..fe0a00a 100644 --- a/rebar.config +++ b/rebar.config @@ -81,6 +81,7 @@ "{rebar.config, elvis.config, src, include,test}/*.{hrl,erl}", "src/controllers/*.erl", "src/plugins/*.erl", + "src/migrations/*.erl", "config/*" ]} ]}. diff --git a/src/migrations/m20260624090455_update_schema.erl b/src/migrations/m20260624090455_update_schema.erl index 056daa8..1248c19 100644 --- a/src/migrations/m20260624090455_update_schema.erl +++ b/src/migrations/m20260624090455_update_schema.erl @@ -4,57 +4,61 @@ -export([up/0, down/0]). up() -> - [{create_table, <<"attachment">>, [ - #kura_column{name = id, type = uuid, primary_key = true}, - #kura_column{name = chat_id, type = uuid, nullable = false}, - #kura_column{name = mime, type = string, nullable = false}, - #kura_column{name = length, type = integer} - ]}, - {create_table, <<"callback">>, [ - #kura_column{name = id, type = uuid, primary_key = true}, - #kura_column{name = user_id, type = uuid, nullable = false}, - #kura_column{name = url, type = string, nullable = false} - ]}, - {create_table, <<"chat">>, [ - #kura_column{name = id, type = uuid, primary_key = true}, - #kura_column{name = name, type = string, nullable = false}, - #kura_column{name = description, type = string}, - #kura_column{name = type, type = string} - ]}, - {create_table, <<"chatli_user">>, [ - #kura_column{name = id, type = uuid, primary_key = true}, - #kura_column{name = username, type = string, nullable = false}, - #kura_column{name = phone_number, type = string}, - #kura_column{name = email, type = string}, - #kura_column{name = avatar, type = string}, - #kura_column{name = password, type = string, nullable = false} - ]}, - {create_table, <<"device">>, [ - #kura_column{name = id, type = uuid, primary_key = true}, - #kura_column{name = user_id, type = uuid, nullable = false}, - #kura_column{name = name, type = string} - ]}, - {create_table, <<"message">>, [ - #kura_column{name = id, type = uuid, primary_key = true}, - #kura_column{name = chat_id, type = uuid, nullable = false}, - #kura_column{name = payload, type = jsonb}, - #kura_column{name = sender, type = uuid, nullable = false}, - #kura_column{name = type, type = string}, - #kura_column{name = action, type = string}, - #kura_column{name = timestamp, type = integer}, - #kura_column{name = sender_info, type = jsonb} - ]}, - {create_table, <<"participant">>, [ - #kura_column{name = id, type = id, primary_key = true}, - #kura_column{name = chat_id, type = uuid}, - #kura_column{name = user_id, type = uuid} - ]}]. + [ + {create_table, <<"attachment">>, [ + #kura_column{name = id, type = uuid, primary_key = true}, + #kura_column{name = chat_id, type = uuid, nullable = false}, + #kura_column{name = mime, type = string, nullable = false}, + #kura_column{name = length, type = integer} + ]}, + {create_table, <<"callback">>, [ + #kura_column{name = id, type = uuid, primary_key = true}, + #kura_column{name = user_id, type = uuid, nullable = false}, + #kura_column{name = url, type = string, nullable = false} + ]}, + {create_table, <<"chat">>, [ + #kura_column{name = id, type = uuid, primary_key = true}, + #kura_column{name = name, type = string, nullable = false}, + #kura_column{name = description, type = string}, + #kura_column{name = type, type = string} + ]}, + {create_table, <<"chatli_user">>, [ + #kura_column{name = id, type = uuid, primary_key = true}, + #kura_column{name = username, type = string, nullable = false}, + #kura_column{name = phone_number, type = string}, + #kura_column{name = email, type = string}, + #kura_column{name = avatar, type = string}, + #kura_column{name = password, type = string, nullable = false} + ]}, + {create_table, <<"device">>, [ + #kura_column{name = id, type = uuid, primary_key = true}, + #kura_column{name = user_id, type = uuid, nullable = false}, + #kura_column{name = name, type = string} + ]}, + {create_table, <<"message">>, [ + #kura_column{name = id, type = uuid, primary_key = true}, + #kura_column{name = chat_id, type = uuid, nullable = false}, + #kura_column{name = payload, type = jsonb}, + #kura_column{name = sender, type = uuid, nullable = false}, + #kura_column{name = type, type = string}, + #kura_column{name = action, type = string}, + #kura_column{name = timestamp, type = integer}, + #kura_column{name = sender_info, type = jsonb} + ]}, + {create_table, <<"participant">>, [ + #kura_column{name = id, type = id, primary_key = true}, + #kura_column{name = chat_id, type = uuid}, + #kura_column{name = user_id, type = uuid} + ]} + ]. down() -> - [{drop_table, <<"attachment">>}, - {drop_table, <<"callback">>}, - {drop_table, <<"chat">>}, - {drop_table, <<"chatli_user">>}, - {drop_table, <<"device">>}, - {drop_table, <<"message">>}, - {drop_table, <<"participant">>}]. + [ + {drop_table, <<"attachment">>}, + {drop_table, <<"callback">>}, + {drop_table, <<"chat">>}, + {drop_table, <<"chatli_user">>}, + {drop_table, <<"device">>}, + {drop_table, <<"message">>}, + {drop_table, <<"participant">>} + ]. From 1908603e4acc3d5f18e505686c098c19158740c7 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 11 Sep 2026 09:14:10 +0200 Subject: [PATCH 8/9] build: pin runtime alpine and test on the shipped OTP The builder was pinned to erlang:28.3.1.0-alpine (Alpine 3.23) while the runtime stage floated on alpine:latest. CI now runs OTP 28.3.1 to match .tool-versions and the image. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011ePHyY6bgheH2WuHz7PUsR --- .github/workflows/erlang.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/erlang.yml b/.github/workflows/erlang.yml index e0be7c6..3b28f6a 100644 --- a/.github/workflows/erlang.yml +++ b/.github/workflows/erlang.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - otp: ['28.0'] + otp: ['28.3.1'] rebar3: ['3.26.0'] steps: - uses: actions/checkout@v4 diff --git a/Dockerfile b/Dockerfile index 2c3d0a0..250d74f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN apk add --update git RUN rebar3 release # Build stage 1 -FROM alpine +FROM alpine:3.23 # Install some libs RUN apk add --no-cache openssl && \ From cf4c799a383673ad6ac52521451b2f2279b5f0f4 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Fri, 11 Sep 2026 09:14:10 +0200 Subject: [PATCH 9/9] test: fix ELP lint errors in the CT suite Explicit exports instead of export_all so helpers no longer read as unreachable tests, chained map matches split to avoid the mutable variable bug, and the dead add_participant case removed - it was never in all() and matched the chat id with an atom key. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011ePHyY6bgheH2WuHz7PUsR --- test/chatli_SUITE.erl | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/test/chatli_SUITE.erl b/test/chatli_SUITE.erl index ab7609b..aeeefc6 100644 --- a/test/chatli_SUITE.erl +++ b/test/chatli_SUITE.erl @@ -1,6 +1,28 @@ -module(chatli_SUITE). --compile(export_all). +-export([ + suite/0, + all/0, + groups/0, + init_per_suite/1, + end_per_suite/1, + init_per_group/2, + end_per_group/2, + init_per_testcase/2, + end_per_testcase/2, + get_all_users/1, + list_participant/1, + get_all_chats/1, + create_same_chat_again/1, + send_message/1, + get_all_message/1, + get_filtered_message/1, + get_historic_message/1, + upload_attachment/1, + remove_participant/1, + get_all_devices/1, + get_callback/1 +]). -include_lib("common_test/include/ct.hrl"). @@ -42,7 +64,6 @@ init_per_suite(_Config) -> }, Path = [?BASEPATH, <<"/v1/signup">>], #{status := {201, _}} = jhn_shttpc:post(Path, encode(User1), opts()), - Path = [?BASEPATH, <<"/v1/signup">>], #{status := {201, _}} = jhn_shttpc:post(Path, encode(User2), opts()), LoginPath = [?BASEPATH, <<"/v1/login">>], #{status := {200, _}, body := LoginRespBody} = jhn_shttpc:post( @@ -233,13 +254,6 @@ get_all_users(Config) -> #{status := {200, _}, body := RespBody} = jhn_shttpc:get(Path, opts(Token)), 4 = length(decode(RespBody)). -add_participant(Config) -> - #{token := Token} = proplists:get_value(user1, Config), - #{object := #{id := UserId2}} = proplists:get_value(user2, Config), - #{id := ChatId} = proplists:get_value(chat, Config), - Path = [?BASEPATH, <<"/client/chat/">>, ChatId, <<"/participant">>], - #{status := {201, _}} = jhn_shttpc:post(Path, encode(#{id => UserId2}), opts(Token)). - list_participant(Config) -> #{token := Token} = proplists:get_value(user1, Config), #{<<"id">> := ChatId} = proplists:get_value(chat, Config), @@ -310,7 +324,7 @@ get_all_message(Config) -> [#{<<"id">> := MessageId}] = [MessageObj] = decode(RespBody), MessagePath = [?BASEPATH, <<"/client/chat/">>, ChatId, <<"/message/">>, MessageId], #{status := {200, _}, body := MessageRespBody} = jhn_shttpc:get(MessagePath, opts(Token)), - #{<<"id">> := MessageId} = MessageObj = decode(MessageRespBody). + MessageObj = decode(MessageRespBody). get_filtered_message(Config) -> #{token := Token} = proplists:get_value(user1, Config), @@ -321,7 +335,7 @@ get_filtered_message(Config) -> [#{<<"id">> := MessageId}] = [MessageObj] = decode(RespBody), MessagePath = [?BASEPATH, <<"/client/chat/">>, ChatId, <<"/message/">>, MessageId], #{status := {200, _}, body := MessageRespBody} = jhn_shttpc:get(MessagePath, opts(Token)), - #{<<"id">> := MessageId} = MessageObj = decode(MessageRespBody), + MessageObj = decode(MessageRespBody), Path2 = [?BASEPATH, <<"/client/chat/">>, ChatId, <<"/message?before=">>, StartTimestamp], #{status := {200, _}, body := RespBody2} = jhn_shttpc:get(Path2, opts(Token)), [] = decode(RespBody2). @@ -344,7 +358,7 @@ get_all_devices(Config) -> DeviceId = DeviceId2, DevicePath = [?BASEPATH, <<"/client/device/">>, DeviceId], #{status := {200, _}, body := DeviceRespBody} = jhn_shttpc:get(DevicePath, opts(Token)), - #{<<"id">> := DeviceId} = DeviceObj = decode(DeviceRespBody). + DeviceObj = decode(DeviceRespBody). get_callback(Config) -> #{<<"id">> := CallbackId} = proplists:get_value(callback, Config),