diff --git a/lib/cgrates_web_jsonapi/cdrs.ex b/lib/cgrates_web_jsonapi/cdrs.ex index aae7df6..d5f7d85 100644 --- a/lib/cgrates_web_jsonapi/cdrs.ex +++ b/lib/cgrates_web_jsonapi/cdrs.ex @@ -58,6 +58,16 @@ defmodule CgratesWebJsonapi.Cdrs do |> Enum.map(&CdrsStats.new/1) end + def extra do + request = "SELECT DISTINCT extra_fields + FROM ( + SELECT jsonb_object_keys(extra_fields) AS extra_fields + FROM cdrs + ) AS subquery" + + request |> Repo.query() |> elem(1) |> Map.get(:rows) |> List.flatten() + end + defp group_by_created_at(q, :daily) do q |> group_by([r], fragment("date_trunc('day', ?)", r.created_at)) end diff --git a/lib/cgrates_web_jsonapi/ets_cache.ex b/lib/cgrates_web_jsonapi/ets_cache.ex new file mode 100644 index 0000000..6100b7e --- /dev/null +++ b/lib/cgrates_web_jsonapi/ets_cache.ex @@ -0,0 +1,46 @@ +defmodule CgratesWebJsonapi.EtsCache do + @moduledoc """ + Cache via ETS. + """ + + @spec get(any, atom | :ets.tid(), any) :: list(any) + def get(args, table_name, opts \\ []) do + case lookup(args, table_name) do + nil -> + ttl = Keyword.get(opts, :ttl, 86400) + cache_apply(args.(), ttl) + + result -> + result + end + end + + defp lookup(args, table_name) do + maybe_create_table(table_name) + + case :ets.lookup(table_name, [args]) do + [result | _] -> check_freshness(result) + [] -> nil + end + end + + defp check_freshness({result, expiration}) do + cond do + expiration > :os.system_time(:seconds) -> result + :else -> nil + end + end + + defp cache_apply(args, ttl) do + result = args + expiration = :os.system_time(:seconds) + ttl + :ets.insert(:extra_fields, {args, result, expiration}) + result + end + + defp maybe_create_table(table_name) do + if Enum.member?(:ets.all(), table_name) == false do + :ets.new(table_name, [:public, :named_table]) + end + end +end diff --git a/lib/cgrates_web_jsonapi/tenants.ex b/lib/cgrates_web_jsonapi/tenants.ex index 811d472..84d2d33 100644 --- a/lib/cgrates_web_jsonapi/tenants.ex +++ b/lib/cgrates_web_jsonapi/tenants.ex @@ -101,4 +101,100 @@ defmodule CgratesWebJsonapi.Tenants do def change_tenant(%Tenant{} = tenant, attrs \\ %{}) do Tenant.update_changeset(tenant, attrs) end + + alias CgratesWebJsonapi.Tenants.Membership + + @doc """ + Returns the list of memberships. + + ## Examples + + iex> list_memberships() + [%Membership{}, ...] + + """ + def list_memberships do + Repo.all(Membership) + end + + @doc """ + Gets a single membership. + + Raises `Ecto.NoResultsError` if the Membership does not exist. + + ## Examples + + iex> get_membership!(123) + %Membership{} + + iex> get_membership!(456) + ** (Ecto.NoResultsError) + + """ + def get_membership!(id), do: Repo.get!(Membership, id) + + @doc """ + Creates a membership. + + ## Examples + + iex> create_membership(%{field: value}) + {:ok, %Membership{}} + + iex> create_membership(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_membership(attrs \\ %{}) do + %Membership{} + |> Membership.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a membership. + + ## Examples + + iex> update_membership(membership, %{field: new_value}) + {:ok, %Membership{}} + + iex> update_membership(membership, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_membership(%Membership{} = membership, attrs) do + membership + |> Membership.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a membership. + + ## Examples + + iex> delete_membership(membership) + {:ok, %Membership{}} + + iex> delete_membership(membership) + {:error, %Ecto.Changeset{}} + + """ + def delete_membership(%Membership{} = membership) do + Repo.delete(membership) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking membership changes. + + ## Examples + + iex> change_membership(membership) + %Ecto.Changeset{data: %Membership{}} + + """ + def change_membership(%Membership{} = membership, attrs \\ %{}) do + Membership.changeset(membership, attrs) + end end diff --git a/lib/cgrates_web_jsonapi/tenants/membership.ex b/lib/cgrates_web_jsonapi/tenants/membership.ex new file mode 100644 index 0000000..0e81299 --- /dev/null +++ b/lib/cgrates_web_jsonapi/tenants/membership.ex @@ -0,0 +1,22 @@ +defmodule CgratesWebJsonapi.Tenants.Membership do + use Ecto.Schema + import Ecto.Changeset + + alias CgratesWebJsonapi.Tenants.Tenant + alias CgratesWebJsonapi.Auth.User + + schema "memberships" do + field :role, :integer + belongs_to :tenant, Tenant, type: :string + belongs_to :user, User + + timestamps() + end + + @doc false + def changeset(membership, attrs) do + membership + |> cast(attrs, [:role]) + |> validate_required([:role]) + end +end diff --git a/lib/cgrates_web_jsonapi_web/controllers/cdr_extra_field_controller.ex b/lib/cgrates_web_jsonapi_web/controllers/cdr_extra_field_controller.ex new file mode 100644 index 0000000..8c0ce5d --- /dev/null +++ b/lib/cgrates_web_jsonapi_web/controllers/cdr_extra_field_controller.ex @@ -0,0 +1,22 @@ +defmodule CgratesWebJsonapiWeb.CdrExtraFieldController do + use CgratesWebJsonapiWeb, :controller + use PhoenixSwagger + + alias CgratesWebJsonapi.Cdrs + alias CgratesWebJsonapi.EtsCache + + swagger_path :index do + get("/api/cdr-extra-fields") + CommonSwaggerParams.authorization() + CommonSwaggerParams.content_type() + + description("An array of string values of used extra_fields") + + response(200, "OK") + end + + def index(conn, _) do + extra_fields = fn -> Cdrs.extra() end + render(conn, "extra_fields.json", data: EtsCache.get(extra_fields, :extra_fields)) + end +end diff --git a/lib/cgrates_web_jsonapi_web/controllers/membership_controller.ex b/lib/cgrates_web_jsonapi_web/controllers/membership_controller.ex new file mode 100644 index 0000000..eb8b206 --- /dev/null +++ b/lib/cgrates_web_jsonapi_web/controllers/membership_controller.ex @@ -0,0 +1,42 @@ +defmodule CgratesWebJsonapiWeb.MembershipController do + use CgratesWebJsonapiWeb, :controller + + alias CgratesWebJsonapi.Tenants + alias CgratesWebJsonapi.Tenants.Membership + + def index(conn, _params) do + memberships = Tenants.list_memberships() + render(conn, "index.json", memberships: memberships) + end + + def create(conn, %{"membership" => membership_params}) do + with {:ok, %Membership{} = membership} <- Tenants.create_membership(membership_params) do + conn + |> put_status(:created) + |> put_resp_header("location", Routes.membership_path(conn, :show, membership)) + |> render("show.json", membership: membership) + end + end + + def show(conn, %{"id" => id}) do + membership = Tenants.get_membership!(id) + render(conn, "show.json", membership: membership) + end + + def update(conn, %{"id" => id, "membership" => membership_params}) do + membership = Tenants.get_membership!(id) + + with {:ok, %Membership{} = membership} <- + Tenants.update_membership(membership, membership_params) do + render(conn, "show.json", membership: membership) + end + end + + def delete(conn, %{"id" => id}) do + membership = Tenants.get_membership!(id) + + with {:ok, %Membership{}} <- Tenants.delete_membership(membership) do + send_resp(conn, :no_content, "") + end + end +end diff --git a/lib/cgrates_web_jsonapi_web/router.ex b/lib/cgrates_web_jsonapi_web/router.ex index 13b7baa..a0e03db 100644 --- a/lib/cgrates_web_jsonapi_web/router.ex +++ b/lib/cgrates_web_jsonapi_web/router.ex @@ -104,8 +104,10 @@ defmodule CgratesWebJsonapiWeb.Router do post("/tp-timings/delete_all", TpTimingController, :delete_all) resources("/tp-timings", TpTimingController, except: [:new, :edit]) resources("/users", UserController, except: [:new, :edit]) + resources("/cdr-extra-fields", CdrExtraFieldController, only: [:index]) resources("/cdr-stats", CdrStatController, only: [:index]) resources("/tenants", TenantController, only: [:show, :update]) + resources("/memberships", MembershipController) end scope "/uploaders", CgratesWebJsonapiWeb do diff --git a/lib/cgrates_web_jsonapi_web/views/cdr_extra_field_view.ex b/lib/cgrates_web_jsonapi_web/views/cdr_extra_field_view.ex new file mode 100644 index 0000000..3770c84 --- /dev/null +++ b/lib/cgrates_web_jsonapi_web/views/cdr_extra_field_view.ex @@ -0,0 +1,6 @@ +defmodule CgratesWebJsonapiWeb.CdrExtraFieldView do + use CgratesWebJsonapiWeb, :view + use JaSerializer.PhoenixView + + def render("extra_fields.json", %{data: data}), do: data +end diff --git a/lib/cgrates_web_jsonapi_web/views/membership_view.ex b/lib/cgrates_web_jsonapi_web/views/membership_view.ex new file mode 100644 index 0000000..79f4736 --- /dev/null +++ b/lib/cgrates_web_jsonapi_web/views/membership_view.ex @@ -0,0 +1,16 @@ +defmodule CgratesWebJsonapiWeb.MembershipView do + use CgratesWebJsonapiWeb, :view + alias CgratesWebJsonapiWeb.MembershipView + + def render("index.json", %{memberships: memberships}) do + %{data: render_many(memberships, MembershipView, "membership.json")} + end + + def render("show.json", %{membership: membership}) do + %{data: render_one(membership, MembershipView, "membership.json")} + end + + def render("membership.json", %{membership: membership}) do + %{id: membership.id, role: membership.role} + end +end diff --git a/priv/repo/migrations/20210517174717_create_memberships.exs b/priv/repo/migrations/20210517174717_create_memberships.exs new file mode 100644 index 0000000..54aaf51 --- /dev/null +++ b/priv/repo/migrations/20210517174717_create_memberships.exs @@ -0,0 +1,16 @@ +defmodule CgratesWebJsonapi.Repo.Migrations.CreateMemberships do + use Ecto.Migration + + def change do + create table(:memberships) do + add(:role, :integer) + add(:tenant_id, references(:tenants, on_delete: :nothing, type: :string)) + add(:user_id, references(:users, on_delete: :nothing)) + + timestamps() + end + + create(index(:memberships, [:tenant_id])) + create(index(:memberships, [:user_id])) + end +end diff --git a/priv/static/swagger.json b/priv/static/swagger.json index 5a4b2e5..7571ecf 100644 --- a/priv/static/swagger.json +++ b/priv/static/swagger.json @@ -384,6 +384,33 @@ "Call" ] } + }, + "/api/cdr-extra-fields": { + "get": { + "description": "An array of string values of used extra_fields", + "operationId": "CgratesWebJsonapiWeb.CdrExtraFieldController.index", + "parameters": [ + { + "description": "OAuth2 access token", + "in": "header", + "name": "Authorization", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/vnd.api+json" + ], + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "", + "tags": [ + "CdrExtraField" + ] + } } }, "swagger": "2.0", diff --git a/test/cgrates_web_jsonapi/tenants_test.exs b/test/cgrates_web_jsonapi/tenants_test.exs index 1f072d0..f032bc3 100644 --- a/test/cgrates_web_jsonapi/tenants_test.exs +++ b/test/cgrates_web_jsonapi/tenants_test.exs @@ -66,4 +66,63 @@ defmodule CgratesWebJsonapi.TenantsTest do assert %Ecto.Changeset{} = Tenants.change_tenant(tenant) end end + + describe "memberships" do + alias CgratesWebJsonapi.Tenants.Membership + + @valid_attrs %{role: 42} + @update_attrs %{role: 43} + @invalid_attrs %{role: nil} + + def membership_fixture(attrs \\ %{}) do + {:ok, membership} = + attrs + |> Enum.into(@valid_attrs) + |> Tenants.create_membership() + + membership + end + + test "list_memberships/0 returns all memberships" do + membership = membership_fixture() + assert Tenants.list_memberships() == [membership] + end + + test "get_membership!/1 returns the membership with given id" do + membership = membership_fixture() + assert Tenants.get_membership!(membership.id) == membership + end + + test "create_membership/1 with valid data creates a membership" do + assert {:ok, %Membership{} = membership} = Tenants.create_membership(@valid_attrs) + assert membership.role == 42 + end + + test "create_membership/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Tenants.create_membership(@invalid_attrs) + end + + test "update_membership/2 with valid data updates the membership" do + membership = membership_fixture() + assert {:ok, %Membership{} = membership} = Tenants.update_membership(membership, @update_attrs) + assert membership.role == 43 + end + + test "update_membership/2 with invalid data returns error changeset" do + membership = membership_fixture() + assert {:error, %Ecto.Changeset{}} = Tenants.update_membership(membership, @invalid_attrs) + assert membership == Tenants.get_membership!(membership.id) + end + + test "delete_membership/1 deletes the membership" do + membership = membership_fixture() + assert {:ok, %Membership{}} = Tenants.delete_membership(membership) + assert_raise Ecto.NoResultsError, fn -> Tenants.get_membership!(membership.id) end + end + + test "change_membership/1 returns a membership changeset" do + membership = membership_fixture() + assert %Ecto.Changeset{} = Tenants.change_membership(membership) + end + end end diff --git a/test/cgrates_web_jsonapi_web/controllers/cdr_extra_field_controller_test.exs b/test/cgrates_web_jsonapi_web/controllers/cdr_extra_field_controller_test.exs new file mode 100644 index 0000000..ee411c2 --- /dev/null +++ b/test/cgrates_web_jsonapi_web/controllers/cdr_extra_field_controller_test.exs @@ -0,0 +1,36 @@ +defmodule CgratesWebJsonapiWeb.CdrExtraFieldControllerTest do + use CgratesWebJsonapi.ConnCase + + alias CgratesWebJsonapi.Cdrs.Cdr + alias CgratesWebJsonapi.Repo + + import CgratesWebJsonapi.Factory + import CgratesWebJsonapi.Guardian + + setup do + user = insert(:user) + + {:ok, token, _} = encode_and_sign(user, %{}, token_type: :access) + + conn = + build_conn() + |> put_req_header("accept", "application/vnd.api+json") + |> put_req_header("content-type", "application/vnd.api+json") + |> put_req_header("authorization", "bearer: " <> token) + + {:ok, conn: conn} + end + + test "cdrs extra fields list", %{conn: conn} do + cdr1 = insert(:cdr, destination: "123", account: "1") + cdr2 = insert(:cdr, destination: "987", account: "2") + + conn = + conn + |> get(Routes.cdr_extra_field_path(conn, :index)) + + response = json_response(conn, 200) + + assert response == ["cost"] + end +end diff --git a/test/cgrates_web_jsonapi_web/controllers/membership_controller_test.exs b/test/cgrates_web_jsonapi_web/controllers/membership_controller_test.exs new file mode 100644 index 0000000..38e2fec --- /dev/null +++ b/test/cgrates_web_jsonapi_web/controllers/membership_controller_test.exs @@ -0,0 +1,93 @@ +defmodule CgratesWebJsonapiWeb.MembershipControllerTest do + use CgratesWebJsonapi.ConnCase + + alias CgratesWebJsonapi.Tenants + alias CgratesWebJsonapi.Tenants.Membership + + alias CgratesWebJsonapi.Cdrs.Cdr + alias CgratesWebJsonapi.Repo + + import CgratesWebJsonapi.Factory + import CgratesWebJsonapi.Guardian + + @create_attrs %{ + role: 42 + } + + @update_attrs %{ + role: 43 + } + + setup do + user = insert(:user) + + {:ok, token, _} = encode_and_sign(user, %{}, token_type: :access) + + conn = + build_conn() + |> put_req_header("accept", "application/vnd.api+json") + |> put_req_header("content-type", "application/vnd.api+json") + |> put_req_header("authorization", "bearer: " <> token) + + {:ok, conn: conn} + end + + describe "index" do + test "lists all memberships", %{conn: conn} do + insert(:membership) + conn = get(conn, Routes.membership_path(conn, :index)) + + assert length(json_response(conn, 200)["data"]) == 1 + end + end + + describe "show" do + test "show one memberships", %{conn: conn} do + membership = insert(:membership, role: 1) + conn = get(conn, Routes.membership_path(conn, :show, membership)) |> doc + + data = json_response(conn, 200)["data"] + assert data["id"] == membership.id + assert data["role"] == 1 + end + end + + describe "create membership" do + test "renders membership when data is valid", %{conn: conn} do + conn = post(conn, Routes.membership_path(conn, :create), membership: @create_attrs) + assert %{"id" => id} = json_response(conn, 201)["data"] + + conn = get(conn, Routes.membership_path(conn, :show, id)) + + assert %{ + "id" => id, + "role" => 42 + } = json_response(conn, 200)["data"] + end + end + + describe "update membership" do + test "update chosen membership", %{conn: conn} do + membership = insert(:membership, role: 1) + + id = membership.id + conn = put(conn, Routes.membership_path(conn, :update, id), membership: @update_attrs) + assert json_response(conn, 200)["data"]["id"] + + assert json_response(conn, 200)["data"]["role"] == @update_attrs[:role] + end + end + + describe "delete membership" do + test "deletes chosen membership", %{conn: conn} do + membership = insert(:membership, role: 1) + + conn = delete(conn, Routes.membership_path(conn, :delete, membership)) + assert response(conn, 204) + + assert_error_sent 404, fn -> + get(conn, Routes.membership_path(conn, :show, membership)) + end + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index 9de0454..6267059 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -310,4 +310,12 @@ defmodule CgratesWebJsonapi.Factory do id: UUID.uuid4() } end + + def membership_factory do + %CgratesWebJsonapi.Tenants.Membership{ + id: sequence(:id, fn n -> n end), + tenant_id: insert(:tenant).id, + user_id: insert(:user).id + } + end end