From 8cb1edb5a249e865a7ca355ff00407396be8cc2a Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Wed, 24 Jun 2026 09:04:03 +0200 Subject: [PATCH 1/2] feat: migrate persistence from pgo to kura Replace raw pgo queries with the kura data layer: - add kura schemas (chat, message, chatli_user, device, participant, attachment, callback, li, ldf_message) and chatli_repo (kura_repo) - rewrite chatli_db, chatli_user_db, chatli_device_db onto kura_query/kura_changeset/chatli_repo - pin kura v2.0.8 (jsonb string-scalar dump support) + kura_postgres v0.4.2, drop the local _checkouts override - add generated sql migrations and a dev docker-compose for local Postgres - switch config to the kura postgres backend --- .gitignore | 6 +- .tool-versions | 4 +- config/sys.config | 23 +- config/sys.config.local | 23 +- docker-compose.yml | 15 + rebar.config | 4 +- rebar.lock | 25 +- sql/10_ldf.sql | 23 ++ sql/20_chatli.sql | 70 ++++ src/chatli.app.src | 2 + src/chatli_db.erl | 364 +++++++++------------ src/chatli_device_db.erl | 56 ++-- src/chatli_repo.erl | 45 +++ src/chatli_user_db.erl | 82 +++-- src/controllers/chatli_chat_controller.erl | 15 +- src/schemas/attachment.erl | 15 + src/schemas/callback.erl | 14 + src/schemas/chat.erl | 15 + src/schemas/chatli_user.erl | 17 + src/schemas/device.erl | 14 + src/schemas/ldf_message.erl | 15 + src/schemas/li.erl | 19 ++ src/schemas/message.erl | 19 ++ src/schemas/participant.erl | 14 + test/chatli_SUITE.erl | 40 +-- 25 files changed, 605 insertions(+), 334 deletions(-) create mode 100644 docker-compose.yml create mode 100755 sql/10_ldf.sql create mode 100755 sql/20_chatli.sql create mode 100644 src/chatli_repo.erl create mode 100644 src/schemas/attachment.erl create mode 100644 src/schemas/callback.erl create mode 100644 src/schemas/chat.erl create mode 100644 src/schemas/chatli_user.erl create mode 100644 src/schemas/device.erl create mode 100644 src/schemas/ldf_message.erl create mode 100644 src/schemas/li.erl create mode 100644 src/schemas/message.erl create mode 100644 src/schemas/participant.erl diff --git a/.gitignore b/.gitignore index ca07b30..719d11f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ _build/ -priv/attachments/* \ No newline at end of file +_checkouts/ +priv/attachments/* +CLAUDE.md +.claude/ +*.crashdump \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index 66dc047..631836c 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -erlang 27.0.1 -rebar 3.23.0 \ No newline at end of file +erlang 28.3.1 +rebar 3.26.0 diff --git a/config/sys.config b/config/sys.config index 80b1cb6..141db21 100644 --- a/config/sys.config +++ b/config/sys.config @@ -4,23 +4,18 @@ {kernel, [ {logger_level, debug} ]}, - {pgo, [ - {pools, [ - {default, #{ - pool_size => 2, + {kura, [ + {repos, #{ + chatli_repo => #{ + backend => kura_backend_postgres, host => "db", port => 5432, database => "chatli", user => "postgres", password => "root", - idle_interval => 500, - decode_opts => [return_rows_as_maps, column_name_as_atom] - }} - ]} - ]}, - {pg_types, [ - {uuid_format, string}, - {json_config, {chatli_json, #{}, #{}}} + pool_size => 2 + } + }} ]}, {nova, [ {cowboy_configuration, #{ @@ -41,6 +36,8 @@ }} ]} ]}, - {chatli, [{download_path, "./priv/attachments/"}]} + {chatli, [ + {download_path, "./priv/attachments/"} + ]} %% Please change your app.src-file instead if you intend to add app-specific configurations ]. diff --git a/config/sys.config.local b/config/sys.config.local index 780fdb7..eb697cb 100644 --- a/config/sys.config.local +++ b/config/sys.config.local @@ -4,23 +4,18 @@ {kernel, [ {logger_level, debug} ]}, - {pgo, [ - {pools, [ - {default, #{ - pool_size => 2, + {kura, [ + {repos, #{ + chatli_repo => #{ + backend => kura_backend_postgres, host => "localhost", port => 5555, database => "chatli", user => "postgres", password => "root", - idle_interval => 500, - decode_opts => [return_rows_as_maps, column_name_as_atom] - }} - ]} - ]}, - {pg_types, [ - {uuid_format, string}, - {json_config, {chatli_json, #{}, #{}}} + pool_size => 2 + } + }} ]}, {nova, [ {cowboy_configuration, #{ @@ -42,6 +37,8 @@ }} ]} ]}, - {chatli, [{download_path, "./priv/attachments/"}]} + {chatli, [ + {download_path, "./priv/attachments/"} + ]} %% Please change your app.src-file instead if you intend to add app-specific configurations ]. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..08304e8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +# Use postgres/example user/password credentials +services: + db: + image: postgres:17-alpine + command: -c 'max_connections=2000' + environment: + POSTGRES_PASSWORD: root + ports: + - 5555:5432 + volumes: + - ./sql:/docker-entrypoint-initdb.d + adminer: + image: adminer + ports: + - 8082:8080 diff --git a/rebar.config b/rebar.config index c98fca2..c4e46ec 100644 --- a/rebar.config +++ b/rebar.config @@ -15,8 +15,9 @@ {deps, [ nova, - pgo, 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"}}}, {jwerl, ".*", {git, "https://github.com/Taure/jwerl.git", {ref, "5f856e7987bb65ceec1593c21a01fd9aef87abda"}}} @@ -49,6 +50,7 @@ %% Plugins for rebar3 {project_plugins, [ erlfmt, + rebar3_kura, rebar3_lint, {rebar3_erlydtl_plugin, ".*", {git, "https://github.com/tsloughter/rebar3_erlydtl_plugin.git", {branch, "master"}}} diff --git a/rebar.lock b/rebar.lock index 148fdc8..cb159c8 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,5 +1,5 @@ {"1.2.0", -[{<<"backoff">>,{pkg,<<"backoff">>,<<"1.1.6">>},1}, +[{<<"backoff">>,{pkg,<<"backoff">>,<<"1.1.6">>},2}, {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.12.0">>},1}, {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.13.0">>},2}, {<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},1}, @@ -9,18 +9,27 @@ {git,"https://github.com/Taure/jwerl.git", {ref,"5f856e7987bb65ceec1593c21a01fd9aef87abda"}}, 0}, + {<<"kura">>, + {git,"https://github.com/Taure/kura.git", + {ref,"fdb1abc5fdc0cc48c34b7d33fd8e0d6748c4a7d1"}}, + 0}, + {<<"kura_postgres">>, + {git,"https://github.com/Taure/kura_postgres.git", + {ref,"30651b35986bda5fc84d89fc141936cb70b636ab"}}, + 0}, {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.3.0">>},0}, {<<"nova">>,{pkg,<<"nova">>,<<"0.10.2">>},0}, - {<<"opentelemetry_api">>,{pkg,<<"opentelemetry_api">>,<<"1.3.1">>},1}, + {<<"opentelemetry_api">>,{pkg,<<"opentelemetry_api">>,<<"1.3.1">>},2}, {<<"opentelemetry_semantic_conventions">>, {pkg,<<"opentelemetry_semantic_conventions">>,<<"0.2.0">>}, - 2}, - {<<"pg_types">>,{pkg,<<"pg_types">>,<<"0.4.0">>},1}, - {<<"pgo">>,{pkg,<<"pgo">>,<<"0.14.0">>},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}, + {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.4.2">>},1}, {<<"thoas">>,{pkg,<<"thoas">>,<<"1.2.0">>},1}, {<<"uuid">>,{pkg,<<"uuid_erl">>,<<"2.0.7">>},1}]}. [ @@ -36,11 +45,12 @@ {<<"opentelemetry_api">>, <<"83B4713593F80562D9643C4AB0B6F80F3C5FA4C6D0632C43E11B2CCB6B04DFA7">>}, {<<"opentelemetry_semantic_conventions">>, <<"B67FE459C2938FCAB341CB0951C44860C62347C005ACE1B50F8402576F241435">>}, {<<"pg_types">>, <<"3CE365C92903C5BB59C0D56382D842C8C610C1B6F165E20C4B652C96FA7E9C14">>}, - {<<"pgo">>, <<"F53711D103D7565DB6FC6061FCF4FF1007AB39892439BE1BB02D9F686D7E6663">>}, + {<<"pgo">>, <<"4F4A1FCB0A4894311BE238195BDAD4DF80312AB091DB87FE5348FAFA4DA75F87">>}, {<<"pmod_transform">>, <<"A3A4FA607B947C90410BC459DCA27F6B60D25D1588D568280F7276AC8767E106">>}, {<<"quickrand">>, <<"D2BD76676A446E6A058D678444B7FDA1387B813710D1AF6D6E29BB92186C8820">>}, {<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>}, {<<"routing_tree">>, <<"F7B95CF21CAEF1F184948C547780BAB4EC2B2CB0CD75B8CB2BE49F86F0DFD523">>}, + {<<"telemetry">>, <<"A0CB522801DFFB1C49FE6E30561BADFFC7B6D0E180DB1300DF759FAA22062855">>}, {<<"thoas">>, <<"73B0546BAFF5008E6ED209558FD7B0606B035D96F9F3DE24EF42879100E094D4">>}, {<<"uuid">>, <<"B2078D2CC814F53AFA52D36C91E08962C7E7373585C623F4C0EA6DFB04B2AF94">>}]}, {pkg_hash_ext,[ @@ -55,11 +65,12 @@ {<<"opentelemetry_api">>, <<"9E8A5CC38671E3AC61BE48ABE5F6B3AFDBBB50A1DC08B7950C56F169611505C1">>}, {<<"opentelemetry_semantic_conventions">>, <<"D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895">>}, {<<"pg_types">>, <<"B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB">>}, - {<<"pgo">>, <<"71016C22599936E042DC0012EE4589D24C71427D266292F775EBF201D97DF9C9">>}, + {<<"pgo">>, <<"2F11E6649CEB38E569EF56B16BE1D04874AE5B11A02867080A2817CE423C683B">>}, {<<"pmod_transform">>, <<"68C7FF7354C0D837681990D093D367FCC617C32570A1009B8E7ACFF6445499AA">>}, {<<"quickrand">>, <<"B8ACBF89A224BC217C3070CA8BEBC6EB236DBE7F9767993B274084EA044D35F0">>}, {<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>}, {<<"routing_tree">>, <<"FB9A05EA0F3A8B77CC39CED3952B98391C617BA448AF63DDD5908F8557D8C029">>}, + {<<"telemetry">>, <<"928F6495066506077862C0D1646609EED891A4326BEE3126BA54B60AF61FEBB1">>}, {<<"thoas">>, <<"540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1">>}, {<<"uuid">>, <<"4E4C5CA3461DC47C5E157ED42AA3981A053B7A186792AF972A27B14A9489324E">>}]} ]. diff --git a/sql/10_ldf.sql b/sql/10_ldf.sql new file mode 100755 index 0000000..05efc8d --- /dev/null +++ b/sql/10_ldf.sql @@ -0,0 +1,23 @@ +CREATE DATABASE ldf; + +\c ldf; + +CREATE TABLE li +( + id SERIAL PRIMARY KEY, + type VARCHAR NOT NULL, + value VARCHAR NOT NULL, + callback_id uuid NOT NULL, + user_id uuid NOT NULL, + username VARCHAR, + phone_number VARCHAR, + email VARCHAR +); + +CREATE TABLE ldf_message +( + id SERIAL PRIMARY KEY, + message_id UUID NOT NULL, + payload VARCHAR, + content_length VARCHAR +); \ No newline at end of file diff --git a/sql/20_chatli.sql b/sql/20_chatli.sql new file mode 100755 index 0000000..318fbe6 --- /dev/null +++ b/sql/20_chatli.sql @@ -0,0 +1,70 @@ +CREATE DATABASE chatli; + +\c chatli; + +CREATE EXTENSION pgcrypto; + +CREATE TABLE chatli_user +( + id uuid PRIMARY KEY, + username varchar NOT NULL UNIQUE, + phone_number varchar UNIQUE, + email varchar UNIQUE, + avatar varchar, + password varchar NOT NULL +); + +CREATE TABLE message +( + id uuid PRIMARY KEY, + chat_id uuid NOT NULL, + payload jsonb, + sender uuid NOT NULL, + type varchar, + action varchar, + timestamp bigint, + sender_info jsonb +); + +CREATE TABLE chat +( + id uuid PRIMARY KEY, + name varchar NOT NULL, + description varchar, + type varchar +); + +CREATE TABLE participant +( + id SERIAL PRIMARY KEY, + chat_id uuid, + user_id uuid, + UNIQUE(chat_id, user_id) +); + +CREATE TABLE callback +( + id UUID PRIMARY KEY, + user_id UUID NOT NULL, + url VARCHAR NOT NULL +); + + +CREATE TABLE device +( + id UUID PRIMARY KEY, + user_id UUID NOT NULL, + name VARCHAR +); + +CREATE TABLE attachment +( + id UUID PRIMARY KEY, + chat_id UUID NOT NULL, + mime VARCHAR NOT NULL, + length INTEGER +); + +INSERT INTO chatli_user (id, username, phone_number, email, password) VALUES (gen_random_uuid(), 'alice', '461234', 'alice@example.com', 'alice'); +INSERT INTO chatli_user (id, username, phone_number, email, password) VALUES (gen_random_uuid(), 'bob', '462345', 'bob@example.com', 'bob'); +INSERT INTO chatli_user (id, username, phone_number, email, password) VALUES (gen_random_uuid(), 'ceasar', '463456', 'ceasar@example.com', 'ceasar'); \ No newline at end of file diff --git a/src/chatli.app.src b/src/chatli.app.src index b9a4ad9..2f65075 100644 --- a/src/chatli.app.src +++ b/src/chatli.app.src @@ -11,6 +11,8 @@ stdlib, nova, pgo, + kura, + kura_postgres, jwerl, runtime_tools, mimerl diff --git a/src/chatli_db.erl b/src/chatli_db.erl index c474356..fb2e1d0 100644 --- a/src/chatli_db.erl +++ b/src/chatli_db.erl @@ -19,254 +19,194 @@ get_user_callbacks/1, delete_callback/1, create_attachment/4, - get_attachment/2, - query/2, - query1/2 + get_attachment/2 ]). -create_message(#{ - <<"id">> := Id, - <<"chat_id">> := ChatId, - <<"payload">> := Payload, - <<"sender">> := UserId, - <<"sender_info">> := SenderInfoJson, - <<"timestamp">> := Timestamp, - <<"type">> := Type, - <<"action">> := Action -}) -> - SQL = - << - "INSERT INTO message (id,\n" - " chat_id,\n" - " payload,\n" - " sender,\n" - " timestamp,\n" - " type,\n" - " action,\n" - " sender_info)\n" - " VALUES ($1, $2, $3, $4, $5, $6, $7, $8)" - >>, - query1(SQL, [Id, ChatId, Payload, UserId, Timestamp, Type, Action, SenderInfoJson]). +-include_lib("kura/include/kura.hrl"). + +%% Messages + +create_message(Params) -> + CS = kura_changeset:cast(message, #{}, encode_payload(Params), [ + id, chat_id, payload, sender, timestamp, type, action, sender_info + ]), + case chatli_repo:insert(CS) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. + +%% A text payload is a JSON string scalar; kura casts jsonb binaries as +%% JSON text, so encode it before the changeset. +encode_payload(#{<<"payload">> := P} = Params) when is_binary(P) -> + Params#{<<"payload">> := iolist_to_binary(json:encode(P))}; +encode_payload(Params) -> + Params. get_message(ChatId, MessageId) -> - SQL = - << - "SELECT id,\n" - " chat_id,\n" - " payload,\n" - " sender,\n" - " timestamp\n" - " FROM message\n" - " WHERE chat_id = $1 AND id = $2" - >>, - query1(SQL, [ChatId, MessageId]). + Q = kura_query:from(message), + Q1 = kura_query:select(Q, [id, chat_id, payload, sender, sender_info, timestamp]), + Q2 = kura_query:where(Q1, {chat_id, ChatId}), + Q3 = kura_query:where(Q2, {id, MessageId}), + one(Q3). get_chat_messages(ChatId) -> - SQL = - << - "SELECT id,\n" - " chat_id,\n" - " payload,\n" - " sender,\n" - " timestamp\n" - " FROM message\n" - " WHERE chat_id = $1 \n" - " ORDER BY timestamp ASC" - >>, - query(SQL, [ChatId]). + Q = kura_query:from(message), + Q1 = kura_query:select(Q, [id, chat_id, payload, sender, sender_info, timestamp]), + Q2 = kura_query:where(Q1, {chat_id, ChatId}), + Q3 = kura_query:order_by(Q2, [{timestamp, asc}]), + chatli_repo:all(Q3). get_filtered_messages(ChatId, QS) -> - Where = <<" WHERE chat_id=$1 ">>, - {Values, SqlWHERE} = - case QS of - #{ - <<"after">> := After, - <<"before">> := Before - } -> - { - [ChatId, binary_to_integer(After), binary_to_integer(Before)], - <= $2 AND timestamp <= $3 ">> - }; - #{<<"after">> := After} -> - {[ChatId, binary_to_integer(After)], <= $2 ">>}; - #{<<"before">> := Before} -> - {[ChatId, binary_to_integer(Before)], <>}; - _ -> - {[ChatId], Where} - end, - SQL = - << - "SELECT id,\n" - " chat_id,\n" - " payload,\n" - " sender,\n" - " sender_info,\n" - " timestamp\n" - " FROM message", - SqlWHERE/binary, - "ORDER BY timestamp ASC" - >>, - query(SQL, Values). - -create_chat(#{ - <<"id">> := Id, - <<"name">> := Name, - <<"description">> := Description, - <<"type">> := Type -}) -> - SQL = <<"INSERT INTO chat (id, name, description, type) VALUES ($1, $2, $3, $4)">>, - query1(SQL, [Id, Name, Description, Type]). + Q = kura_query:from(message), + Q1 = kura_query:select(Q, [id, chat_id, payload, sender, sender_info, timestamp]), + Q2 = kura_query:where(Q1, {chat_id, ChatId}), + Q3 = maybe_after(Q2, QS), + Q4 = maybe_before(Q3, QS), + Q5 = kura_query:order_by(Q4, [{timestamp, asc}]), + chatli_repo:all(Q5). + +maybe_after(Q, #{<<"after">> := After}) -> + kura_query:where(Q, {timestamp, '>=', binary_to_integer(After)}); +maybe_after(Q, _) -> + Q. + +maybe_before(Q, #{<<"before">> := Before}) -> + kura_query:where(Q, {timestamp, '<=', binary_to_integer(Before)}); +maybe_before(Q, _) -> + Q. + +%% Chats + +create_chat(Params) -> + CS = kura_changeset:cast(chat, #{}, Params, [id, name, description, type]), + case chatli_repo:insert(CS) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. get_chat(ChatId) -> - SQL = <<"SELECT * FROM chat WHERE id = $1">>, - query1(SQL, [ChatId]). + case chatli_repo:get(chat, ChatId) of + {ok, Chat} -> {ok, Chat}; + {error, not_found} -> undefined + end. get_dm_chat(User1, User2) -> SQL = - << - "SELECT chat.*\n" - " FROM chat\n" - " INNER JOIN participant AS p1 ON p1.user_id = $1 AND p1.chat_id = chat.id\n" - " INNER JOIN participant AS p2 ON p2.user_id = $2 AND p2.chat_id = chat.id\n" - " WHERE chat.type = '1to1' LIMIT 1" - >>, - query1(SQL, [User1, User2]). + <<"SELECT chat.*" + " FROM chat" + " INNER JOIN participant AS p1 ON p1.user_id = $1 AND p1.chat_id = chat.id" + " INNER JOIN participant AS p2 ON p2.user_id = $2 AND p2.chat_id = chat.id" + " WHERE chat.type = '1to1' LIMIT 1">>, + case chatli_repo:query(SQL, [User1, User2]) of + {ok, [Row | _]} -> {ok, Row}; + {ok, []} -> undefined; + {error, _} = Error -> Error + end. get_all_chats(UserId) -> SQL = - << - "SELECT chat.*\n" - " FROM chat\n" - " INNER JOIN participant\n" - " ON participant.user_id = $1 AND\n" - " participant.chat_id = chat.id" - >>, - query(SQL, [UserId]). + <<"SELECT chat.*" + " FROM chat" + " INNER JOIN participant ON participant.user_id = $1 AND participant.chat_id = chat.id">>, + chatli_repo:query(SQL, [UserId]). delete_chat(ChatId) -> - SQL = <<"DELETE FROM chat WHERE id = $1">>, - query1(SQL, [ChatId]). + Q = kura_query:from(chat), + Q1 = kura_query:where(Q, {id, ChatId}), + case chatli_repo:delete_all(Q1) of + {ok, 1} -> ok; + {ok, 0} -> undefined; + {error, _} = Error -> Error + end. + +%% Participants add_participant(ChatId, UserId) -> - SQL = <<"INSERT INTO participant (chat_id, user_id) VALUES ($1, $2)">>, - query1(SQL, [ChatId, UserId]). + CS = kura_changeset:cast(participant, #{}, #{<<"chat_id">> => ChatId, <<"user_id">> => UserId}, [chat_id, user_id]), + case chatli_repo:insert(CS) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. remove_participant(ChatId, UserId) -> - SQL = <<"DELETE FROM participant WHERE chat_id = $1 AND user_id = $2">>, - query1(SQL, [ChatId, UserId]). + Q = kura_query:from(participant), + Q1 = kura_query:where(Q, {chat_id, ChatId}), + Q2 = kura_query:where(Q1, {user_id, UserId}), + case chatli_repo:delete_all(Q2) of + {ok, 1} -> ok; + {ok, 0} -> undefined; + {error, _} = Error -> Error + end. + +get_participants(ChatId) -> + Q = kura_query:from(participant), + Q1 = kura_query:select(Q, [user_id]), + Q2 = kura_query:where(Q1, {chat_id, ChatId}), + chatli_repo:all(Q2). get_all_other_participants(ChatId, UserId) -> SQL = - << - "SELECT chatli_user.id,\n" - " chatli_user.username,\n" - " chatli_user.email\n" - " FROM participant\n" - " INNER JOIN chatli_user ON chatli_user.id = participant.user_id\n" - " WHERE participant.chat_id = $1 AND participant.user_id != $2" - >>, - query(SQL, [ChatId, UserId]). + <<"SELECT chatli_user.id, chatli_user.username, chatli_user.email" + " FROM participant" + " INNER JOIN chatli_user ON chatli_user.id = participant.user_id" + " WHERE participant.chat_id = $1 AND participant.user_id != $2">>, + chatli_repo:query(SQL, [ChatId, UserId]). -get_participants(ChatId) -> - SQL = <<"SELECT user_id FROM participant WHERE chat_id = $1">>, - query(SQL, [ChatId]). +%% Callbacks create_callback(CallbackId, UserId, Url) -> - SQL = - << - "INSERT INTO callback\n" - " (\n" - " id,\n" - " user_id,\n" - " url\n" - " )\n" - " VALUES\n" - " (\n" - " $1,\n" - " $2,\n" - " $3\n" - " )" - >>, - query1(SQL, [CallbackId, UserId, Url]). + CS = kura_changeset:cast(callback, #{}, #{<<"id">> => CallbackId, <<"user_id">> => UserId, <<"url">> => Url}, [id, user_id, url]), + case chatli_repo:insert(CS) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. get_callback(CallbackId) -> - SQL = <<"SELECT * FROM callback WHERE id = $1">>, - query1(SQL, [CallbackId]). + case chatli_repo:get(callback, CallbackId) of + {ok, Result} -> {ok, Result}; + {error, not_found} -> undefined + end. get_user_callbacks(UserId) -> - SQL = <<"SELECT url FROM callback where user_id = $1">>, - query(SQL, [UserId]). + Q = kura_query:from(callback), + Q1 = kura_query:select(Q, [url]), + Q2 = kura_query:where(Q1, {user_id, UserId}), + chatli_repo:all(Q2). delete_callback(CallbackId) -> - SQL = <<"DELETE FROM callback WHERE id = $1">>, - query1(SQL, [CallbackId]). + Q = kura_query:from(callback), + Q1 = kura_query:where(Q, {id, CallbackId}), + case chatli_repo:delete_all(Q1) of + {ok, 1} -> ok; + {ok, 0} -> undefined; + {error, _} = Error -> Error + end. -create_attachment(AttachmentId, ChatId, Mime, ByteSize) -> - SQL = <<"INSERT INTO attachment (id, chat_id, mime, length) VALUES ($1, $2, $3, $4)">>, - query1(SQL, [AttachmentId, ChatId, Mime, ByteSize]). +%% Attachments -get_attachment(AttachmentId, ChatId) -> - SQL = <<"SELECT * FROM attachment WHERE id = $1 AND chat_id = $2">>, - query1(SQL, [AttachmentId, ChatId]). - -% Expect 1 result -query1(SQL, Values) -> - case pgo:query(SQL, Values) of - #{ - command := insert, - num_rows := 1 - } -> - ok; - #{ - command := select, - rows := [] - } -> - undefined; - #{ - command := select, - rows := [Row] - } -> - {ok, Row}; - #{ - command := update, - num_rows := Num - } -> - {ok, Num}; - #{ - command := delete, - num_rows := 1 - } -> - ok; - #{command := delete} -> - undefined; - {error, Error} -> - logger:error("Error: ~p on SQL ~p Values ~p", [Error, SQL, Values]), - {error, Error} +create_attachment(AttachmentId, ChatId, Mime, ByteSize) -> + CS = kura_changeset:cast(attachment, #{}, #{ + <<"id">> => AttachmentId, + <<"chat_id">> => ChatId, + <<"mime">> => Mime, + <<"length">> => ByteSize + }, [id, chat_id, mime, length]), + case chatli_repo:insert(CS) of + {ok, _} -> ok; + {error, _} = Error -> Error end. -query(SQL, Values) -> - case pgo:query(SQL, Values) of - #{ - command := insert, - num_rows := Num - } -> - {ok, Num}; - #{ - command := select, - rows := Rows - } -> - {ok, Rows}; - #{ - command := update, - num_rows := Num - } -> - {ok, Num}; - #{ - command := delete, - num_rows := Num - } -> - {ok, Num}; - {error, Error} -> - logger:error("Error: ~p on SQL ~p Values ~p", [Error, SQL, Values]), - {error, Error} +get_attachment(AttachmentId, ChatId) -> + Q = kura_query:from(attachment), + Q1 = kura_query:where(Q, {id, AttachmentId}), + Q2 = kura_query:where(Q1, {chat_id, ChatId}), + one(Q2). + +%% Internal + +one(Q) -> + case chatli_repo:one(Q) of + {ok, Row} -> {ok, Row}; + {error, not_found} -> undefined; + {error, _} = Error -> Error end. diff --git a/src/chatli_device_db.erl b/src/chatli_device_db.erl index 631dd99..ed03771 100644 --- a/src/chatli_device_db.erl +++ b/src/chatli_device_db.erl @@ -7,39 +7,39 @@ get_all/1 ]). +-include_lib("kura/include/kura.hrl"). + upsert(DeviceId, UserId, Name) -> delete(DeviceId, UserId), - SQL = - << - "INSERT INTO\n" - " device\n" - " (\n" - " id,\n" - " user_id,\n" - " name\n" - " ) VALUES (\n" - " $1,\n" - " $2,\n" - " $3\n" - " )" - >>, - chatli_db:query1(SQL, [DeviceId, UserId, Name]). + CS = kura_changeset:cast(device, #{}, #{ + <<"id">> => DeviceId, + <<"user_id">> => UserId, + <<"name">> => Name + }, [id, user_id, name]), + case chatli_repo:insert(CS) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. delete(DeviceId, UserId) -> - SQL = - << - "DELETE FROM\n" - " device\n" - " WHERE\n" - " id = $1 AND\n" - " user_id = $2" - >>, - chatli_db:query(SQL, [DeviceId, UserId]). + Q = kura_query:from(device), + Q1 = kura_query:where(Q, {id, DeviceId}), + Q2 = kura_query:where(Q1, {user_id, UserId}), + chatli_repo:delete_all(Q2). get(DeviceId, UserId) -> - SQL = <<"SELECT id, name FROM device WHERE id = $1 AND user_id = $2">>, - chatli_db:query1(SQL, [DeviceId, UserId]). + Q = kura_query:from(device), + Q1 = kura_query:select(Q, [id, name]), + Q2 = kura_query:where(Q1, {id, DeviceId}), + Q3 = kura_query:where(Q2, {user_id, UserId}), + case chatli_repo:one(Q3) of + {ok, Row} -> {ok, Row}; + {error, not_found} -> undefined; + {error, _} = Error -> Error + end. get_all(UserId) -> - SQL = <<"SELECT id, name FROM device WHERE user_id = $1">>, - chatli_db:query(SQL, [UserId]). + Q = kura_query:from(device), + Q1 = kura_query:select(Q, [id, name]), + Q2 = kura_query:where(Q1, {user_id, UserId}), + chatli_repo:all(Q2). diff --git a/src/chatli_repo.erl b/src/chatli_repo.erl new file mode 100644 index 0000000..fa6608b --- /dev/null +++ b/src/chatli_repo.erl @@ -0,0 +1,45 @@ +-module(chatli_repo). +-behaviour(kura_repo). + +-export([ + otp_app/0, + all/1, + get/2, + get_by/2, + one/1, + insert/1, + insert/2, + update/1, + delete/1, + insert_all/2, + update_all/2, + delete_all/1, + preload/3, + transaction/1, + multi/1, + query/2, + exists/1, + reload/2, + insert_all/3 +]). + +otp_app() -> chatli. + +all(Q) -> kura_repo_worker:all(?MODULE, Q). +get(Schema, Id) -> kura_repo_worker:get(?MODULE, Schema, Id). +get_by(Schema, Clauses) -> kura_repo_worker:get_by(?MODULE, Schema, Clauses). +one(Q) -> kura_repo_worker:one(?MODULE, Q). +insert(CS) -> kura_repo_worker:insert(?MODULE, CS). +insert(CS, Opts) -> kura_repo_worker:insert(?MODULE, CS, Opts). +update(CS) -> kura_repo_worker:update(?MODULE, CS). +delete(CS) -> kura_repo_worker:delete(?MODULE, CS). +insert_all(Schema, Entries) -> kura_repo_worker:insert_all(?MODULE, Schema, Entries). +update_all(Q, Updates) -> kura_repo_worker:update_all(?MODULE, Q, Updates). +delete_all(Q) -> kura_repo_worker:delete_all(?MODULE, Q). +preload(Schema, Records, Assocs) -> kura_repo_worker:preload(?MODULE, Schema, Records, Assocs). +transaction(Fun) -> kura_repo_worker:transaction(?MODULE, Fun). +multi(Multi) -> kura_repo_worker:multi(?MODULE, Multi). +query(SQL, Params) -> kura_repo_worker:query(?MODULE, SQL, Params). +exists(Q) -> kura_repo_worker:exists(?MODULE, Q). +reload(Schema, Record) -> kura_repo_worker:reload(?MODULE, Schema, Record). +insert_all(Schema, Entries, Opts) -> kura_repo_worker:insert_all(?MODULE, Schema, Entries, Opts). diff --git a/src/chatli_user_db.erl b/src/chatli_user_db.erl index 104b78e..4fa1b2b 100644 --- a/src/chatli_user_db.erl +++ b/src/chatli_user_db.erl @@ -10,44 +10,64 @@ get_all_other/1 ]). -create(#{ - id := Id, - username := Username, - phone_number := PhoneNumber, - email := Email, - password := Password -}) -> - SQL = - <<"INSERT INTO chatli_user (id, username, phone_number, email, password) VALUES ($1, $2, $3, $4, $5)">>, - chatli_db:query1(SQL, [Id, Username, PhoneNumber, Email, Password]). +-include_lib("kura/include/kura.hrl"). + +create(Params) -> + CS = kura_changeset:cast(chatli_user, #{}, Params, [id, username, phone_number, email, password]), + case chatli_repo:insert(CS) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. get(UserId) -> - SQL = <<"SELECT id, username, phone_number, email FROM chatli_user WHERE id = $1">>, - chatli_db:query1(SQL, [UserId]). + Q = kura_query:from(chatli_user), + Q1 = kura_query:select(Q, [id, username, phone_number, email]), + Q2 = kura_query:where(Q1, {id, UserId}), + case chatli_repo:one(Q2) of + {ok, Row} -> {ok, Row}; + {error, not_found} -> undefined; + {error, _} = Error -> Error + end. get_login(Username, Password) -> - SQL = <<"SELECT * FROM chatli_user WHERE username = $1 AND password = $2">>, - chatli_db:query1(SQL, [Username, Password]). - -find(Type, Value) -> - SQL = <<"SELECT * FROM chatli_user WHERE ">>, - WhereSQL = - case Type of - <<"email">> -> - <<"email = $1">>; - <<"phone_number">> -> - <<"phone_number = $1">> - end, - chatli_db:query1(<>, [Value]). + Q = kura_query:from(chatli_user), + Q1 = kura_query:where(Q, {username, Username}), + Q2 = kura_query:where(Q1, {password, Password}), + case chatli_repo:one(Q2) of + {ok, Row} -> {ok, Row}; + {error, not_found} -> undefined; + {error, _} = Error -> Error + end. + +find(<<"email">>, Value) -> + case chatli_repo:get_by(chatli_user, [{email, Value}]) of + {ok, Row} -> {ok, Row}; + {error, not_found} -> undefined; + {error, _} = Error -> Error + end; +find(<<"phone_number">>, Value) -> + case chatli_repo:get_by(chatli_user, [{phone_number, Value}]) of + {ok, Row} -> {ok, Row}; + {error, not_found} -> undefined; + {error, _} = Error -> Error + end. delete(UserId) -> - SQL = <<"DELETE FROM chatli_user WHERE id = $1">>, - chatli_db:query1(SQL, [UserId]). + Q = kura_query:from(chatli_user), + Q1 = kura_query:where(Q, {id, UserId}), + case chatli_repo:delete_all(Q1) of + {ok, 1} -> ok; + {ok, 0} -> undefined; + {error, _} = Error -> Error + end. get_all() -> - SQL = <<"SELECT id, avatar, email, phone_number, username FROM chatli_user">>, - chatli_db:query(SQL, []). + Q = kura_query:from(chatli_user), + Q1 = kura_query:select(Q, [id, avatar, email, phone_number, username]), + chatli_repo:all(Q1). get_all_other(UserId) -> - SQL = <<"SELECT id, avatar, email, phone_number, username FROM chatli_user WHERE id != $1">>, - chatli_db:query(SQL, [UserId]). + Q = kura_query:from(chatli_user), + Q1 = kura_query:select(Q, [id, avatar, email, phone_number, username]), + Q2 = kura_query:where(Q1, {id, '!=', UserId}), + chatli_repo:all(Q2). diff --git a/src/controllers/chatli_chat_controller.erl b/src/controllers/chatli_chat_controller.erl index 49a4b3d..41d4e9e 100644 --- a/src/controllers/chatli_chat_controller.erl +++ b/src/controllers/chatli_chat_controller.erl @@ -136,7 +136,7 @@ get_history(#{ get_chat_messages([], _, Acc) -> Acc; get_chat_messages([#{id := ChatId} | T], Timestamp, Acc) -> - case chatli_db:get_filtered_messages(ChatId, [{<<"after">>, integer_to_binary(Timestamp)}]) of + case chatli_db:get_filtered_messages(ChatId, #{<<"after">> => integer_to_binary(Timestamp)}) of {ok, Messages} -> get_chat_messages(T, Timestamp, Messages ++ Acc); _ -> @@ -163,8 +163,8 @@ encode(Message) -> get_archive(#{ bindings := #{<<"chatid">> := ChatId}, - parsed_qs := [] -}) -> + parsed_qs := QS +}) when map_size(QS) =:= 0 -> {ok, Result} = chatli_db:get_chat_messages(ChatId), {json, 200, #{}, Result}; get_archive(#{ @@ -347,14 +347,15 @@ attachments_message(Id, ChatId, Sender, Attachments, UserAgent) -> save_file([], Acc, _) -> Acc; save_file([{file, Bytes, Mime, ByteSize} | T], Acc, ChatId) -> - UUID = binary_to_list(chatli_uuid:get_v4()), + UUID = chatli_uuid:get_v4(), {ok, Path} = application:get_env(chatli, download_path), - {ok, Dir} = file:get_cwd(), - case file:write_file(Path ++ UUID, Bytes) of + File = Path ++ binary_to_list(UUID), + ok = filelib:ensure_dir(File), + case file:write_file(File, Bytes) of ok -> case chatli_db:create_attachment(UUID, ChatId, Mime, ByteSize) of ok -> - save_file(T, [{ok, list_to_binary(UUID), Mime, ByteSize} | Acc], ChatId); + save_file(T, [{ok, UUID, Mime, ByteSize} | Acc], ChatId); _ -> save_file([{error, create_attachment} | T], Acc, ChatId) end; diff --git a/src/schemas/attachment.erl b/src/schemas/attachment.erl new file mode 100644 index 0000000..90dda18 --- /dev/null +++ b/src/schemas/attachment.erl @@ -0,0 +1,15 @@ +-module(attachment). +-behaviour(kura_schema). + +-include_lib("kura/include/kura.hrl"). + +-export([table/0, fields/0]). + +table() -> ~"attachment". + +fields() -> [ + #kura_field{name = id, type = uuid, primary_key = true}, + #kura_field{name = chat_id, type = uuid, nullable = false}, + #kura_field{name = mime, type = string, nullable = false}, + #kura_field{name = length, type = integer} +]. diff --git a/src/schemas/callback.erl b/src/schemas/callback.erl new file mode 100644 index 0000000..df18e8d --- /dev/null +++ b/src/schemas/callback.erl @@ -0,0 +1,14 @@ +-module(callback). +-behaviour(kura_schema). + +-include_lib("kura/include/kura.hrl"). + +-export([table/0, fields/0]). + +table() -> ~"callback". + +fields() -> [ + #kura_field{name = id, type = uuid, primary_key = true}, + #kura_field{name = user_id, type = uuid, nullable = false}, + #kura_field{name = url, type = string, nullable = false} +]. diff --git a/src/schemas/chat.erl b/src/schemas/chat.erl new file mode 100644 index 0000000..46bd0e1 --- /dev/null +++ b/src/schemas/chat.erl @@ -0,0 +1,15 @@ +-module(chat). +-behaviour(kura_schema). + +-include_lib("kura/include/kura.hrl"). + +-export([table/0, fields/0]). + +table() -> ~"chat". + +fields() -> [ + #kura_field{name = id, type = uuid, primary_key = true}, + #kura_field{name = name, type = string, nullable = false}, + #kura_field{name = description, type = string}, + #kura_field{name = type, type = string} +]. diff --git a/src/schemas/chatli_user.erl b/src/schemas/chatli_user.erl new file mode 100644 index 0000000..ac1af40 --- /dev/null +++ b/src/schemas/chatli_user.erl @@ -0,0 +1,17 @@ +-module(chatli_user). +-behaviour(kura_schema). + +-include_lib("kura/include/kura.hrl"). + +-export([table/0, fields/0]). + +table() -> ~"chatli_user". + +fields() -> [ + #kura_field{name = id, type = uuid, primary_key = true}, + #kura_field{name = username, type = string, nullable = false}, + #kura_field{name = phone_number, type = string}, + #kura_field{name = email, type = string}, + #kura_field{name = avatar, type = string}, + #kura_field{name = password, type = string, nullable = false} +]. diff --git a/src/schemas/device.erl b/src/schemas/device.erl new file mode 100644 index 0000000..f20fcc6 --- /dev/null +++ b/src/schemas/device.erl @@ -0,0 +1,14 @@ +-module(device). +-behaviour(kura_schema). + +-include_lib("kura/include/kura.hrl"). + +-export([table/0, fields/0]). + +table() -> ~"device". + +fields() -> [ + #kura_field{name = id, type = uuid, primary_key = true}, + #kura_field{name = user_id, type = uuid, nullable = false}, + #kura_field{name = name, type = string} +]. diff --git a/src/schemas/ldf_message.erl b/src/schemas/ldf_message.erl new file mode 100644 index 0000000..4defab8 --- /dev/null +++ b/src/schemas/ldf_message.erl @@ -0,0 +1,15 @@ +-module(ldf_message). +-behaviour(kura_schema). + +-include_lib("kura/include/kura.hrl"). + +-export([table/0, fields/0]). + +table() -> ~"ldf_message". + +fields() -> [ + #kura_field{name = id, type = id, primary_key = true}, + #kura_field{name = message_id, type = uuid, nullable = false}, + #kura_field{name = payload, type = string}, + #kura_field{name = content_length, type = string} +]. diff --git a/src/schemas/li.erl b/src/schemas/li.erl new file mode 100644 index 0000000..4a2c0b9 --- /dev/null +++ b/src/schemas/li.erl @@ -0,0 +1,19 @@ +-module(li). +-behaviour(kura_schema). + +-include_lib("kura/include/kura.hrl"). + +-export([table/0, fields/0]). + +table() -> ~"li". + +fields() -> [ + #kura_field{name = id, type = id, primary_key = true}, + #kura_field{name = type, type = string, nullable = false}, + #kura_field{name = value, type = string, nullable = false}, + #kura_field{name = callback_id, type = uuid, nullable = false}, + #kura_field{name = user_id, type = uuid, nullable = false}, + #kura_field{name = username, type = string}, + #kura_field{name = phone_number, type = string}, + #kura_field{name = email, type = string} +]. diff --git a/src/schemas/message.erl b/src/schemas/message.erl new file mode 100644 index 0000000..33c0397 --- /dev/null +++ b/src/schemas/message.erl @@ -0,0 +1,19 @@ +-module(message). +-behaviour(kura_schema). + +-include_lib("kura/include/kura.hrl"). + +-export([table/0, fields/0]). + +table() -> ~"message". + +fields() -> [ + #kura_field{name = id, type = uuid, primary_key = true}, + #kura_field{name = chat_id, type = uuid, nullable = false}, + #kura_field{name = payload, type = jsonb}, + #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 = sender_info, type = jsonb} +]. diff --git a/src/schemas/participant.erl b/src/schemas/participant.erl new file mode 100644 index 0000000..249bed3 --- /dev/null +++ b/src/schemas/participant.erl @@ -0,0 +1,14 @@ +-module(participant). +-behaviour(kura_schema). + +-include_lib("kura/include/kura.hrl"). + +-export([table/0, fields/0]). + +table() -> ~"participant". + +fields() -> [ + #kura_field{name = id, type = id, primary_key = true}, + #kura_field{name = chat_id, type = uuid}, + #kura_field{name = user_id, type = uuid} +]. diff --git a/test/chatli_SUITE.erl b/test/chatli_SUITE.erl index 8f8ed44..8ee4d4b 100644 --- a/test/chatli_SUITE.erl +++ b/test/chatli_SUITE.erl @@ -4,9 +4,9 @@ -include_lib("common_test/include/ct.hrl"). --define(BASEPATH, ct:get_config(basepath)). --define(IP, ct:get_config(ip)). --define(PORT, ct:get_config(port)). +-define(BASEPATH, <<"http://localhost:8090">>). +-define(IP, "localhost"). +-define(PORT, 8090). %%-------------------------------------------------------------------- %% @spec suite() -> Info @@ -55,7 +55,7 @@ init_per_suite(_Config) -> ), #{<<"access_token">> := Token} = decode(LoginRespBody), [_, Payload, _] = jhn_bstring:tokens(Token, <<".">>), - UserObj1 = decode(base64:decode(Payload)), + UserObj1 = decode(base64:decode(Payload, #{mode => urlsafe, padding => false})), #{status := {200, _}, body := LoginRespBody2} = jhn_shttpc:post( LoginPath, encode(#{ @@ -66,7 +66,8 @@ init_per_suite(_Config) -> ), #{<<"access_token">> := Token2} = decode(LoginRespBody2), [_, Payload2, _] = jhn_bstring:tokens(Token2, <<".">>), - #{<<"id">> := UserId2} = UserObj2 = decode(base64:decode(Payload2)), + #{<<"id">> := UserId2} = + UserObj2 = decode(base64:decode(Payload2, #{mode => urlsafe, padding => false})), CallbackPath = [?BASEPATH, <<"/v1/callback">>], CallbackObject = #{ type => <<"email">>, @@ -299,13 +300,7 @@ send_message(Config) -> opts(Token) ), #{<<"id">> := MessageId} = decode(MessageBody), - receive - {gun_ws, _ConnPid, _StreamRef0, {text, Msg}} -> - io:format("~p", [Msg]), - #{<<"id">> := MessageId} = decode(Msg) - after 8000 -> - exit(timeout) - end. + await_ws_message(MessageId). get_all_message(Config) -> #{token := Token} = proplists:get_value(user1, Config), @@ -380,13 +375,7 @@ upload_attachment(Config) -> Path, Formatted, opts(attachment, Token, Boundary) ), #{<<"id">> := MessageId} = decode(MessageBody), - receive - {gun_ws, _ConnPid, _StreamRef0, {text, Msg}} -> - io:format("~p", [Msg]), - #{<<"id">> := MessageId} = decode(Msg) - after 8000 -> - exit(timeout) - end, + await_ws_message(MessageId), MessagePath = [?BASEPATH, <<"/client/chat/">>, ChatId, <<"/message/">>, MessageId], #{status := {200, _}, body := MessageRespBody} = jhn_shttpc:get(MessagePath, opts(Token)), #{ @@ -439,6 +428,19 @@ decode(Json) -> {ok, Decode} = thoas:decode(Json), Decode. +%% The server redelivers undelivered messages on reconnect, so skip +%% anything that isn't the message we are waiting for. +await_ws_message(MessageId) -> + receive + {gun_ws, _ConnPid, _StreamRef, {text, Msg}} -> + case decode(Msg) of + #{<<"id">> := MessageId} -> ok; + _ -> await_ws_message(MessageId) + end + after 8000 -> + exit(timeout) + end. + encode(Json) -> thoas:encode(Json). From 5fef69c0896a453aff645e00f97d697ad51f414e Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Wed, 24 Jun 2026 09:41:24 +0200 Subject: [PATCH 2/2] ci: pin rebar3_lint to 4.2.2 for deterministic elvis Unpinned rebar3_lint let CI fetch elvis_core 5.x, which rejects the existing elvis.config format and broke linting. Pin to 4.2.2 so CI and local agree. Also wrap an over-long kura_changeset:cast call in chatli_db to satisfy the 120-char line_length rule. --- rebar.config | 2 +- src/chatli_db.erl | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index c4e46ec..983c26e 100644 --- a/rebar.config +++ b/rebar.config @@ -51,7 +51,7 @@ {project_plugins, [ erlfmt, rebar3_kura, - rebar3_lint, + {rebar3_lint, "4.2.2"}, {rebar3_erlydtl_plugin, ".*", {git, "https://github.com/tsloughter/rebar3_erlydtl_plugin.git", {branch, "master"}}} ]}. diff --git a/src/chatli_db.erl b/src/chatli_db.erl index fb2e1d0..c4bd980 100644 --- a/src/chatli_db.erl +++ b/src/chatli_db.erl @@ -155,7 +155,12 @@ get_all_other_participants(ChatId, UserId) -> %% Callbacks create_callback(CallbackId, UserId, Url) -> - CS = kura_changeset:cast(callback, #{}, #{<<"id">> => CallbackId, <<"user_id">> => UserId, <<"url">> => Url}, [id, user_id, url]), + CS = kura_changeset:cast( + callback, + #{}, + #{<<"id">> => CallbackId, <<"user_id">> => UserId, <<"url">> => Url}, + [id, user_id, url] + ), case chatli_repo:insert(CS) of {ok, _} -> ok; {error, _} = Error -> Error