diff --git a/.dialyzerignore b/.dialyzerignore new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6ac400..3c251f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,10 +12,13 @@ jobs: lint: runs-on: ubuntu-latest + env: + MIX_ENV: test + strategy: matrix: elixir: [1.18.1] - otp: [27.2] + otp: [27.0] steps: - name: Checkout code @@ -60,3 +63,127 @@ jobs: - name: Run Credo run: mix credo --strict + + static-analisys: + runs-on: ubuntu-latest + + env: + MIX_ENV: test + + strategy: + matrix: + elixir: [1.18.1] + otp: [27.0] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + + - name: Cache Elixir deps + uses: actions/cache@v1 + id: deps-cache + with: + path: deps + key: ${{ runner.os }}-mix-${{ env.MIX_ENV }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + + - name: Cache Elixir _build + uses: actions/cache@v1 + id: build-cache + with: + path: _build + key: ${{ runner.os }}-build-${{ env.MIX_ENV }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + + - name: Install deps + if: steps.deps-cache.outputs.cache-hit != 'true' + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get --only ${{ env.MIX_ENV }} + + - name: Compile deps + if: steps.build-cache.outputs.cache-hit != 'true' + run: mix deps.compile --warnings-as-errors + + # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones + # Cache key based on Elixir & Erlang version (also useful when running in matrix) + - name: Restore PLT cache + uses: actions/cache/restore@v3 + id: plt_cache + with: + key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plt + restore-keys: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plt + path: priv/plts + + # Create PLTs if no cache was found + - name: Create PLTs + if: steps.plt_cache.outputs.cache-hit != 'true' + run: mix dialyzer --plt + + - name: Save PLT cache + uses: actions/cache/save@v3 + if: steps.plt_cache.outputs.cache-hit != 'true' + id: plt_cache_save + with: + key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-plt + path: priv/plts + + - name: Run dialyzer + run: mix dialyzer --format github + + test: + runs-on: ubuntu-latest + + env: + MIX_ENV: test + + strategy: + matrix: + elixir: [1.18.1] + otp: [27.0] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + + - name: Cache Elixir deps + uses: actions/cache@v1 + id: deps-cache + with: + path: deps + key: ${{ runner.os }}-mix-${{ env.MIX_ENV }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + + - name: Cache Elixir _build + uses: actions/cache@v1 + id: build-cache + with: + path: _build + key: ${{ runner.os }}-build-${{ env.MIX_ENV }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + + - name: Install deps + if: steps.deps-cache.outputs.cache-hit != 'true' + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get --only ${{ env.MIX_ENV }} + + - name: Compile deps + if: steps.build-cache.outputs.cache-hit != 'true' + run: mix deps.compile --warnings-as-errors + + - name: Clean build + run: mix clean + + - name: Run tests + run: mix test diff --git a/.wakatime-project b/.wakatime-project new file mode 100644 index 0000000..05e7963 --- /dev/null +++ b/.wakatime-project @@ -0,0 +1 @@ +postgrest-ex diff --git a/lib/supabase/postgrest.ex b/lib/supabase/postgrest.ex index a28b43b..66f8374 100644 --- a/lib/supabase/postgrest.ex +++ b/lib/supabase/postgrest.ex @@ -11,7 +11,7 @@ defmodule Supabase.PostgREST do alias Supabase.Client alias Supabase.Fetcher - alias Supabase.PostgREST.Builder + alias Supabase.Fetcher.Request alias Supabase.PostgREST.Error alias Supabase.PostgREST.FilterBuilder @@ -78,7 +78,8 @@ defmodule Supabase.PostgREST do @impl true def from(%Client{} = client, table) do client - |> Builder.new(relation: table) + |> Request.new() + |> Request.with_database_url(table) |> with_custom_media_type(:default) end @@ -94,8 +95,8 @@ defmodule Supabase.PostgREST do iex> Supabase.PostgREST.schema(builder, private) """ @impl true - def schema(%Builder{} = b, schema) when is_binary(schema) do - %{b | schema: schema} + def schema(%Request{} = b, schema) when is_binary(schema) do + put_in(b.client.db.schema, schema) end @doc """ @@ -112,10 +113,10 @@ defmodule Supabase.PostgREST do - [PostgREST resource represation docs](https://docs.postgrest.org/en/v12/references/api/resource_representation.html) """ @impl true - def with_custom_media_type(%Builder{} = b, media_type) + def with_custom_media_type(%Request{} = b, media_type) when is_atom(media_type) do header = @accept_headers[media_type] || @accept_headers[:default] - Builder.add_request_header(b, "accept", header) + Request.with_headers(b, %{"accept" => header}) end @doc """ @@ -131,26 +132,7 @@ defmodule Supabase.PostgREST do - Supabase query execution: https://supabase.com/docs/reference/javascript/performing-queries """ @impl true - def execute(%Builder{} = b), do: do_execute(b) - - @doc """ - Executes the query and returns the result as a JSON-encoded string. - - ## Parameters - - `builder`: The Builder or Builder instance to execute. - - ## Examples - iex> PostgREST.execute_string(builder) - - ## See also - - Supabase query execution and response handling: https://supabase.com/docs/reference/javascript/performing-queries - """ - @impl true - def execute_string(%Builder{} = b) do - with {:ok, body} <- do_execute(b) do - Jason.encode(body) - end - end + def execute(%Request{} = b), do: do_execute(b) @doc """ Executes the query and maps the resulting data to a specified schema struct, useful for casting the results to Elixir structs. @@ -166,88 +148,44 @@ defmodule Supabase.PostgREST do - Supabase query execution and schema casting: https://supabase.com/docs/reference/javascript/performing-queries """ @impl true - def execute_to(%Builder{} = b, schema) when is_atom(schema) do - with {:ok, body} <- do_execute(b) do - if is_list(body) do - {:ok, Enum.map(body, &struct(schema, &1))} - else - {:ok, struct(schema, body)} - end - end + def execute_to(%Request{} = b, schema) when is_atom(schema) do + alias Supabase.PostgREST.SchemaDecoder + + Request.with_body_decoder(b, SchemaDecoder, schema: schema) + |> do_execute() end @doc """ - Executes a query using the Finch HTTP client, formatting the request appropriately. Returns the HTTP request without executing it. + Builds a query using the Finch HTTP client, formatting the request appropriately. Returns the HTTP request without executing it. ## Parameters - `builder`: The Builder or Builder instance to execute. - - `schema`: Optional schema module to map the results. ## Examples - iex> PostgREST.execute_to_finch_request(builder, User) + iex> PostgREST.execute_to_finch_request(builder) ## See also - Supabase query execution: https://supabase.com/docs/reference/javascript/performing-queries """ @impl true - def execute_to_finch_request(%Builder{client: client} = b) do - headers = Fetcher.apply_client_headers(client, nil, Map.to_list(b.headers)) - query = URI.encode_query(b.params) - url = URI.new!(b.url) |> URI.append_query(query) + def execute_to_finch_request(%Request{} = b) do + query = URI.encode_query(b.query) + url = URI.parse(b.url) |> URI.append_query(query) - Supabase.Fetcher.new_connection(b.method, url, b.body, headers) + Finch.build(b.method, url, b.headers, b.body) end - defp do_execute(%Builder{client: client} = b) do - headers = Fetcher.apply_client_headers(client, nil, Map.to_list(b.headers)) - query = URI.encode_query(b.params) - url = URI.new!(b.url) |> URI.append_query(query) - request = request_fun_from_method(b.method) + defp do_execute(%Request{client: client} = b) do + schema = client.db.schema - url - |> request.(b.body, headers) - |> parse_response() - end - - defp request_fun_from_method(:get), do: &Supabase.Fetcher.get/3 - defp request_fun_from_method(:head), do: &Supabase.Fetcher.head/3 - defp request_fun_from_method(:post), do: &Supabase.Fetcher.post/3 - defp request_fun_from_method(:delete), do: &Supabase.Fetcher.delete/3 - defp request_fun_from_method(:patch), do: &Supabase.Fetcher.patch/3 - - defp parse_response({:error, reason}), do: {:error, reason} - - defp parse_response({:ok, %{status: _, body: ""}}) do - {:ok, nil} - end - - defp parse_response({:ok, %{status: status, body: raw, headers: headers}}) do - if json_content?(headers) do - with {:ok, body} <- Jason.decode(raw, keys: :atoms) do - cond do - error_resp?(status) -> {:error, Error.from_raw_body(body)} - success_resp?(status) -> {:ok, body} - end - end - else - {:ok, raw} - end - end - - defp json_content?(headers) when is_list(headers) do - headers - |> Enum.find_value(fn - {"content-type", type} -> type - _ -> false - end) - |> String.match?(~r/json/) - end - - defp error_resp?(status) do - Kernel.in(status, 400..599) - end + schema_header = + if b.method in [:get, :head], + do: %{"accept-profile" => schema}, + else: %{"content-profile" => schema} - defp success_resp?(status) do - Kernel.in(status, 200..399) + b + |> Request.with_error_parser(Error) + |> Request.with_headers(schema_header) + |> Fetcher.request() end end diff --git a/lib/supabase/postgrest/behaviour.ex b/lib/supabase/postgrest/behaviour.ex index a723b5b..b8b0bc1 100644 --- a/lib/supabase/postgrest/behaviour.ex +++ b/lib/supabase/postgrest/behaviour.ex @@ -2,22 +2,16 @@ defmodule Supabase.PostgREST.Behaviour do @moduledoc "Defines the interface for the main module Supabase.PostgREST" alias Supabase.Client - alias Supabase.PostgREST.Builder - alias Supabase.PostgREST.Error + alias Supabase.Fetcher.Request @type media_type :: :json | :csv | :openapi | :geojson | :pgrst_plan | :pgrst_object | :pgrst_array - @callback with_custom_media_type(builder, media_type) :: builder - when builder: Builder.t() | Builder.t() - @callback from(Client.t(), relation :: String.t()) :: Builder.t() - @callback schema(Builder.t(), schema :: String.t()) :: Builder.t() + @callback with_custom_media_type(Request.t(), media_type) :: Request.t() + @callback from(Client.t(), relation :: String.t()) :: Request.t() + @callback schema(Request.t(), schema :: String.t()) :: Request.t() - @callback execute(Builder.t() | Builder.t()) :: {:ok, term} | {:error, Error.t()} - @callback execute_string(Builder.t() | Builder.t()) :: - {:ok, binary} | {:error, Error.t() | atom} - @callback execute_to(Builder.t() | Builder.t(), atom) :: - {:ok, term} | {:error, Error.t() | atom} - @callback execute_to_finch_request(Builder.t() | Builder.t()) :: - Finch.Request.t() + @callback execute(Request.t()) :: Supabase.result(term) + @callback execute_to(Request.t(), module) :: Supabase.result(term) + @callback execute_to_finch_request(Request.t()) :: Finch.Request.t() end diff --git a/lib/supabase/postgrest/builder.ex b/lib/supabase/postgrest/builder.ex deleted file mode 100644 index a1b6fa4..0000000 --- a/lib/supabase/postgrest/builder.ex +++ /dev/null @@ -1,80 +0,0 @@ -defmodule Supabase.PostgREST.Builder do - @moduledoc """ - Defines a struct to centralize and accumulate data to - be sent to the PostgREST server, parsed. - """ - - alias Supabase.Client - - defstruct ~w[method body url headers params client schema]a - - @type t :: %__MODULE__{ - url: String.t(), - method: :get | :post | :put | :patch | :delete, - params: %{String.t() => String.t()}, - headers: %{String.t() => String.t()}, - body: map, - client: Supabase.Client.t(), - schema: String.t() - } - - defp get_version do - :supabase_postgrest - |> :application.get_key(:vsn) - |> elem(1) - |> List.to_string() - end - - @doc "Creates a new `#{__MODULE__}` instance" - def new(%Client{} = client, relation: relation) do - %__MODULE__{ - client: client, - schema: client.db.schema, - method: :get, - params: %{}, - url: - URI.parse(client.conn.base_url) - |> URI.append_path("/rest/v1") - |> URI.append_path("/" <> relation), - headers: %{ - "x-client-info" => "postgrest-ex/#{get_version()}", - "accept-profile" => client.db.schema, - "content-profile" => client.db.schema, - "content-type" => "application/json" - } - } - end - - @doc """ - Updates the key `#{__MODULE__}.params` and adds a new query params - """ - def add_query_param(%__MODULE__{} = b, _, nil), do: b - - def add_query_param(%__MODULE__{} = b, key, value) do - %{b | params: Map.put(b.params, key, value)} - end - - @doc """ - Updates the key `#{__MODULE__}.headers` and adds a new request header - """ - def add_request_header(%__MODULE__{} = b, _, nil), do: b - - def add_request_header(%__MODULE__{} = b, key, value) do - %{b | headers: Map.put(b.headers, key, value)} - end - - @doc "Removes a request header" - def del_request_header(%__MODULE__{} = b, key) do - %{b | headers: Map.delete(b.headers, key)} - end - - @doc "Changes the HTTP method that'll be used to execute the query" - def change_method(%__MODULE__{} = q, method) do - %{q | method: method} - end - - @doc "Changes the request body that will be sent to the PostgREST server" - def change_body(%__MODULE__{} = q, body) do - %{q | body: body} - end -end diff --git a/lib/supabase/postgrest/error.ex b/lib/supabase/postgrest/error.ex index 730fac6..b673194 100644 --- a/lib/supabase/postgrest/error.ex +++ b/lib/supabase/postgrest/error.ex @@ -1,22 +1,27 @@ defmodule Supabase.PostgREST.Error do - @moduledoc false + @moduledoc "Custom error parser for PostgREST" - @derive Jason.Encoder - defstruct [:hint, :details, :code, :message] + alias Supabase.Fetcher.Request + alias Supabase.Fetcher.Response - @type t :: %__MODULE__{ - hint: String.t() | nil, - details: String.t() | nil, - code: String.t() | nil, - message: String.t() - } + @behaviour Supabase.Error - def from_raw_body(%{message: message} = err) do - %__MODULE__{ - message: message, - hint: err[:hint], - details: err[:details], - code: err[:code] - } + @impl true + def from(%Response{body: body}, %Request{} = ctx) do + metadata = Supabase.Error.make_default_http_metadata(ctx) + + metadata = + Map.merge(metadata, %{ + database_error_hint: body[:hint], + database_error_code: body[:code], + database_error_detail: body[:details] + }) + + Supabase.Error.new( + code: :database_error, + message: body[:message], + service: :database, + metadata: metadata + ) end end diff --git a/lib/supabase/postgrest/filter_builder.ex b/lib/supabase/postgrest/filter_builder.ex index 278a87c..4ebd315 100644 --- a/lib/supabase/postgrest/filter_builder.ex +++ b/lib/supabase/postgrest/filter_builder.ex @@ -5,7 +5,7 @@ defmodule Supabase.PostgREST.FilterBuilder do This module allows you to define conditions that restrict the data returned by the query. Filters can include equality checks, range conditions, pattern matching, and more. These operations translate into query parameters that control the subset of data fetched or manipulated. """ - alias Supabase.PostgREST.Builder + alias Supabase.Fetcher.Request @behaviour Supabase.PostgREST.FilterBuilder.Behaviour @@ -64,10 +64,10 @@ defmodule Supabase.PostgREST.FilterBuilder do iex> PostgREST.filter(builder, "id", "not", 12) """ @impl true - def filter(%Builder{} = b, column, op, value) + def filter(%Request{} = b, column, op, value) when is_binary(column) and is_filter_op(op) do condition = process_condition({op, column, value}) - Builder.add_query_param(b, column, condition) + Request.with_query(b, %{column => condition}) end @doc """ @@ -81,7 +81,7 @@ defmodule Supabase.PostgREST.FilterBuilder do You can optionally use the custom DSL to represent conditions instead of a raw string, look at the examples and the `Supabase.PostgREST.FilterBuilder.Behaviour.condition()` type spec. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `columns`: A list of conditions that should all be met. - `opts`: Optional parameters, which can include specifying a foreign table. @@ -101,21 +101,21 @@ defmodule Supabase.PostgREST.FilterBuilder do @impl true def all_of(builder, patterns, opts \\ []) - def all_of(%Builder{} = b, patterns, opts) when is_binary(patterns) do + def all_of(%Request{} = b, patterns, opts) when is_binary(patterns) do if foreign = Keyword.get(opts, :foreign_table) do - Builder.add_query_param(b, "#{foreign}.and", "(#{patterns})") + Request.with_query(b, %{"#{foreign}.and" => "(#{patterns})"}) else - Builder.add_query_param(b, "and", "(#{patterns})") + Request.with_query(b, %{"and" => "(#{patterns})"}) end end - def all_of(%Builder{} = b, patterns, opts) when is_list(patterns) do + def all_of(%Request{} = b, patterns, opts) when is_list(patterns) do filters = Enum.map_join(patterns, ",", &process_condition/1) if foreign = Keyword.get(opts, :foreign_table) do - Builder.add_query_param(b, "#{foreign}.and", "(#{filters})") + Request.with_query(b, %{"#{foreign}.and" => "(#{filters})"}) else - Builder.add_query_param(b, "and", "(#{filters})") + Request.with_query(b, %{"and" => "(#{filters})"}) end end @@ -130,7 +130,7 @@ defmodule Supabase.PostgREST.FilterBuilder do You can optionally use the custom DSL to represent conditions instead of a raw string, look at the examples and the `Supabase.PostgREST.FilterBuilder.Behaviour.condition()` type spec. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `columns`: A list of conditions where at least one should be met. - `opts`: Optional parameters, which can include specifying a foreign table. @@ -150,21 +150,21 @@ defmodule Supabase.PostgREST.FilterBuilder do @impl true def any_of(builder, patterns, opts \\ []) - def any_of(%Builder{} = b, patterns, opts) when is_binary(patterns) do + def any_of(%Request{} = b, patterns, opts) when is_binary(patterns) do if foreign = Keyword.get(opts, :foreign_table) do - Builder.add_query_param(b, "#{foreign}.or", "(#{patterns})") + Request.with_query(b, %{"#{foreign}.or" => "(#{patterns})"}) else - Builder.add_query_param(b, "or", "(#{patterns})") + Request.with_query(b, %{"or" => "(#{patterns})"}) end end - def any_of(%Builder{} = b, patterns, opts) when is_list(patterns) do + def any_of(%Request{} = b, patterns, opts) when is_list(patterns) do filters = Enum.map_join(patterns, ",", &process_condition/1) if foreign = Keyword.get(opts, :foreign_table) do - Builder.add_query_param(b, "#{foreign}.or", "(#{filters})") + Request.with_query(b, %{"#{foreign}.or" => "(#{filters})"}) else - Builder.add_query_param(b, "or", "(#{filters})") + Request.with_query(b, %{"or" => "(#{filters})"}) end end @@ -176,7 +176,7 @@ defmodule Supabase.PostgREST.FilterBuilder do to make sure they are properly sanitized. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the negation. - `op`: The operator used in the condition (e.g., "eq", "gt"). - `value`: The value to compare against, must implement the `String.Chars` protocol @@ -188,9 +188,9 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase negation filters: https://supabase.com/docs/reference/javascript/using-filters#negation """ @impl true - def negate(%Builder{} = b, column, op, value) + def negate(%Request{} = b, column, op, value) when is_binary(column) and is_filter_op(op) do - Builder.add_query_param(b, column, "not.#{op}.#{value}") + Request.with_query(b, %{column => "not.#{op}.#{value}"}) end alias Supabase.PostgREST.FilterBuilder.Behaviour, as: Interface @@ -246,9 +246,9 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase ordering results: https://supabase.com/docs/reference/javascript/using-filters#match """ @impl true - def match(%Builder{} = b, %{} = query) do + def match(%Request{} = b, %{} = query) do for {k, v} <- Map.to_list(query), reduce: b do - b -> Builder.add_query_param(b, k, "eq.#{v}") + b -> Request.with_query(b, %{k => "eq.#{v}"}) end end @@ -258,7 +258,7 @@ defmodule Supabase.PostgREST.FilterBuilder do To check if the value of `column` is NULL, you should use `.is()` instead. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The value the column must equal, must implement `String.Chars` protocol @@ -269,15 +269,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase equality filters: https://supabase.com/docs/reference/javascript/using-filters#equality """ @impl true - def eq(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "eq.#{value}") + def eq(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "eq.#{value}"}) end @doc """ Match only rows where `column` is not equal to `value`. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The value that the column must not equal, must implement `String.Chars` protocol @@ -288,15 +288,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase not equal filter: https://supabase.com/docs/reference/javascript/using-filters#not-equal """ @impl true - def neq(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "neq.#{value}") + def neq(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "neq.#{value}"}) end @doc """ Adds a 'greater than' filter to the query, specifying that the column's value must be greater than the specified value. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The value that the column must be greater than, must implement the `String.Chars` protocol @@ -307,15 +307,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase greater than filter: https://supabase.com/docs/reference/javascript/using-filters#greater-than """ @impl true - def gt(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "gt.#{value}") + def gt(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "gt.#{value}"}) end @doc """ Adds a 'greater than or equal to' filter to the query, specifying that the column's value must be greater than or equal to the specified value. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The value that the column must be greater than or equal to, must implement the `String.Chars` protocol @@ -326,15 +326,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase greater than or equal filter: https://supabase.com/docs/reference/javascript/using-filters#greater-than-or-equal """ @impl true - def gte(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "gte.#{value}") + def gte(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "gte.#{value}"}) end @doc """ Adds a 'less than' filter to the query, specifying that the column's value must be less than the specified value. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The value that the column must be less than, must implement the `String.Chars` protocol @@ -345,15 +345,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase less than filter: https://supabase.com/docs/reference/javascript/using-filters#less-than """ @impl true - def lt(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "lt.#{value}") + def lt(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "lt.#{value}"}) end @doc """ Adds a 'less than or equal to' filter to the query, specifying that the column's value must be less than or equal to the specified value. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The value that the column must be less than or equal to, must implement the `String.Chars` protocol @@ -364,15 +364,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase less than or equal filter: https://supabase.com/docs/reference/javascript/using-filters#less-than-or-equal """ @impl true - def lte(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "lte.#{value}") + def lte(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "lte.#{value}"}) end @doc """ Adds a 'like' filter to the query, allowing for simple pattern matching (SQL LIKE). ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The pattern to match against the column's value, must implement the `String.Chars` protocol @@ -383,8 +383,8 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase like filter: https://supabase.com/docs/reference/javascript/using-filters#like """ @impl true - def like(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "like.#{value}") + def like(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "like.#{value}"}) end @doc """ @@ -398,9 +398,9 @@ defmodule Supabase.PostgREST.FilterBuilder do iex> PostgREST.like_all_of(builder, "name", ["jhon", "maria", "joão"]) """ @impl true - def like_all_of(%Builder{} = b, column, values) + def like_all_of(%Request{} = b, column, values) when is_binary(column) and is_list(values) do - Builder.add_query_param(b, column, "like(all).{#{Enum.join(values, ",")}}") + Request.with_query(b, %{column => "like(all).{#{Enum.join(values, ",")}}"}) end @doc """ @@ -414,16 +414,16 @@ defmodule Supabase.PostgREST.FilterBuilder do iex> PostgREST.like_any_of(builder, "name", ["jhon", "maria", "joão"]) """ @impl true - def like_any_of(%Builder{} = b, column, values) + def like_any_of(%Request{} = b, column, values) when is_binary(column) and is_list(values) do - Builder.add_query_param(b, column, "like(any).{#{Enum.join(values, ",")}}") + Request.with_query(b, %{column => "like(any).{#{Enum.join(values, ",")}}"}) end @doc """ Adds an 'ilike' filter to the query, allowing for case-insensitive pattern matching (SQL ILIKE). ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The pattern to match against the column's value, ignoring case, must implement the `String.Chars` protocol @@ -434,8 +434,8 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase ilike filter: https://supabase.com/docs/reference/javascript/using-filters#ilike """ @impl true - def ilike(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "ilike.#{value}") + def ilike(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "ilike.#{value}"}) end @doc """ @@ -449,9 +449,9 @@ defmodule Supabase.PostgREST.FilterBuilder do iex> PostgREST.ilike_all_of(builder, "name", ["jhon", "maria", "joão"]) """ @impl true - def ilike_all_of(%Builder{} = f, column, values) + def ilike_all_of(%Request{} = f, column, values) when is_binary(column) and is_list(values) do - Builder.add_query_param(f, column, "ilike(all).{#{Enum.join(values, ",")}}") + Request.with_query(f, %{column => "ilike(all).{#{Enum.join(values, ",")}}"}) end @doc """ @@ -465,9 +465,9 @@ defmodule Supabase.PostgREST.FilterBuilder do iex> PostgREST.ilike_any_of(builder, "name", ["jhon", "maria", "joão"]) """ @impl true - def ilike_any_of(%Builder{} = f, column, values) + def ilike_any_of(%Request{} = f, column, values) when is_binary(column) and is_list(values) do - Builder.add_query_param(f, column, "ilike(any).{#{Enum.join(values, ",")}}") + Request.with_query(f, %{column => "ilike(any).{#{Enum.join(values, ",")}}"}) end @doc """ @@ -480,7 +480,7 @@ defmodule Supabase.PostgREST.FilterBuilder do will behave the same way as `.eq()`. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The value to check the column against (typically nil or a boolean). @@ -491,19 +491,19 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase is filter: https://supabase.com/docs/reference/javascript/using-filters#is """ @impl true - def is(%Builder{} = f, column, nil) when is_binary(column) do - Builder.add_query_param(f, column, "is.null") + def is(%Request{} = f, column, nil) when is_binary(column) do + Request.with_query(f, %{column => "is.null"}) end - def is(%Builder{} = f, column, value) when is_binary(column) and is_boolean(value) do - Builder.add_query_param(f, column, "is.#{value}") + def is(%Request{} = f, column, value) when is_binary(column) and is_boolean(value) do + Request.with_query(f, %{column => "is.#{value}"}) end @doc """ Filters the query by checking if the column's value is within an array of specified values. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to filter. - `values`: A list of acceptable values for the column, all elements must implement the `String.Chars` protocol @@ -514,21 +514,21 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase "IN" filters: https://supabase.com/docs/reference/javascript/using-filters#in """ @impl true - def within(%Builder{} = f, column, values) + def within(%Request{} = f, column, values) when is_binary(column) and is_list(values) do values = Enum.map_join(values, ",", fn v -> if String.match?(v, ~r/[,()]/), do: "#{to_string(v)}", else: to_string(v) end) - Builder.add_query_param(f, column, "in.(#{values})") + Request.with_query(f, %{column => "in.(#{values})"}) end @doc """ Only relevant for jsonb, array, and range columns. Match only rows where `column` contains every element appearing in `value`. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `values`: It can be a single value (string), a list of values to filter or a map (aka json) @@ -539,30 +539,30 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase contains filter: https://supabase.com/docs/reference/javascript/using-filters#contains """ @impl true - def contains(%Builder{} = b, column, value) + def contains(%Request{} = b, column, value) when is_binary(column) and is_binary(value) do do_contains(b, column, value) end - def contains(%Builder{} = b, column, values) + def contains(%Request{} = b, column, values) when is_binary(column) and is_list(values) do do_contains(b, column, "{#{Enum.join(values, ",")}}") end - def contains(%Builder{} = b, column, values) + def contains(%Request{} = b, column, values) when is_binary(column) and is_map(values) do do_contains(b, column, Jason.encode!(values)) end - defp do_contains(%Builder{} = b, column, value) do - Builder.add_query_param(b, column, "cs.#{value}") + defp do_contains(%Request{} = b, column, value) do + Request.with_query(b, %{column => "cs.#{value}"}) end @doc """ Only relevant for jsonb, array, and range columns. Match only rows where every element appearing in `column` is contained by `value`. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `values`: It can be a single value (string), a list of values to filter or a map (aka json) @@ -573,30 +573,30 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase contained by filter: https://supabase.com/docs/reference/javascript/using-filters#contained-by """ @impl true - def contained_by(%Builder{} = b, column, value) + def contained_by(%Request{} = b, column, value) when is_binary(column) and is_binary(value) do do_contained_by(b, column, value) end - def contained_by(%Builder{} = b, column, values) + def contained_by(%Request{} = b, column, values) when is_binary(column) and is_list(values) do do_contained_by(b, column, "{#{Enum.join(values, ",")}}") end - def contained_by(%Builder{} = b, column, values) + def contained_by(%Request{} = b, column, values) when is_binary(column) and is_map(values) do do_contained_by(b, column, Jason.encode!(values)) end - defp do_contained_by(%Builder{} = b, column, value) do - Builder.add_query_param(b, column, "cd.#{value}") + defp do_contained_by(%Request{} = b, column, value) do + Request.with_query(b, %{column => "cd.#{value}"}) end @doc """ Only relevant for range columns. Match only rows where every element in `column` is less than any element in `range`. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The upper bound value of the range, must implement the `String.Chars` protocol @@ -607,15 +607,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase range filters: https://supabase.com/docs/reference/javascript/using-filters#range """ @impl true - def range_lt(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "sl.#{value}") + def range_lt(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "sl.#{value}"}) end @doc """ Only relevant for range columns. Match only rows where every element in `column` is greater than any element in `range`. ## Parameters - - `builder`: The Builder instance. + - `builder`: The Request instance. - `column`: The column to apply the filter. - `value`: The lower bound value of the range, must implement the `String.Chars` protocol @@ -626,15 +626,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - More on range filters at Supabase: https://supabase.com/docs/reference/javascript/using-filters#range """ @impl true - def range_gt(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "sr.#{value}") + def range_gt(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "sr.#{value}"}) end @doc """ Only relevant for range columns. Match only rows where every element in `column` is either contained in `range` or greater than any element in `range`. ## Parameters - - `builder`: The Builder instance. + - `builder`: The Request instance. - `column`: The column to apply the filter. - `value`: The starting value of the range, must implement the `String.Chars` protocol @@ -645,15 +645,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase documentation on range filters: https://supabase.com/docs/reference/javascript/using-filters#range """ @impl true - def range_gte(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "nxl.#{value}") + def range_gte(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "nxl.#{value}"}) end @doc """ Only relevant for range columns. Match only rows where every element in `column` is either contained in `range` or less than any element in `range`. ## Parameters - - `builder`: The Builder instance. + - `builder`: The Request instance. - `column`: The column to apply the filter. - `value`: The ending value of the range, must implement the `String.Chars` protocol @@ -664,15 +664,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase guide on using range filters: https://supabase.com/docs/reference/javascript/using-filters#range """ @impl true - def range_lte(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "nxr.#{value}") + def range_lte(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "nxr.#{value}"}) end @doc """ Only relevant for range columns. Match only rows where `column` is mutually exclusive to `range` and there can be no element between the two ranges. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `value`: The adjacent range value, must implement the `String.Chars` protocol @@ -683,15 +683,15 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase adjacent range filters: https://supabase.com/docs/reference/javascript/using-filters#adjacent """ @impl true - def range_adjacent(%Builder{} = f, column, value) when is_binary(column) do - Builder.add_query_param(f, column, "adj.#{value}") + def range_adjacent(%Request{} = f, column, value) when is_binary(column) do + Request.with_query(f, %{column => "adj.#{value}"}) end @doc """ Only relevant for array and range columns. Match only rows where `column` and `value` have an element in common. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to apply the filter. - `values`: The array of values that must overlap with the column's value, all elements must implement the `String.Chars` protocol @@ -702,23 +702,23 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase overlaps filter: https://supabase.com/docs/reference/javascript/using-filters#overlaps """ @impl true - def overlaps(%Builder{} = b, column, value) + def overlaps(%Request{} = b, column, value) when is_binary(column) and is_binary(value) do - Builder.add_query_param(b, column, "ov.#{value}") + Request.with_query(b, %{column => "ov.#{value}"}) end - def overlaps(%Builder{} = b, column, values) + def overlaps(%Request{} = b, column, values) when is_binary(column) and is_list(values) do values |> Enum.join(",") - |> then(&Builder.add_query_param(b, column, "ov.{#{&1}}")) + |> then(&Request.with_query(b, %{column => "ov.{#{&1}}"})) end @doc """ Only relevant for text and tsvector columns. Match only rows where `column` matches the query string in `query`. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column to search. - `query`: The text query for the search. - `opts`: Options for the search, such as type of search (`:plain`, `:phrase`, or `:websearch`) and configuration. @@ -730,11 +730,11 @@ defmodule Supabase.PostgREST.FilterBuilder do - Supabase full-text search capabilities: https://supabase.com/docs/reference/javascript/using-filters#full-text-search """ @impl true - def text_search(%Builder{} = f, column, query, opts \\ []) when is_binary(column) do + def text_search(%Request{} = f, column, query, opts \\ []) when is_binary(column) do type = search_type_to_code(Keyword.get(opts, :type)) config = if config = Keyword.get(opts, :config), do: "(#{config})", else: "" - Builder.add_query_param(f, column, "#{type}fts#{config}.#{query}") + Request.with_query(f, %{column => "#{type}fts#{config}.#{query}"}) end defp search_type_to_code(:plain), do: "pl" diff --git a/lib/supabase/postgrest/filter_builder/behaviour.ex b/lib/supabase/postgrest/filter_builder/behaviour.ex index 2c9517f..005ae80 100644 --- a/lib/supabase/postgrest/filter_builder/behaviour.ex +++ b/lib/supabase/postgrest/filter_builder/behaviour.ex @@ -1,7 +1,7 @@ defmodule Supabase.PostgREST.FilterBuilder.Behaviour do @moduledoc "Defines the interface for the FilterBuilder module" - alias Supabase.PostgREST.Builder + alias Supabase.Fetcher.Request @type operator :: :eq @@ -45,42 +45,42 @@ defmodule Supabase.PostgREST.FilterBuilder.Behaviour do list({:any | :all, boolean})} @type text_search_options :: [type: :plain | :phrase | :websearch] - @callback filter(Builder.t(), column :: String.t(), operator, String.Chars.t()) :: Builder.t() - @callback all_of(Builder.t(), list(condition)) :: Builder.t() - @callback all_of(Builder.t(), list(condition), foreign_table: String.t()) :: Builder.t() - @callback any_of(Builder.t(), list(condition)) :: Builder.t() - @callback any_of(Builder.t(), list(condition), foreign_table: String.t()) :: Builder.t() - @callback negate(Builder.t(), column :: String.t(), operator, String.Chars.t()) :: Builder.t() - @callback match(Builder.t(), query :: matcher) :: Builder.t() + @callback filter(Request.t(), column :: String.t(), operator, String.Chars.t()) :: Request.t() + @callback all_of(Request.t(), list(condition)) :: Request.t() + @callback all_of(Request.t(), list(condition), foreign_table: String.t()) :: Request.t() + @callback any_of(Request.t(), list(condition)) :: Request.t() + @callback any_of(Request.t(), list(condition), foreign_table: String.t()) :: Request.t() + @callback negate(Request.t(), column :: String.t(), operator, String.Chars.t()) :: Request.t() + @callback match(Request.t(), query :: matcher) :: Request.t() when matcher: %{String.t() => String.Chars.t()} - @callback eq(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback neq(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback gt(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback gte(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback lt(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback lte(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback like(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback like_all_of(Builder.t(), column :: String.t(), list(String.Chars.t())) :: Builder.t() - @callback like_any_of(Builder.t(), column :: String.t(), list(String.Chars.t())) :: Builder.t() - @callback ilike(Builder.t(), column :: String.t(), term) :: Builder.t() - @callback ilike_all_of(Builder.t(), column :: String.t(), list(String.Chars.t())) :: Builder.t() - @callback ilike_any_of(Builder.t(), column :: String.t(), list(String.Chars.t())) :: Builder.t() - @callback is(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback within(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback contains(Builder.t(), column :: String.t(), list(String.Chars.t())) :: Builder.t() - @callback contained_by(Builder.t(), column :: String.t(), list(String.Chars.t())) :: Builder.t() - @callback range_lt(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback range_gt(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback range_gte(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback range_lte(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback range_adjacent(Builder.t(), column :: String.t(), String.Chars.t()) :: Builder.t() - @callback overlaps(Builder.t(), column :: String.t(), list(String.Chars.t())) :: Builder.t() - @callback text_search(Builder.t(), column :: String.t(), query :: String.t()) :: - Builder.t() + @callback eq(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback neq(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback gt(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback gte(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback lt(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback lte(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback like(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback like_all_of(Request.t(), column :: String.t(), list(String.Chars.t())) :: Request.t() + @callback like_any_of(Request.t(), column :: String.t(), list(String.Chars.t())) :: Request.t() + @callback ilike(Request.t(), column :: String.t(), term) :: Request.t() + @callback ilike_all_of(Request.t(), column :: String.t(), list(String.Chars.t())) :: Request.t() + @callback ilike_any_of(Request.t(), column :: String.t(), list(String.Chars.t())) :: Request.t() + @callback is(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback within(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback contains(Request.t(), column :: String.t(), list(String.Chars.t())) :: Request.t() + @callback contained_by(Request.t(), column :: String.t(), list(String.Chars.t())) :: Request.t() + @callback range_lt(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback range_gt(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback range_gte(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback range_lte(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback range_adjacent(Request.t(), column :: String.t(), String.Chars.t()) :: Request.t() + @callback overlaps(Request.t(), column :: String.t(), list(String.Chars.t())) :: Request.t() + @callback text_search(Request.t(), column :: String.t(), query :: String.t()) :: + Request.t() @callback text_search( - Builder.t(), + Request.t(), column :: String.t(), query :: String.t(), text_search_options - ) :: Builder.t() + ) :: Request.t() end diff --git a/lib/supabase/postgrest/query_builder.ex b/lib/supabase/postgrest/query_builder.ex index 5c5c4d9..78838c5 100644 --- a/lib/supabase/postgrest/query_builder.ex +++ b/lib/supabase/postgrest/query_builder.ex @@ -5,7 +5,7 @@ defmodule Supabase.PostgREST.QueryBuilder do This module includes functionality for selecting fields, inserting new records, updating existing ones, and deleting records from a specified table. These operations define the high-level intent of the query, such as whether it retrieves, modifies, or removes data. """ - alias Supabase.PostgREST.Builder + alias Supabase.Fetcher.Request @behaviour Supabase.PostgREST.QueryBuilder.Behaviour @@ -18,7 +18,7 @@ defmodule Supabase.PostgREST.QueryBuilder do response you need to pass `returning: true`. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `columns`: A list of column names to fetch or '*' for all columns. - `opts`: Options such as `:count` and `:returning`. @@ -31,29 +31,29 @@ defmodule Supabase.PostgREST.QueryBuilder do @impl true def select(builder, columns, opts \\ []) - def select(%Builder{} = b, "*", opts) do + def select(%Request{} = b, "*", opts) do do_select(b, "*", opts) end - def select(%Builder{} = b, columns, opts) + def select(%Request{} = b, columns, opts) when is_list(columns) do do_select(b, Enum.join(columns, ","), opts) end - @spec do_select(Builder.t(), String.t(), keyword) :: Builder.t() - defp do_select(%Builder{} = b, columns, opts) do + @spec do_select(Request.t(), String.t(), keyword) :: Request.t() + defp do_select(%Request{} = b, columns, opts) do count = Keyword.get(opts, :count, :exact) returning = Keyword.get(opts, :returning, false) b - |> Builder.change_method(:get) - |> Builder.add_query_param("select", columns) - |> Builder.add_request_header("prefer", "count=#{count}") + |> Request.with_method(:get) + |> Request.with_query(%{"select" => columns}) + |> Request.with_headers(%{"prefer" => "count=#{count}"}) |> then(fn builder -> if returning do - Builder.change_method(builder, :get) + Request.with_method(builder, :get) else - Builder.change_method(builder, :head) + Request.with_method(builder, :head) end end) end @@ -63,7 +63,7 @@ defmodule Supabase.PostgREST.QueryBuilder do result should be returned. ## Parameters - - `builder`: The Builder to use. + - `builder`: The `Supabase.Fetcher.Request` to use. - `data`: The data to be inserted, typically a map or a list of maps. - `opts`: Options like `:on_conflict`, `:returning`, and `:count`. @@ -74,7 +74,7 @@ defmodule Supabase.PostgREST.QueryBuilder do - Supabase documentation on inserts: https://supabase.com/docs/reference/javascript/insert """ @impl true - def insert(%Builder{} = b, data, opts \\ []) when is_map(data) do + def insert(%Request{} = b, data, opts \\ []) when is_map(data) do on_conflict = Keyword.get(opts, :on_conflict) on_conflict = if on_conflict, do: "on_conflict=#{on_conflict}" upsert = if on_conflict, do: "resolution=merge-duplicates" @@ -84,17 +84,17 @@ defmodule Supabase.PostgREST.QueryBuilder do prefer = Enum.join(Enum.reject(prefer, &is_nil/1), ",") b - |> Builder.change_method(:post) - |> Builder.add_request_header("prefer", prefer) - |> Builder.add_query_param("on_conflict", on_conflict) - |> Builder.change_body(data) + |> Request.with_method(:post) + |> Request.with_headers(%{"prefer" => prefer}) + |> Request.with_query(%{"on_conflict" => on_conflict}) + |> Request.with_body(data) end @doc """ Upserts data into a table, allowing for conflict resolution and specifying return options. ## Parameters - - `builder`: The Builder to use. + - `builder`: The `Supabase.Fetcher.Request` to use. - `data`: The data to upsert, typically a map or a list of maps. - `opts`: Options like `:on_conflict`, `:returning`, and `:count`. @@ -105,7 +105,7 @@ defmodule Supabase.PostgREST.QueryBuilder do - Supabase documentation on upserts: https://supabase.com/docs/reference/javascript/upsert """ @impl true - def upsert(%Builder{} = b, data, opts \\ []) when is_map(data) do + def upsert(%Request{} = b, data, opts \\ []) when is_map(data) do on_conflict = Keyword.get(opts, :on_conflict) returning = Keyword.get(opts, :returning, :representation) count = Keyword.get(opts, :count, :exact) @@ -114,17 +114,17 @@ defmodule Supabase.PostgREST.QueryBuilder do Enum.join(["resolution=merge-duplicates", "return=#{returning}", "count=#{count}"], ",") b - |> Builder.change_method(:post) - |> Builder.add_request_header("prefer", prefer) - |> Builder.add_query_param("on_conflict", on_conflict) - |> Builder.change_body(data) + |> Request.with_method(:post) + |> Request.with_headers(%{"prefer" => prefer}) + |> Request.with_query(%{"on_conflict" => on_conflict}) + |> Request.with_body(data) end @doc """ - Deletes records from a table based on the conditions specified in the Builder. + Deletes records from a table based on the conditions specified. ## Parameters - - `builder`: The Builder to use. + - `builder`: The `Supabase.Fetcher.Request` to use. - `opts`: Options such as `:returning` and `:count`. ## Examples @@ -134,21 +134,21 @@ defmodule Supabase.PostgREST.QueryBuilder do - Supabase documentation on deletes: https://supabase.com/docs/reference/javascript/delete """ @impl true - def delete(%Builder{} = b, opts \\ []) do + def delete(%Request{} = b, opts \\ []) do returning = Keyword.get(opts, :returning, :representation) count = Keyword.get(opts, :count, :exact) prefer = Enum.join(["return=#{returning}", "count=#{count}"], ",") b - |> Builder.change_method(:delete) - |> Builder.add_request_header("prefer", prefer) + |> Request.with_method(:delete) + |> Request.with_headers(%{"prefer" => prefer}) end @doc """ Updates existing records in the database. Allows specifying return options and how the update is counted. ## Parameters - - `builder`: The Builder to use. + - `builder`: The `Supabase.Fetcher.Request` to use. - `data`: The new data for the update, typically a map or list of maps. - `opts`: Options such as `:returning` and `:count`. @@ -159,14 +159,14 @@ defmodule Supabase.PostgREST.QueryBuilder do - Supabase documentation on updates: https://supabase.com/docs/reference/javascript/update """ @impl true - def update(%Builder{} = b, data, opts \\ []) when is_map(data) do + def update(%Request{} = b, data, opts \\ []) when is_map(data) do returning = Keyword.get(opts, :returning, :representation) count = Keyword.get(opts, :count, :exact) prefer = Enum.join(["return=#{returning}", "count=#{count}"], ",") b - |> Builder.change_method(:patch) - |> Builder.add_request_header("prefer", prefer) - |> Builder.change_body(data) + |> Request.with_method(:patch) + |> Request.with_headers(%{"prefer" => prefer}) + |> Request.with_body(data) end end diff --git a/lib/supabase/postgrest/query_builder/behaviour.ex b/lib/supabase/postgrest/query_builder/behaviour.ex index b17df03..c60b772 100644 --- a/lib/supabase/postgrest/query_builder/behaviour.ex +++ b/lib/supabase/postgrest/query_builder/behaviour.ex @@ -1,19 +1,19 @@ defmodule Supabase.PostgREST.QueryBuilder.Behaviour do @moduledoc "Defines the interface for the QueryBuilder module" - alias Supabase.PostgREST.Builder + alias Supabase.Fetcher.Request @type options :: [count: :exact, returning: boolean | :representation] @type insert_options :: [count: :exact, returning: boolean | :representation, on_conflict: any] - @callback select(Builder.t(), list(String.t()) | String.t()) :: Builder.t() - @callback select(Builder.t(), list(String.t()) | String.t(), options) :: Builder.t() - @callback insert(Builder.t(), map) :: Builder.t() - @callback insert(Builder.t(), map, insert_options) :: Builder.t() - @callback upsert(Builder.t(), map) :: Builder.t() - @callback upsert(Builder.t(), map, insert_options) :: Builder.t() - @callback delete(Builder.t()) :: Builder.t() - @callback delete(Builder.t(), options) :: Builder.t() - @callback update(Builder.t(), map) :: Builder.t() - @callback update(Builder.t(), map, options) :: Builder.t() + @callback select(Request.t(), list(String.t()) | String.t()) :: Request.t() + @callback select(Request.t(), list(String.t()) | String.t(), options) :: Request.t() + @callback insert(Request.t(), map) :: Request.t() + @callback insert(Request.t(), map, insert_options) :: Request.t() + @callback upsert(Request.t(), map) :: Request.t() + @callback upsert(Request.t(), map, insert_options) :: Request.t() + @callback delete(Request.t()) :: Request.t() + @callback delete(Request.t(), options) :: Request.t() + @callback update(Request.t(), map) :: Request.t() + @callback update(Request.t(), map, options) :: Request.t() end diff --git a/lib/supabase/postgrest/schema_decoder.ex b/lib/supabase/postgrest/schema_decoder.ex new file mode 100644 index 0000000..956e0ca --- /dev/null +++ b/lib/supabase/postgrest/schema_decoder.ex @@ -0,0 +1,21 @@ +defmodule Supabase.PostgREST.SchemaDecoder do + @moduledoc "Custom body decoder that decodes a PostgREST response into a custom schema" + + alias Supabase.Fetcher.JSONDecoder + alias Supabase.Fetcher.Response + + @behaviour Supabase.Fetcher.BodyDecoder + + @impl true + def decode(%Response{} = resp, opts \\ []) do + schema = Keyword.fetch!(opts, :schema) + + with {:ok, body} <- JSONDecoder.decode(resp, keys: :atoms) do + cond do + resp.status < 400 and is_list(body) -> {:ok, Enum.map(body, &struct(schema, &1))} + resp.status < 400 -> {:ok, struct(schema, body)} + true -> {:ok, body} + end + end + end +end diff --git a/lib/supabase/postgrest/transform_builder.ex b/lib/supabase/postgrest/transform_builder.ex index bd34012..3db8c12 100644 --- a/lib/supabase/postgrest/transform_builder.ex +++ b/lib/supabase/postgrest/transform_builder.ex @@ -5,7 +5,7 @@ defmodule Supabase.PostgREST.TransformBuilder do This module provides functionality for ordering, limiting, and paginating query results. These transformations modify how data is structured or retrieved, enabling precise control over the format and amount of data returned. """ - alias Supabase.PostgREST.Builder + alias Supabase.Fetcher.Request @behaviour Supabase.PostgREST.TransformBuilder.Behaviour @@ -13,7 +13,7 @@ defmodule Supabase.PostgREST.TransformBuilder do Limits the number of results returned by the query, optionally scoping this limit to a specific foreign table. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `count`: The maximum number of results to return. - `opts`: Optional parameters, which may include a foreign table. @@ -24,11 +24,11 @@ defmodule Supabase.PostgREST.TransformBuilder do - Supabase query limits: https://supabase.com/docs/reference/javascript/using-filters#limit """ @impl true - def limit(%Builder{} = f, count, opts \\ []) do + def limit(%Request{} = f, count, opts \\ []) do if foreign = Keyword.get(opts, :foreign_table) do - Builder.add_query_param(f, "#{foreign}.limit", to_string(count)) + Request.with_query(f, %{"#{foreign}.limit" => to_string(count)}) else - Builder.add_query_param(f, "limit", to_string(count)) + Request.with_query(f, %{"limit" => to_string(count)}) end end @@ -38,7 +38,7 @@ defmodule Supabase.PostgREST.TransformBuilder do You can order referenced tables, but it only affects the ordering of the parent table if you use `!inner` in the query. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `column`: The column by which to order the results. - `opts`: Options such as direction (`:asc` or `:desc`) and null handling (`:null_first` or `:null_last`). @@ -49,17 +49,14 @@ defmodule Supabase.PostgREST.TransformBuilder do - Supabase ordering results: https://supabase.com/docs/reference/javascript/using-filters#order """ @impl true - def order(%Builder{} = f, column, opts \\ []) do + def order(%Request{} = f, column, opts \\ []) do order = if opts[:asc], do: "asc", else: "desc" nulls_first = if opts[:null_first], do: "nullsfirst", else: "nullslast" foreign = Keyword.get(opts, :foreign_table) key = if foreign, do: "#{foreign}.order", else: "order" + order = Enum.join([column, order, nulls_first], ".") - if curr = f.params[key] do - Builder.add_query_param(f, key, "#{curr},#{column}.#{order}.#{nulls_first}") - else - Builder.add_query_param(f, key, "#{column}.#{order}.#{nulls_first}") - end + Request.merge_query_param(f, key, order, with: ",") end defguardp is_number(a, b) @@ -74,7 +71,7 @@ defmodule Supabase.PostgREST.TransformBuilder do The `from` and `to` values are 0-based and inclusive: `range(1, 3)` will include the second, third and fourth rows of the query. ## Parameters - - `builder`: The Builder instance. + - `builder`: The `Supabase.Fetcher.Request` instance. - `from`: The starting index for the results. - `to`: The ending index for the results, inclusive. @@ -85,15 +82,15 @@ defmodule Supabase.PostgREST.TransformBuilder do - Supabase range queries: https://supabase.com/docs/reference/javascript/using-filters#range """ @impl true - def range(%Builder{} = f, from, to, opts \\ []) when is_number(from, to) do + def range(%Request{} = f, from, to, opts \\ []) when is_number(from, to) do if foreign = Keyword.get(opts, :foreign_table) do f - |> Builder.add_query_param("#{foreign}.offset", to_string(from)) - |> Builder.add_query_param("#{foreign}.limit", to_string(to - from + 1)) + |> Request.with_query(%{"#{foreign}.offset" => to_string(from)}) + |> Request.with_query(%{"#{foreign}.limit" => to_string(to - from + 1)}) else f - |> Builder.add_query_param("offset", to_string(from)) - |> Builder.add_query_param("limit", to_string(to - from + 1)) + |> Request.with_query(%{"offset" => to_string(from)}) + |> Request.with_query(%{"limit" => to_string(to - from + 1)}) end end @@ -102,7 +99,7 @@ defmodule Supabase.PostgREST.TransformBuilder do Query result must be one row (e.g. using `.limit(1)`), otherwise this returns an error. ## Parameters - - `builder`: The Builder instance to modify. + - `builder`: The `Supabase.Fetcher.Request` instance to modify. ## Examples iex> PostgREST.single(builder) @@ -111,7 +108,7 @@ defmodule Supabase.PostgREST.TransformBuilder do - Supabase single row mode: https://supabase.com/docs/reference/javascript/using-filters#single-row """ @impl true - def single(%Builder{} = b) do + def single(%Request{} = b) do Supabase.PostgREST.with_custom_media_type(b, :pgrst_object) end @@ -120,7 +117,7 @@ defmodule Supabase.PostgREST.TransformBuilder do Query result must be one row (e.g. using `.limit(1)`), otherwise this returns an error. ## Parameters - - `builder`: The Builder instance to modify. + - `builder`: The `Supabase.Fetcher.Request` instance to modify. ## Examples iex> PostgREST.single(builder) @@ -129,11 +126,11 @@ defmodule Supabase.PostgREST.TransformBuilder do - Supabase single row mode: https://supabase.com/docs/reference/javascript/using-filters#single-row """ @impl true - def maybe_single(%Builder{} = b) when b.method == :get do + def maybe_single(%Request{} = b) when b.method == :get do Supabase.PostgREST.with_custom_media_type(b, :json) end - def maybe_single(%Builder{} = b) do + def maybe_single(%Request{} = b) do Supabase.PostgREST.with_custom_media_type(b, :pgrst_object) end @@ -142,14 +139,14 @@ defmodule Supabase.PostgREST.TransformBuilder do ## Examples iex> PostgREST.csv(builder) - %Builder{headers: %{"accept" => "text/csv"}} + %Supabase.Fetcher.Request{headers: %{"accept" => "text/csv"}} ## See also https://supabase.com/docs/reference/javascript/db-csv """ @impl true - def csv(%Builder{} = b) do - Builder.add_request_header(b, "accept", "text/csv") + def csv(%Request{} = b) do + Request.with_headers(b, %{"accept" => "text/csv"}) end @doc """ @@ -157,11 +154,11 @@ defmodule Supabase.PostgREST.TransformBuilder do ## Examples iex> PostgREST.csv(builder) - %Builder{headers: %{"accept" => "application/geo+json"}} + %Supabase.Fetcher.Request{headers: %{"accept" => "application/geo+json"}} """ @impl true - def geojson(%Builder{} = b) do - Builder.add_request_header(b, "accept", "application/geo+json") + def geojson(%Request{} = b) do + Request.with_headers(b, %{"accept" => "application/geo+json"}) end @explain_default [ @@ -187,13 +184,13 @@ defmodule Supabase.PostgREST.TransformBuilder do ## Examples iex> PostgREST.explain(builder, analyze: true, format: :json, wal: false) - %Builder{} + %Supabase.Fetcher.Request{} ## See also https://supabase.com/docs/reference/javascript/explain """ @impl true - def explain(%Builder{} = b, opts \\ []) do + def explain(%Request{} = b, opts \\ []) do format = opts |> Keyword.get(:format, :text) @@ -218,7 +215,7 @@ defmodule Supabase.PostgREST.TransformBuilder do plan = "application/vnd.pgrst.plan#{format};#{for_mediatype};#{opts}" - Builder.add_request_header(b, "accept", plan) + Request.with_headers(b, %{"accept" => plan}) end @doc """ @@ -226,15 +223,11 @@ defmodule Supabase.PostgREST.TransformBuilder do ## Examples iex> PostgREST.rollback(builder) - %Builder{headers: %{"prefer" => "tx=rollback"}} + %Supabase.Fetcher.Request{headers: %{"prefer" => "tx=rollback"}} """ @impl true - def rollback(%Builder{} = b) do - if prefer = b.headers["prefer"] do - Builder.add_request_header(b, "prefer", "#{prefer},tx=rollback") - else - Builder.add_request_header(b, "prefer", "tx=rollback") - end + def rollback(%Request{} = b) do + Request.merge_req_header(b, "prefer", "tx=rollback", with: ",") end @doc """ @@ -258,25 +251,21 @@ defmodule Supabase.PostgREST.TransformBuilder do https://supabase.com/docs/reference/javascript/db-modifiers-select """ @impl true - def returning(%Builder{} = b) do - prefer = if p = b.headers["prefer"], do: p <> ",", else: "" - + def returning(%Request{} = b) do b - |> Builder.add_query_param("select", "*") - |> Builder.add_request_header("prefer", "#{prefer}return=representation") + |> Request.with_query(%{"select" => "*"}) + |> Request.merge_req_header("prefer", "return=representation") end @impl true - def returning(%Builder{} = b, columns) when is_list(columns) do + def returning(%Request{} = b, columns) when is_list(columns) do cols = Enum.map_join(columns, ",", fn c -> if String.match?(c, ~r/\"/), do: c, else: String.trim(c) end) - prefer = if p = b.headers["prefer"], do: p <> ",", else: "" - b - |> Builder.add_query_param("select", cols) - |> Builder.add_request_header("prefer", "#{prefer}return=representation") + |> Request.with_query(%{"select" => cols}) + |> Request.merge_req_header("prefer", "return=representation") end end diff --git a/lib/supabase/postgrest/transform_builder/behaviour.ex b/lib/supabase/postgrest/transform_builder/behaviour.ex index 12bd33f..6140cb3 100644 --- a/lib/supabase/postgrest/transform_builder/behaviour.ex +++ b/lib/supabase/postgrest/transform_builder/behaviour.ex @@ -1,7 +1,7 @@ defmodule Supabase.PostgREST.TransformBuilder.Behaviour do @moduledoc "Defines the interface for the TransformBuilder module" - alias Supabase.PostgREST.Builder + alias Supabase.Fetcher.Request @type order_options :: [ asc: boolean | nil, @@ -9,22 +9,22 @@ defmodule Supabase.PostgREST.TransformBuilder.Behaviour do foreign_table: String.t() | nil ] - @callback limit(Builder.t(), count :: integer) :: Builder.t() - @callback limit(Builder.t(), count :: integer, foreign_table: String.t()) :: - Builder.t() - @callback single(Builder.t()) :: Builder.t() - @callback maybe_single(Builder.t()) :: Builder.t() - @callback order(Builder.t(), column :: String.t(), order_options) :: Builder.t() - @callback order(Builder.t(), column :: String.t(), order_options) :: Builder.t() - @callback range(Builder.t(), from :: integer, to :: integer) :: Builder.t() - @callback range(Builder.t(), from :: integer, to :: integer, foreign_table: String.t()) :: - Builder.t() - @callback rollback(Builder.t()) :: Builder.t() - @callback returning(Builder.t()) :: Builder.t() - @callback returning(Builder.t(), list(String.t()) | String.t()) :: Builder.t() - @callback csv(Builder.t()) :: Builder.t() - @callback geojson(Builder.t()) :: Builder.t() - @callback explain(Builder.t(), options :: explain) :: Builder.t() + @callback limit(Request.t(), count :: integer) :: Request.t() + @callback limit(Request.t(), count :: integer, foreign_table: String.t()) :: + Request.t() + @callback single(Request.t()) :: Request.t() + @callback maybe_single(Request.t()) :: Request.t() + @callback order(Request.t(), column :: String.t(), order_options) :: Request.t() + @callback order(Request.t(), column :: String.t(), order_options) :: Request.t() + @callback range(Request.t(), from :: integer, to :: integer) :: Request.t() + @callback range(Request.t(), from :: integer, to :: integer, foreign_table: String.t()) :: + Request.t() + @callback rollback(Request.t()) :: Request.t() + @callback returning(Request.t()) :: Request.t() + @callback returning(Request.t(), list(String.t()) | String.t()) :: Request.t() + @callback csv(Request.t()) :: Request.t() + @callback geojson(Request.t()) :: Request.t() + @callback explain(Request.t(), options :: explain) :: Request.t() when explain: list({opt, boolean} | {:format, :json | :text}), opt: :analyze | :verbose | :settings | :buffers | :wal end diff --git a/mix.exs b/mix.exs index a91f5d6..d39d675 100644 --- a/mix.exs +++ b/mix.exs @@ -13,7 +13,8 @@ defmodule PostgREST.MixProject do package: package(), docs: docs(), description: description(), - source_url: @source_url + source_url: @source_url, + dialyzer: [plt_local_path: "priv/plts", ignore_warnings: ".dialyzerignore"] ] end @@ -27,10 +28,10 @@ defmodule PostgREST.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:supabase_potion, "~> 0.4"}, - {:ex_doc, ">= 0.0.0", runtime: false}, + {:supabase_potion, "~> 0.6"}, + {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, - {:dialyxir, "~> 1.3", only: [:dev], runtime: false} + {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index e56df73..f795f99 100644 --- a/mix.lock +++ b/mix.lock @@ -1,14 +1,14 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, - "credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"}, + "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, - "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -20,6 +20,6 @@ "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"}, - "supabase_potion": {:hex, :supabase_potion, "0.5.1", "3f604c875edc8895010552f6b36ba03fe5f281813234e337adb930dd2f7df178", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_doc, ">= 0.0.0", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c26a9e99fd61fc546694c7a5ae48c4c8ab36295230eb28de04818e1b59610c23"}, + "supabase_potion": {:hex, :supabase_potion, "0.6.0", "0375ec2415e0a5176d63456bba1f589fb196bcbc7c72e40c19094c1299b5d23a", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "ac45726b5098cf8b351d974f874631b1c2159174a712086552043a043f5e3f05"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, } diff --git a/priv/plts/dialyxir_erlang-27.1.2_elixir-1.18.1_deps-dev.plt b/priv/plts/dialyxir_erlang-27.1.2_elixir-1.18.1_deps-dev.plt new file mode 100644 index 0000000..4cf21e6 Binary files /dev/null and b/priv/plts/dialyxir_erlang-27.1.2_elixir-1.18.1_deps-dev.plt differ diff --git a/priv/plts/dialyxir_erlang-27.1.2_elixir-1.18.1_deps-dev.plt.hash b/priv/plts/dialyxir_erlang-27.1.2_elixir-1.18.1_deps-dev.plt.hash new file mode 100644 index 0000000..8930f99 --- /dev/null +++ b/priv/plts/dialyxir_erlang-27.1.2_elixir-1.18.1_deps-dev.plt.hash @@ -0,0 +1 @@ +ÓSŒ;ëÔ´O+Ú©-ɃVù½YÜ \ No newline at end of file diff --git a/supabase/.gitignore b/supabase/.gitignore deleted file mode 100644 index a3ad880..0000000 --- a/supabase/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Supabase -.branches -.temp -.env diff --git a/supabase/config.toml b/supabase/config.toml deleted file mode 100644 index cee03d7..0000000 --- a/supabase/config.toml +++ /dev/null @@ -1,278 +0,0 @@ -# For detailed configuration reference documentation, visit: -# https://supabase.com/docs/guides/local-development/cli/config -# A string used to distinguish different Supabase projects on the same host. Defaults to the -# working directory name when running `supabase init`. -project_id = "postgrest-ex" - -[api] -enabled = true -# Port to use for the API URL. -port = 54321 -# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API -# endpoints. `public` and `graphql_public` schemas are included by default. -schemas = ["public", "graphql_public"] -# Extra schemas to add to the search_path of every request. -extra_search_path = ["public", "extensions"] -# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size -# for accidental or malicious requests. -max_rows = 1000 - -[api.tls] -# Enable HTTPS endpoints locally using a self-signed certificate. -enabled = false - -[db] -# Port to use for the local database URL. -port = 54322 -# Port used by db diff command to initialize the shadow database. -shadow_port = 54320 -# The database major version to use. This has to be the same as your remote database's. Run `SHOW -# server_version;` on the remote database to check. -major_version = 15 - -[db.pooler] -enabled = false -# Port to use for the local connection pooler. -port = 54329 -# Specifies when a server connection can be reused by other clients. -# Configure one of the supported pooler modes: `transaction`, `session`. -pool_mode = "transaction" -# How many server connections to allow per user/database pair. -default_pool_size = 20 -# Maximum number of client connections allowed. -max_client_conn = 100 - -[db.seed] -# If enabled, seeds the database after migrations during a db reset. -enabled = true -# Specifies an ordered list of seed files to load during db reset. -# Supports glob patterns relative to supabase directory: './seeds/*.sql' -sql_paths = ['./seed.sql'] - -[realtime] -enabled = false -# Bind realtime via either IPv4 or IPv6. (default: IPv4) -# ip_version = "IPv6" -# The maximum length in bytes of HTTP request headers. (default: 4096) -# max_header_length = 4096 - -[studio] -enabled = false -# Port to use for Supabase Studio. -port = 54323 -# External URL of the API server that frontend connects to. -api_url = "http://127.0.0.1" -# OpenAI API Key to use for Supabase AI in the Supabase Studio. -openai_api_key = "env(OPENAI_API_KEY)" - -# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they -# are monitored, and you can view the emails that would have been sent from the web interface. -[inbucket] -enabled = false -# Port to use for the email testing server web interface. -port = 54324 -# Uncomment to expose additional ports for testing user applications that send emails. -# smtp_port = 54325 -# pop3_port = 54326 -# admin_email = "admin@email.com" -# sender_name = "Admin" - -[storage] -enabled = false -# The maximum file size allowed (e.g. "5MB", "500KB"). -file_size_limit = "50MiB" - -# Image transformation API is available to Supabase Pro plan. -# [storage.image_transformation] -# enabled = true - -# Uncomment to configure local storage buckets -# [storage.buckets.images] -# public = false -# file_size_limit = "50MiB" -# allowed_mime_types = ["image/png", "image/jpeg"] -# objects_path = "./images" - -[auth] -enabled = false -# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used -# in emails. -site_url = "http://127.0.0.1:3000" -# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000"] -# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). -jwt_expiry = 3600 -# If disabled, the refresh token will never expire. -enable_refresh_token_rotation = true -# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. -# Requires enable_refresh_token_rotation = true. -refresh_token_reuse_interval = 10 -# Allow/disallow new user signups to your project. -enable_signup = true -# Allow/disallow anonymous sign-ins to your project. -enable_anonymous_sign_ins = false -# Allow/disallow testing manual linking of accounts -enable_manual_linking = false -# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. -minimum_password_length = 6 -# Passwords that do not meet the following requirements will be rejected as weak. Supported values -# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` -password_requirements = "" - -[auth.email] -# Allow/disallow new user signups via email to your project. -enable_signup = false -# If enabled, a user will be required to confirm any email change on both the old, and new email -# addresses. If disabled, only the new email is required to confirm. -double_confirm_changes = false -# If enabled, users need to confirm their email address before signing in. -enable_confirmations = false -# If enabled, users will need to reauthenticate or have logged in recently to change their password. -secure_password_change = false -# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. -max_frequency = "1s" -# Number of characters used in the email OTP. -otp_length = 6 -# Number of seconds before the email OTP expires (defaults to 1 hour). -otp_expiry = 3600 - -# Use a production-ready SMTP server -# [auth.email.smtp] -# enabled = true -# host = "smtp.sendgrid.net" -# port = 587 -# user = "apikey" -# pass = "env(SENDGRID_API_KEY)" -# admin_email = "admin@email.com" -# sender_name = "Admin" - -# Uncomment to customize email template -# [auth.email.template.invite] -# subject = "You have been invited" -# content_path = "./supabase/templates/invite.html" - -[auth.sms] -# Allow/disallow new user signups via SMS to your project. -enable_signup = false -# If enabled, users need to confirm their phone number before signing in. -enable_confirmations = false -# Template for sending OTP to users -template = "Your code is {{ .Code }}" -# Controls the minimum amount of time that must pass before sending another sms otp. -max_frequency = "5s" - -# Use pre-defined map of phone number to OTP for testing. -# [auth.sms.test_otp] -# 4152127777 = "123456" - -# Configure logged in session timeouts. -# [auth.sessions] -# Force log out after the specified duration. -# timebox = "24h" -# Force log out if the user has been inactive longer than the specified duration. -# inactivity_timeout = "8h" - -# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. -# [auth.hook.custom_access_token] -# enabled = true -# uri = "pg-functions:////" - -# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. -[auth.sms.twilio] -enabled = false -account_sid = "" -message_service_sid = "" -# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: -auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" - -# Multi-factor-authentication is available to Supabase Pro plan. -[auth.mfa] -# Control how many MFA factors can be enrolled at once per user. -max_enrolled_factors = 10 - -# Control MFA via App Authenticator (TOTP) -[auth.mfa.totp] -enroll_enabled = false -verify_enabled = false - -# Configure MFA via Phone Messaging -[auth.mfa.phone] -enroll_enabled = false -verify_enabled = false -otp_length = 6 -template = "Your code is {{ .Code }}" -max_frequency = "5s" - -# Configure MFA via WebAuthn -# [auth.mfa.web_authn] -# enroll_enabled = true -# verify_enabled = true - -# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, -# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, -# `twitter`, `slack`, `spotify`, `workos`, `zoom`. -[auth.external.apple] -enabled = false -client_id = "" -# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: -secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" -# Overrides the default auth redirectUrl. -redirect_uri = "" -# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, -# or any other third-party OIDC providers. -url = "" -# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. -skip_nonce_check = false - -# Use Firebase Auth as a third-party provider alongside Supabase Auth. -[auth.third_party.firebase] -enabled = false -# project_id = "my-firebase-project" - -# Use Auth0 as a third-party provider alongside Supabase Auth. -[auth.third_party.auth0] -enabled = false -# tenant = "my-auth0-tenant" -# tenant_region = "us" - -# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. -[auth.third_party.aws_cognito] -enabled = false -# user_pool_id = "my-user-pool-id" -# user_pool_region = "us-east-1" - -[edge_runtime] -enabled = false -# Configure one of the supported request policies: `oneshot`, `per_worker`. -# Use `oneshot` for hot reload, or `per_worker` for load testing. -policy = "oneshot" -# Port to attach the Chrome inspector for debugging edge functions. -inspector_port = 8083 - -# Use these configurations to customize your Edge Function. -# [functions.MY_FUNCTION_NAME] -# enabled = true -# verify_jwt = true -# import_map = "./functions/MY_FUNCTION_NAME/deno.json" -# Uncomment to specify a custom file path to the entrypoint. -# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx -# entrypoint = "./functions/MY_FUNCTION_NAME/index.ts" - -[analytics] -enabled = false -port = 54327 -# Configure one of the supported backends: `postgres`, `bigquery`. -backend = "postgres" - -# Experimental features may be deprecated any time -[experimental] -# Configures Postgres storage engine to use OrioleDB (S3) -orioledb_version = "" -# Configures S3 bucket URL, eg. .s3-.amazonaws.com -s3_host = "env(S3_HOST)" -# Configures S3 bucket region, eg. us-east-1 -s3_region = "env(S3_REGION)" -# Configures AWS_ACCESS_KEY_ID for S3 bucket -s3_access_key = "env(S3_ACCESS_KEY)" -# Configures AWS_SECRET_ACCESS_KEY for S3 bucket -s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/supabase/seed.sql b/supabase/seed.sql deleted file mode 100644 index f92e848..0000000 --- a/supabase/seed.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS films ( - id serial PRIMARY KEY, - code char(5), - title varchar(40) NOT NULL, - did integer NOT NULL, - date_prod date, - kind varchar(10), - len interval hour to minute -); - -CREATE TABLE IF NOT EXISTS distributors ( - did integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, - name varchar(40) NOT NULL CHECK (name <> ''), - film_id integer, - FOREIGN KEY (film_id) REFERENCES films (id) -); diff --git a/test/supabase/postgrest_test.exs b/test/supabase/postgrest_test.exs index 1fc2af8..c92433e 100644 --- a/test/supabase/postgrest_test.exs +++ b/test/supabase/postgrest_test.exs @@ -1,8 +1,10 @@ defmodule Supabase.PostgRESTTest do use ExUnit.Case + alias Supabase.Fetcher.Request alias Supabase.PostgREST - alias Supabase.PostgREST.Builder + + import Supabase.Fetcher.Request setup do client = Supabase.init_client!("http://some/url", "test-api-key") @@ -11,129 +13,129 @@ defmodule Supabase.PostgRESTTest do end describe "from/2" do - test "initializes a Builder correctly", %{client: client} do + test "initializes a Fetcher correctly", %{client: client} do table = "users" - assert %Builder{url: url} = PostgREST.from(client, table) + assert %Request{url: url} = PostgREST.from(client, table) assert url.path =~ table end end describe "select/3" do test "builds a select query with specific columns", %{client: client} do - builder = Builder.new(client, relation: "users") + builder = PostgREST.from(client, "users") columns = ["id", "name", "email"] opts = [count: :exact, returning: true] result = PostgREST.select(builder, columns, opts) - assert %Builder{} = result - assert result.params["select"] == "id,name,email" - assert result.headers["prefer"] == "count=exact" + assert %Request{} = result + assert get_query_param(result, "select") == "id,name,email" + assert get_header(result, "prefer") == "count=exact" end test "builds a select query with all columns using '*'", %{client: client} do - builder = Builder.new(client, relation: "users") + builder = PostgREST.from(client, "users") opts = [count: :exact, returning: false] result = PostgREST.select(builder, "*", opts) - assert %Builder{} = result - assert result.params["select"] == "*" - assert result.headers["prefer"] == "count=exact" + assert %Request{} = result + assert get_query_param(result, "select") == "*" + assert get_header(result, "prefer") == "count=exact" end end describe "insert/3" do test "builds an insert query with correct headers and body", %{client: client} do - builder = Builder.new(client, relation: "users") + builder = PostgREST.from(client, "users") data = %{name: "John Doe", age: 28} opts = [on_conflict: "name", returning: :minimal, count: :exact] result = PostgREST.insert(builder, data, opts) - assert %Builder{} = result + assert %Request{} = result assert result.method == :post - assert result.headers["prefer"] == + assert get_header(result, "prefer") == "return=minimal,count=exact,on_conflict=name,resolution=merge-duplicates" end end describe "update/3" do test "creates an update operation with custom options", %{client: client} do - builder = Builder.new(client, relation: "users") + builder = PostgREST.from(client, "users") data = %{name: "Jane Doe"} opts = [returning: :representation, count: :exact] result = PostgREST.update(builder, data, opts) - assert %Builder{} = result + assert %Request{} = result assert result.method == :patch - assert result.headers["prefer"] == "return=representation,count=exact" + assert get_header(result, "prefer") == "return=representation,count=exact" end end describe "delete/2" do test "builds a delete query with custom preferences", %{client: client} do - builder = Builder.new(client, relation: "users") + builder = PostgREST.from(client, "users") opts = [returning: :representation, count: :exact] result = PostgREST.delete(builder, opts) - assert %Builder{} = result + assert %Request{} = result assert result.method == :delete - assert result.headers["prefer"] == "return=representation,count=exact" + assert get_header(result, "prefer") == "return=representation,count=exact" end end describe "upsert/3" do test "builds an upsert query with conflict resolution", %{client: client} do - builder = Builder.new(client, relation: "users") + builder = PostgREST.from(client, "users") data = %{name: "Jane Doe"} opts = [on_conflict: "name", returning: :representation, count: :exact] result = PostgREST.upsert(builder, data, opts) - assert %Builder{} = result + assert %Request{} = result assert result.method == :post - assert result.headers["prefer"] == + assert get_header(result, "prefer") == "resolution=merge-duplicates,return=representation,count=exact" end end describe "filter functions" do setup ctx do - builder = Builder.new(ctx.client, relation: "users") + builder = PostgREST.from(ctx.client, "users") {:ok, Map.put(ctx, :builder, builder)} end test "eq function adds an equality filter", %{builder: fb} do - assert %Builder{params: %{"id" => "eq.123"}} = PostgREST.eq(fb, "id", 123) + assert %Request{query: [{"id", "eq.123"}]} = PostgREST.eq(fb, "id", 123) end test "neq function adds a not-equal filter", %{builder: fb} do - assert %Builder{params: %{"status" => "neq.inactive"}} = + assert %Request{query: [{"status", "neq.inactive"}]} = PostgREST.neq(fb, "status", "inactive") end test "gt function adds a greater-than filter", %{builder: fb} do - assert %Builder{params: %{"age" => "gt.21"}} = PostgREST.gt(fb, "age", 21) + assert %Request{query: [{"age", "gt.21"}]} = PostgREST.gt(fb, "age", 21) end test "lte function adds a less-than-or-equal filter", %{builder: fb} do - assert %Builder{params: %{"age" => "lte.65"}} = PostgREST.lte(fb, "age", 65) + assert %Request{query: [{"age", "lte.65"}]} = PostgREST.lte(fb, "age", 65) end test "like function adds a LIKE SQL pattern filter", %{builder: fb} do - assert %Builder{params: %{"name" => "like.%John%"}} = + assert %Request{query: [{"name", "like.%John%"}]} = PostgREST.like(fb, "name", "%John%") end test "ilike function adds a case-insensitive LIKE filter", %{builder: fb} do - assert %Builder{params: %{"name" => "ilike.%john%"}} = + assert %Request{query: [{"name", "ilike.%john%"}]} = PostgREST.ilike(fb, "name", "%john%") end test "within function checks if a column's value is within a specified list", %{ builder: fb } do - assert %Builder{params: %{"status" => "in.(active,pending,closed)"}} = + assert %Request{query: [{"status", "in.(active,pending,closed)"}]} = PostgREST.within(fb, "status", ["active", "pending", "closed"]) end end