lib/immudb/client.ex

defmodule Immudb.Client do
  use Immudb.Grpc, :schema

  alias Immudb.Socket
  alias Immudb.Util

  @spec connection(String.t(), integer) :: GRPC.Channel.t()
  defp connection(host, port) do
    "#{host}:#{port}"
    |> GRPC.Stub.connect(interceptors: [GRPC.Logger.Client])
  end

  @spec new(
          host: String.t(),
          port: integer(),
          username: String.t(),
          password: String.t(),
          database: String.t()
        ) :: {:ok, Socket.t()} | {:error, String.t()}
  def new(
        host: host,
        port: port,
        username: username,
        password: password,
        database: database
      ) do
    with {:grpc_connect, {:ok, channel}} <-
           {:grpc_connect, connection(host, port)},
         {:immudb_login, {:ok, token}} <-
           {:immudb_login,
            channel
            |> login(username, password)},
         socket <- %Socket{channel: channel, token: token},
         {:immudb_use_database, {:ok, token}} <-
           {:immudb_use_database,
            socket
            |> Immudb.Database.use_database(database)} do
      {:ok, %Socket{channel: channel, token: token}}
    else
      {:grpc_connect, {:error, e}} ->
        {:error, e}

      {:immudb_login, {:error, e}} ->
        {:error, e}

      {:immudb_use_database, {:error, e}} ->
        {:error, e}
    end
  end

  @spec new(url: String.t()) :: {:ok, Socket.t()} | {:error, String.t()}
  def new(url: url) do
    with {:parse_uri, {:ok, uri}} <- {:parse_uri, URI.new(url)},
         {:is_immudb_schema, true} <- {:is_immudb_schema, uri.scheme == "immudb"},
         userinfo <- uri.userinfo |> String.split(":", trim: true),
         {:is_userinfo, true} <- {:is_userinfo, userinfo |> length > 0} do
      {username, password} =
        userinfo
        |> case do
          [username] -> {username, ""}
          [username, password] -> {username, password}
          _ -> {"", ""}
        end

      new(
        host: uri.host,
        port: uri.port,
        username: username,
        password: password,
        database: uri.path |> String.slice(1, String.length(uri.path) - 1)
      )
    else
      {:parse_uri, {:error, _}} -> {:error, "Invalid connection string"}
      {:is_immudb_schema, false} -> {:error, "Invalid connection string"}
      {:is_userinfo, false} -> {:error, "Invalid user info"}
    end
  end

  def new(_) do
    {:error, :invalid_params}
  end

  @spec list_users(Socket.t()) ::
          {:error, String.t() | atom()} | {:ok, [User.t()]}
  def list_users(%Socket{channel: %GRPC.Channel{} = channel, token: token}) do
    channel
    |> Stub.list_users(Google.Protobuf.Empty.new(), metadata: token |> Util.metadata())
    |> case do
      {:ok, %{users: users}} ->
        {:ok, users |> Immudb.Schemas.User.convert()}

      {:error, %GRPC.RPCError{message: message}} ->
        {:error, message}

      _ ->
        {:error, :unknown}
    end
  end

  def list_users(_) do
    {:error, :invalid_params}
  end

  @spec create_user(Socket.t(),
          user: String.t(),
          password: String.t(),
          database: String.t(),
          permission: atom()
        ) ::
          {:error, String.t() | atom()} | {:ok, nil}
  def create_user(%Socket{channel: %GRPC.Channel{} = channel, token: token},
        user: user,
        password: password,
        database: database,
        permission: permission
      ) do
    channel
    |> Stub.create_user(
      Schema.CreateUserRequest.new(
        user: user,
        password: password,
        permission: permission |> Immudb.Schemas.Permission.to_int(),
        database: database
      ),
      metadata: token |> Util.metadata()
    )
    |> case do
      {:error, %GRPC.RPCError{message: message}} ->
        {:error, message}

      {:ok, %Google.Protobuf.Empty{}} ->
        {:ok, nil}

      _ ->
        {:error, :unknown}
    end
  end

  def create_user(_) do
    {:error, :invalid_params}
  end

  @spec change_password(Socket.t(),
          user: String.t(),
          old_password: String.t(),
          new_password: String.t()
        ) ::
          {:error, String.t() | atom()} | {:ok, String.t()}
  def change_password(%Socket{channel: %GRPC.Channel{} = channel, token: token},
        user: user,
        old_password: old_password,
        new_password: new_password
      ) do
    channel
    |> Stub.change_password(
      Schema.ChangePasswordRequest.new(
        user: user,
        oldPassword: old_password,
        newPassword: new_password
      ),
      metadata: token |> Util.metadata()
    )
    |> case do
      {:error, %GRPC.RPCError{message: message}} ->
        {:error, message}

      {:ok, %Google.Protobuf.Empty{}} ->
        {:ok, nil}

      _ ->
        {:error, :unknown}
    end
  end

  def change_password(_, _) do
    {:error, :invalid_params}
  end

  def update_auth_config(socket, params) do
    socket.channel
    |> Stub.update_auth_config(Schema.AuthConfig.new(kind: params.kind))
  end

  def update_mtls_confg(socket, params) do
    socket.channel
    |> Stub.update_mtls_config(Schema.MTLSConfig.new(enabled: params.enabled))
  end

  @spec login(GRPC.Channel.t(), String.t(), String.t()) ::
          {:error, String.t() | atom()} | {:ok, String.t()}
  def login(%GRPC.Channel{} = channel, user, password) do
    channel
    |> Stub.login(Schema.LoginRequest.new(user: user, password: password))
    |> case do
      {:error, %GRPC.RPCError{message: message}} ->
        {:error, message}

      {:ok, %{token: token}} ->
        {:ok, token}

      _ ->
        {:error, :unknown}
    end
  end

  def login(_, _, _) do
    {:error, :invalid_params}
  end

  @spec logout(GRPC.Channel.t()) ::
          {:error, String.t() | atom()} | {:ok, nil}
  def logout(%Socket{channel: %GRPC.Channel{} = channel, token: token}) do
    channel
    |> Stub.logout(Protobuf.Empty.new(), metadata: token |> Util.metadata())
    |> case do
      {:error, %GRPC.RPCError{message: message}} ->
        {:error, message}

      {:ok, %Google.Protobuf.Empty{}} ->
        {:ok, nil}

      _ ->
        {:error, :unknown}
    end
  end

  def logout(_) do
    {:error, :invalid_params}
  end

  @spec health(Socket.t()) ::
          {:error, String.t() | atom()} | {:ok, nil}
  def health(%Socket{channel: %GRPC.Channel{} = channel}) do
    channel
    |> Stub.health(Protobuf.Empty.new())
    |> case do
      {:ok, v} ->
        {:ok, v}

      {:error, %GRPC.RPCError{message: message}} ->
        {:error, message}

      _ ->
        {:error, :unknown}
    end
  end

  def health(_) do
    {:error, :invalid_params}
  end

  @spec current_state(Socket.t()) ::
          {:error, String.t() | atom()} | {:ok, nil}
  def current_state(%Socket{channel: %GRPC.Channel{} = channel, token: token}) do
    channel
    |> Stub.current_state(Protobuf.Empty.new(), metadata: token |> Util.metadata())
    |> case do
      {:ok, v} ->
        {:ok, v}

      {:error, %GRPC.RPCError{message: message}} ->
        {:error, message}

      _ ->
        {:error, :unknown}
    end
  end

  def current_state(_) do
    {:error, :invalid_params}
  end
end