Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: follow supabase-ex release #14

Merged
merged 4 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .dialyzerignore
Empty file.
129 changes: 128 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions .wakatime-project
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
postgrest-ex
120 changes: 29 additions & 91 deletions lib/supabase/postgrest.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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 """
Expand All @@ -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 """
Expand All @@ -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.
Expand All @@ -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
20 changes: 7 additions & 13 deletions lib/supabase/postgrest/behaviour.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading