aboutsummaryrefslogtreecommitdiff
path: root/server/src/jchat_db.erl
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/jchat_db.erl')
-rw-r--r--server/src/jchat_db.erl390
1 files changed, 390 insertions, 0 deletions
diff --git a/server/src/jchat_db.erl b/server/src/jchat_db.erl
new file mode 100644
index 0000000..65a76f4
--- /dev/null
+++ b/server/src/jchat_db.erl
@@ -0,0 +1,390 @@
+-module(jchat_db).
+
+-export([init/0,
+ create_tables/0,
+ create_table/2,
+ init_state_counters/0,
+ get_user/1, create_user/1, update_user/1, get_user_by_id/1, get_user_by_email/1,
+ get_conversation/1, create_conversation/2, update_conversation/2,
+ get_message/1, create_message/2, update_message/2,
+ get_participant/1, create_participant/2, update_participant/2,
+ get_presence/1, update_presence/2,
+ query_conversations/2, query_messages/2]).
+
+-include("jchat.hrl").
+
+%% Initialize database
+init() ->
+ case mnesia:create_schema([node()]) of
+ ok -> ok;
+ {error, {_, {already_exists, _}}} -> ok;
+ Error -> error({schema_creation_failed, Error})
+ end,
+ mnesia:start(),
+ create_tables(),
+ ok.
+
+%% Create database tables
+create_tables() ->
+ % Users table
+ create_table(user, [
+ {attributes, record_info(fields, user)},
+ {ram_copies, [node()]},
+ {type, set}
+ ]),
+
+ % Conversations table
+ create_table(conversation, [
+ {attributes, record_info(fields, conversation)},
+ {ram_copies, [node()]},
+ {type, set}
+ ]),
+
+ % Messages table
+ create_table(message, [
+ {attributes, record_info(fields, message)},
+ {ram_copies, [node()]},
+ {type, set}
+ ]),
+
+ % Participants table
+ create_table(participant, [
+ {attributes, record_info(fields, participant)},
+ {ram_copies, [node()]},
+ {type, set}
+ ]),
+
+ % Presence table
+ create_table(presence, [
+ {attributes, record_info(fields, presence)},
+ {ram_copies, [node()]},
+ {type, set}
+ ]),
+
+ % State counters for JMAP
+ create_table(state_counter, [
+ {attributes, record_info(fields, state_counter)},
+ {ram_copies, [node()]},
+ {type, set}
+ ]),
+
+ mnesia:wait_for_tables([user, conversation, message, participant,
+ presence, state_counter], 5000),
+
+ % Initialize state counters
+ init_state_counters().
+
+%% Helper function to create table with error handling
+create_table(TableName, Options) ->
+ case mnesia:create_table(TableName, Options) of
+ {atomic, ok} -> ok;
+ {aborted, {already_exists, TableName}} -> ok;
+ Error -> error({table_creation_failed, TableName, Error})
+ end.
+
+%% User operations
+get_user(Id) ->
+ get_user_by_id(Id).
+
+get_user_by_id(Id) ->
+ case mnesia:dirty_read(user, Id) of
+ [User] -> {ok, User};
+ [] -> {error, not_found}
+ end.
+
+get_user_by_email(Email) ->
+ MatchSpec = [{#user{email = '$1', _ = '_'}, [{'=:=', '$1', Email}], ['$_']}],
+ case mnesia:dirty_select(user, MatchSpec) of
+ [User] -> {ok, User};
+ [] -> {error, not_found};
+ Users -> {ok, hd(Users)} % Take first match if multiple (shouldn't happen)
+ end.
+
+create_user(User) when is_record(User, user) ->
+ case mnesia:dirty_write(User) of
+ ok -> {ok, User};
+ Error -> {error, Error}
+ end.
+
+update_user(User) when is_record(User, user) ->
+ case mnesia:dirty_write(User) of
+ ok -> {ok, User};
+ Error -> {error, Error}
+ end.
+
+%% Conversation operations
+get_conversation(Id) ->
+ case mnesia:dirty_read(conversation, Id) of
+ [Conv] -> {ok, Conv};
+ [] -> {error, not_found}
+ end.
+
+create_conversation(Id, Attrs) ->
+ Now = jchat_utils:now_iso8601(),
+ Conv = #conversation{
+ id = Id,
+ title = maps:get(<<"title">>, Attrs, null),
+ description = maps:get(<<"description">>, Attrs, null),
+ created_at = Now,
+ updated_at = Now,
+ is_archived = maps:get(<<"isArchived">>, Attrs, false),
+ is_muted = maps:get(<<"isMuted">>, Attrs, false),
+ participant_ids = maps:get(<<"participantIds">>, Attrs, []),
+ last_message_id = null,
+ last_message_at = null,
+ unread_count = 0,
+ message_count = 0,
+ metadata = maps:get(<<"metadata">>, Attrs, null)
+ },
+ case mnesia:dirty_write(Conv) of
+ ok ->
+ update_state_counter(conversation),
+ {ok, Conv};
+ Error -> Error
+ end.
+
+update_conversation(Id, Updates) ->
+ case mnesia:dirty_read(conversation, Id) of
+ [Conv] ->
+ UpdatedConv = apply_updates(Conv, Updates),
+ UpdatedConv2 = UpdatedConv#conversation{updated_at = jchat_utils:now_iso8601()},
+ case mnesia:dirty_write(UpdatedConv2) of
+ ok ->
+ update_state_counter(conversation),
+ {ok, UpdatedConv2};
+ Error -> Error
+ end;
+ [] ->
+ {error, not_found}
+ end.
+
+%% Message operations
+get_message(Id) ->
+ case mnesia:dirty_read(message, Id) of
+ [Msg] -> {ok, Msg};
+ [] -> {error, not_found}
+ end.
+
+create_message(Id, Attrs) ->
+ Now = jchat_utils:now_iso8601(),
+ ConvId = maps:get(<<"conversationId">>, Attrs),
+ Msg = #message{
+ id = Id,
+ conversation_id = ConvId,
+ sender_id = maps:get(<<"senderId">>, Attrs, <<"unknown">>),
+ sent_at = Now,
+ received_at = Now,
+ edited_at = null,
+ body = maps:get(<<"body">>, Attrs),
+ body_type = maps:get(<<"bodyType">>, Attrs, <<"text/plain">>),
+ attachments = maps:get(<<"attachments">>, Attrs, null),
+ reply_to_message_id = maps:get(<<"replyToMessageId">>, Attrs, null),
+ is_system_message = maps:get(<<"isSystemMessage">>, Attrs, false),
+ is_deleted = false,
+ reactions = null,
+ delivery_status = <<"sent">>,
+ read_by = [],
+ metadata = maps:get(<<"metadata">>, Attrs, null)
+ },
+ case mnesia:dirty_write(Msg) of
+ ok ->
+ % Update conversation with new message
+ update_conversation_last_message(ConvId, Id, Now),
+ update_state_counter(message),
+ {ok, Msg};
+ Error -> Error
+ end.
+
+update_message(Id, Updates) ->
+ case mnesia:dirty_read(message, Id) of
+ [Msg] ->
+ UpdatedMsg = apply_updates(Msg, Updates),
+ UpdatedMsg2 = case maps:get(body, Updates, undefined) of
+ undefined -> UpdatedMsg;
+ _ -> UpdatedMsg#message{edited_at = jchat_utils:now_iso8601()}
+ end,
+ case mnesia:dirty_write(UpdatedMsg2) of
+ ok ->
+ update_state_counter(message),
+ {ok, UpdatedMsg2};
+ Error -> Error
+ end;
+ [] ->
+ {error, not_found}
+ end.
+
+%% Participant operations
+get_participant(Id) ->
+ case mnesia:dirty_read(participant, Id) of
+ [Part] -> {ok, Part};
+ [] -> {error, not_found}
+ end.
+
+create_participant(Id, Attrs) ->
+ Now = jchat_utils:now_iso8601(),
+ Part = #participant{
+ id = Id,
+ conversation_id = maps:get(conversation_id, Attrs),
+ user_id = maps:get(user_id, Attrs),
+ display_name = maps:get(display_name, Attrs),
+ avatar_blob_id = maps:get(avatar_blob_id, Attrs, null),
+ role = maps:get(role, Attrs, <<"member">>),
+ joined_at = Now,
+ last_active_at = null,
+ is_active = true,
+ permissions = maps:get(permissions, Attrs, [<<"send">>]),
+ metadata = maps:get(metadata, Attrs, null)
+ },
+ case mnesia:dirty_write(Part) of
+ ok ->
+ update_state_counter(participant),
+ {ok, Part};
+ Error -> Error
+ end.
+
+update_participant(Id, Updates) ->
+ case mnesia:dirty_read(participant, Id) of
+ [Part] ->
+ UpdatedPart = apply_updates(Part, Updates),
+ case mnesia:dirty_write(UpdatedPart) of
+ ok ->
+ update_state_counter(participant),
+ {ok, UpdatedPart};
+ Error -> Error
+ end;
+ [] ->
+ {error, not_found}
+ end.
+
+%% Presence operations
+get_presence(UserId) ->
+ case mnesia:dirty_read(presence, UserId) of
+ [Pres] -> {ok, Pres};
+ [] -> {error, not_found}
+ end.
+
+update_presence(UserId, Updates) ->
+ Now = jchat_utils:now_iso8601(),
+ Presence = case mnesia:dirty_read(presence, UserId) of
+ [Existing] -> apply_updates(Existing, Updates);
+ [] -> #presence{
+ user_id = UserId,
+ status = maps:get(status, Updates, <<"offline">>),
+ status_message = maps:get(status_message, Updates, null),
+ last_seen_at = null,
+ updated_at = Now
+ }
+ end,
+ UpdatedPresence = Presence#presence{updated_at = Now},
+ case mnesia:dirty_write(UpdatedPresence) of
+ ok ->
+ update_state_counter(presence),
+ {ok, UpdatedPresence};
+ Error -> Error
+ end.
+
+%% Query operations
+query_conversations(UserId, Filter) ->
+ MatchSpec = build_conversation_match_spec(UserId, Filter),
+ Conversations = mnesia:dirty_select(conversation, MatchSpec),
+ {ok, Conversations}.
+
+query_messages(Filter, Sort) ->
+ MatchSpec = build_message_match_spec(Filter),
+ Messages = mnesia:dirty_select(message, MatchSpec),
+ SortedMessages = sort_messages(Messages, Sort),
+ {ok, SortedMessages}.
+
+%% Internal functions
+apply_updates(Record, Updates) ->
+ Fields = case Record of
+ #conversation{} -> record_info(fields, conversation);
+ #message{} -> record_info(fields, message);
+ #participant{} -> record_info(fields, participant);
+ #presence{} -> record_info(fields, presence)
+ end,
+ apply_updates(Record, Updates, Fields, 2).
+
+apply_updates(Record, _Updates, [], _Index) ->
+ Record;
+apply_updates(Record, Updates, [Field|Rest], Index) ->
+ UpdatedRecord = case maps:get(Field, Updates, undefined) of
+ undefined -> Record;
+ Value -> setelement(Index, Record, Value)
+ end,
+ apply_updates(UpdatedRecord, Updates, Rest, Index + 1).
+
+update_conversation_last_message(ConvId, MsgId, Timestamp) ->
+ case mnesia:dirty_read(conversation, ConvId) of
+ [Conv] ->
+ UpdatedConv = Conv#conversation{
+ last_message_id = MsgId,
+ last_message_at = Timestamp,
+ message_count = Conv#conversation.message_count + 1,
+ updated_at = jchat_utils:now_iso8601()
+ },
+ mnesia:dirty_write(UpdatedConv);
+ [] ->
+ ok
+ end.
+
+update_state_counter(Type) ->
+ Key = {<<"default">>, Type}, % {account_id, object_type}
+ case mnesia:dirty_read(state_counter, Key) of
+ [#state_counter{state = State}] ->
+ NewState = integer_to_binary(binary_to_integer(State) + 1),
+ Now = jchat_utils:now_iso8601(),
+ mnesia:dirty_write(#state_counter{
+ account_id = <<"default">>,
+ object_type = Type,
+ state = NewState,
+ updated_at = Now
+ });
+ [] ->
+ Now = jchat_utils:now_iso8601(),
+ mnesia:dirty_write(#state_counter{
+ account_id = <<"default">>,
+ object_type = Type,
+ state = <<"1">>,
+ updated_at = Now
+ })
+ end.
+
+build_conversation_match_spec(_UserId, _Filter) ->
+ % Simplified - in production would build proper match specs
+ [{'$1', [], ['$1']}].
+
+build_message_match_spec(Filter) ->
+ case maps:get(<<"inConversation">>, Filter, null) of
+ null ->
+ % No filter - return all messages
+ [{'$1', [], ['$1']}];
+ ConversationId ->
+ % Filter by conversation ID
+ % Match: #message{conversation_id = ConversationId, _ = '_'}
+ [{{message, '_', ConversationId, '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_'}, [], ['$_']}]
+ end.
+
+sort_messages(Messages, _Sort) ->
+ % Simplified - in production would implement proper sorting
+ Messages.
+
+%% Initialize state counters for JMAP
+init_state_counters() ->
+ Types = [conversation, message, participant, presence],
+ lists:foreach(fun(Type) ->
+ Key = {<<"default">>, Type}, % {account_id, object_type}
+ case mnesia:dirty_read(state_counter, Key) of
+ [] ->
+ Now = jchat_utils:now_iso8601(),
+ Counter = #state_counter{
+ account_id = <<"default">>,
+ object_type = Type,
+ state = <<"0">>,
+ updated_at = Now
+ },
+ mnesia:dirty_write(Counter);
+ _ ->
+ ok % Already exists
+ end
+ end, Types).