From 745897369d7ca01e036ac5ec9405c19441ad7f11 Mon Sep 17 00:00:00 2001 From: Zoey de Souza Pessanha Date: Thu, 5 Oct 2023 17:29:29 -0300 Subject: [PATCH] Refactor/project restructure (#11) * feat: remove umbrella project * refactor: use client to storage * fix: credo * refactor: documentation * feat: add bang option to init_client/1 * fix: improve ci * feat: supabase basic tests --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- Earthfile | 10 +- README.md | 112 ++++---- apps/supabase/.formatter.exs | 4 - apps/supabase/.gitignore | 26 -- apps/supabase/README.md | 99 ------- apps/supabase/lib/supabase.ex | 156 ---------- apps/supabase/lib/supabase/application.ex | 12 - apps/supabase/lib/supabase/client.ex | 192 ------------- apps/supabase/lib/supabase/client_options.ex | 192 ------------- apps/supabase/mix.exs | 82 ------ apps/supabase_connection/.formatter.exs | 4 - apps/supabase_connection/.gitignore | 26 -- apps/supabase_connection/README.md | 89 ------ .../lib/supabase/connection.ex | 152 ---------- .../lib/supabase/connection/application.ex | 13 - .../lib/supabase/connection_options.ex | 45 --- .../lib/supabase/connection_supervisor.ex | 34 --- apps/supabase_connection/mix.exs | 68 ----- .../test/supabase_connection_test.exs | 8 - apps/supabase_connection/test/test_helper.exs | 1 - apps/supabase_fetcher/.formatter.exs | 4 - apps/supabase_fetcher/.gitignore | 26 -- .../lib/supabase/fetcher/application.ex | 15 - apps/supabase_fetcher/mix.exs | 64 ----- apps/supabase_fetcher/test/test_helper.exs | 1 - apps/supabase_storage/.formatter.exs | 4 - apps/supabase_storage/.gitignore | 26 -- .../lib/supabase/storage/application.ex | 33 --- apps/supabase_storage/mix.exs | 77 ----- apps/supabase_storage/test/test_helper.exs | 1 - apps/supabase_types/.formatter.exs | 4 - apps/supabase_types/.gitignore | 26 -- apps/supabase_types/README.md | 1 - apps/supabase_types/mix.exs | 61 ---- config/runtime.exs | 4 +- flake.nix | 2 +- .../README.md => guides/fetcher.md | 35 +-- .../README.md => guides/storage.md | 18 +- lib/supabase.ex | 141 +++++++++ lib/supabase/application.ex | 42 +++ lib/supabase/client.ex | 160 +++++++++++ lib/supabase/client/auth.ex | 82 ++++++ lib/supabase/client/conn.ex | 55 ++++ lib/supabase/client/db.ex | 28 ++ lib/supabase/client/global.ex | 26 ++ lib/supabase/client_registry.ex | 44 +++ .../lib => lib}/supabase/client_supervisor.ex | 10 + .../lib => lib}/supabase/fetcher.ex | 8 +- .../lib => lib}/supabase/fetcher_behaviour.ex | 0 .../supabase/missing_supabase_config.ex | 11 +- .../lib => lib}/supabase/storage.ex | 267 ++++++++++-------- .../supabase/storage/action_error.ex | 0 .../lib => lib}/supabase/storage/bucket.ex | 0 .../lib => lib}/supabase/storage/cache.ex | 0 .../supabase/storage/cache_reloader.ex | 0 .../lib => lib}/supabase/storage/endpoints.ex | 0 .../storage/handlers/bucket_handler.ex | 0 .../storage/handlers/object_handler.ex | 10 +- .../lib => lib}/supabase/storage/object.ex | 0 .../supabase/storage/object_options.ex | 0 .../supabase/storage/search_options.ex | 0 .../lib => lib}/supabase/storage_behaviour.ex | 10 +- .../lib => lib}/supabase/types/atom.ex | 0 mix.exs | 52 +++- test/supabase_test.exs | 74 +++++ {apps/supabase/test => test}/test_helper.exs | 0 68 files changed, 968 insertions(+), 1783 deletions(-) delete mode 100644 apps/supabase/.formatter.exs delete mode 100644 apps/supabase/.gitignore delete mode 100644 apps/supabase/README.md delete mode 100644 apps/supabase/lib/supabase.ex delete mode 100644 apps/supabase/lib/supabase/application.ex delete mode 100644 apps/supabase/lib/supabase/client.ex delete mode 100644 apps/supabase/lib/supabase/client_options.ex delete mode 100644 apps/supabase/mix.exs delete mode 100644 apps/supabase_connection/.formatter.exs delete mode 100644 apps/supabase_connection/.gitignore delete mode 100644 apps/supabase_connection/README.md delete mode 100644 apps/supabase_connection/lib/supabase/connection.ex delete mode 100644 apps/supabase_connection/lib/supabase/connection/application.ex delete mode 100644 apps/supabase_connection/lib/supabase/connection_options.ex delete mode 100644 apps/supabase_connection/lib/supabase/connection_supervisor.ex delete mode 100644 apps/supabase_connection/mix.exs delete mode 100644 apps/supabase_connection/test/supabase_connection_test.exs delete mode 100644 apps/supabase_connection/test/test_helper.exs delete mode 100644 apps/supabase_fetcher/.formatter.exs delete mode 100644 apps/supabase_fetcher/.gitignore delete mode 100644 apps/supabase_fetcher/lib/supabase/fetcher/application.ex delete mode 100644 apps/supabase_fetcher/mix.exs delete mode 100644 apps/supabase_fetcher/test/test_helper.exs delete mode 100644 apps/supabase_storage/.formatter.exs delete mode 100644 apps/supabase_storage/.gitignore delete mode 100644 apps/supabase_storage/lib/supabase/storage/application.ex delete mode 100644 apps/supabase_storage/mix.exs delete mode 100644 apps/supabase_storage/test/test_helper.exs delete mode 100644 apps/supabase_types/.formatter.exs delete mode 100644 apps/supabase_types/.gitignore delete mode 100644 apps/supabase_types/README.md delete mode 100644 apps/supabase_types/mix.exs rename apps/supabase_fetcher/README.md => guides/fetcher.md (66%) rename apps/supabase_storage/README.md => guides/storage.md (71%) create mode 100644 lib/supabase.ex create mode 100644 lib/supabase/application.ex create mode 100644 lib/supabase/client.ex create mode 100644 lib/supabase/client/auth.ex create mode 100644 lib/supabase/client/conn.ex create mode 100644 lib/supabase/client/db.ex create mode 100644 lib/supabase/client/global.ex create mode 100644 lib/supabase/client_registry.ex rename {apps/supabase/lib => lib}/supabase/client_supervisor.ex (75%) rename {apps/supabase_fetcher/lib => lib}/supabase/fetcher.ex (93%) rename {apps/supabase_fetcher/lib => lib}/supabase/fetcher_behaviour.ex (100%) rename {apps/supabase_connection/lib => lib}/supabase/missing_supabase_config.ex (83%) rename {apps/supabase_storage/lib => lib}/supabase/storage.ex (59%) rename {apps/supabase_storage/lib => lib}/supabase/storage/action_error.ex (100%) rename {apps/supabase_storage/lib => lib}/supabase/storage/bucket.ex (100%) rename {apps/supabase_storage/lib => lib}/supabase/storage/cache.ex (100%) rename {apps/supabase_storage/lib => lib}/supabase/storage/cache_reloader.ex (100%) rename {apps/supabase_storage/lib => lib}/supabase/storage/endpoints.ex (100%) rename {apps/supabase_storage/lib => lib}/supabase/storage/handlers/bucket_handler.ex (100%) rename {apps/supabase_storage/lib => lib}/supabase/storage/handlers/object_handler.ex (96%) rename {apps/supabase_storage/lib => lib}/supabase/storage/object.ex (100%) rename {apps/supabase_storage/lib => lib}/supabase/storage/object_options.ex (100%) rename {apps/supabase_storage/lib => lib}/supabase/storage/search_options.ex (100%) rename {apps/supabase_storage/lib => lib}/supabase/storage_behaviour.ex (86%) rename {apps/supabase_types/lib => lib}/supabase/types/atom.ex (100%) create mode 100644 test/supabase_test.exs rename {apps/supabase/test => test}/test_helper.exs (100%) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e7c87cf..38a7de0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,4 +31,4 @@ jobs: - name: Earthly version run: earthly --version - name: Run lint - run: earthly -P --build-arg GITHUB_REPO=${{ github.repository }} +ci + run: earthly -P +ci diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 163bcde..e491d75 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,4 +31,4 @@ jobs: - name: Earthly version run: earthly --version - name: Run unit tests - run: earthly -P --build-arg GITHUB_REPO=${{ github.repository }} +unit-test + run: earthly -P +unit-test diff --git a/Earthfile b/Earthfile index 969f0c8..e3d8ede 100644 --- a/Earthfile +++ b/Earthfile @@ -1,11 +1,10 @@ VERSION 0.7 deps: - ARG ELIXIR=1.15.4 - FROM hexpm/elixir:${ELIXIR}-alpine + ARG ELIXIR=1.15.6 + FROM hexpm/elixir:${ELIXIR}-erlang-26.1.1-alpine-3.18.2 WORKDIR /src COPY mix.exs mix.lock ./ - COPY --dir apps . # check .earthlyignore RUN mix local.rebar --force RUN mix local.hex --force RUN mix deps.get @@ -13,7 +12,6 @@ deps: ci: FROM +deps - COPY .credo.exs . COPY .formatter.exs . RUN mix clean RUN mix compile --warning-as-errors @@ -27,7 +25,7 @@ unit-test: FROM +deps RUN MIX_ENV=test mix deps.compile COPY mix.exs mix.lock ./ - COPY .env-sample ./ COPY --dir config ./ - COPY --dir apps ./ + COPY --dir lib ./ + COPY --dir test ./ RUN mix test diff --git a/README.md b/README.md index 3cacfc9..ed9be63 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,13 @@ This monorepo houses the collection of Elixir SDK packages for integrating with ## Packages Overview -- **Supabase**: Main entrypoint for the Supabase SDK library, providing easy management for Supabase clients and connections. -- **Supabase Connection**: Handles individual connections to Supabase, encapsulating the API endpoint and credentials. -- **Supabase Storage**: Offers developers a way to store large objects like images, videos, and other files. -- **Supabase PostgREST**: Directly turns your PostgreSQL database into a RESTful API using PostgREST. -- **Supabase Realtime**: Provides a realtime websocket API, enabling listening to database changes. -- **Supabase Auth**: A comprehensive user authentication system, complete with email sign-in, password recovery, session management, and more. -- **Supabase UI**: UI components to help build Supabase-powered applications quickly. -- **Supabase Fetcher**: Customized HTTP client for making requests to Supabase APIs. +- **Supabase**: Main entrypoint for the Supabase SDK library, providing easy management for Supabase clients and connections. [Guide](#usage). +- **Supabase Storage**: Offers developers a way to store large objects like images, videos, and other files. [Guide](./guides/storage.md) +- **Supabase PostgREST**: Directly turns your PostgreSQL database into a RESTful API using PostgREST. [Guide](#) +- **Supabase Realtime**: Provides a realtime websocket API, enabling listening to database changes. [Guide](#) +- **Supabase Auth**: A comprehensive user authentication system, complete with email sign-in, password recovery, session management, and more. [Guide](#) +- **Supabase UI**: UI components to help build Supabase-powered applications quickly. [Guide](#) +- **Supabase Fetcher**: Customized HTTP client for making requests to Supabase APIs. [Guide](./guides/fetcher.md) ## Getting Started @@ -24,86 +23,75 @@ To install the complete SDK: ```elixir def deps do [ - {:supabase_potion, "~> 0.1"} + {:supabase_potion, "~> 0.2"} ] end ``` -Or, install specific packages as needed: - -```elixir -def deps do - [ - {:supabase_storage, "~> 0.1"}, - {:supabase_realtime, "~> 0.1"}, - # ... add other packages - ] -end -``` - -### Clients vs Connections +### Clients A `Supabase.Client` is an Agent that holds general information about Supabase, that can be used to intereact with any of the children integrations, for example: `Supabase.Storage` or `Supabase.UI`. -Also a `Supabase.Client` holds a list of `Supabase.Connection` that can be used to perform operations on different buckets, for example. - `Supabase.Client` is defined as: - `:name` - the name of the client, started by `start_link/1` -- `:connections` - a list of `%{conn_alias => conn_name}`, where `conn_alias` is the alias of the connection and `conn_name` is the name of the connection. +- `:conn` - connection information, the only required option as it is vital to the `Supabase.Client`. + - `:base_url` - The base url of the Supabase API, it is usually in the form `https://.supabase.io`. + - `:api_key` - The API key used to authenticate requests to the Supabase API. + - `:access_token` - Token with specific permissions to access the Supabase API, it is usually the same as the API key. - `:db` - default database options -- `:schema` - default schema to use, defaults to `"public"` + - `:schema` - default schema to use, defaults to `"public"` - `:global` - global options config -- `:headers` - additional headers to use on each request + - `:headers` - additional headers to use on each request - `:auth` - authentication options -- `:auto_refresh_token` - automatically refresh the token when it expires, defaults to `true` -- `:debug` - enable debug mode, defaults to `false` -- `:detect_session_in_url` - detect session in URL, defaults to `true` -- `:flow_type` - authentication flow type, defaults to `"web"` -- `:persist_session` - persist session, defaults to `true` -- `:storage` - storage type -- `:storage_key` - storage key - + - `:auto_refresh_token` - automatically refresh the token when it expires, defaults to `true` + - `:debug` - enable debug mode, defaults to `false` + - `:detect_session_in_url` - detect session in URL, defaults to `true` + - `:flow_type` - authentication flow type, defaults to `"web"` + - `:persist_session` - persist session, defaults to `true` + - `:storage` - storage type + - `:storage_key` - storage key -On the other side, a `Supabase.Connection` is an Agent that holds the connection information and the current bucket, being defined as: +## Usage -- `:base_url` - The base url of the Supabase API, it is usually in the form `https://.supabase.io`. -- `:api_key` - The API key used to authenticate requests to the Supabase API. -- `:access_token` - Token with specific permissions to access the Supabase API, it is usually the same as the API key. -- `:name` - Simple field to track the name of the connection, started by `start_link/1`. -- `:alias` - Field to easily manage multiple connections on a `Supabase.Client` Agent. -- `:bucket` - The current bucket to perform operations on. +The Supabase Elixir SDK provides a flexible way to manage `Supabase.Client` instances, which can, in turn, manage multiple `Supabase.Client` instances. Here's a brief overview of the key concepts: -In simple words, a `Supabase.Client` is a container for multiple `Supabase.Connection`, and each `Supabase.Connection` is a container for a single bucket. +### Starting a Client -### Establishing a Connection - -To start a Supabase connection: +You can start a client using the `Supabase.Client.start_link/1` function. However, it's recommended to use `Supabase.init_client!/1`, which allows you to pass client options and automatically manage `Supabase.Client` processes. ```elixir -Supabase.init_connection(%{base_url: "https://myapp.supabase.io", api_key: "my_api_key", name: :my_conn, alias: :conn}) +iex> Supabase.Client.init_client!(%{conn: %{base_url: "", api_key: ""}}) +{:ok, #PID<0.123.0>} ``` -This will automatically adds the Connection instance to a `DynamicSupervisor` that can be found in the [`Supabase.Connection`](./apps/supabase_connection/lib/supabase/connection_supervisor.ex) specific module documentation. +## Supabase Services -For manually Connection creation and management, please refer to the corresponding documentation: +The Supabase Elixir SDK allows you to interact with various Supabase services: -- [Supabase.Connection](./apps/supabase_connection/lib/supabase/connection.ex) +### Supabase Storage -### Creating a Client +Supabase Storage is a service for storing large objects like images, videos, and other files. It provides a simple API with strong consistency, similar to AWS S3. -To start a Supabase Client: +### Supabase PostgREST -```elixir -Supabase.init_client(%{name: :my_client}, [conn_list]) -``` +PostgREST is a web server that turns your PostgreSQL database into a RESTful API. It automatically generates API endpoints and operations based on your database's structure and permissions. + +### Supabase Realtime + +Supabase Realtime offers a realtime WebSocket API powered by PostgreSQL notifications. You can use it to listen to changes in your database and receive updates instantly as they happen. -This will automatically adds the Client instance to a `DynamicSupervisor` that can be found in the [`Supabase.ClientSupervisor`](./apps/supabase/lib/supabase/client_supervisor.ex) specific module documentation. +### Supabase Auth -For manually Client creation and management, please refer to the corresponding documentation: +Supabase Auth is a comprehensive user authentication system that includes features like email and password sign-in, email verification, password recovery, session management, and more, out of the box. -- [Supabase.Client](./apps/supabase/lib/supabase/client.ex) +### Supabase UI +Supabase UI provides a set of UI components to help you build Supabase-powered applications quickly. It's built on top of Tailwind CSS and Headless UI, and it's fully customizable. The package even includes `Phoenix.LiveView` components! + +### Supabase Fetcher + +Supabase Fetcher is a customized HTTP client for Supabase, mainly used in Supabase Potion. It gives you complete control over how you make requests to any Supabase API. ## Configuration @@ -112,7 +100,8 @@ Ensure your Supabase configurations are set: ```elixir import Config -config :supabase_fetch, +config :supabase, + manage_clients?: true, supabase_url: System.fetch_env!("SUPABASE_BASE_URL"), supabase_key: System.fetch_env!("SUPABASE_API_KEY"), ``` @@ -127,6 +116,7 @@ If you want to track integration-specific roadmaps, check their own README. - [x] Fetcher to interact with the Supabase API in a low-level way - [x] Supabase Storage integration +- [ ] Supabase UI for Phoenix Live View - [ ] Supabase Postgrest integration - [ ] Supabase Auth integration - [ ] Supabase Realtime API integration @@ -154,3 +144,7 @@ This SDK is a comprehensive representation of Supabase's client integrations. Th ## License [MIT](LICENSE) + +--- + +With the Supabase Elixir SDK, you have the tools you need to supercharge your Elixir applications by seamlessly integrating them with Supabase's powerful cloud services. Happy coding! 😄 diff --git a/apps/supabase/.formatter.exs b/apps/supabase/.formatter.exs deleted file mode 100644 index d2cda26..0000000 --- a/apps/supabase/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/supabase/.gitignore b/apps/supabase/.gitignore deleted file mode 100644 index 768926c..0000000 --- a/apps/supabase/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# Ignore .fetch files in case you like to edit your project deps locally. -/.fetch - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -supabase-*.tar - -# Temporary files, for example, from tests. -/tmp/ diff --git a/apps/supabase/README.md b/apps/supabase/README.md deleted file mode 100644 index bbdbdb0..0000000 --- a/apps/supabase/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Supabase Potion - -![Supabase Logo](https://supabase.io/img/supabase-logo.svg) - -The Supabase Elixir SDK is a powerful library that enables seamless integration with [Supabase](https://supabase.io/), a cloud-based platform that provides a suite of tools and services for building modern web and mobile applications. This SDK allows you to interact with various Supabase services, such as storage, authentication, and realtime functionality, directly from your Elixir application. - -## Installation - -To get started with the Supabase Elixir SDK, you need to add it to your Elixir project's dependencies. Open your `mix.exs` file and add the following line to the `deps` function: - -```elixir -def deps do - [ - {:supabase_potion, "~> 0.1"} - ] -end -``` - -After adding the dependency, run `mix deps.get` to fetch and install the SDK. - -## Usage - -The Supabase Elixir SDK provides a flexible way to manage `Supabase.Client` instances, which can, in turn, manage multiple `Supabase.Connection` instances. Here's a brief overview of the key concepts: - -- **Supabase.Client**: This represents a container for multiple connections and holds general information about your Supabase setup. It can be used to interact with various Supabase services. - -- **Supabase.Connection**: A connection holds information about the connection to the Supabase API, including the base URL, API key, and access token. Each connection can be associated with a specific bucket for performing operations. - -### Starting a Connection - -To start a new connection, you can use the `Supabase.Connection.start_link/1` function. For example: - -```elixir -iex> Supabase.Connection.start_link(name: :my_conn, conn_info: %{base_url: "https://myapp.supabase.io", api_key: "my_api_key"}) -{:ok, #PID<0.123.0>} -``` - -Alternatively, you can use the higher-level API provided by the `Supabase` module, using the `Supabase.init_connection/1` function: - -```elixir -iex> Supabase.init_connection(%{base_url: "https://myapp.supabase.io", api_key: "my_api_key", name: :my_conn, alias: :conn1}) -{:ok, #PID<0.123.0>} -``` - -### Starting a Client - -After starting one or more connections, you can start a client using the `Supabase.Client.start_link/1` function. However, it's recommended to use `Supabase.init_client/2`, which allows you to pass client options and a list of connections that the client will manage. For example: - -```elixir -iex> Supabase.Client.init_client(%{db: %{schema: "public"}}, conn_list) -{:ok, #PID<0.123.0>} -``` - -## Acknowledgements - -This SDK package represents the complete SDK for Supabase, encompassing all the functionality of various Supabase client integrations, including: - -- [supabase-storage](https://hex.pm/packages/supabase_storage) -- [supabase-postgrest](https://hex.pm/packages/supabase_postgrest) -- [supabase-realtime](https://hex.pm/packages/supabase_realtime) -- [supabase-auth](https://hex.pm/packages/supabase_auth) -- [supabase-ui](https://hex.pm/packages/supabase_ui) -- [supabase-fetcher](https://hex.pm/packages/supabase_fetcher) - -You can choose to install only specific packages if you don't need the complete functionality. Just add the desired packages to your `deps` list in the `mix.exs` file. - -For more detailed documentation, refer to the [supabase_connection documentation](https://hexdocs.pm/supabase_connection). - -## Supabase Services - -The Supabase Elixir SDK allows you to interact with various Supabase services: - -### Supabase Storage - -Supabase Storage is a service for storing large objects like images, videos, and other files. It provides a simple API with strong consistency, similar to AWS S3. - -### Supabase PostgREST - -PostgREST is a web server that turns your PostgreSQL database into a RESTful API. It automatically generates API endpoints and operations based on your database's structure and permissions. - -### Supabase Realtime - -Supabase Realtime offers a realtime WebSocket API powered by PostgreSQL notifications. You can use it to listen to changes in your database and receive updates instantly as they happen. - -### Supabase Auth - -Supabase Auth is a comprehensive user authentication system that includes features like email and password sign-in, email verification, password recovery, session management, and more, out of the box. - -### Supabase UI - -Supabase UI provides a set of UI components to help you build Supabase-powered applications quickly. It's built on top of Tailwind CSS and Headless UI, and it's fully customizable. The package even includes `Phoenix.LiveView` components! - -### Supabase Fetcher - -Supabase Fetcher is a customized HTTP client for Supabase, mainly used in Supabase Potion. It gives you complete control over how you make requests to any Supabase API. - ---- - -With the Supabase Elixir SDK, you have the tools you need to supercharge your Elixir applications by seamlessly integrating them with Supabase's powerful cloud services. Happy coding! 😄 diff --git a/apps/supabase/lib/supabase.ex b/apps/supabase/lib/supabase.ex deleted file mode 100644 index 8834f26..0000000 --- a/apps/supabase/lib/supabase.ex +++ /dev/null @@ -1,156 +0,0 @@ -defmodule Supabase do - @moduledoc """ - The main entrypoint for the Supabase SDK library. - - ## Installation - - The package can be installed by adding `supabase` to your list of dependencies in `mix.exs`: - - def deps do - [ - {:supabase_potion, "~> 0.1"} - ] - end - - ## Usage - - After installing `:supabase_potion`, you can easily and dynamically manage different `Supabase.Client` and their `Supabase.Connection`. That means you can have multiple Supabase clients that manage multiple Supabase connections. - - ### Clients vs Connections - - A `Supabase.Client` is an Agent that holds general information about Supabase, that can be used to intereact with any of the children integrations, for example: `Supabase.Storage` or `Supabase.UI`. - - Also a `Supabase.Client` holds a list of `Supabase.Connection` that can be used to perform operations on different buckets, for example. - - `Supabase.Client` is defined as: - - - `:name` - the name of the client, started by `start_link/1` - - `:connections` - a list of `%{conn_alias => conn_name}`, where `conn_alias` is the alias of the connection and `conn_name` is the name of the connection. - - `:db` - default database options - - `:schema` - default schema to use, defaults to `"public"` - - `:global` - global options config - - `:headers` - additional headers to use on each request - - `:auth` - authentication options - - `:auto_refresh_token` - automatically refresh the token when it expires, defaults to `true` - - `:debug` - enable debug mode, defaults to `false` - - `:detect_session_in_url` - detect session in URL, defaults to `true` - - `:flow_type` - authentication flow type, defaults to `"web"` - - `:persist_session` - persist session, defaults to `true` - - `:storage` - storage type - - `:storage_key` - storage key - - - On the other side, a `Supabase.Connection` is an Agent that holds the connection information and the current bucket, being defined as: - - - `:base_url` - The base url of the Supabase API, it is usually in the form `https://.supabase.io`. - - `:api_key` - The API key used to authenticate requests to the Supabase API. - - `:access_token` - Token with specific permissions to access the Supabase API, it is usually the same as the API key. - - `:name` - Simple field to track the name of the connection, started by `start_link/1`. - - `:alias` - Field to easily manage multiple connections on a `Supabase.Client` Agent. - - `:bucket` - The current bucket to perform operations on. - - In simple words, a `Supabase.Client` is a container for multiple `Supabase.Connection`, and each `Supabase.Connection` is a container for a single bucket. - - ## Starting a Connection - - To start a new Connection you need to call `Supabase.Connection.start_link/1`: - - iex> Supabase.Connection.start_link(name: :my_conn, conn_info: %{base_url: "https://myapp.supabase.io", api_key: "my_api_key"}) - {:ok, #PID<0.123.0>} - - But usually you would start a Connection using a higher level API, defined in `Supabase` module, using the `Supabase.init_connection/1` function: - - iex> Supabase.init_connection(%{base_url: "https://myapp.supabase.io", api_key: "my_api_key", name: :my_conn, alias: :conn1}) - {:ok, #PID<0.123.0>} - - ## Starting a Client - - After starting some Connections, you then can start a Client calling `Supabase.Client.start_link/1`: - - iex> Supabase.Client.start_link(name: :my_client, client_info: %{db: %{schema: "public"}}) - {:ok, #PID<0.123.0>} - - Notice that this way to start a Client is not recommended, since you will need to manage the `Supabase.Client` manually. Instead, you can use `Supabase.init_client/2`, passing the Client options, and also a list of connections that the Client will manage: - - iex> Supabase.Client.init_client(%{db: %{schema: "public"}}, conn_list) - {:ok, #PID<0.123.0>} - - ## Acknowledgements - - This package represents the complete SDK for Supabase. That means - that it includes all of the functionality of the Supabase client integrations, as: - - - `supabase-storage` - [Hex documentation](https://hex.pm/packages/supabase_storage) - - `supabase-postgrest` - [Hex documentation](https://hex.pm/packages/supabase_postgrest) - - `supabase-realtime` - [Hex documentation](https://hex.pm/packages/supabase_realtime) - - `supabase-auth` - [Hex documentation](https://hex.pm/packages/supabase_auth) - - `supabase-ui` - [Hex documentation](https://hex.pm/packages/supabase_ui) - - `supabase-fetcher` - [Hex documentation](https://hex.pm/packages/supabase_fetcher) - - Of course, if you would like to use only a specific functionality, you can use the following a desired number of packages, example: - - defp deps do - [ - {:supabase_storage, "~> 0.1"}, - {:supabase_realtime, "~> 0.1"}, - ] - end - - Notice that if you prefer to install only a specific package, you will need to manage a `Supabase.Connection` manually. More documentation can be found in [supabase_connection documentation](https://hexdocs.pm/supabase_connection). - - ### Supabase Storage - - Supabase Storage is a service for developers to store large objects like images, videos, and other files. It is a hosted object storage service, like AWS S3, but with a simple API and strong consistency. - - ### Supabase PostgREST - - PostgREST is a web server that turns your PostgreSQL database directly into a RESTful API. The structural constraints and permissions in the database determine the API endpoints and operations. - - ### Supabase Realtime - - Supabase Realtime provides a realtime websocket API powered by PostgreSQL notifications. It allows you to listen to changes in your database, and instantly receive updates as soon as they happen. - - ### Supabase Auth - - Supabase Auth is a feature-complete user authentication system. It provides email & password sign in, email verification, password recovery, session management, and more, out of the box. - - ### Supabase UI - - Supabase UI is a set of UI components that help you quickly build Supabase-powered applications. It is built on top of Tailwind CSS and Headless UI, and is fully customizable. The package provides `Phoenix.LiveView` components! - - ### Supabase Fetcher - - Supabase Fetcher is a customized HTTP client for Supabase. Mainly used in Supabase Potion. If you want a complete control on how to make requests to any Supabase API, you would use this package directly. - """ - - alias Supabase.Client - alias Supabase.ClientOptions - alias Supabase.ClientSupervisor - alias Supabase.Connection - alias Supabase.ConnectionOptions - alias Supabase.ConnectionSupervisor - - @typep changeset :: Ecto.Changeset.t() - - @spec init_client(params, list(Connection.t())) :: - {:ok, pid} | {:error, changeset} | {:error, {atom, changeset}} - when params: ClientOptions.t() - def init_client(%{} = opts, connections) do - with {:ok, opts} <- ClientOptions.parse(opts), - client_opts = ClientOptions.to_client_info(opts, connections), - {:ok, pid} <- ClientSupervisor.start_child({Client, client_opts}) do - {:ok, Client.retrieve_client(pid)} - end - end - - @spec init_connection(params) :: - {:ok, %{pid: pid, name: atom, alias: atom | String.t()}} | {:error, changeset} - when params: ConnectionOptions.t() - def init_connection(%{} = opts) do - with {:ok, opts} <- ConnectionOptions.parse(opts), - conn_params = ConnectionOptions.to_connection_info(opts), - {:ok, pid} <- ConnectionSupervisor.start_child({Connection, conn_params}) do - {:ok, Connection.retrieve_connection(pid)} - end - end -end diff --git a/apps/supabase/lib/supabase/application.ex b/apps/supabase/lib/supabase/application.ex deleted file mode 100644 index b0c79fc..0000000 --- a/apps/supabase/lib/supabase/application.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Supabase.Application do - @moduledoc false - - use Application - - @impl true - def start(_start_type, _args) do - children = [Supabase.ClientSupervisor] - opts = [strategy: :one_for_one, name: Supabase.Supervisor] - Supervisor.start_link(children, opts) - end -end diff --git a/apps/supabase/lib/supabase/client.ex b/apps/supabase/lib/supabase/client.ex deleted file mode 100644 index 1dc5658..0000000 --- a/apps/supabase/lib/supabase/client.ex +++ /dev/null @@ -1,192 +0,0 @@ -defmodule Supabase.Client do - @moduledoc """ - A client for interacting with Supabase. This module is responsible for - managing the connection pool and the connection options. - - ## Usage - - Usually you don't need to use this module directly, instead you should - use the `Supabase` module, available on `:supabase_potion` application. - - However, if you want to manage clients manually, you can leverage this - module to start and stop clients dynamically. To start a single - client manually, you need to add it to your supervision tree: - - defmodule MyApp.Application do - use Application - - def start(_type, _args) do - children = [ - {Supabase.Client, name: :supabase, client_info: %{connections: %{default: :supabase}}} - ] - - opts = [strategy: :one_for_one, name: MyApp.Supervisor] - Supervisor.start_link(children, opts) - end - end - - Notice that starting a Client in this way, Client options will not be - validated, so you need to make sure that the options are correct. Otherwise - application will crash. - - ## Examples - - iex> Supabase.Client.start_link(name: :supabase, client_info: client_info) - {:ok, #PID<0.123.0>} - - iex> Supabase.Client.retrieve_client(:supabase) - %Supabase.Client{ - name: :supabase, - connections: %{ - default: :supabase - }, - db: %Supabase.Client.Db{ - schema: "public" - }, - global: %Supabase.Client.Global{ - headers: %{} - }, - auth: %Supabase.Client.Auth{ - auto_refresh_token: true, - debug: false, - detect_session_in_url: true, - flow_type: nil, - persist_session: true, - storage: nil, - storage_key: nil - } - } - - iex> Supabase.Client.retrieve_connections(:supabase) - %{ - default: :supabase - } - """ - - use Agent - use Ecto.Schema - - import Ecto.Changeset - - @type t :: %__MODULE__{ - name: atom, - connections: %{atom => atom}, - db: db, - global: global, - auth: auth - } - - @type db :: %__MODULE__.Db{ - schema: String.t() - } - - @type global :: %__MODULE__.Global{ - headers: Map.t() - } - - @type auth :: %__MODULE__.Auth{ - auto_refresh_token: boolean(), - debug: boolean(), - detect_session_in_url: boolean(), - flow_type: String.t(), - persist_session: boolean(), - storage: String.t(), - storage_key: String.t() - } - - @type params :: [ - name: atom, - client_info: %{ - connections: %{atom => atom}, - db: %{ - schema: String.t() - }, - global: %{ - headers: Map.t() - }, - auth: %{ - auto_refresh_token: boolean(), - debug: boolean(), - detect_session_in_url: boolean(), - flow_type: String.t(), - persist_session: boolean(), - storage: String.t(), - storage_key: String.t() - } - } - ] - - @primary_key false - embedded_schema do - field(:name, Supabase.Types.Atom) - field(:connections, {:map, Supabase.Types.Atom}) - - embeds_one :db, Db, primary_key: false do - field(:schema, :string) - end - - embeds_one :global, Global, primary_key: false do - field(:headers, :map) - end - - embeds_one :auth, Auth, primary_key: false do - field(:auto_refresh_token, :boolean, default: true) - field(:debug, :boolean, default: false) - field(:detect_session_in_url, :boolean, default: true) - field(:flow_type, :string) - field(:persist_session, :boolean, default: true) - field(:storage, :string) - field(:storage_key, :string) - end - end - - @spec parse!(map) :: Supabase.Client.t() - def parse!(attrs) do - %__MODULE__{} - |> cast(attrs, [:name]) - |> cast_embed(:db, required: true, with: &db_changeset/2) - |> cast_embed(:global, required: true, with: &global_changeset/2) - |> cast_embed(:auth, required: true, with: &auth_changeset/2) - |> put_change(:connections, attrs[:connections] || %{}) - |> validate_required([:name, :connections]) - |> apply_action!(:parse) - end - - defp db_changeset(schema, params) do - schema - |> cast(params, [:schema]) - |> validate_required([:schema]) - end - - defp global_changeset(schema, params) do - cast(schema, params, [:headers]) - end - - defp auth_changeset(schema, params) do - schema - |> cast( - params, - ~w[auto_refresh_token debug detect_session_in_url persist_session flow_type storage storage_key]a - ) - |> validate_required( - ~w[auto_refresh_token debug detect_session_in_url persist_session flow_type]a - ) - end - - def start_link(config) do - name = Keyword.get(config, :name) - client_info = Keyword.get(config, :client_info) - - Agent.start_link(fn -> parse!(client_info) end, name: name || __MODULE__) - end - - @spec retrieve_client(pid) :: Supabase.Client.t() - def retrieve_client(pid) do - Agent.get(pid, & &1) - end - - @spec retrieve_connections(pid) :: %{atom => atom} - def retrieve_connections(pid) do - Agent.get(pid, &Map.get(&1, :connections)) - end -end diff --git a/apps/supabase/lib/supabase/client_options.ex b/apps/supabase/lib/supabase/client_options.ex deleted file mode 100644 index 67a59bd..0000000 --- a/apps/supabase/lib/supabase/client_options.ex +++ /dev/null @@ -1,192 +0,0 @@ -defmodule Supabase.ClientOptions do - @moduledoc """ - A simple changeset that validates and parses client options. - Usually this is used internally by `Supabase` module, but can be used to - validate and parse client options manually using `parse/1` function. - - ## Options - - - `:client_name` - The name of the client. This is used to identify the - client in the connection pool. This option is required. - - `:db` - The database options. This is used to configure the database - connection. This option is required. - - `:schema` - The default schema to use. Defaults to `"public"`. - - `:global` - Global options. This is used to configure global options - that will be used on each request. This option is required. - - `:headers` - Additional headers to use on each request. - - `:auth` - Authentication options. This is used to configure authentication - options. This option is required. - - `:auto_refresh_token` - Automatically refresh the token when it expires. - Defaults to `true`. - - `:debug` - Enable debug mode. Defaults to `false`. - - `:detect_session_in_url` - Detect session in URL. Defaults to `true`. - - `:flow_type` - Authentication flow type. Defaults to `"web"`. - - `:persist_session` - Persist session. Defaults to `true`. - - `:storage` - Storage type. - - `:storage_key` - Storage key. - """ - - import Ecto.Changeset - - alias Supabase.Types.Atom - - @type t :: %{ - client_name: atom, - db: %{ - schema: String.t() - }, - global: %{ - headers: Map.t() - }, - auth: %{ - auto_refresh_token: boolean(), - debug: boolean(), - detect_session_in_url: boolean(), - flow_type: String.t(), - persist_session: boolean(), - storage: String.t(), - storage_key: String.t() - } - } - - @base_types %{ - db: :map, - global: :map, - auth: :map, - client_name: Ecto.ParameterizedType.init(Atom, []) - } - - @db_types %{schema: :string} - @global_types %{headers: :map} - - @auth_types %{ - auto_refresh_token: :boolean, - debug: :boolean, - detect_session_in_url: :boolean, - flow_type: :string, - persist_session: :boolean, - storage: :string, - storage_key: :string - } - - @spec parse(map) :: - {:ok, Supabase.ClientOptions.t()} - | {:error, Ecto.Changeset.t()} - | {:error, {atom, Ecto.Changeset.t()}} - def parse(attrs) do - with {:ok, db} <- cast_db(attrs[:db] || %{}), - {:ok, global} <- cast_global(attrs[:global] || %{}), - {:ok, auth} <- cast_auth(attrs[:auth] || %{}) do - {%{}, @base_types} - |> cast(attrs, Map.keys(@base_types)) - |> validate_required(~w[client_name]a) - |> put_change(:db, db) - |> put_change(:global, global) - |> put_change(:auth, auth) - |> apply_action(:parse) - end - end - - @spec cast_db(map) :: {:ok, map} | {:error, {:db, Ecto.Changeset.t()}} - defp cast_db(attrs) do - {%{}, @db_types} - |> cast(attrs, Map.keys(@db_types)) - |> maybe_put_default_schema() - |> apply_action(:parse_db) - |> case do - {:ok, data} -> {:ok, data} - {:error, changeset} -> {:error, {:db, changeset}} - end - end - - defp maybe_put_default_schema(changeset) do - if get_change(changeset, :schema) do - changeset - else - put_change(changeset, :schema, "public") - end - end - - @spec cast_global(map) :: {:ok, map} | {:error, {:global, Ecto.Changeset.t()}} - defp cast_global(attrs) do - {%{}, @global_types} - |> cast(attrs, Map.keys(@global_types)) - |> apply_action(:parse_global) - |> case do - {:ok, data} -> {:ok, data} - {:error, changeset} -> {:error, {:global, changeset}} - end - end - - @spec cast_auth(map) :: {:ok, map} | {:error, {:auth, Ecto.Changeset.t()}} - defp cast_auth(attrs) do - {%{}, @auth_types} - |> cast(attrs, Map.keys(@auth_types)) - |> maybe_put_default_flow_type() - |> maybe_persist_session() - |> maybe_debug() - |> maybe_auto_refresh_token() - |> maybe_detect_session_in_url() - |> apply_action(:parse_auth) - |> case do - {:ok, data} -> {:ok, data} - {:error, changeset} -> {:error, {:auth, changeset}} - end - end - - defp maybe_put_default_flow_type(changeset) do - if get_change(changeset, :flow_type) do - changeset - else - put_change(changeset, :flow_type, "magicLink") - end - end - - defp maybe_persist_session(changeset) do - if get_change(changeset, :persist_session) do - changeset - else - put_change(changeset, :persist_session, true) - end - end - - defp maybe_debug(changeset) do - if get_change(changeset, :debug) do - changeset - else - put_change(changeset, :debug, false) - end - end - - defp maybe_auto_refresh_token(changeset) do - if get_change(changeset, :auto_refresh_token) do - changeset - else - put_change(changeset, :auto_refresh_token, true) - end - end - - defp maybe_detect_session_in_url(changeset) do - if get_change(changeset, :detect_session_in_url) do - changeset - else - put_change(changeset, :detect_session_in_url, true) - end - end - - @spec to_client_info(t, list(Supabase.Connection.t())) :: Supabase.Client.params() - def to_client_info(data, conns) do - connections = Enum.map(conns, &Map.new([{&1.alias, &1.name}])) - - client_info = - data - |> Map.take(~w[db global auth]a) - |> Map.put(:connections, connections) - |> Map.put(:name, data[:client_name]) - - [ - name: data[:client_name], - client_info: client_info - ] - end -end diff --git a/apps/supabase/mix.exs b/apps/supabase/mix.exs deleted file mode 100644 index 6adf4b4..0000000 --- a/apps/supabase/mix.exs +++ /dev/null @@ -1,82 +0,0 @@ -defmodule Supabase.MixProject do - use Mix.Project - - @version "0.1.0" - @source_url "https://github.com/zoedsoupe/supabase" - - def project do - [ - app: :supabase, - version: @version, - build_path: "../../_build", - config_path: "../../config/config.exs", - deps_path: "../../deps", - lockfile: "../../mix.lock", - elixir: "~> 1.14", - start_permanent: Mix.env() == :prod, - deps: deps(), - docs: docs(), - package: package(), - description: description() - ] - end - - def application do - [ - mod: {Supabase.Application, []}, - extra_applications: [:logger] - ] - end - - defp deps do - [ - {:ecto, "~> 3.10"}, - {:ex_doc, ">= 0.0.0", runtime: false} - ] ++ child_deps(Mix.env()) - end - - defp child_deps(:prod) do - [ - {:supabase_types, "~> 0.1"}, - {:supabase_connection, "~> 0.1"}, - {:supabase_fetcher, "~> 0.1"}, - {:supabase_storage, "~> 0.1"} - ] - end - - defp child_deps(_) do - [ - {:supabase_types, in_umbrella: true}, - {:supabase_connection, in_umbrella: true}, - {:supabase_fetcher, in_umbrella: true}, - {:supabase_storage, in_umbrella: true} - ] - end - - defp package do - %{ - name: "supabase_potion", - licenses: ["MIT"], - contributors: ["zoedsoupe"], - links: %{ - "GitHub" => @source_url, - "Docs" => "https://hexdocs.pm/supabase_potion" - }, - files: - ~w[lib mix.exs README.md ../../LICENSE ../supabase_storage/doc ../supabase_fetcher/doc ../supabase_connection/doc] - } - end - - defp docs do - [ - main: "Supabase", - extras: ["README.md"] - ] - end - - defp description do - """ - Complete Elixir client for Supabase. - """ - end -end diff --git a/apps/supabase_connection/.formatter.exs b/apps/supabase_connection/.formatter.exs deleted file mode 100644 index d2cda26..0000000 --- a/apps/supabase_connection/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/supabase_connection/.gitignore b/apps/supabase_connection/.gitignore deleted file mode 100644 index 32ea5af..0000000 --- a/apps/supabase_connection/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# Ignore .fetch files in case you like to edit your project deps locally. -/.fetch - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -supabase_connection-*.tar - -# Temporary files, for example, from tests. -/tmp/ diff --git a/apps/supabase_connection/README.md b/apps/supabase_connection/README.md deleted file mode 100644 index 4d8820e..0000000 --- a/apps/supabase_connection/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Supabase Connection - -![Supabase Logo](https://supabase.io/img/supabase-logo.svg) - -The **Supabase Connection** is a fundamental component of the Supabase ecosystem, designed to streamline your interaction with the Supabase platform from your Elixir applications. This package enables you to manage connections to Supabase, allowing you to perform various operations on Supabase services such as storage, authentication, and more. - -## Installation - -To get started, you can add the `supabase_connection` package to your Elixir project's dependencies by including it in your `mix.exs` file: - -```elixir -def deps do - [ - {:supabase_connection, "~> 0.1"} - ] -end -``` - -After adding the dependency, run `mix deps.get` to fetch and install the SDK. - -## Starting a Connection - -The core concept of this package is the `Supabase.Connection`, which represents your connection to Supabase. You can start a connection using the `Supabase.Connection.start_link/1` function. For example: - -```elixir -iex> Supabase.Connection.start_link(name: :my_conn, conn_info: %{base_url: "https://myapp.supabase.io", api_key: "my_api_key"}) -{:ok, #PID<0.123.0>} -``` - -However, it's more common to add the connection to your supervision tree for better management. Here's an example of how to do this in your application module: - -```elixir -defmodule MyApp.Application do - use Application - - def start(_type, _args) do - conn_info = %{base_url: "https://myapp.supabase.io", api_key: "my_api_key"} - - children = [ - {Supabase.Connection, conn_info: conn_info, name: :my_conn} - ] - - opts = [strategy: :one_for_one, name: MyApp.Supervisor] - Supervisor.start_link(children, opts) - end -end -``` - -## Using Connections - -Once you have started a connection, you can use it to perform various operations on Supabase services. For example, you can list all the buckets in the storage service: - -```elixir -iex> conn = Supabase.Connection.fetch_current_bucket!(:my_conn) -iex> Supabase.Storage.list_buckets(conn) -{:ok, [ - %Supabase.Storage.Bucket{ - allowed_mime_types: nil, - file_size_limit: nil, - id: "my-bucket-id", - name: "my-bucket", - public: true - } -]} -``` - -You can start multiple connections, each with different credentials, to perform operations on different buckets. - -## Configuration and Fields - -A `Supabase.Connection` holds various fields, including: - -- `:base_url`: The base URL of the Supabase API. -- `:api_key`: The API key used for authentication. -- `:access_token`: A token with specific permissions. -- `:name`: A simple field to track the name of the connection. -- `:alias`: A field to manage multiple connections on a `Supabase.Client` Agent. - -## Acknowledgements - -This package is a critical part of the Supabase Elixir ecosystem, enabling seamless integration with Supabase services. It plays a central role in connecting your Elixir applications to the power of Supabase. - -## Additional Information - -For more details on using this package and the Supabase Elixir SDK as a whole, refer to the [Supabase Elixir SDK documentation](https://hexdocs.pm/supabase_connection). - ---- - -With the **Supabase Connection Elixir SDK**, you can effortlessly manage connections to the Supabase platform, empowering your Elixir applications to leverage the full potential of Supabase's cloud services. Enjoy coding with Supabase! 😄 diff --git a/apps/supabase_connection/lib/supabase/connection.ex b/apps/supabase_connection/lib/supabase/connection.ex deleted file mode 100644 index 95f2f71..0000000 --- a/apps/supabase_connection/lib/supabase/connection.ex +++ /dev/null @@ -1,152 +0,0 @@ -defmodule Supabase.Connection do - @moduledoc """ - Defines the connection to Supabase, it is an Agent that holds the connection - information and the current bucket. - - To start the connection you need to call `Supabase.Connection.start_link/1`: - - iex> Supabase.Connection.start_link(name: :my_conn, conn_info: %{base_url: "https://myapp.supabase.io", api_key: "my_api_key"}) - {:ok, #PID<0.123.0>} - - But usually you would add the connection to your supervision tree: - - defmodule MyApp.Application do - use Application - - def start(_type, _args) do - conn_info = %{base_url: "https://myapp.supabase.io", api_key: "my_api_key"} - - children = [ - {Supabase.Connection, conn_info: conn_info, name: :my_conn} - ] - - opts = [strategy: :one_for_one, name: MyApp.Supervisor] - Supervisor.start_link(children, opts) - end - end - - Once the connection is started you can use it to perform operations on the - storage service, for example to list all the buckets: - - iex> conn = Supabase.Connection.fetch_current_bucket!(:my_conn) - iex> Supabase.Storage.list_buckets(conn) - {:ok, [ - %Supabase.Storage.Bucket{ - allowed_mime_types: nil, - file_size_limit: nil, - id: "my-bucket-id", - name: "my-bucket", - public: true - } - ]} - - Notice that you can start multiple connections, each one with different - credentials, and you can use them to perform operations on different buckets! - - ## Fields - - Currently the connection holds the following fields: - - - `:base_url` - The base url of the Supabase API, it is usually in the form - `https://.supabase.io`. - - `:api_key` - The API key used to authenticate requests to the Supabase API. - - `:access_token` - Token with specific permissions to access the Supabase API, it is usually the same as the API key. - - `name`: Simple field to track the name of the connection, started by `start_link/1`. - - `alias`: Field to easily manage multiple connections on a `Supabase.Client` Agent. - """ - - use Agent - use Ecto.Schema - - alias Supabase.MissingSupabaseConfig - - @type t :: %__MODULE__{ - base_url: base_url, - api_key: api_key, - access_token: access_token, - bucket: bucket - } - - @type params :: [ - name: atom, - conn_info: %{ - base_url: base_url, - api_key: api_key, - access_token: access_token, - bucket: bucket - } - ] - - @type base_url :: String.t() - @type api_key :: String.t() - @type access_token :: String.t() - @type bucket :: struct - - @primary_key false - embedded_schema do - field(:alias, Supabase.Types.Atom) - field(:name, Supabase.Types.Atom) - field(:base_url, :string) - field(:api_key, :string) - field(:access_token, :string) - field(:bucket, :map) - end - - def start_link(args) do - name = Keyword.fetch!(args, :name) - conn_info = Keyword.fetch!(args, :conn_info) - - Agent.start_link(fn -> parse_init_args!(conn_info) end, name: name) - end - - defp parse_init_args!(conn_info) do - base_url = Map.get(conn_info, :base_url) || raise MissingSupabaseConfig, :url - api_key = Map.get(conn_info, :api_key) || raise MissingSupabaseConfig, :key - access_token = Map.get(conn_info, :access_token, api_key) - bucket = Map.get(conn_info, :bucket) - alias = Map.get(conn_info, :alias) - name = Map.get(conn_info, :name) - - %__MODULE__{ - alias: alias, - name: name, - base_url: base_url, - api_key: api_key, - access_token: access_token, - bucket: bucket - } - end - - def fetch_current_bucket!(conn) do - Agent.get(conn, &Map.get(&1, :bucket)) || - raise "No current bucket configured on connection #{inspect(conn)}" - end - - def get_base_url(conn) do - Agent.get(conn, &Map.get(&1, :base_url)) - end - - def get_api_key(conn) do - Agent.get(conn, &Map.get(&1, :api_key)) - end - - def get_access_token(conn) do - Agent.get(conn, &Map.get(&1, :access_token)) - end - - def put_access_token(conn, token) do - Agent.update(conn, &Map.put(&1, :access_token, token)) - end - - def put_current_bucket(conn, bucket) do - Agent.update(conn, &Map.put(&1, :bucket, bucket)) - end - - def remove_current_bucket(conn) do - Agent.update(conn, &Map.delete(&1, :bucket)) - end - - def retrieve_connection(name) do - Agent.get(name, & &1) - end -end diff --git a/apps/supabase_connection/lib/supabase/connection/application.ex b/apps/supabase_connection/lib/supabase/connection/application.ex deleted file mode 100644 index c95c456..0000000 --- a/apps/supabase_connection/lib/supabase/connection/application.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Supabase.Connection.Application do - @moduledoc false - - use Application - - @impl true - def start(_type, _args) do - children = [Supabase.ConnectionSupervisor] - - opts = [strategy: :one_for_one, name: Supabase.Connection.Supervisor] - Supervisor.start_link(children, opts) - end -end diff --git a/apps/supabase_connection/lib/supabase/connection_options.ex b/apps/supabase_connection/lib/supabase/connection_options.ex deleted file mode 100644 index 470b091..0000000 --- a/apps/supabase_connection/lib/supabase/connection_options.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule Supabase.ConnectionOptions do - @moduledoc """ - A changeset for validating and parsing connection options. This is mainly - used internally by `Supabase` module, but can be used to validate and parse - connection options manually. - """ - - import Ecto.Changeset - - alias Supabase.Types.Atom - - @type t :: %{ - alias: atom, - name: atom, - base_url: String.t(), - api_key: String.t(), - access_token: String.t(), - bucket: struct - } - - @types %{ - base_url: :string, - api_key: :string, - access_token: :string, - bucket: :map, - alias: Ecto.ParameterizedType.init(Atom, []), - name: Ecto.ParameterizedType.init(Atom, []) - } - - @spec parse(map) :: {:ok, Supabase.ConnectionOptions.t()} | {:error, Ecto.Changeset.t()} - def parse(attrs) do - {%{}, @types} - |> cast(attrs, Map.keys(@types)) - |> validate_required(~w[base_url api_key alias name]a) - |> apply_action(:parse_connection_options) - end - - @spec to_connection_info(t) :: Supabase.Connection.params() - def to_connection_info(data) do - [ - name: data[:name], - conn_info: data - ] - end -end diff --git a/apps/supabase_connection/lib/supabase/connection_supervisor.ex b/apps/supabase_connection/lib/supabase/connection_supervisor.ex deleted file mode 100644 index 1432268..0000000 --- a/apps/supabase_connection/lib/supabase/connection_supervisor.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Supabase.ConnectionSupervisor do - @moduledoc """ - A supervisor for all connections. In most cases this should be started - automatically by the application supervisor and be used mainly by - the `Supabase` module, availaton on `:supabase_potion` application. - - Although if you want to manage connections manually, you can leverage - this module to start and stop connections dynamically. To see how to start - a single connection manually, check `Supabase.Connection` module docs. - - ## Examples - - iex> Supabase.ConnectionSupervisor.start_link([]) - {:ok, #PID<0.123.0>} - - iex> Supabase.ConnectionSupervisor.start_child({Supabase.Connection, opts}) - {:ok, #PID<0.123.0>} - """ - - use DynamicSupervisor - - @impl true - def init(_init_arg) do - DynamicSupervisor.init(strategy: :one_for_one) - end - - def start_link(init) do - DynamicSupervisor.start_link(__MODULE__, init, name: __MODULE__) - end - - def start_child(child_spec) do - DynamicSupervisor.start_child(__MODULE__, child_spec) - end -end diff --git a/apps/supabase_connection/mix.exs b/apps/supabase_connection/mix.exs deleted file mode 100644 index 013fb22..0000000 --- a/apps/supabase_connection/mix.exs +++ /dev/null @@ -1,68 +0,0 @@ -defmodule Supabase.Connection.MixProject do - use Mix.Project - - @version "0.1.0" - @source_url "https://github.com/zoedsoupe/supabase/tree/main/apps/supabase_connection" - - def project do - [ - app: :supabase_connection, - version: @version, - build_path: "../../_build", - config_path: "../../config/config.exs", - deps_path: "../../deps", - lockfile: "../../mix.lock", - elixir: "~> 1.14", - start_permanent: Mix.env() == :prod, - deps: deps(), - package: package(), - description: description(), - docs: docs() - ] - end - - # Run "mix help compile.app" to learn about applications. - def application do - [ - extra_applications: [:logger], - mod: {Supabase.Connection.Application, []} - ] - end - - defp deps do - [ - {:ecto, "~> 3.10"}, - if(Mix.env() == :prod, - do: {:supabase_types, "~> 0.1"}, - else: {:supabase_types, in_umbrella: true} - ), - {:ex_doc, ">= 0.0.0", runtime: false} - ] - end - - defp package do - %{ - name: "supabase_connection", - licenses: ["MIT"], - contributors: ["zoedsoupe"], - links: %{ - "GitHub" => @source_url, - "Docs" => "https://hexdocs.pm/supabase_connection" - }, - files: ~w[lib mix.exs README.md ../../LICENSE] - } - end - - defp docs do - [ - main: "Supabase.Connection", - extras: ["README.md"] - ] - end - - defp description do - """ - Defines a Supabase Connection for usage in the Supabase Potion. - """ - end -end diff --git a/apps/supabase_connection/test/supabase_connection_test.exs b/apps/supabase_connection/test/supabase_connection_test.exs deleted file mode 100644 index 1ec5760..0000000 --- a/apps/supabase_connection/test/supabase_connection_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule SupabaseConnectionTest do - use ExUnit.Case - doctest SupabaseConnection - - test "greets the world" do - assert SupabaseConnection.hello() == :world - end -end diff --git a/apps/supabase_connection/test/test_helper.exs b/apps/supabase_connection/test/test_helper.exs deleted file mode 100644 index 869559e..0000000 --- a/apps/supabase_connection/test/test_helper.exs +++ /dev/null @@ -1 +0,0 @@ -ExUnit.start() diff --git a/apps/supabase_fetcher/.formatter.exs b/apps/supabase_fetcher/.formatter.exs deleted file mode 100644 index d2cda26..0000000 --- a/apps/supabase_fetcher/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/supabase_fetcher/.gitignore b/apps/supabase_fetcher/.gitignore deleted file mode 100644 index 3bd5786..0000000 --- a/apps/supabase_fetcher/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# Ignore .fetch files in case you like to edit your project deps locally. -/.fetch - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -supa_fetch-*.tar - -# Temporary files, for example, from tests. -/tmp/ diff --git a/apps/supabase_fetcher/lib/supabase/fetcher/application.ex b/apps/supabase_fetcher/lib/supabase/fetcher/application.ex deleted file mode 100644 index 8b2d68c..0000000 --- a/apps/supabase_fetcher/lib/supabase/fetcher/application.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Supabase.Fetcher.Application do - @moduledoc "Simple Supervisor to manage a Finch pool" - - use Application - - @impl true - def start(_type, _args) do - children = [ - {Finch, name: Supabase.Finch, pools: %{:default => [size: 10]}} - ] - - opts = [strategy: :one_for_one, name: Supabase.Fetch.Supervisor] - Supervisor.start_link(children, opts) - end -end diff --git a/apps/supabase_fetcher/mix.exs b/apps/supabase_fetcher/mix.exs deleted file mode 100644 index aeef797..0000000 --- a/apps/supabase_fetcher/mix.exs +++ /dev/null @@ -1,64 +0,0 @@ -defmodule Supabase.Fetcher.MixProject do - use Mix.Project - - @version "0.1.0" - @source_url "https://github.com/zoedsoupe/supabase/tree/main/apps/supabase_fetcher" - - def project do - [ - app: :supabase_fetcher, - version: @version, - build_path: "../../_build", - config_path: "../../config/config.exs", - deps_path: "../../deps", - lockfile: "../../mix.lock", - elixir: "~> 1.14", - start_permanent: Mix.env() == :prod, - deps: deps(), - package: package(), - description: description(), - docs: docs() - ] - end - - def application do - [ - mod: {Supabase.Fetcher.Application, []}, - extra_applications: [:logger] - ] - end - - defp deps do - [ - {:finch, "~> 0.16"}, - {:jason, "~> 1.4"}, - {:ex_doc, ">= 0.0.0", runtime: false} - ] - end - - defp package do - %{ - name: "supabase_fetcher", - licenses: ["MIT"], - contributors: ["zoedsoupe"], - links: %{ - "GitHub" => @source_url, - "Docs" => "https://hexdocs.pm/supabase_fetcher" - }, - files: ~w[lib mix.exs README.md ../../LICENSE] - } - end - - defp docs do - [ - main: "Supabase.Fetcher", - extras: ["README.md"] - ] - end - - defp description do - """ - A customized HTTP client for Supabase. Mainly used in Supabase Potion. - """ - end -end diff --git a/apps/supabase_fetcher/test/test_helper.exs b/apps/supabase_fetcher/test/test_helper.exs deleted file mode 100644 index 869559e..0000000 --- a/apps/supabase_fetcher/test/test_helper.exs +++ /dev/null @@ -1 +0,0 @@ -ExUnit.start() diff --git a/apps/supabase_storage/.formatter.exs b/apps/supabase_storage/.formatter.exs deleted file mode 100644 index d2cda26..0000000 --- a/apps/supabase_storage/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/supabase_storage/.gitignore b/apps/supabase_storage/.gitignore deleted file mode 100644 index 18bdad3..0000000 --- a/apps/supabase_storage/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# Ignore .fetch files in case you like to edit your project deps locally. -/.fetch - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -supabase_storage-*.tar - -# Temporary files, for example, from tests. -/tmp/ diff --git a/apps/supabase_storage/lib/supabase/storage/application.ex b/apps/supabase_storage/lib/supabase/storage/application.ex deleted file mode 100644 index ace0457..0000000 --- a/apps/supabase_storage/lib/supabase/storage/application.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Supabase.Storage.Application do - @moduledoc "Entrypoint for the Apllication, defines the Supervision tree" - - use Application - - @impl true - def start(_type, _args) do - children = - if start_cache?() do - [ - {Supabase.Storage.Cache, cache_max_size: cache_max_size()}, - {Supabase.Storage.CacheReloader, reload_interval: reload_interval()} - ] - else - [] - end - - opts = [strategy: :one_for_one, name: Supabase.Storage.Supervisor] - Supervisor.start_link(children, opts) - end - - defp cache_max_size do - Application.get_env(:supabase_storage, :cache_max_size, 100) - end - - defp start_cache? do - Application.get_env(:supabase_storage, :cache_buckets?) - end - - defp reload_interval do - Application.get_env(:supabase_storage, :reload_interval) - end -end diff --git a/apps/supabase_storage/mix.exs b/apps/supabase_storage/mix.exs deleted file mode 100644 index 239105b..0000000 --- a/apps/supabase_storage/mix.exs +++ /dev/null @@ -1,77 +0,0 @@ -defmodule Supabase.Storage.MixProject do - use Mix.Project - - @version "0.1.0" - @source_url "https://github.com/zoedsoupe/supabase/tree/main/apps/supabase_storage" - - def project do - [ - app: :supabase_storage, - version: @version, - build_path: "../../_build", - config_path: "../../config/config.exs", - deps_path: "../../deps", - lockfile: "../../mix.lock", - elixir: "~> 1.14", - start_permanent: Mix.env() == :prod, - deps: deps(), - package: package(), - description: description(), - docs: docs() - ] - end - - def application do - [ - extra_applications: [:logger], - mod: {Supabase.Storage.Application, []} - ] - end - - defp deps do - [ - {:ecto, "~> 3.10"}, - {:ex_doc, ">= 0.0.0", runtime: false} - ] ++ child_deps(Mix.env()) - end - - defp child_deps(:prod) do - [ - {:supabase_fetcher, "~> 0.1"}, - {:supabase_connection, "~> 0.1"} - ] - end - - defp child_deps(_) do - [ - {:supabase_fetcher, in_umbrella: true}, - {:supabase_connection, in_umbrella: true} - ] - end - - defp package do - %{ - name: "supabase_storage", - licenses: ["MIT"], - contributors: ["zoedsoupe"], - links: %{ - "GitHub" => @source_url, - "Docs" => "https://hexdocs.pm/supabase_storage" - }, - files: ~w[lib mix.exs README.md ../../LICENSE] - } - end - - defp docs do - [ - main: "Supabase.Storage", - extras: ["README.md"] - ] - end - - defp description do - """ - High level Elixir client for Supabase Storage. - """ - end -end diff --git a/apps/supabase_storage/test/test_helper.exs b/apps/supabase_storage/test/test_helper.exs deleted file mode 100644 index 869559e..0000000 --- a/apps/supabase_storage/test/test_helper.exs +++ /dev/null @@ -1 +0,0 @@ -ExUnit.start() diff --git a/apps/supabase_types/.formatter.exs b/apps/supabase_types/.formatter.exs deleted file mode 100644 index d2cda26..0000000 --- a/apps/supabase_types/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/apps/supabase_types/.gitignore b/apps/supabase_types/.gitignore deleted file mode 100644 index 218d2ea..0000000 --- a/apps/supabase_types/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where third-party dependencies like ExDoc output generated docs. -/doc/ - -# Ignore .fetch files in case you like to edit your project deps locally. -/.fetch - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Ignore package tarball (built via "mix hex.build"). -supabase_types-*.tar - -# Temporary files, for example, from tests. -/tmp/ diff --git a/apps/supabase_types/README.md b/apps/supabase_types/README.md deleted file mode 100644 index 2311cec..0000000 --- a/apps/supabase_types/README.md +++ /dev/null @@ -1 +0,0 @@ -# Supabase Types diff --git a/apps/supabase_types/mix.exs b/apps/supabase_types/mix.exs deleted file mode 100644 index 902aa92..0000000 --- a/apps/supabase_types/mix.exs +++ /dev/null @@ -1,61 +0,0 @@ -defmodule Supabase.Types.MixProject do - use Mix.Project - - @version "0.1.1" - @source_url "https://github.com/zoedsoupe/supabase/tree/main/apps/supabase_types" - - def project do - [ - app: :supabase_types, - version: @version, - build_path: "../../_build", - config_path: "../../config/config.exs", - deps_path: "../../deps", - lockfile: "../../mix.lock", - elixir: "~> 1.14", - start_permanent: Mix.env() == :prod, - deps: deps(), - package: package(), - description: description(), - docs: docs() - ] - end - - def application do - [ - extra_applications: [:logger] - ] - end - - defp deps do - [ - {:ecto, "~> 3.10"}, - {:ex_doc, ">= 0.0.0", runtime: false} - ] - end - - defp package do - %{ - name: "supabase_types", - licenses: ["MIT"], - contributors: ["zoedsoupe"], - links: %{ - "GitHub" => @source_url, - "Docs" => "https://hexdocs.pm/supabase_types" - }, - files: ~w[lib mix.exs README.md ../../LICENSE] - } - end - - defp docs do - [ - extras: ["README.md"] - ] - end - - defp description do - """ - Define some common entities and types for Supabase. - """ - end -end diff --git a/config/runtime.exs b/config/runtime.exs index 05c79d0..f47a4e9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,5 +1,5 @@ import Config -config :supabase_storage, - cache_buckets?: true, +config :supabase, + cache_buckets?: false, reload_interval: 60_000 diff --git a/flake.nix b/flake.nix index ddb6a1f..2f6d0e3 100644 --- a/flake.nix +++ b/flake.nix @@ -15,7 +15,7 @@ name = "supabase-potion"; shellHook = "mkdir -p $PWD/.nix-mix"; packages = with pkgs; - [elixir postgresql_15] + [beam.packages.erlangR26.elixir postgresql_15] ++ lib.optional stdenv.isDarwin [ darwin.apple_sdk.frameworks.CoreServices darwin.apple_sdk.frameworks.CoreFoundation diff --git a/apps/supabase_fetcher/README.md b/guides/fetcher.md similarity index 66% rename from apps/supabase_fetcher/README.md rename to guides/fetcher.md index a011269..3ac253d 100644 --- a/apps/supabase_fetcher/README.md +++ b/guides/fetcher.md @@ -1,22 +1,6 @@ # Supabase Fetcher -![Supabase Logo](https://supabase.io/img/supabase-logo.svg) - -The **Supabase Fetcher** is a versatile HTTP client that serves as an entry point for interacting with Supabase APIs from your Elixir applications. While it's often recommended to use higher-level APIs for specific Supabase services like [supabase-storage](https://github.com/zoedsoupe/supabase/tree/main/apps/supabase_storage) or the all-in-one package [supabase-potion](https://github.com/zoedsoupe/supabase), this SDK provides low-level capabilities for fine-grained control and customization. - -## Installation - -To get started with the Supabase Fetcher Elixir SDK, add it to your Elixir project's dependencies by including it in your `mix.exs` file: - -```elixir -def deps do - [ - {:supabase_fetcher, "~> 0.1"} - ] -end -``` - -After adding the dependency, run `mix deps.get` to fetch and install the SDK. +The **Supabase Fetcher** is a versatile HTTP client that serves as an entry point for interacting with Supabase APIs from your Elixir applications. While it's often recommended to use higher-level APIs for specific Supabase services like [`Supabase.Storage`](https://github.com/zoedsoupe/supabase/tree/main/lib/supabase/storage.ex), this SDK provides low-level capabilities for fine-grained control and customization. ## Overview @@ -24,16 +8,25 @@ This SDK allows you to make HTTP requests to Supabase and handle responses effic ## Usage +### Basic Request + +TODO + ### Streaming Large Files You can use `Supabase.Fetcher.stream/3` to make a GET request to a URL and stream back the response. This function is especially useful for streaming large file downloads. Custom `Finch` options can also be passed for more control over the request. ```elixir -{iex> {status, stream} = Supabase.Fetcher.stream("https://example.com") +iex> {status, stream} = Supabase.Fetcher.stream("https://example.com") iex> file = File.stream!("path/to/file", [], 4096) Stream.run Stream.into(stream, file) ``` +```elixir +iex> headers = [{"authorization", ""}] +iex> Supabase.Fetcher.stram("", headers, opts) # opts are passed directly to Finch.stream/5 +``` + ### Making HTTP Requests The SDK provides convenient functions for making common HTTP requests: @@ -68,8 +61,4 @@ While the Supabase Fetcher Elixir SDK offers low-level control for making HTTP r ## Additional Information -For more details on using this package, refer to the [Supabase Fetcher documentation](https://hexdocs.pm/supabase_fetcher). - ---- - -With the **Supabase Fetcher Elixir SDK**, you have the power to interact with Supabase APIs directly from your Elixir applications. Whether you need to stream large files, make custom HTTP requests, or upload files, this SDK has you covered. Enjoy the flexibility and control it offers in integrating with Supabase services! 😄 +For more details on using this package, refer to the [Supabase Fetcher documentation](https://hexdocs.pm/supabase_potion). diff --git a/apps/supabase_storage/README.md b/guides/storage.md similarity index 71% rename from apps/supabase_storage/README.md rename to guides/storage.md index d15c421..18b99cf 100644 --- a/apps/supabase_storage/README.md +++ b/guides/storage.md @@ -1,18 +1,6 @@ # Supabase Storage -This package provides a set of Elixir functions that integrate seamlessly with Supabase's Storage API, allowing developers to perform various operations on buckets and objects. - -### Installation - -To add this package to your project, include it in your mix.exs file: - -```elixir -def deps do - [ - {:supabase_storage, "~> 0.1"} - ] -end -``` +This module provides a set of Elixir functions that integrate seamlessly with Supabase's Storage API, allowing developers to perform various operations on buckets and objects. ### Features @@ -61,7 +49,3 @@ Supabase.Storage.create_signed_url(conn, bucket, "avatars/some.png", 3600) ### Permissions Ensure that the appropriate policy permissions are set in Supabase to carry out the required operations. Refer to each method's documentation for detailed information on permissions. - -### Contributing - -We welcome contributions from the community! Please submit PRs for bug fixes, features, or any improvements. diff --git a/lib/supabase.ex b/lib/supabase.ex new file mode 100644 index 0000000..6104a25 --- /dev/null +++ b/lib/supabase.ex @@ -0,0 +1,141 @@ +defmodule Supabase do + @moduledoc """ + The main entrypoint for the Supabase SDK library. + + ## Installation + + The package can be installed by adding `supabase` to your list of dependencies in `mix.exs`: + + def deps do + [ + {:supabase_potion, "~> 0.1"} + ] + end + + ## Usage + + After installing `:supabase_potion`, you can easily and dynamically manage different `Supabase.Client`! + + ### Config + + The library offers a bunch of config options that can be used to control management of clients and other options. + + - `manage_clients` - whether to manage clients automatically, defaults to `true` + + You can set up the library on your `config.exs`: + + config :supabase, manage_clients: false + + ### Clients + + A `Supabase.Client` is an Agent that holds general information about Supabase, that can be used to intereact with any of the children integrations, for example: `Supabase.Storage` or `Supabase.UI`. + + Also a `Supabase.Client` holds a list of `Supabase.Connection` that can be used to perform operations on different buckets, for example. + + `Supabase.Client` is defined as: + + - `:name` - the name of the client, started by `start_link/1` + - `:conn` - connection information, the only required option as it is vital to the `Supabase.Client`. + - `:base_url` - The base url of the Supabase API, it is usually in the form `https://.supabase.io`. + - `:api_key` - The API key used to authenticate requests to the Supabase API. + - `:access_token` - Token with specific permissions to access the Supabase API, it is usually the same as the API key. + - `:db` - default database options + - `:schema` - default schema to use, defaults to `"public"` + - `:global` - global options config + - `:headers` - additional headers to use on each request + - `:auth` - authentication options + - `:auto_refresh_token` - automatically refresh the token when it expires, defaults to `true` + - `:debug` - enable debug mode, defaults to `false` + - `:detect_session_in_url` - detect session in URL, defaults to `true` + - `:flow_type` - authentication flow type, defaults to `"web"` + - `:persist_session` - persist session, defaults to `true` + - `:storage` - storage type + - `:storage_key` - storage key + + + ## Starting a Client + + You then can start a Client calling `Supabase.Client.start_link/1`: + + iex> Supabase.Client.start_link(name: :my_client, client_info: %{db: %{schema: "public"}}) + {:ok, #PID<0.123.0>} + + Notice that this way to start a Client is not recommended, since you will need to manage the `Supabase.Client` manually. Instead, you can use `Supabase.init_client!/1`, passing the Client options: + + iex> Supabase.Client.init_client!(%{conn: %{base_url: "", api_key: ""}}) + {:ok, #PID<0.123.0>} + + ## Acknowledgements + + This package represents the complete SDK for Supabase. That means + that it includes all of the functionality of the Supabase client integrations, as: + + - `Supabase.Fetcher` + - `Supabase.Storage` + - `supabase-postgrest` - TODO + - `supabase-realtime` - TODO + - `supabase-auth`- TODO + - `supabase-ui` - TODO + + ### Supabase Storage + + Supabase Storage is a service for developers to store large objects like images, videos, and other files. It is a hosted object storage service, like AWS S3, but with a simple API and strong consistency. + + ### Supabase PostgREST + + PostgREST is a web server that turns your PostgreSQL database directly into a RESTful API. The structural constraints and permissions in the database determine the API endpoints and operations. + + ### Supabase Realtime + + Supabase Realtime provides a realtime websocket API powered by PostgreSQL notifications. It allows you to listen to changes in your database, and instantly receive updates as soon as they happen. + + ### Supabase Auth + + Supabase Auth is a feature-complete user authentication system. It provides email & password sign in, email verification, password recovery, session management, and more, out of the box. + + ### Supabase UI + + Supabase UI is a set of UI components that help you quickly build Supabase-powered applications. It is built on top of Tailwind CSS and Headless UI, and is fully customizable. The package provides `Phoenix.LiveView` components! + + ### Supabase Fetcher + + Supabase Fetcher is a customized HTTP client for Supabase. Mainly used in Supabase Potion. If you want a complete control on how to make requests to any Supabase API, you would use this package directly. + """ + + alias Supabase.Client + alias Supabase.ClientRegistry + alias Supabase.ClientSupervisor + + alias Supabase.MissingSupabaseConfig + + @typep changeset :: Ecto.Changeset.t() + + @spec init_client(params) :: {:ok, pid} | {:error, changeset} + when params: Client.params() + def init_client(%{} = opts) do + with {:ok, opts} <- Client.parse(opts) do + name = ClientRegistry.named(opts.name) + client_opts = [name: name, client_info: opts] + ClientSupervisor.start_child({Client, client_opts}) + end + end + + def init_client!(%{} = opts) do + conn = Map.get(opts, :conn, %{}) + opts = maybe_merge_config_from_application(conn, opts) + + case init_client(opts) do + {:ok, pid} -> pid + {:error, changeset} -> raise Ecto.InvalidChangesetError, changeset: changeset, action: :init + end + end + + defp maybe_merge_config_from_application(%{base_url: _, api_key: _}, opts), do: opts + + defp maybe_merge_config_from_application(%{}, opts) do + base_url = Application.get_env(:supabase, :supabase_url) || raise MissingSupabaseConfig, :url + api_key = Application.get_env(:supabase, :supabase_key) || raise MissingSupabaseConfig, :key + + Map.put(opts, :conn, %{base_url: base_url, api_key: api_key}) + end +end diff --git a/lib/supabase/application.ex b/lib/supabase/application.ex new file mode 100644 index 0000000..08ad874 --- /dev/null +++ b/lib/supabase/application.ex @@ -0,0 +1,42 @@ +defmodule Supabase.Application do + @moduledoc false + + use Application + + alias Supabase.Storage + + @finch_opts [name: Supabase.Finch, pools: %{:default => [size: 10]}] + + @impl true + def start(_start_type, _args) do + children = [ + {Finch, @finch_opts}, + if(manage_clients?(), do: Supabase.ClientSupervisor), + if(manage_clients?(), do: Supabase.ClientRegistry), + if(start_cache?(), do: {Storage.Cache, cache_max_size: cache_max_size()}), + if(start_cache?(), do: {Storage.CacheSupervisor, reload_interval: reload_interval()}) + ] + + opts = [strategy: :one_for_one, name: Supabase.Supervisor] + + children + |> Enum.reject(&is_nil/1) + |> Supervisor.start_link(opts) + end + + defp manage_clients? do + Application.get_env(:supabase, :manage_clients, true) + end + + defp cache_max_size do + Application.get_env(:supabase, :cache_max_size, 100) + end + + defp start_cache? do + Application.get_env(:supabase, :cache_buckets?) + end + + defp reload_interval do + Application.get_env(:supabase, :reload_interval, 60_000) + end +end diff --git a/lib/supabase/client.ex b/lib/supabase/client.ex new file mode 100644 index 0000000..f0cc115 --- /dev/null +++ b/lib/supabase/client.ex @@ -0,0 +1,160 @@ +defmodule Supabase.Client do + @moduledoc """ + A client for interacting with Supabase. This module is responsible for + managing the connection pool and the connection options. + + ## Usage + + Usually you don't need to use this module directly, instead you should + use the `Supabase` module, available on `:supabase_potion` application. + + However, if you want to manage clients manually, you can leverage this + module to start and stop clients dynamically. To start a single + client manually, you need to add it to your supervision tree: + + defmodule MyApp.Application do + use Application + + def start(_type, _args) do + children = [ + {Supabase.Client, name: :supabase, client_info: %Supabase.Client{}} + ] + + opts = [strategy: :one_for_one, name: MyApp.Supervisor] + Supervisor.start_link(children, opts) + end + end + + Notice that starting a Client in this way, Client options will not be + validated, so you need to make sure that the options are correct. Otherwise + application will crash. + + ## Examples + + iex> Supabase.Client.start_link(name: :supabase, client_info: client_info) + {:ok, #PID<0.123.0>} + + iex> Supabase.Client.retrieve_client(:supabase) + %Supabase.Client{ + name: :supabase, + conn: %{ + base_url: "https://.supabase.io", + api_key: "", + access_token: "" + }, + db: %Supabase.Client.Db{ + schema: "public" + }, + global: %Supabase.Client.Global{ + headers: %{} + }, + auth: %Supabase.Client.Auth{ + auto_refresh_token: true, + debug: false, + detect_session_in_url: true, + flow_type: :implicit, + persist_session: true, + storage: nil, + storage_key: "sb--auth-token" + } + } + + iex> Supabase.Client.retrieve_connection(:supabase) + %Supabase.Client.Conn{ + base_url: "https://.supabase.io", + api_key: "", + access_token: "" + } + """ + + use Agent + use Ecto.Schema + + import Ecto.Changeset + + alias Supabase.Client.Auth + alias Supabase.Client.Conn + alias Supabase.Client.Db + alias Supabase.Client.Global + + alias Supabase.ClientRegistry + + defguard is_client(v) when is_atom(v) or is_pid(v) + + @type t :: %__MODULE__{ + name: atom, + conn: Conn.t(), + db: Db.t(), + global: Global.t(), + auth: Auth.t() + } + + @type params :: %{ + name: atom, + conn: Conn.params(), + db: Db.params(), + global: Global.params(), + auth: Auth.params() + } + + @primary_key false + embedded_schema do + field(:name, Supabase.Types.Atom) + + embeds_one(:conn, Conn) + embeds_one(:db, Db) + embeds_one(:global, Global) + embeds_one(:auth, Auth) + end + + @spec parse(params) :: {:ok, Supabase.Client.t()} | {:error, Ecto.Changeset.t()} + def parse(attrs) do + %__MODULE__{} + |> cast(attrs, [:name]) + |> cast_embed(:conn, required: true) + |> cast_embed(:db, required: false) + |> cast_embed(:global, required: false) + |> cast_embed(:auth, required: false) + |> validate_required([:name]) + |> apply_action(:parse) + end + + @spec parse!(params) :: Supabase.Client.t() + def parse!(attrs) do + case parse(attrs) do + {:ok, changeset} -> + changeset + + {:error, changeset} -> + raise Ecto.InvalidChangesetError, changeset: changeset, action: :parse + end + end + + def start_link(config) do + name = Keyword.get(config, :name) + client_info = Keyword.get(config, :client_info) + + Agent.start_link(fn -> maybe_parse(client_info) end, name: name || __MODULE__) + end + + defp maybe_parse(%__MODULE__{} = client), do: client + defp maybe_parse(params), do: parse!(params) + + @spec retrieve_client(name) :: Supabase.Client.t() | nil + when name: atom | pid + def retrieve_client(name) when is_atom(name) do + pid = ClientRegistry.lookup(name) + pid && Agent.get(pid, & &1) + end + + def retrieve_client(pid) when is_pid(pid), do: Agent.get(pid, & &1) + + @spec retrieve_connection(name) :: Conn.t() | nil + when name: atom | pid + def retrieve_connection(name) when is_atom(name) do + pid = ClientRegistry.lookup(name) + pid && Agent.get(pid, &Map.get(&1, :conn)) + end + + def retrieve_connection(pid) when is_pid(pid), do: Agent.get(pid, &Map.get(&1, :conn)) +end diff --git a/lib/supabase/client/auth.ex b/lib/supabase/client/auth.ex new file mode 100644 index 0000000..39a74d7 --- /dev/null +++ b/lib/supabase/client/auth.ex @@ -0,0 +1,82 @@ +defmodule Supabase.Client.Auth do + @moduledoc """ + Auth configuration schema. This schema is used to configure the auth + options. This schema is embedded in the `Supabase.Client` schema. + + ## Fields + + - `:auto_refresh_token` - Automatically refresh the token when it expires. Defaults to `true`. + - `:debug` - Enable debug mode. Defaults to `false`. + - `:detect_session_in_url` - Detect session in URL. Defaults to `true`. + - `:flow_type` - Authentication flow type. Defaults to `"implicit"`. + - `:persist_session` - Persist session. Defaults to `true`. + - `:storage` - Storage type. + - `:storage_key` - Storage key. Default to `"sb-$host-auth-token"` where $host is the hostname of your Supabase URL. + + For more information about the auth options, see the documentation for + the [client](https://supabase.com/docs/reference/javascript/initializing) and + [auth guides](https://supabase.com/docs/guides/auth) + """ + + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{ + auto_refresh_token: boolean(), + debug: boolean(), + detect_session_in_url: boolean(), + flow_type: String.t(), + persist_session: boolean(), + storage: String.t(), + storage_key: String.t() + } + + @type params :: %{ + auto_refresh_token: boolean(), + debug: boolean(), + detect_session_in_url: boolean(), + flow_type: String.t(), + persist_session: boolean(), + storage: String.t(), + storage_key: String.t() + } + + @storage_key_template "sb-$host-auth-token" + + @primary_key false + embedded_schema do + field(:auto_refresh_token, :boolean, default: true) + field(:debug, :boolean, default: false) + field(:detect_session_in_url, :boolean, default: true) + field(:flow_type, Ecto.Enum, values: ~w[implicit pkce magicLink]a, default: :implicit) + field(:persist_session, :boolean, default: true) + field(:storage, :string) + field(:storage_key, :string) + end + + def changeset(schema, params, supabase_url) do + schema + |> cast( + params, + ~w[auto_refresh_token debug detect_session_in_url persist_session flow_type storage]a + ) + |> validate_required( + ~w[auto_refresh_token debug detect_session_in_url persist_session flow_type]a + ) + |> put_storage_key(supabase_url) + end + + defp put_storage_key(%{valid?: false} = changeset, _), do: changeset + + defp put_storage_key(changeset, url) do + host = + url + |> URI.new() + |> Map.get(:host) + |> String.split(".") + |> List.first() + + storage_key = String.replace(@storage_key_template, "$host", host) + put_change(changeset, :storage_key, storage_key) + end +end diff --git a/lib/supabase/client/conn.ex b/lib/supabase/client/conn.ex new file mode 100644 index 0000000..f535667 --- /dev/null +++ b/lib/supabase/client/conn.ex @@ -0,0 +1,55 @@ +defmodule Supabase.Client.Conn do + @moduledoc """ + Conn configuration for Supabase Client. This schema is used to configure + the connection options. This schema is embedded in the `Supabase.Client`. + + ## Fields + + - `:base_url` - The Supabase Project URL to use. This option is required. + - `:api_key` - The Supabase ProjectAPI Key to use. This option is required. + - `:access_token` - The access token to use. Default to the API key. + + For more information about the connection options, see the documentation for + the [client](https://supabase.com/docs/reference/javascript/initializing). + """ + + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{ + api_key: String.t(), + access_token: String.t(), + base_url: String.t() + } + + @type params :: %{ + api_key: String.t(), + access_token: String.t(), + base_url: String.t() + } + + @primary_key false + embedded_schema do + field(:api_key, :string) + field(:access_token, :string) + field(:base_url, :string) + end + + def changeset(schema, params) do + schema + |> cast(params, ~w[api_key access_token base_url]a) + |> maybe_put_access_token() + |> validate_required(~w[api_key base_url]a) + end + + defp maybe_put_access_token(changeset) do + api_key = get_change(changeset, :api_key) + token = get_change(changeset, :access_token) + + cond do + not changeset.valid? -> changeset + token -> changeset + true -> put_change(changeset, :access_token, api_key) + end + end +end diff --git a/lib/supabase/client/db.ex b/lib/supabase/client/db.ex new file mode 100644 index 0000000..0330e1d --- /dev/null +++ b/lib/supabase/client/db.ex @@ -0,0 +1,28 @@ +defmodule Supabase.Client.Db do + @moduledoc """ + DB configuration schema. This schema is used to configure the database + options. This schema is embedded in the `Supabase.Client` schema. + + ## Fields + + - `:schema` - The default schema to use. Defaults to `"public"`. + + For more information about the database options, see the documentation for + the [client](https://supabase.com/docs/reference/javascript/initializing) and + [database guides](https://supabase.com/docs/guides/database). + """ + + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{schema: String.t()} + @type params :: %{schema: String.t()} + + embedded_schema do + field(:schema, :string, default: "public") + end + + def changeset(schema, params) do + cast(schema, params, [:schema]) + end +end diff --git a/lib/supabase/client/global.ex b/lib/supabase/client/global.ex new file mode 100644 index 0000000..b2b6b6e --- /dev/null +++ b/lib/supabase/client/global.ex @@ -0,0 +1,26 @@ +defmodule Supabase.Client.Global do + @moduledoc """ + Global configuration schema. This schema is used to configure the global + options. This schema is embedded in the `Supabase.Client` schema. + + ## Fields + + - `:headers` - The default headers to use in any Supabase request. Defaults to `%{}`. + """ + + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{headers: Map.t()} + @type params :: %{headers: Map.t()} + + embedded_schema do + field(:headers, {:map, :string}, default: %{}) + end + + def changeset(schema, params) do + schema + |> cast(params, [:headers]) + |> validate_required([:headers]) + end +end diff --git a/lib/supabase/client_registry.ex b/lib/supabase/client_registry.ex new file mode 100644 index 0000000..1a9c027 --- /dev/null +++ b/lib/supabase/client_registry.ex @@ -0,0 +1,44 @@ +defmodule Supabase.ClientRegistry do + @moduledoc """ + Registry for the Supabase multiple Clients. This registry is used to + register and lookup the Supabase Clients defined by the user. + + This Registry is used by the `Supabase.ClientSupervisor` to register and + any `Supabase.Client` that is defined. That way, the `Supabase.ClientSupervisor` + can lookup the `Supabase.Client` by name and start it if it is not running. + + ## Usage + + This Registry is used internally by the `Supabase.Application` and should + start automatically when the application starts. + """ + + def start_link(_) do + Registry.start_link(keys: :unique, name: __MODULE__) + end + + def child_spec(opts) do + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, [opts]}, + type: :worker, + restart: :permanent, + shutdown: 500 + } + end + + def named(key) when is_atom(key) do + {:via, Registry, {__MODULE__, key}} + end + + def register(key) when is_atom(key) do + Registry.register(__MODULE__, key, nil) + end + + def lookup(key) when is_atom(key) do + case Registry.lookup(__MODULE__, key) do + [{pid, _}] -> pid + [] -> nil + end + end +end diff --git a/apps/supabase/lib/supabase/client_supervisor.ex b/lib/supabase/client_supervisor.ex similarity index 75% rename from apps/supabase/lib/supabase/client_supervisor.ex rename to lib/supabase/client_supervisor.ex index ff805c6..6f96099 100644 --- a/apps/supabase/lib/supabase/client_supervisor.ex +++ b/lib/supabase/client_supervisor.ex @@ -25,6 +25,16 @@ defmodule Supabase.ClientSupervisor do iex> Supabase.ClientSupervisor.start_child({Supabase.Client, opts}) {:ok, #PID<0.123.0>} + + Notice that the Supabase Elixir SDK already starts a `Supabase.ClientSupervisor` + internally, so you don't need to start it manually. However, if you want to + manage clients manually, you can leverage this module to start and stop + clients dynamically. + + To manage manually the clients, you need to disable the internal management + into your application: + + config :supabase, :manage_clients, false """ use DynamicSupervisor diff --git a/apps/supabase_fetcher/lib/supabase/fetcher.ex b/lib/supabase/fetcher.ex similarity index 93% rename from apps/supabase_fetcher/lib/supabase/fetcher.ex rename to lib/supabase/fetcher.ex index 9fe89a3..4b7a724 100644 --- a/apps/supabase_fetcher/lib/supabase/fetcher.ex +++ b/lib/supabase/fetcher.ex @@ -15,13 +15,11 @@ defmodule Supabase.Fetcher do While `Supabase.Fetcher` is versatile and comprehensive, it operates at a very granular level. For most applications and needs, leveraging higher-level APIs that correspond to specific Supabase services is advisable: - - [supabase-storage](https://github.com/zoedsoupe/supabase/tree/main/apps/supabase_storage) - - For those seeking a more comprehensive integration with the entirety of Supabase's offerings, the [supabase-potion](https://github.com/zoedsoupe/supabase) package is available. This package provides an all-encompassing interface, streamlining your Supabase interactions. + - `Supabase.Storage` - API to interact directly with buckets and objects in Supabase Storage. ## Disclaimer - If your aim is to directly harness this module as a low-level HTTP client, due to missing features in other packages or a desire to craft a unique Supabase integration, you can certainly do so. However, always keep in mind that `Supabase.Potion` and other Supabase-oriented packages might offer better abstractions and ease-of-use. + If your aim is to directly harness this module as a low-level HTTP client, due to missing features in other packages or a desire to craft a unique Supabase integration, you can certainly do so. However, always keep in mind that `Supabase.Storage` and other Supabase-oriented packages might offer better abstractions and ease-of-use. Use `Supabase.Fetcher` with a clear understanding of its features and operations. """ @@ -30,7 +28,7 @@ defmodule Supabase.Fetcher do @spec version :: String.t() def version do - {:ok, vsn} = :application.get_key(:supabase_fetcher, :vsn) + {:ok, vsn} = :application.get_key(:supabase, :vsn) List.to_string(vsn) end diff --git a/apps/supabase_fetcher/lib/supabase/fetcher_behaviour.ex b/lib/supabase/fetcher_behaviour.ex similarity index 100% rename from apps/supabase_fetcher/lib/supabase/fetcher_behaviour.ex rename to lib/supabase/fetcher_behaviour.ex diff --git a/apps/supabase_connection/lib/supabase/missing_supabase_config.ex b/lib/supabase/missing_supabase_config.ex similarity index 83% rename from apps/supabase_connection/lib/supabase/missing_supabase_config.ex rename to lib/supabase/missing_supabase_config.ex index b19ea94..1aff82a 100644 --- a/apps/supabase_connection/lib/supabase/missing_supabase_config.ex +++ b/lib/supabase/missing_supabase_config.ex @@ -9,13 +9,22 @@ defmodule Supabase.MissingSupabaseConfig do import Config - config :supabase_fetch, + config :supabase, supabase_url: System.fetch_env!("SUPABASE_BASE_URL"), supabase_key: System.fetch_env!("SUPABASE_API_KEY"), Remember to set the environment variables SUPABASE_BASE_URL and SUPABASE_API_KEY if you choose this option. Otherwise you can pass the values directly to the config file. + Alternatively you can pass the values directly to the `Supabase.Client.init_client!/1` function: + + iex> Supabase.init_client!(%{ + conn: %{ + base_url: System.fetch_env!("SUPABASE_BASE_URL"), + api_key: System.fetch_env!("SUPABASE_API_KEY") + } + }) + #{if config == :key, do: missing_key_config_walktrough(), else: missing_url_config_walktrough()} """ diff --git a/apps/supabase_storage/lib/supabase/storage.ex b/lib/supabase/storage.ex similarity index 59% rename from apps/supabase_storage/lib/supabase/storage.ex rename to lib/supabase/storage.ex index f5c1242..4f53ebc 100644 --- a/apps/supabase_storage/lib/supabase/storage.ex +++ b/lib/supabase/storage.ex @@ -15,21 +15,21 @@ defmodule Supabase.Storage do You can start by creating or managing buckets: - Supabase.Storage.create_bucket(conn, "my_new_bucket") + Supabase.Storage.create_bucket(client, "my_new_bucket") Once a bucket is set up, objects within the bucket can be managed: - Supabase.Storage.upload_object(conn, "my_bucket", "path/on/server.png", "path/on/local.png") + Supabase.Storage.upload_object(client, "my_bucket", "path/on/server.png", "path/on/local.png") ## Examples Here are some basic examples: # Removing an object - Supabase.Storage.remove_object(conn, "my_bucket", "path/on/server.png") + Supabase.Storage.remove_object(client, "my_bucket", "path/on/server.png") # Moving an object - Supabase.Storage.move_object(conn, "my_bucket", "path/on/server1.png", "path/on/server2.png") + Supabase.Storage.move_object(client, "my_bucket", "path/on/server1.png", "path/on/server2.png") Ensure to refer to method-specific documentation for detailed examples and explanations. @@ -39,8 +39,10 @@ defmodule Supabase.Storage do operations can be performed without any hitches. """ - import Supabase.Connection + import Supabase.Client, only: [is_client: 1] + alias Supabase.Client + alias Supabase.Client.Conn alias Supabase.Storage.Bucket alias Supabase.Storage.BucketHandler alias Supabase.Storage.Object @@ -61,7 +63,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.list_buckets(conn) + iex> Supabase.Storage.list_buckets(client) {:ok, [%Supabase.Storage.Bucket{...}, ...]} iex> Supabase.Storage.list_buckets(invalid_conn) @@ -69,12 +71,14 @@ defmodule Supabase.Storage do """ @impl true - def list_buckets(conn) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) + def list_buckets(client) when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} - BucketHandler.list(base_url, api_key, token) + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + {:ok, BucketHandler.list(base_url, api_key, token)} + end end @doc """ @@ -88,7 +92,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.retrieve_bucket_info(conn, "avatars") + iex> Supabase.Storage.retrieve_bucket_info(client, "avatars") {:ok, %Supabase.Storage.Bucket{...}} iex> Supabase.Storage.retrieve_bucket_info(invalid_conn, "avatars") @@ -96,12 +100,14 @@ defmodule Supabase.Storage do """ @impl true - def retrieve_bucket_info(conn, id) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) + def retrieve_bucket_info(client, id) when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} - BucketHandler.retrieve_info(base_url, api_key, token, id) + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + BucketHandler.retrieve_info(base_url, api_key, token, id) + end end @doc """ @@ -123,7 +129,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.create_bucket(conn, %{id: "avatars"}) + iex> Supabase.Storage.create_bucket(client, %{id: "avatars"}) {:ok, %Supabase.Storage.Bucket{...}} iex> Supabase.Storage.create_bucket(invalid_conn, %{id: "avatars"}) @@ -131,13 +137,18 @@ defmodule Supabase.Storage do """ @impl true - def create_bucket(conn, attrs) do + def create_bucket(client, attrs) when is_client(client) do with {:ok, bucket_params} <- Bucket.create_changeset(attrs), - base_url = get_base_url(conn), - api_key = get_api_key(conn), - token = get_access_token(conn), + %Conn{access_token: token, api_key: api_key, base_url: base_url} <- + Client.retrieve_connection(client), {:ok, _} <- BucketHandler.create(base_url, api_key, token, bucket_params) do - retrieve_bucket_info(conn, bucket_params.id) + retrieve_bucket_info(client, bucket_params.id) + else + nil -> + {:error, :invalid_client} + + {:error, changeset} -> + {:error, changeset} end end @@ -161,7 +172,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.update_bucket(conn, bucket, %{public: true}) + iex> Supabase.Storage.update_bucket(client, bucket, %{public: true}) {:ok, %Supabase.Storage.Bucket{...}} iex> Supabase.Storage.update_bucket(invalid_conn, bucket, %{public: true}) @@ -169,13 +180,18 @@ defmodule Supabase.Storage do """ @impl true - def update_bucket(conn, bucket, attrs) do + def update_bucket(client, bucket, attrs) when is_client(client) do with {:ok, bucket_params} <- Bucket.update_changeset(bucket, attrs), - base_url = get_base_url(conn), - api_key = get_api_key(conn), - token = get_access_token(conn), + %Conn{access_token: token, api_key: api_key, base_url: base_url} <- + Client.retrieve_connection(client), {:ok, _} <- BucketHandler.update(base_url, api_key, token, bucket.id, bucket_params) do - retrieve_bucket_info(conn, bucket.id) + retrieve_bucket_info(client, bucket.id) + else + nil -> + {:error, :invalid_client} + + {:error, changeset} -> + {:error, changeset} end end @@ -190,7 +206,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.empty_bucket(conn, bucket) + iex> Supabase.Storage.empty_bucket(client, bucket) {:ok, :emptied} iex> Supabase.Storage.empty_bucket(invalid_conn, bucket) @@ -198,12 +214,14 @@ defmodule Supabase.Storage do """ @impl true - def empty_bucket(conn, %Bucket{} = bucket) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) + def empty_bucket(client, %Bucket{} = bucket) when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} - BucketHandler.empty(base_url, api_key, token, bucket.id) + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + BucketHandler.empty(base_url, api_key, token, bucket.id) + end end @doc """ @@ -217,7 +235,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.delete_bucket(conn, bucket) + iex> Supabase.Storage.delete_bucket(client, bucket) {:ok, :deleted} iex> Supabase.Storage.delete_bucket(invalid_conn, bucket) @@ -225,14 +243,17 @@ defmodule Supabase.Storage do """ @impl true - def delete_bucket(conn, %Bucket{} = bucket) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) - - with {:ok, _} <- BucketHandler.delete(base_url, api_key, token, bucket.id) do - remove_current_bucket(conn) + def delete_bucket(client, %Bucket{} = bucket) when is_client(client) do + with %Conn{access_token: token, api_key: api_key, base_url: base_url} <- + Client.retrieve_connection(client), + {:ok, _} <- BucketHandler.delete(base_url, api_key, token, bucket.id) do {:ok, :deleted} + else + nil -> + {:error, :invalid_client} + + {:error, changeset} -> + {:error, changeset} end end @@ -247,7 +268,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.remove_object(conn, bucket, object) + iex> Supabase.Storage.remove_object(client, bucket, object) {:ok, :deleted} iex> Supabase.Storage.remove_object(invalid_conn, bucket, object) @@ -255,12 +276,14 @@ defmodule Supabase.Storage do """ @impl true - def remove_object(conn, %Bucket{} = bucket, %Object{} = object) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) + def remove_object(client, %Bucket{} = bucket, %Object{} = object) when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} - ObjectHandler.remove(base_url, api_key, token, bucket.name, object.path) + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + ObjectHandler.remove(base_url, api_key, token, bucket.name, object.path) + end end @doc """ @@ -276,7 +299,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.move_object(conn, bucket, object) + iex> Supabase.Storage.move_object(client, bucket, object) {:ok, :moved} iex> Supabase.Storage.move_object(invalid_conn, bucket, object) @@ -284,12 +307,14 @@ defmodule Supabase.Storage do """ @impl true - def move_object(conn, %Bucket{} = bucket, %Object{} = object, to) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) + def move_object(client, %Bucket{} = bucket, %Object{} = object, to) when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} - ObjectHandler.move(base_url, api_key, token, bucket.name, object.path, to) + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + ObjectHandler.move(base_url, api_key, token, bucket.name, object.path, to) + end end @doc """ @@ -305,7 +330,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.copy_object(conn, bucket, object) + iex> Supabase.Storage.copy_object(client, bucket, object) {:ok, :copied} iex> Supabase.Storage.copy_object(invalid_conn, bucket, object) @@ -313,12 +338,14 @@ defmodule Supabase.Storage do """ @impl true - def copy_object(conn, %Bucket{} = bucket, %Object{} = object, to) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) + def copy_object(client, %Bucket{} = bucket, %Object{} = object, to) when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} - ObjectHandler.copy(base_url, api_key, token, bucket.name, object.path, to) + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + ObjectHandler.copy(base_url, api_key, token, bucket.name, object.path, to) + end end @doc """ @@ -332,7 +359,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.retrieve_object_info(conn, bucket, "some.png") + iex> Supabase.Storage.retrieve_object_info(client, bucket, "some.png") {:ok, %Supabase.Storage.Object{...}} iex> Supabase.Storage.retrieve_object_info(invalid_conn, bucket, "some.png") @@ -340,12 +367,14 @@ defmodule Supabase.Storage do """ @impl true - def retrieve_object_info(conn, %Bucket{} = bucket, wildcard) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) + def retrieve_object_info(client, %Bucket{} = bucket, wildcard) when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} - ObjectHandler.get_info(base_url, api_key, token, bucket.name, wildcard) + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + ObjectHandler.get_info(base_url, api_key, token, bucket.name, wildcard) + end end @doc """ @@ -365,7 +394,7 @@ defmodule Supabase.Storage do And you want to list only the objects inside the `avatars` folder, you can do: - iex> Supabase.Storage.list_objects(conn, bucket, "avatars/") + iex> Supabase.Storage.list_objects(client, bucket, "avatars/") {:ok, [%Supabase.Storage.Object{...}]} Also you can pass some search options as a `Supabase.Storage.SearchOptions` struct. Available @@ -385,7 +414,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.list_objects(conn, bucket) + iex> Supabase.Storage.list_objects(client, bucket) {:ok, [%Supabase.Storage.Object{...}, ...]} iex> Supabase.Storage.list_objects(invalid_conn, bucket) @@ -393,12 +422,15 @@ defmodule Supabase.Storage do """ @impl true - def list_objects(conn, %Bucket{} = bucket, prefix \\ "", opts \\ %SearchOptions{}) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) - - ObjectHandler.list(base_url, api_key, token, bucket.name, prefix, opts) + def list_objects(client, %Bucket{} = bucket, prefix \\ "", opts \\ %SearchOptions{}) + when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} + + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + ObjectHandler.list(base_url, api_key, token, bucket.name, prefix, opts) + end end @doc """ @@ -423,7 +455,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.upload_object(conn, bucket, "avatars/some.png", "path/to/file.png") + iex> Supabase.Storage.upload_object(client, bucket, "avatars/some.png", "path/to/file.png") {:ok, %Supabase.Storage.Object{...}} iex> Supabase.Storage.upload_object(invalid_conn, bucket, "avatars/some.png", "path/to/file.png") @@ -431,13 +463,16 @@ defmodule Supabase.Storage do """ @impl true - def upload_object(conn, %Bucket{} = bucket, path, file, opts \\ %ObjectOptions{}) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) - file = Path.expand(file) - - ObjectHandler.create_file(base_url, api_key, token, bucket.name, path, file, opts) + def upload_object(client, %Bucket{} = bucket, path, file, opts \\ %ObjectOptions{}) + when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} + + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + file = Path.expand(file) + ObjectHandler.create_file(base_url, api_key, token, bucket.name, path, file, opts) + end end @doc """ @@ -452,7 +487,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.download_object(conn, %Bucket{}, "avatars/some.png") + iex> Supabase.Storage.download_object(client, %Bucket{}, "avatars/some.png") {:ok, <<>>} iex> Supabase.Storage.download_object(invalid_conn, %Bucket{}, "avatars/some.png") @@ -460,12 +495,14 @@ defmodule Supabase.Storage do """ @impl true - def download_object(conn, %Bucket{} = bucket, wildcard) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) + def download_object(client, %Bucket{} = bucket, wildcard) when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} - ObjectHandler.get(base_url, api_key, token, bucket.name, wildcard) + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + ObjectHandler.get(base_url, api_key, token, bucket.name, wildcard) + end end @doc """ @@ -481,7 +518,7 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.download_object_lazy(conn, %Bucket{}, "avatars/some.png") + iex> Supabase.Storage.download_object_lazy(client, %Bucket{}, "avatars/some.png") {:ok, #Function<59.128620087/2 in Stream.resource/3>} iex> Supabase.Storage.download_object_lazy(invalid_conn, %Bucket{}, "avatars/some.png") @@ -489,12 +526,14 @@ defmodule Supabase.Storage do """ @impl true - def download_object_lazy(conn, %Bucket{} = bucket, wildcard) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) + def download_object_lazy(client, %Bucket{} = bucket, wildcard) when is_client(client) do + case Client.retrieve_connection(client) do + nil -> + {:error, :invalid_client} - ObjectHandler.get_lazy(base_url, api_key, token, bucket.name, wildcard) + %Conn{access_token: token, api_key: api_key, base_url: base_url} -> + ObjectHandler.get_lazy(base_url, api_key, token, bucket.name, wildcard) + end end @doc """ @@ -508,16 +547,16 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.save_object(conn, "./some.png", %Bucket{}, "avatars/some.png") + iex> Supabase.Storage.save_object(client, "./some.png", %Bucket{}, "avatars/some.png") :ok - iex> Supabase.Storage.save_object(conn, "./some.png", %Bucket{}, "do_not_exist.png") + iex> Supabase.Storage.save_object(client, "./some.png", %Bucket{}, "do_not_exist.png") {:error, reason} """ @impl true - def save_object(conn, path, %Bucket{} = bucket, wildcard) do - with {:ok, bin} <- download_object(conn, bucket, wildcard) do + def save_object(client, path, %Bucket{} = bucket, wildcard) when is_client(client) do + with {:ok, bin} <- download_object(client, bucket, wildcard) do File.write(Path.expand(path), bin) end end @@ -534,16 +573,16 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.save_object_stream(conn, "./some.png", %Bucket{}, "avatars/some.png") + iex> Supabase.Storage.save_object_stream(client, "./some.png", %Bucket{}, "avatars/some.png") :ok - iex> Supabase.Storage.save_object_stream(conn, "./some.png", %Bucket{}, "do_not_exist.png") + iex> Supabase.Storage.save_object_stream(client, "./some.png", %Bucket{}, "do_not_exist.png") {:error, reason} """ @impl true - def save_object_stream(conn, path, %Bucket{} = bucket, wildcard) do - with {:ok, stream} <- download_object_lazy(conn, bucket, wildcard) do + def save_object_stream(client, path, %Bucket{} = bucket, wildcard) when is_client(client) do + with {:ok, stream} <- download_object_lazy(client, bucket, wildcard) do fs = File.stream!(Path.expand(path)) stream @@ -565,20 +604,18 @@ defmodule Supabase.Storage do ## Examples - iex> Supabase.Storage.create_signed_url(conn, bucket, "avatars/some.png", 3600) + iex> Supabase.Storage.create_signed_url(client, bucket, "avatars/some.png", 3600) {:ok, "https://.supabase.co"/object/sign//?token=} - iex> Supabase.Storage.create_signed_url(invalid_conn, bucket, "avatars/some.png", 3600) - {:error, reason} + iex> Supabase.Storage.create_signed_url(invalid_client, bucket, "avatars/some.png", 3600) + {:error, :invalid_client} """ @impl true - def create_signed_url(conn, %Bucket{} = bucket, path, expires_in) do - base_url = get_base_url(conn) - api_key = get_api_key(conn) - token = get_access_token(conn) - - with {:ok, sign_url} <- + def create_signed_url(client, %Bucket{} = bucket, path, expires_in) when is_client(client) do + with %Conn{access_token: token, api_key: api_key, base_url: base_url} <- + Client.retrieve_connection(client), + {:ok, sign_url} <- ObjectHandler.create_signed_url( base_url, api_key, @@ -588,6 +625,12 @@ defmodule Supabase.Storage do expires_in ) do {:ok, URI.to_string(URI.merge(base_url, sign_url))} + else + nil -> + {:error, :invalid_client} + + err -> + err end end end diff --git a/apps/supabase_storage/lib/supabase/storage/action_error.ex b/lib/supabase/storage/action_error.ex similarity index 100% rename from apps/supabase_storage/lib/supabase/storage/action_error.ex rename to lib/supabase/storage/action_error.ex diff --git a/apps/supabase_storage/lib/supabase/storage/bucket.ex b/lib/supabase/storage/bucket.ex similarity index 100% rename from apps/supabase_storage/lib/supabase/storage/bucket.ex rename to lib/supabase/storage/bucket.ex diff --git a/apps/supabase_storage/lib/supabase/storage/cache.ex b/lib/supabase/storage/cache.ex similarity index 100% rename from apps/supabase_storage/lib/supabase/storage/cache.ex rename to lib/supabase/storage/cache.ex diff --git a/apps/supabase_storage/lib/supabase/storage/cache_reloader.ex b/lib/supabase/storage/cache_reloader.ex similarity index 100% rename from apps/supabase_storage/lib/supabase/storage/cache_reloader.ex rename to lib/supabase/storage/cache_reloader.ex diff --git a/apps/supabase_storage/lib/supabase/storage/endpoints.ex b/lib/supabase/storage/endpoints.ex similarity index 100% rename from apps/supabase_storage/lib/supabase/storage/endpoints.ex rename to lib/supabase/storage/endpoints.ex diff --git a/apps/supabase_storage/lib/supabase/storage/handlers/bucket_handler.ex b/lib/supabase/storage/handlers/bucket_handler.ex similarity index 100% rename from apps/supabase_storage/lib/supabase/storage/handlers/bucket_handler.ex rename to lib/supabase/storage/handlers/bucket_handler.ex diff --git a/apps/supabase_storage/lib/supabase/storage/handlers/object_handler.ex b/lib/supabase/storage/handlers/object_handler.ex similarity index 96% rename from apps/supabase_storage/lib/supabase/storage/handlers/object_handler.ex rename to lib/supabase/storage/handlers/object_handler.ex index 83b7462..111eda0 100644 --- a/apps/supabase_storage/lib/supabase/storage/handlers/object_handler.ex +++ b/lib/supabase/storage/handlers/object_handler.ex @@ -197,7 +197,7 @@ defmodule Supabase.Storage.ObjectHandler do end @spec get(Conn.base_url(), Conn.api_key(), Conn.access_token(), bucket_name, object_path) :: - {:ok, String.t()} | {:error, String.t()} + {:ok, binary} | {:error, String.t()} def get(base_url, api_key, token, bucket_name, wildcard) do url = Fetcher.get_full_url(base_url, Endpoints.file_download(bucket_name, wildcard)) headers = Fetcher.apply_headers(api_key, token) @@ -210,6 +210,14 @@ defmodule Supabase.Storage.ObjectHandler do end end + @spec get_lazy( + Conn.base_url(), + Conn.api_key(), + Conn.access_token(), + bucket_name, + wildcard + ) :: + {:ok, Stream.t()} | {:error, atom} def get_lazy(base_url, api_key, token, bucket_name, wildcard) do url = Fetcher.get_full_url(base_url, Endpoints.file_download(bucket_name, wildcard)) headers = Fetcher.apply_headers(api_key, token) diff --git a/apps/supabase_storage/lib/supabase/storage/object.ex b/lib/supabase/storage/object.ex similarity index 100% rename from apps/supabase_storage/lib/supabase/storage/object.ex rename to lib/supabase/storage/object.ex diff --git a/apps/supabase_storage/lib/supabase/storage/object_options.ex b/lib/supabase/storage/object_options.ex similarity index 100% rename from apps/supabase_storage/lib/supabase/storage/object_options.ex rename to lib/supabase/storage/object_options.ex diff --git a/apps/supabase_storage/lib/supabase/storage/search_options.ex b/lib/supabase/storage/search_options.ex similarity index 100% rename from apps/supabase_storage/lib/supabase/storage/search_options.ex rename to lib/supabase/storage/search_options.ex diff --git a/apps/supabase_storage/lib/supabase/storage_behaviour.ex b/lib/supabase/storage_behaviour.ex similarity index 86% rename from apps/supabase_storage/lib/supabase/storage_behaviour.ex rename to lib/supabase/storage_behaviour.ex index aea9b21..7dee4fb 100644 --- a/apps/supabase_storage/lib/supabase/storage_behaviour.ex +++ b/lib/supabase/storage_behaviour.ex @@ -6,9 +6,9 @@ defmodule Supabase.StorageBehaviour do alias Supabase.Storage.ObjectOptions, as: Opts alias Supabase.Storage.SearchOptions, as: Search - @type conn :: atom | pid + @type conn :: atom @type reason :: String.t() | atom - @type result(a) :: {:ok, a} | {:error, reason} + @type result(a) :: {:ok, a} | {:error, reason} | {:error, :invalid_client} @callback list_buckets(conn) :: result([Bucket.t()]) @callback retrieve_bucket_info(conn, id) :: result(Bucket.t()) @@ -31,10 +31,12 @@ defmodule Supabase.StorageBehaviour do when wildcard: String.t() @callback download_object_lazy(conn, Bucket.t(), wildcard) :: result(Stream.t()) when wildcard: String.t() - @callback save_object(conn, dest, Bucket.t(), wildcard) :: :ok | {:error, atom} + @callback save_object(conn, dest, Bucket.t(), wildcard) :: + :ok | {:error, atom} | {:error, :invalid_client} when wildcard: String.t(), dest: Path.t() - @callback save_object_stream(conn, dest, Bucket.t(), wildcard) :: :ok | {:error, atom} + @callback save_object_stream(conn, dest, Bucket.t(), wildcard) :: + :ok | {:error, atom} | {:error, :invalid_client} when wildcard: String.t(), dest: Path.t() @callback create_signed_url(conn, Bucket.t(), String.t(), integer) :: result(String.t()) diff --git a/apps/supabase_types/lib/supabase/types/atom.ex b/lib/supabase/types/atom.ex similarity index 100% rename from apps/supabase_types/lib/supabase/types/atom.ex rename to lib/supabase/types/atom.ex diff --git a/mix.exs b/mix.exs index f73f04c..baa571c 100644 --- a/mix.exs +++ b/mix.exs @@ -1,19 +1,63 @@ -defmodule SupabasePotion.MixProject do +defmodule Supabase.MixProject do use Mix.Project + @version "0.2.0" + @source_url "https://github.com/zoedsoupe/supabase" + def project do [ - apps_path: "apps", - version: "0.1.0", + app: :supabase, + version: @version, + elixir: "~> 1.14", start_permanent: Mix.env() == :prod, - deps: deps() + deps: deps(), + docs: docs(), + package: package(), + description: description() + ] + end + + def application do + [ + mod: {Supabase.Application, []}, + extra_applications: [:logger] ] end defp deps do [ + {:finch, "~> 0.16"}, + {:jason, "~> 1.4"}, + {:ecto, "~> 3.10"}, + {:ex_doc, ">= 0.0.0", runtime: false}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.3", only: [:dev], runtime: false} ] end + + defp package do + %{ + name: "supabase_potion", + licenses: ["MIT"], + contributors: ["zoedsoupe"], + links: %{ + "GitHub" => @source_url, + "Docs" => "https://hexdocs.pm/supabase_potion" + }, + files: ~w[lib mix.exs README.md LICENSE] + } + end + + defp docs do + [ + main: "Supabase", + extras: ["README.md"] + ] + end + + defp description do + """ + Complete Elixir client for Supabase. + """ + end end diff --git a/test/supabase_test.exs b/test/supabase_test.exs new file mode 100644 index 0000000..2ea2f88 --- /dev/null +++ b/test/supabase_test.exs @@ -0,0 +1,74 @@ +defmodule SupabaseTest do + use ExUnit.Case, async: true + + alias Supabase.MissingSupabaseConfig + alias Supabase.Client + alias Supabase.ClientRegistry + + describe "init_client/1" do + test "should return a valid PID on valid attrs" do + {:ok, pid} = + Supabase.init_client(%{ + name: :test, + conn: %{ + base_url: "https://test.supabase.co", + api_key: "test" + } + }) + + assert pid == ClientRegistry.lookup(:test) + assert client = Client.retrieve_client(:test) + assert client.name == :test + assert client.conn.base_url == "https://test.supabase.co" + assert client.conn.api_key == "test" + end + + test "should return an error changeset on invalid attrs" do + {:error, changeset} = Supabase.init_client(%{}) + + assert changeset.errors == [ + name: {"can't be blank", [validation: :required]}, + conn: {"can't be blank", [validation: :required]} + ] + + {:error, changeset} = Supabase.init_client(%{name: :test, conn: %{}}) + assert conn = changeset.changes.conn + + assert conn.errors == [ + api_key: {"can't be blank", [validation: :required]}, + base_url: {"can't be blank", [validation: :required]} + ] + end + end + + describe "init_client!/1" do + test "should return a valid PID on valid attrs" do + pid = + Supabase.init_client!(%{ + name: :test2, + conn: %{ + base_url: "https://test.supabase.co", + api_key: "test" + } + }) + + assert pid == ClientRegistry.lookup(:test2) + assert client = Client.retrieve_client(:test2) + assert client.name == :test2 + assert client.conn.base_url == "https://test.supabase.co" + assert client.conn.api_key == "test" + end + + test "should raise MissingSupabaseConfig on missing base_url" do + assert_raise MissingSupabaseConfig, fn -> + Supabase.init_client!(%{name: :test, conn: %{api_key: "test"}}) + end + end + + test "should raise MissingSupabaseConfig on missing api_key" do + assert_raise MissingSupabaseConfig, fn -> + Supabase.init_client!(%{name: :test, conn: %{base_url: "https://test.supabase.co"}}) + end + end + end +end diff --git a/apps/supabase/test/test_helper.exs b/test/test_helper.exs similarity index 100% rename from apps/supabase/test/test_helper.exs rename to test/test_helper.exs