From f048c64eafdd00c7d638f3669b9aaad63bd4b22b Mon Sep 17 00:00:00 2001 From: zoeyrinha Date: Sun, 29 Dec 2024 16:59:15 -0300 Subject: [PATCH] Do not encode json request body (#6) * upgrade: simplify nix flake and update elixir * init a dev sample app * fix: do not encode request body\n\nand also correctly handle media types headers * update ci --- .envrc | 3 - .github/workflows/ci.yml | 30 ++- .github/workflows/release.yml | 30 --- flake.lock | 63 +------ flake.nix | 69 ++++--- lib/supabase/postgrest.ex | 136 +++++++++----- lib/supabase/postgrest_behaviour.ex | 5 + mix.lock | 18 +- supabase/.gitignore | 4 + supabase/config.toml | 278 ++++++++++++++++++++++++++++ supabase/seed.sql | 16 ++ test/supabase/postgrest_test.exs | 12 +- 12 files changed, 471 insertions(+), 193 deletions(-) delete mode 100644 .github/workflows/release.yml create mode 100644 supabase/.gitignore create mode 100644 supabase/config.toml create mode 100644 supabase/seed.sql diff --git a/.envrc b/.envrc index 6972dcf..cfbecd0 100644 --- a/.envrc +++ b/.envrc @@ -9,6 +9,3 @@ export ERL_AFLAGS="-kernel shell_history enabled" export LANG=en_US.UTF-8 use flake - -# get supabase real keys -source .env diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 911454d..f6ac400 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,8 @@ jobs: strategy: matrix: - elixir: [1.17.0] - otp: [27.0] + elixir: [1.18.1] + otp: [27.2] steps: - name: Checkout code @@ -27,8 +27,30 @@ jobs: elixir-version: ${{ matrix.elixir }} otp-version: ${{ matrix.otp }} - - name: Install dependencies - run: mix deps.get + - 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index c2e38c5..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: release - -on: - push: - tags: - - '*' - -env: - MIX_ENV: prod - -jobs: - publish: - runs-on: ubuntu-latest - strategy: - matrix: - elixir: [1.17.0] - otp: [27.0] - steps: - - uses: actions/checkout@v3 - - name: Set up Elixir - uses: erlef/setup-beam@v1 - with: - elixir-version: ${{ matrix.elixir }} - otp-version: ${{ matrix.otp }} - - name: Publish to Hex - uses: synchronal/hex-publish-action@v3 - with: - name: supabase_potion - key: ${{ secrets.HEX_PM_KEY }} - tag-release: true diff --git a/flake.lock b/flake.lock index f2b51b0..f9bae3d 100644 --- a/flake.lock +++ b/flake.lock @@ -1,77 +1,24 @@ { "nodes": { - "flake-parts": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib" - }, - "locked": { - "lastModified": 1704982712, - "narHash": "sha256-2Ptt+9h8dczgle2Oo6z5ni5rt/uLMG47UFTR1ry/wgg=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "07f6395285469419cf9d078f59b5b49993198c00", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1724819573, - "narHash": "sha256-GnR7/ibgIH1vhoy8cYdmXE6iyZqKqFxQSVkFgosBh6w=", + "lastModified": 1734991663, + "narHash": "sha256-8T660guvdaOD+2/Cj970bWlQwAyZLKrrbkhYOFcY1YE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "71e91c409d1e654808b2621f28a327acfdad8dc2", + "rev": "6c90912761c43e22b6fb000025ab96dd31c971ff", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-lib": { - "locked": { - "dir": "lib", - "lastModified": 1703961334, - "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9", - "type": "github" - }, - "original": { - "dir": "lib", - "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "nixos-24.11", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "flake-parts": "flake-parts", - "nixpkgs": "nixpkgs", - "systems": "systems" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 636a15d..e9d6c1e 100644 --- a/flake.nix +++ b/flake.nix @@ -1,43 +1,40 @@ { description = "Supabase SDK for Elixir"; - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-parts.url = "github:hercules-ci/flake-parts"; - systems.url = "github:nix-systems/default"; - }; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + + outputs = {nixpkgs, ...}: let + inherit (nixpkgs.lib) genAttrs; + inherit (nixpkgs.lib.systems) flakeExposed; + forAllSystems = f: + genAttrs flakeExposed (system: f (import nixpkgs {inherit system;})); + in { + devShells = forAllSystems (pkgs: let + inherit (pkgs) mkShell; + inherit (pkgs.beam.interpreters) erlang_27; + inherit (pkgs.beam) packagesWith; + beam = packagesWith erlang_27; + elixir_1_18 = beam.elixir.override { + version = "1.18.1"; - outputs = { - flake-parts, - systems, - ... - } @ inputs: - flake-parts.lib.mkFlake {inherit inputs;} { - systems = import systems; - perSystem = { - pkgs, - system, - ... - }: let - inherit (pkgs.beam.interpreters) erlang_27; - inherit (pkgs.beam) packagesWith; - beam = packagesWith erlang_27; - in { - _module.args.pkgs = import inputs.nixpkgs { - inherit system; - config.allowUnfree = true; + src = pkgs.fetchFromGitHub { + owner = "elixir-lang"; + repo = "elixir"; + rev = "v1.18.1"; + sha256 = "sha256-zJNAoyqSj/KdJ1Cqau90QCJihjwHA+HO7nnD1Ugd768="; }; - devShells.default = with pkgs; - mkShell { - name = "postgrest-ex"; - packages = with pkgs; - [beam.elixir_1_17] - ++ lib.optional stdenv.isLinux [inotify-tools] - ++ lib.optional stdenv.isDarwin [ - darwin.apple_sdk.frameworks.CoreServices - darwin.apple_sdk.frameworks.CoreFoundation - ]; - }; }; - }; + in { + default = mkShell { + name = "postgrest-ex"; + packages = with pkgs; + [elixir_1_18 postgresql] + ++ lib.optional stdenv.isLinux [inotify-tools] + ++ lib.optional stdenv.isDarwin [ + darwin.apple_sdk.frameworks.CoreServices + darwin.apple_sdk.frameworks.CoreFoundation + ]; + }; + }); + }; } diff --git a/lib/supabase/postgrest.ex b/lib/supabase/postgrest.ex index 9d09418..2811b54 100644 --- a/lib/supabase/postgrest.ex +++ b/lib/supabase/postgrest.ex @@ -18,6 +18,17 @@ defmodule Supabase.PostgREST do @behaviour Supabase.PostgRESTBehaviour + @accept_headers %{ + default: "*/*", + csv: "text/csv", + json: "application/json", + openapi: "application/openapi+json", + postgis: "application/geo+json", + pgrst_plan: "application/vnd.pgrst.plan+json", + pgrst_object: "application/vnd.pgrst.object+json", + pgrst_array: "application/vnd.pgrst.array+json" + } + @doc """ Initializes a `QueryBuilder` for a specified table and client. @@ -34,13 +45,45 @@ defmodule Supabase.PostgREST do """ @impl true def from(%Client{} = client, table) do - QueryBuilder.new(table, client) + table + |> QueryBuilder.new(client) + |> with_custom_media_type(:default) + end + + @doc """ + Overrides the default media type `accept` header, which can control + the represation of the PostgREST response + + ## Examples + iex> q = PostgREST.from(client, "users") + iex> q = PostgREST.with_custom_media_type(q, :csv) + iex> PostgREST.execute(q) + {:ok, "id,name\n1,john\n2,maria"} + + ## See also + - [PostgREST resource represation docs](https://docs.postgrest.org/en/v12/references/api/resource_representation.html) + """ + @impl true + def with_custom_media_type(%QueryBuilder{} = q, media_type) + when is_atom(media_type) do + header = @accept_headers[media_type] || @accept_headers[:default] + QueryBuilder.add_header(q, "accept", header) + end + + def with_custom_media_type(%FilterBuilder{} = f, media_type) + when is_atom(media_type) do + header = @accept_headers[media_type] || @accept_headers[:default] + FilterBuilder.add_header(f, "accept", header) end @doc """ Selects records from a table. You can specify specific columns or use '*' for all columns. Options such as counting results and specifying return types can be configured. + Note that this function does not return by default, it only build the select + expression for the query. If you want to have the selected fields returned as + response you need to pass `returning: true`. + ## Parameters - `query_builder`: The QueryBuilder instance. - `columns`: A list of column names to fetch or '*' for all columns. @@ -62,7 +105,7 @@ defmodule Supabase.PostgREST do q |> QueryBuilder.change_method(:get) |> QueryBuilder.add_param("select", "*") - |> QueryBuilder.add_header("Prefer", "count=#{count}") + |> QueryBuilder.add_header("prefer", "count=#{count}") |> maybe_return(returning) |> FilterBuilder.from_query_builder() end @@ -75,7 +118,7 @@ defmodule Supabase.PostgREST do q |> QueryBuilder.change_method(:get) |> QueryBuilder.add_param("select", Enum.join(columns, ",")) - |> QueryBuilder.add_header("Prefer", "count=#{count}") + |> QueryBuilder.add_header("prefer", "count=#{count}") |> maybe_return(returning) |> FilterBuilder.from_query_builder() end @@ -99,7 +142,7 @@ defmodule Supabase.PostgREST do - Supabase documentation on inserts: https://supabase.com/docs/reference/javascript/insert """ @impl true - def insert(%QueryBuilder{} = q, data, opts \\ []) do + def insert(%QueryBuilder{} = q, 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" @@ -108,18 +151,12 @@ defmodule Supabase.PostgREST do prefer = ["return=#{returning}", "count=#{count}", on_conflict, upsert] prefer = Enum.join(Enum.reject(prefer, &is_nil/1), ",") - case Jason.encode(data) do - {:ok, body} -> - q - |> QueryBuilder.change_method(:post) - |> QueryBuilder.add_header("Prefer", prefer) - |> QueryBuilder.add_param("on_conflict", on_conflict) - |> QueryBuilder.change_body(body) - |> FilterBuilder.from_query_builder() - - _err -> - FilterBuilder.new() - end + q + |> QueryBuilder.change_method(:post) + |> QueryBuilder.add_header("prefer", prefer) + |> QueryBuilder.add_param("on_conflict", on_conflict) + |> QueryBuilder.change_body(data) + |> FilterBuilder.from_query_builder() end @doc """ @@ -137,7 +174,7 @@ defmodule Supabase.PostgREST do - Supabase documentation on upserts: https://supabase.com/docs/reference/javascript/upsert """ @impl true - def upsert(%QueryBuilder{} = q, data, opts \\ []) do + def upsert(%QueryBuilder{} = q, 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) @@ -145,18 +182,12 @@ defmodule Supabase.PostgREST do prefer = Enum.join(["resolution=merge-duplicates", "return=#{returning}", "count=#{count}"], ",") - case Jason.encode(data) do - {:ok, body} -> - q - |> QueryBuilder.change_method(:post) - |> QueryBuilder.add_header("Prefer", prefer) - |> QueryBuilder.add_param("on_conflict", on_conflict) - |> QueryBuilder.change_body(body) - |> FilterBuilder.from_query_builder() - - _err -> - FilterBuilder.new() - end + q + |> QueryBuilder.change_method(:post) + |> QueryBuilder.add_header("prefer", prefer) + |> QueryBuilder.add_param("on_conflict", on_conflict) + |> QueryBuilder.change_body(data) + |> FilterBuilder.from_query_builder() end @doc """ @@ -180,7 +211,7 @@ defmodule Supabase.PostgREST do q |> QueryBuilder.change_method(:delete) - |> QueryBuilder.add_header("Prefer", prefer) + |> QueryBuilder.add_header("prefer", prefer) |> FilterBuilder.from_query_builder() end @@ -199,22 +230,16 @@ defmodule Supabase.PostgREST do - Supabase documentation on updates: https://supabase.com/docs/reference/javascript/update """ @impl true - def update(%QueryBuilder{} = q, data, opts \\ []) do + def update(%QueryBuilder{} = q, 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}"], ",") - case Jason.encode(data) do - {:ok, body} -> - q - |> QueryBuilder.change_method(:patch) - |> QueryBuilder.add_header("Prefer", prefer) - |> QueryBuilder.change_body(body) - |> FilterBuilder.from_query_builder() - - _err -> - FilterBuilder.new() - end + q + |> QueryBuilder.change_method(:patch) + |> QueryBuilder.add_header("prefer", prefer) + |> QueryBuilder.change_body(data) + |> FilterBuilder.from_query_builder() end @impl true @@ -968,15 +993,32 @@ defmodule Supabase.PostgREST do defp parse_response({:error, reason}), do: {:error, reason} - defp parse_response({:ok, %{status: status, body: raw}}) 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} + 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 diff --git a/lib/supabase/postgrest_behaviour.ex b/lib/supabase/postgrest_behaviour.ex index fefbdec..ca6c173 100644 --- a/lib/supabase/postgrest_behaviour.ex +++ b/lib/supabase/postgrest_behaviour.ex @@ -17,6 +17,11 @@ defmodule Supabase.PostgRESTBehaviour do foreign_table: String.t() | nil ] + @type media_type :: + :json | :csv | :openapi | :geojson | :pgrst_plan | :pgrst_object | :pgrst_array + + @callback with_custom_media_type(builder, media_type) :: builder + when builder: QueryBuilder.t() | FilterBuilder.t() @callback from(Client.t(), schema :: String.t()) :: QueryBuilder.t() @callback select(QueryBuilder.t(), list(String.t()) | String.t()) :: FilterBuilder.t() @callback select(QueryBuilder.t(), list(String.t()) | String.t(), options) :: FilterBuilder.t() diff --git a/mix.lock b/mix.lock index c74ec29..e56df73 100644 --- a/mix.lock +++ b/mix.lock @@ -1,19 +1,19 @@ %{ "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.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [: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", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, - "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [: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", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"}, + "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"}, + "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.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [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", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "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"}, "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.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, + "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"}, - "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 0000000..a3ad880 --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,4 @@ +# Supabase +.branches +.temp +.env diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..cee03d7 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,278 @@ +# 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 new file mode 100644 index 0000000..f92e848 --- /dev/null +++ b/supabase/seed.sql @@ -0,0 +1,16 @@ +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 0d81bd5..833eec4 100644 --- a/test/supabase/postgrest_test.exs +++ b/test/supabase/postgrest_test.exs @@ -32,7 +32,7 @@ defmodule Supabase.PostgRESTTest do result = PostgREST.select(query_builder, columns, opts) assert %FilterBuilder{} = result assert result.params["select"] == "id,name,email" - assert result.headers["Prefer"] == "count=exact" + assert result.headers["prefer"] == "count=exact" end test "builds a select query with all columns using '*'", %{client: client} do @@ -42,7 +42,7 @@ defmodule Supabase.PostgRESTTest do result = PostgREST.select(query_builder, "*", opts) assert %FilterBuilder{} = result assert result.params["select"] == "*" - assert result.headers["Prefer"] == "count=exact" + assert result.headers["prefer"] == "count=exact" end end @@ -56,7 +56,7 @@ defmodule Supabase.PostgRESTTest do assert %FilterBuilder{} = result assert result.method == :post - assert result.headers["Prefer"] == + assert result.headers["prefer"] == "return=minimal,count=exact,on_conflict=name,resolution=merge-duplicates" end end @@ -70,7 +70,7 @@ defmodule Supabase.PostgRESTTest do result = PostgREST.update(query_builder, data, opts) assert %FilterBuilder{} = result assert result.method == :patch - assert result.headers["Prefer"] == "return=representation,count=exact" + assert result.headers["prefer"] == "return=representation,count=exact" end end @@ -82,7 +82,7 @@ defmodule Supabase.PostgRESTTest do result = PostgREST.delete(query_builder, opts) assert %FilterBuilder{} = result assert result.method == :delete - assert result.headers["Prefer"] == "return=representation,count=exact" + assert result.headers["prefer"] == "return=representation,count=exact" end end @@ -96,7 +96,7 @@ defmodule Supabase.PostgRESTTest do assert %FilterBuilder{} = result assert result.method == :post - assert result.headers["Prefer"] == + assert result.headers["prefer"] == "resolution=merge-duplicates,return=representation,count=exact" end end