diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9d4752..6255799 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,9 +64,9 @@ jobs: path: | deps _build - key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + key: ${{ runner.os }}-mix-typesense-${{ matrix.typesense-version }}-${{ hashFiles('**/mix.lock') }} restore-keys: | - ${{ runner.os }}-mix- + ${{ runner.os }}-mix-typesense-${{ matrix.typesense-version }} - name: Install Dependencies run: | diff --git a/.iex.exs b/.iex.exs index 1362b41..115c75d 100644 --- a/.iex.exs +++ b/.iex.exs @@ -1,3 +1,8 @@ +alias ExTypesense.Cluster alias ExTypesense.Collection +alias ExTypesense.Connection alias ExTypesense.Document alias ExTypesense.HttpClient +alias ExTypesense.Search +alias ExTypesense.TestSchema.Credential +alias ExTypesense.TestSchema.Person diff --git a/CHANGELOG.md b/CHANGELOG.md index af3eb31..be8df2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.4.3 (2024.07.03) + +### Changed + +* `README` regarding test and connection strings. +* Replacing connection struct to map. + ## 0.4.2 (2024.06.19) ### Changed diff --git a/README.md b/README.md index 3a2fbdb..876fc75 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Typesense client for Elixir with support for your Ecto schemas. -**Note**: Breaking changes if you're upgrading from `0.3.x` to upcoming `0.5.x` version. +> **Note**: Breaking changes if you're upgrading from `0.3.x` to upcoming `0.5.x` version. ## Todo @@ -55,13 +55,17 @@ After you have setup a [local](./guides/running_local_typesense.md) Typesense or You can set the following config details to the config file: ```elixir -config :ex_typesense, - api_key: "xyz", - host: "localhost", - port: 8108, - scheme: "http" +if config_env() == :prod do # if you'll use this in prod environment + config :ex_typesense, + api_key: "xyz", + host: "localhost", + port: 8108, + scheme: "http" + ... ``` +> **Note**: If you use this for adding test in your app, you might want to add this in `config/test.exs`: + For Cloud hosted, you can generate and obtain the credentials from cluster instance admin interface: ```elixir @@ -72,12 +76,12 @@ config :ex_typesense, scheme: "https" ``` -#### Option 2: Dynamic connection using an Ecto schema +#### Option 2: Set credentials from a map > By default you don't need to pass connections every > time you use a function, if you use "Option 1" above. -You may have a `Connection` Ecto schema in your app and want to pass your own creds dynamically. +You may have a `Connection` Ecto schema in your app and want to pass your own creds dynamically: ```elixir defmodule MyApp.Credential do @@ -89,24 +93,11 @@ defmodule MyApp.Credential do end ``` -using Connection struct +As long as the keys matches in `ExTypesense.Connection.t()`: ```elixir credential = MyApp.Credential |> where(id: ^8888) |> Repo.one() -conn = %ExTypesense.Connection{ - host: credential.node, - api_key: credential.secret_key, - port: credential.port, - scheme: "https" -} - -ExTypesense.search(conn, collection_name, query) -``` - -or maps, as long as the keys matches in `ExTypesense.Connection.t()` - -```elixir conn = %{ host: credential.node, api_key: credential.secret_key, @@ -115,10 +106,9 @@ conn = %{ } ExTypesense.search(conn, collection_name, query) - ``` -or convert your struct to map, as long as the keys matches in `ExTypesense.Connection.t()` +Or convert your struct to map, as long as the keys matches in `ExTypesense.Connection.t()`: ```elixir conn = Map.from_struct(MyApp.Credential) @@ -127,7 +117,7 @@ ExTypesense.search(conn, collection_name, query) ``` -or you don't want to change the fields in your schema, thus you convert it to map +Or you don't want to change the fields in your Ecto schema, thus you convert it to map: ```elixir conn = %Credential{ diff --git a/lib/ex_typesense.ex b/lib/ex_typesense.ex index b724a92..444b56f 100644 --- a/lib/ex_typesense.ex +++ b/lib/ex_typesense.ex @@ -87,19 +87,8 @@ defmodule ExTypesense do to: ExTypesense.Document defdelegate create_document(conn \\ Connection.new(), document), to: ExTypesense.Document - - @deprecated "use delete_document_by_id/3" defdelegate delete_document(document), to: ExTypesense.Document - - @deprecated "use delete_document_by_struct/2" defdelegate delete_document(collection_name, document_id), to: ExTypesense.Document - - defdelegate delete_document_by_struct(conn \\ Connection.new(), struct), - to: ExTypesense.Document - - defdelegate delete_document_by_id(conn \\ Connection.new(), collection_name, document_id), - to: ExTypesense.Document - defdelegate update_document(conn \\ Connection.new(), document), to: ExTypesense.Document defdelegate upsert_document(conn \\ Connection.new(), document), to: ExTypesense.Document diff --git a/lib/ex_typesense/cluster.ex b/lib/ex_typesense/cluster.ex index 8aea329..8338bde 100644 --- a/lib/ex_typesense/cluster.ex +++ b/lib/ex_typesense/cluster.ex @@ -7,6 +7,7 @@ defmodule ExTypesense.Cluster do alias ExTypesense.Connection alias ExTypesense.HttpClient + @typedoc since: "0.3.0" @type response() :: any() | {:ok, any()} | {:error, map()} @doc """ diff --git a/lib/ex_typesense/collection.ex b/lib/ex_typesense/collection.ex index afceff8..9abcd34 100644 --- a/lib/ex_typesense/collection.ex +++ b/lib/ex_typesense/collection.ex @@ -29,6 +29,7 @@ defmodule ExTypesense.Collection do :vec_dist ] + @typedoc since: "0.1.0" @type t() :: %__MODULE__{ facet: boolean(), index: boolean(), @@ -44,6 +45,7 @@ defmodule ExTypesense.Collection do vec_dist: String.t() } + @typedoc since: "0.1.0" @type field_type() :: :string | :"string[]" @@ -95,6 +97,7 @@ defmodule ExTypesense.Collection do symbols_to_index: list() } + @typedoc since: "0.1.0" @type response() :: t() | [t() | map()] | map() | {:error, map()} @doc """ diff --git a/lib/ex_typesense/connection.ex b/lib/ex_typesense/connection.ex index c616206..ebb7a55 100644 --- a/lib/ex_typesense/connection.ex +++ b/lib/ex_typesense/connection.ex @@ -4,25 +4,44 @@ defmodule ExTypesense.Connection do Fetches credentials either from application env or map. """ - @derive {Inspect, except: [:api_key]} - defstruct host: ExTypesense.HttpClient.get_host(), - api_key: ExTypesense.HttpClient.api_key(), - port: ExTypesense.HttpClient.get_port(), - scheme: ExTypesense.HttpClient.get_scheme() + alias ExTypesense.HttpClient - @type t() :: %__MODULE__{} + # @derive {Inspect, except: [:api_key]} + # defstruct host: HttpClient.get_host(), + # api_key: HttpClient.api_key(), + # port: HttpClient.get_port(), + # scheme: HttpClient.get_scheme() + + @typedoc since: "0.4.0" + @type t() :: %{ + host: String.t() | nil, + api_key: String.t() | nil, + port: non_neg_integer() | nil, + scheme: String.t() | nil + } @doc """ Fetches credentials either from application env or map. """ @doc since: "0.4.0" - @spec new(connection :: struct() | map()) :: ExTypesense.Connection.t() - def new(connection \\ %__MODULE__{}) when is_struct(connection) do - %__MODULE__{ + @spec new(connection :: t() | map()) :: ExTypesense.Connection.t() + def new(connection \\ defaults()) when is_map(connection) do + %{ host: Map.get(connection, :host), api_key: Map.get(connection, :api_key), port: Map.get(connection, :port), scheme: Map.get(connection, :scheme) } end + + @doc since: "0.4.3" + @spec defaults :: map() + defp defaults do + %{ + host: HttpClient.get_host(), + api_key: HttpClient.api_key(), + port: HttpClient.get_port(), + scheme: HttpClient.get_scheme() + } + end end diff --git a/lib/ex_typesense/document.ex b/lib/ex_typesense/document.ex index 6d5ffb4..268acbf 100644 --- a/lib/ex_typesense/document.ex +++ b/lib/ex_typesense/document.ex @@ -12,6 +12,8 @@ defmodule ExTypesense.Document do @collections_path @root_path <> "collections" @documents_path "documents" @import_path "import" + + @typedoc since: "0.1.0" @type response :: :ok | {:ok, map()} | {:error, map()} @doc """ @@ -369,72 +371,10 @@ defmodule ExTypesense.Document do HttpClient.request(conn, opts) end - @doc """ - Deletes a document by struct. - """ - @doc since: "0.4.0" - @spec delete_document_by_struct(Connection.t(), struct()) :: response() - def delete_document_by_struct(conn \\ Connection.new(), struct) when is_struct(struct) do - document_id = struct.id - collection_name = struct.__struct__.__schema__(:source) - do_delete_document(conn, collection_name, document_id) - end - - @doc """ - Deletes a document by collection name and document id. - - ## Examples - iex> schema = %{ - ...> name: "posts", - ...> fields: [ - ...> %{name: "title", type: "string"} - ...> ], - ...> } - ...> ExTypesense.create_collection(schema) - iex> post = - ...> %{ - ...> id: "12", - ...> collection_name: "posts", - ...> post_id: 22, - ...> title: "the quick brown fox" - ...> } - iex> ExTypesense.create_document(post) - iex> ExTypesense.delete_document("posts", 12) - {:ok, - %{ - "id" => "12", - "post_id" => 22, - "title" => "the quick brown fox", - "collection_name" => "posts" - } - } - """ - @doc since: "0.4.0" - @spec delete_document_by_id(Connection.t(), String.t(), integer()) :: response() - def delete_document_by_id(conn \\ Connection.new(), collection_name, document_id) - when is_binary(collection_name) and is_integer(document_id) do - do_delete_document(conn, collection_name, document_id) - end - - @doc since: "0.4.0" - @spec do_delete_document(Connection.t(), String.t(), integer()) :: response() - defp do_delete_document(conn, collection_name, document_id) do - path = - Path.join([ - @collections_path, - collection_name, - @documents_path, - Jason.encode!(document_id) - ]) - - HttpClient.request(conn, %{method: :delete, path: path}) - end - @doc """ Deletes a document by struct. """ @doc since: "0.3.0" - @deprecated "use delete_document_by_struct/2" @spec delete_document(struct()) :: response() def delete_document(struct) when is_struct(struct) do document_id = struct.id @@ -472,7 +412,6 @@ defmodule ExTypesense.Document do } """ @doc since: "0.3.0" - @deprecated "use delete_document_by_id/3" @spec delete_document(String.t(), integer()) :: response() def delete_document(collection_name, document_id) when is_binary(collection_name) and is_integer(document_id) do diff --git a/lib/ex_typesense/http_client.ex b/lib/ex_typesense/http_client.ex index 39e914f..138e129 100644 --- a/lib/ex_typesense/http_client.ex +++ b/lib/ex_typesense/http_client.ex @@ -6,15 +6,41 @@ defmodule ExTypesense.HttpClient do alias ExTypesense.Connection + @typedoc since: "0.1.0" @type request_body() :: iodata() | nil + + @typedoc since: "0.1.0" @type request_method() :: :get | :post | :delete | :patch | :put + + @typedoc since: "0.1.0" @type request_path() :: String.t() @api_header_name ~c"X-TYPESENSE-API-KEY" + @doc since: "0.1.0" + @spec get_host :: String.t() | nil def get_host, do: Application.get_env(:ex_typesense, :host) - def get_port, do: Application.get_env(:ex_typesense, :port) + + @doc since: "0.1.0" + @spec get_scheme :: String.t() | nil def get_scheme, do: Application.get_env(:ex_typesense, :scheme) + + @doc since: "0.1.0" + @spec get_port :: non_neg_integer() | nil + def get_port do + Application.get_env(:ex_typesense, :port) + end + + @doc """ + Returns the Typesense's API key + + > #### Warning {: .warning} + > + > Even if `api_key` is hidden in `Connection` struct, this + > function will still return the key and accessible inside + > shell (assuming bad actors [pun unintended `:/`] can get in as well). + """ + @spec api_key :: String.t() | nil def api_key, do: Application.get_env(:ex_typesense, :api_key) @doc """ @@ -49,8 +75,25 @@ defmodule ExTypesense.HttpClient do } """ @doc since: "0.4.0" - @spec request(Connection.t(), map()) :: nil + @spec request(Connection.t(), map()) :: {:ok, any()} | {:error, String.t()} def request(conn, opts \\ %{}) do + # Req.Request.append_error_steps and its retry option are used here. + # options like retry, max_retries, etc. can be found in: + # https://hexdocs.pm/req/Req.Steps.html#retry/1 + # NOTE: look at source code in Github + retry = fn request -> + if Mix.env() === :test do + {req, resp_or_err} = request + + # disabled in order to cut time in tests + req = %{req | options: %{retry: false}} + + Req.Steps.retry({req, resp_or_err}) + else + Req.Steps.retry(request) + end + end + url = %URI{ scheme: conn.scheme, @@ -68,7 +111,7 @@ defmodule ExTypesense.HttpClient do } |> Req.Request.put_header("x-typesense-api-key", conn.api_key) |> Req.Request.put_header("content-type", opts[:content_type] || "application/json") - |> Req.Request.append_error_steps(retry: &Req.Steps.retry/1) + |> Req.Request.append_error_steps(retry: retry) |> Req.Request.run!() case response.status in 200..299 do diff --git a/lib/ex_typesense/test_schema/catalog.ex b/lib/ex_typesense/test_schema/catalog.ex new file mode 100644 index 0000000..03f28a9 --- /dev/null +++ b/lib/ex_typesense/test_schema/catalog.ex @@ -0,0 +1,36 @@ +defmodule ExTypesense.TestSchema.Catalog do + use Ecto.Schema + @behaviour ExTypesense + + @moduledoc false + + defimpl Jason.Encoder, for: __MODULE__ do + def encode(value, opts) do + value + |> Map.take([:catalog_id, :name, :description]) + |> Enum.map(fn {key, val} -> + if key === :catalog_id, do: {key, Map.get(value, :id)}, else: {key, val} + end) + |> Enum.into(%{}) + |> Jason.Encode.map(opts) + end + end + + schema "catalogs" do + field(:name, :string) + field(:description, :string) + field(:catalog_id, :integer, virtual: true) + end + + @impl ExTypesense + def get_field_types do + %{ + default_sorting_field: "catalog_id", + fields: [ + %{name: "catalog_id", type: "int32"}, + %{name: "name", type: "string"}, + %{name: "description", type: "string"} + ] + } + end +end diff --git a/mix.exs b/mix.exs index 7cdd8d0..edebc77 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule ExTypesense.MixProject do use Mix.Project @source_url "https://github.com/jaeyson/ex_typesense" - @version "0.4.2" + @version "0.4.3" def project do [ diff --git a/mix.lock b/mix.lock index 1e576cb..7e5cf11 100644 --- a/mix.lock +++ b/mix.lock @@ -15,12 +15,12 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"}, + "mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"}, "mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "req": {:hex, :req, "0.5.0", "6d8a77c25cfc03e06a439fb12ffb51beade53e3fe0e2c5e362899a18b50298b3", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dda04878c1396eebbfdec6db6f3d4ca609e5c8846b7ee88cc56eb9891406f7a3"}, + "req": {:hex, :req, "0.5.1", "90584216d064389a4ff2d4279fe2c11ff6c812ab00fa01a9fb9d15457f65ba70", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7ea96a1a95388eb0fefa92d89466cdfedba24032794e5c1147d78ec90db7edca"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, diff --git a/test/cluster_test.exs b/test/cluster_test.exs index f5e7c54..10fddf2 100644 --- a/test/cluster_test.exs +++ b/test/cluster_test.exs @@ -1,26 +1,15 @@ defmodule ClusterTest do - use ExUnit.Case, async: false + use ExUnit.Case, async: true - setup_all do - %{ - conn: %ExTypesense.Connection{ - host: "localhost", - api_key: "xyz", - port: 8108, - scheme: "http" - } - } + test "health" do + assert {:ok, true} = ExTypesense.health() end - test "health", context do - assert {:ok, true} = ExTypesense.health(context.conn) + test "api status" do + assert {:ok, _} = ExTypesense.api_stats() end - test "api status", context do - assert {:ok, _} = ExTypesense.api_stats(context.conn) - end - - test "cluster metrics", context do - assert {:ok, _} = ExTypesense.cluster_metrics(context.conn) + test "cluster metrics" do + assert {:ok, _} = ExTypesense.cluster_metrics() end end diff --git a/test/collection_test.exs b/test/collection_test.exs index 295435a..836e3bd 100644 --- a/test/collection_test.exs +++ b/test/collection_test.exs @@ -1,16 +1,9 @@ defmodule CollectionTest do - use ExUnit.Case, async: false + use ExUnit.Case, async: true setup_all do - conn = %ExTypesense.Connection{ - host: "localhost", - api_key: "xyz", - port: 8108, - scheme: "http" - } - schema = %{ - name: "companies", + name: "collection_companies", fields: [ %{name: "company_name", type: "string"}, %{name: "company_id", type: "int32"}, @@ -19,23 +12,27 @@ defmodule CollectionTest do default_sorting_field: "company_id" } - %{conn: conn, schema: schema} + on_exit(fn -> + ExTypesense.drop_collection(schema.name) + end) + + %{schema: schema} end - test "success: create and drop collection", %{conn: conn, schema: schema} do - collection = ExTypesense.create_collection(conn, schema) + test "success: create and drop collection", %{schema: schema} do + collection = ExTypesense.create_collection(schema) assert %ExTypesense.Collection{} = collection - ExTypesense.drop_collection(conn, schema.name) - assert {:error, "Not Found"} === ExTypesense.get_collection(conn, schema.name) + ExTypesense.drop_collection(schema.name) + assert {:error, "Not Found"} === ExTypesense.get_collection(schema.name) end - test "error: dropping unknown collection", %{conn: conn, schema: schema} do + test "error: dropping unknown collection", %{schema: schema} do message = ~s(No collection with name `#{schema.name}` found.) - assert {:error, message} === ExTypesense.drop_collection(conn, schema.name) + assert {:error, message} === ExTypesense.drop_collection(schema.name) end - test "success: get specific collection", %{conn: conn} do + test "success: get specific collection" do schema = %{ name: "specific_collection", fields: [ @@ -45,18 +42,18 @@ defmodule CollectionTest do default_sorting_field: "collection_id" } - assert %ExTypesense.Collection{} = ExTypesense.create_collection(conn, schema) - collection = ExTypesense.get_collection(conn, schema.name) + assert %ExTypesense.Collection{} = ExTypesense.create_collection(schema) + collection = ExTypesense.get_collection(schema.name) assert collection.name === schema.name - ExTypesense.drop_collection(conn, schema.name) + ExTypesense.drop_collection(schema.name) end - test "error: get unknown collection", %{conn: conn, schema: schema} do - assert {:error, "Not Found"} === ExTypesense.get_collection(conn, schema.name) + test "error: get unknown collection", %{schema: schema} do + assert {:error, "Not Found"} === ExTypesense.get_collection(schema.name) end - test "success: update schema fields", %{conn: conn, schema: schema} do - ExTypesense.create_collection(conn, schema) + test "success: update schema fields", %{schema: schema} do + ExTypesense.create_collection(schema) fields = %{ fields: [ @@ -65,67 +62,68 @@ defmodule CollectionTest do ] } - ExTypesense.update_collection_fields(conn, schema.name, fields) + ExTypesense.update_collection_fields(schema.name, fields) - collection = ExTypesense.get_collection(conn, schema.name) + collection = ExTypesense.get_collection(schema.name) test = Enum.find(collection.fields, fn map -> map["name"] === "test" end) assert test["name"] === "test" - ExTypesense.drop_collection(conn, schema.name) + ExTypesense.drop_collection(schema.name) end + # ISSUE: https://github.com/typesense/typesense/issues/1700 # test "success: update collection name" do # end - test "success: count list of collections", %{conn: conn, schema: schema} do - ExTypesense.create_collection(conn, schema) - refute ExTypesense.list_collections(conn) |> Enum.empty?() - ExTypesense.drop_collection(conn, schema.name) + test "success: count list of collections", %{schema: schema} do + ExTypesense.create_collection(schema) + refute ExTypesense.list_collections() |> Enum.empty?() + ExTypesense.drop_collection(schema.name) end - test "success: list aliases", %{conn: conn, schema: schema} do - ExTypesense.upsert_collection_alias(conn, schema.name, schema.name) - count = length(ExTypesense.list_collection_aliases(conn)) + test "success: list aliases", %{schema: schema} do + ExTypesense.upsert_collection_alias(schema.name, schema.name) + count = length(ExTypesense.list_collection_aliases()) assert count === 1 - ExTypesense.delete_collection_alias(conn, schema.name) + ExTypesense.delete_collection_alias(schema.name) end - test "success: create and delete alias", %{conn: conn, schema: schema} do - collection_alias = ExTypesense.upsert_collection_alias(conn, schema.name, schema.name) + test "success: create and delete alias", %{schema: schema} do + collection_alias = ExTypesense.upsert_collection_alias(schema.name, schema.name) assert is_map(collection_alias) === true assert Enum.empty?(collection_alias) === false - ExTypesense.delete_collection_alias(conn, schema.name) - assert {:error, "Not Found"} === ExTypesense.get_collection_alias(conn, schema.name) + ExTypesense.delete_collection_alias(schema.name) + assert {:error, "Not Found"} === ExTypesense.get_collection_alias(schema.name) end - test "success: get collection name by alias", %{conn: conn, schema: schema} do + test "success: get collection name by alias", %{schema: schema} do %{"collection_name" => collection_name, "name" => collection_alias} = - ExTypesense.upsert_collection_alias(conn, schema.name, schema.name) + ExTypesense.upsert_collection_alias(schema.name, schema.name) - assert collection_name === ExTypesense.get_collection_name(conn, collection_alias) + assert collection_name === ExTypesense.get_collection_name(collection_alias) - ExTypesense.delete_collection_alias(conn, schema.name) + ExTypesense.delete_collection_alias(schema.name) end - test "success: get specific alias", %{conn: conn, schema: schema} do - ExTypesense.upsert_collection_alias(conn, schema.name, schema.name) - collection_alias = ExTypesense.get_collection_alias(conn, schema.name) + test "success: get specific alias", %{schema: schema} do + ExTypesense.upsert_collection_alias(schema.name, schema.name) + collection_alias = ExTypesense.get_collection_alias(schema.name) assert is_map(collection_alias) assert collection_alias["name"] === schema.name - ExTypesense.delete_collection_alias(conn, schema.name) + ExTypesense.delete_collection_alias(schema.name) end - test "error: get unknown alias", %{conn: conn, schema: schema} do - assert {:error, "Not Found"} === ExTypesense.get_collection_alias(conn, schema.name) + test "error: get unknown alias", %{schema: schema} do + assert {:error, "Not Found"} === ExTypesense.get_collection_alias(schema.name) end - test "error: delete unknown alias", %{conn: conn, schema: schema} do - assert {:error, "Not Found"} === ExTypesense.delete_collection_alias(conn, schema.name) + test "error: delete unknown alias", %{schema: schema} do + assert {:error, "Not Found"} === ExTypesense.delete_collection_alias(schema.name) end end diff --git a/test/connection_test.exs b/test/connection_test.exs index e5d7056..b9ce7ad 100644 --- a/test/connection_test.exs +++ b/test/connection_test.exs @@ -1,34 +1,76 @@ defmodule ConnectionTest do - use ExUnit.Case, async: false + use ExUnit.Case, async: true alias ExTypesense.TestSchema.Credential + @forbidden "Forbidden - a valid `x-typesense-api-key` header must be sent." + setup_all do - %{ - conn: %ExTypesense.Connection{ - host: "localhost", - api_key: "xyz", - port: 8108, - scheme: "http" - } + schema = %{ + name: "connection_companies", + fields: [ + %{name: "company_name", type: "string"}, + %{name: "company_id", type: "int32"}, + %{name: "country", type: "string"} + ], + default_sorting_field: "company_id" } + + %{schema: schema} end - test "Using connection struct", context do - assert {:ok, true} = ExTypesense.health(context.conn) + setup %{schema: schema} do + ExTypesense.create_collection(schema) + + on_exit(fn -> + ExTypesense.drop_collection(schema.name) + end) + + :ok end - test "Using map" do + test "error: health check with empty credentials" do + conn = %{api_key: nil, host: nil, port: nil, scheme: nil} + + assert_raise FunctionClauseError, fn -> + ExTypesense.health(conn) + end + end + + test "error: health check, with incorrect API key" do + conn = %{api_key: "abc", host: "localhost", port: 8109, scheme: "http"} + + assert_raise Req.TransportError, fn -> + ExTypesense.health(conn) + end + end + + test "error: wrong API key was configured" do conn = %{ host: "localhost", - api_key: "xyz", + api_key: "another_key", port: 8108, scheme: "http" } - assert {:ok, true} = ExTypesense.health(conn) + assert {:error, @forbidden} == ExTypesense.list_collections(conn) + end + + test "error: overriding config with a wrong API key" do + conn = %{ + host: "localhost", + api_key: "another_key", + port: 8108, + scheme: "http" + } + + assert {:error, @forbidden} = ExTypesense.list_collections(conn) + end + + test "success: health check" do + assert {:ok, true} = ExTypesense.health() end - test "Using a struct converted to map and update its keys" do + test "success: Using a struct converted to map and update its keys" do conn = %Credential{ node: "localhost", secret_key: "xyz", diff --git a/test/document_test.exs b/test/document_test.exs index d6d98ef..ef63716 100644 --- a/test/document_test.exs +++ b/test/document_test.exs @@ -1,22 +1,11 @@ defmodule DocumentTest do - use ExUnit.Case, async: false + use ExUnit.Case, async: true alias ExTypesense.TestSchema.Person setup_all do - # this is for deprecated function to set the creds - [{:api_key, "xyz"}, {:host, "localhost"}, {:port, 8108}, {:scheme, "http"}] - |> Enum.each(fn {key, val} -> Application.put_env(:ex_typesense, key, val) end) - - conn = %ExTypesense.Connection{ - host: "localhost", - api_key: "xyz", - port: 8108, - scheme: "http" - } - schema = %{ - name: "companies", + name: "doc_companies", fields: [ %{name: "company_name", type: "string"}, %{name: "company_id", type: "int32"}, @@ -26,14 +15,14 @@ defmodule DocumentTest do } document = %{ - collection_name: "companies", + collection_name: "doc_companies", company_name: "Test", company_id: 1001, country: "US" } multiple_documents = %{ - collection_name: "companies", + collection_name: "doc_companies", documents: [ %{ company_name: "Industrial Mills, Co.", @@ -48,49 +37,44 @@ defmodule DocumentTest do ] } - ExTypesense.create_collection(conn, schema) - ExTypesense.create_collection(conn, Person) + %{schema: schema, document: document, multiple_documents: multiple_documents} + end + + setup %{schema: schema} do + ExTypesense.create_collection(schema) + ExTypesense.create_collection(Person) on_exit(fn -> - ExTypesense.drop_collection(conn, schema.name) - ExTypesense.drop_collection(conn, Person) - # from doctest - ExTypesense.drop_collection(conn, "posts") - - # this is for deprecated function to set the creds - [:api_key, :host, :port, :scheme] - |> Enum.each(&Application.delete_env(:ex_typesense, &1)) + ExTypesense.drop_collection(schema.name) + ExTypesense.drop_collection(Person) end) - %{conn: conn, schema: schema, document: document, multiple_documents: multiple_documents} + :ok end - test "error: get unknown document", %{conn: conn, schema: schema} do + test "error: get unknown document", %{schema: schema} do unknown_id = 999 message = ~s(Could not find a document with id: #{unknown_id}) - assert {:error, message} === ExTypesense.get_document(conn, schema.name, unknown_id) + assert {:error, message} === ExTypesense.get_document(schema.name, unknown_id) end - test "success: index a document using a map then fetch if indexed", %{ - conn: conn, - document: document - } do + test "success: index a document using a map then fetch if indexed", %{document: document} do {:ok, %{"id" => id, "company_name" => company_name}} = - ExTypesense.create_document(conn, document) + ExTypesense.create_document(document) id = String.to_integer(id) - {:ok, result} = ExTypesense.get_document(conn, "companies", id) + {:ok, result} = ExTypesense.get_document("doc_companies", id) assert result["company_name"] === company_name end - test "success: adding unknown field", %{conn: conn, document: document} do + test "success: adding unknown field", %{document: document} do document = Map.put(document, :unknown_field, "unknown_value") - {:ok, collection} = ExTypesense.create_document(conn, document) + {:ok, collection} = ExTypesense.create_document(document) assert Map.has_key?(collection, "unknown_field") end - test "success: upsert to update a document", %{conn: conn, document: document} do - {:ok, %{"id" => id}} = ExTypesense.create_document(conn, document) + test "success: upsert to update a document", %{document: document} do + {:ok, %{"id" => id}} = ExTypesense.create_document(document) id = String.to_integer(id) company_name = "Stark Industries" @@ -99,25 +83,25 @@ defmodule DocumentTest do |> Map.put(:company_name, company_name) |> Map.put(:id, id) - {:ok, result} = ExTypesense.upsert_document(conn, updated_document) + {:ok, result} = ExTypesense.upsert_document(updated_document) assert company_name === result["company_name"] end - test "success: create document using upsert_document/2", %{conn: conn, document: document} do + test "success: create document using upsert_document/2", %{document: document} do document = Map.put(document, :id, "9999") - assert {:ok, %{"id" => "9999"}} = ExTypesense.upsert_document(conn, document) + assert {:ok, %{"id" => "9999"}} = ExTypesense.upsert_document(document) end - test "error: creates a document with a specific id twice", %{conn: conn, document: document} do + test "error: creates a document with a specific id twice", %{document: document} do document = Map.put(document, :id, "99") - assert {:ok, %{"id" => id}} = ExTypesense.create_document(conn, document) - assert {:error, message} = ExTypesense.create_document(conn, document) + assert {:ok, %{"id" => id}} = ExTypesense.create_document(document) + assert {:error, message} = ExTypesense.create_document(document) assert message === "A document with id #{id} already exists." end - test "success: update a document", %{conn: conn, document: document} do - {:ok, %{"id" => id}} = ExTypesense.create_document(conn, document) + test "success: update a document", %{document: document} do + {:ok, %{"id" => id}} = ExTypesense.create_document(document) company_name = "Stark Industries" updated_document = @@ -125,46 +109,39 @@ defmodule DocumentTest do |> Map.put(:company_name, company_name) |> Map.put(:id, id) - {:ok, result} = ExTypesense.update_document(conn, updated_document) + {:ok, result} = ExTypesense.update_document(updated_document) assert company_name === result["company_name"] end - test "success: (deprecate) deletes a document using map", %{conn: conn, document: document} do - {:ok, %{"id" => id}} = ExTypesense.create_document(conn, document) + test "success: deletes a document using map", %{document: document} do + {:ok, %{"id" => id}} = ExTypesense.create_document(document) assert {:ok, _} = ExTypesense.delete_document(document.collection_name, String.to_integer(id)) end - test "success: deletes a document by struct", %{conn: conn} do - person = %Person{id: 0, name: "Tar Zan", person_id: 0, country: "Brazil"} + test "success: deletes a document by struct" do + person = %Person{id: 0, name: "John Smith", person_id: 0, country: "Brazil"} - assert {:ok, %{"country" => "Brazil", "id" => "0", "name" => "Tar Zan", "person_id" => 0}} = - ExTypesense.create_document(conn, person) + assert {:ok, %{"country" => "Brazil", "id" => "0", "name" => "John Smith", "person_id" => 0}} = + ExTypesense.create_document(person) - assert {:ok, _} = ExTypesense.delete_document_by_struct(conn, person) + assert {:ok, _} = ExTypesense.delete_document(person) end - test "success: deletes a document by id", %{conn: conn, document: document} do - {:ok, %{"id" => id}} = ExTypesense.create_document(conn, document) + test "success: deletes a document by id", %{document: document} do + {:ok, %{"id" => id}} = ExTypesense.create_document(document) - assert {:ok, _} = - ExTypesense.delete_document_by_id( - conn, - document.collection_name, - String.to_integer(id) - ) + assert {:ok, _} = ExTypesense.delete_document(document.collection_name, String.to_integer(id)) end - test "success: index multiple documents", %{conn: conn, multiple_documents: multiple_documents} do - conn - |> ExTypesense.index_multiple_documents(multiple_documents) + test "success: index multiple documents", %{multiple_documents: multiple_documents} do + ExTypesense.index_multiple_documents(multiple_documents) |> Kernel.===({:ok, [%{"success" => true}, %{"success" => true}]}) |> assert() end test "success: update multiple documents", %{ - conn: conn, multiple_documents: multiple_documents, schema: schema } do @@ -173,8 +150,8 @@ defmodule DocumentTest do first_update = "first_update" second_update = "second_update" - {:ok, %{"id" => first_id}} = ExTypesense.create_document(conn, first) - {:ok, %{"id" => second_id}} = ExTypesense.create_document(conn, second) + {:ok, %{"id" => first_id}} = ExTypesense.create_document(first) + {:ok, %{"id" => second_id}} = ExTypesense.create_document(second) update_1 = first @@ -188,21 +165,19 @@ defmodule DocumentTest do multiple_documents = Map.put(multiple_documents, :documents, [update_1, update_2]) - conn - |> ExTypesense.update_multiple_documents(multiple_documents) + ExTypesense.update_multiple_documents(multiple_documents) |> Kernel.===({:ok, [%{"success" => true}, %{"success" => true}]}) |> assert() - {:ok, first} = ExTypesense.get_document(conn, schema.name, String.to_integer(first_id)) - {:ok, second} = ExTypesense.get_document(conn, schema.name, String.to_integer(second_id)) + {:ok, first} = ExTypesense.get_document(schema.name, String.to_integer(first_id)) + {:ok, second} = ExTypesense.get_document(schema.name, String.to_integer(second_id)) assert first["company_name"] === first_update assert second["company_name"] === second_update end - test "success: upsert multiple documents", %{conn: conn, multiple_documents: multiple_documents} do - conn - |> ExTypesense.upsert_multiple_documents(multiple_documents) + test "success: upsert multiple documents", %{multiple_documents: multiple_documents} do + ExTypesense.upsert_multiple_documents(multiple_documents) |> Kernel.===({:ok, [%{"success" => true}, %{"success" => true}]}) |> assert() end diff --git a/test/search_test.exs b/test/search_test.exs index 26ea741..a5577ef 100644 --- a/test/search_test.exs +++ b/test/search_test.exs @@ -1,18 +1,12 @@ defmodule SearchTest do - use ExUnit.Case, async: false + use ExUnit.Case, async: true import Ecto.Query, warn: false - alias ExTypesense.TestSchema.Person - setup_all do - conn = %ExTypesense.Connection{ - host: "localhost", - api_key: "xyz", - port: 8108, - scheme: "http" - } + alias ExTypesense.TestSchema.Catalog + setup_all do schema = %{ - name: "companies", + name: "search_companies", fields: [ %{name: "company_name", type: "string"}, %{name: "company_id", type: "int32"}, @@ -22,58 +16,60 @@ defmodule SearchTest do } document = %{ - collection_name: "companies", + collection_name: "search_companies", company_name: "Test", company_id: 1001, country: "US" } - person = %Person{ + catalog = %Catalog{ id: 1002, - name: "John Smith", - country: "UK", - person_id: 1002 + name: "Rubber Ducky", + description: "A tool by articulating a problem in spoken or written natural language.", + catalog_id: 1002 } - ExTypesense.create_collection(conn, schema) - ExTypesense.create_collection(conn, Person) + with %ExTypesense.Collection{} <- ExTypesense.create_collection(schema) do + {:ok, _} = ExTypesense.create_document(document) + end - {:ok, _} = ExTypesense.create_document(conn, document) - {:ok, _} = ExTypesense.create_document(conn, person) + with %ExTypesense.Collection{} <- ExTypesense.create_collection(Catalog) do + {:ok, _} = ExTypesense.create_document(catalog) + end on_exit(fn -> - ExTypesense.drop_collection(conn, schema.name) - ExTypesense.drop_collection(conn, Person) + ExTypesense.drop_collection(schema.name) + ExTypesense.drop_collection(Catalog) end) - %{conn: conn, schema: schema, document: document, person: person} + %{schema: schema, document: document, catalog: catalog} end - test "success: search with result", %{conn: conn, schema: schema} do + test "success: search with result", %{schema: schema} do params = %{ q: "test", query_by: "company_name" } - assert {:ok, _} = ExTypesense.search(conn, schema.name, params) + assert {:ok, _} = ExTypesense.search(schema.name, params) end - test "success: search with Ecto", %{conn: conn, person: person} do + test "success: search with Ecto", %{catalog: catalog} do params = %{ - q: "UK", - query_by: "country" + q: "duck", + query_by: "name" } - assert %Ecto.Query{} = Person |> where([p], p.id in ^[person.person_id]) - assert %Ecto.Query{} = ExTypesense.search(conn, Person, params) + assert %Ecto.Query{} = Catalog |> where([p], p.id in ^[catalog.catalog_id]) + assert %Ecto.Query{} = ExTypesense.search(Catalog, params) end - test "success: empty result", %{conn: conn, schema: schema} do + test "success: empty result", %{schema: schema} do params = %{ q: "unknown", query_by: "company_name" } - assert {:ok, _} = ExTypesense.search(conn, schema.name, params) + assert {:ok, %{"found" => 0, "hits" => []}} = ExTypesense.search(schema.name, params) end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..cfc20f1 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,15 @@ +Application.put_all_env( + ex_typesense: [ + api_key: "xyz", + host: "localhost", + port: 8108, + scheme: "http" + ] +) + ExUnit.start() + +ExUnit.after_suite(fn %{failures: 0, skipped: 0, excluded: 0} -> + [:api_key, :host, :port, :scheme] + |> Enum.each(&Application.delete_env(:ex_typesense, &1)) +end)