diff --git a/.formatter.exs b/.formatter.exs index 689c1ad..439399a 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -35,7 +35,6 @@ spark_locals_without_parens = [ statement: 2, strict?: 1, table: 1, - tenant_repo: 1, unique: 1, unique_index_names: 1, up: 1, diff --git a/documentation/dsls/DSL-AshSqlite.DataLayer.md b/documentation/dsls/DSL-AshSqlite.DataLayer.md index cb4e0c2..1b38078 100644 --- a/documentation/dsls/DSL-AshSqlite.DataLayer.md +++ b/documentation/dsls/DSL-AshSqlite.DataLayer.md @@ -36,7 +36,6 @@ end | Name | Type | Default | Docs | |------|------|---------|------| | [`repo`](#sqlite-repo){: #sqlite-repo .spark-required} | `module \| (any, any -> any)` | | The repo that will be used to fetch your data. See the `AshSqlite.Repo` documentation for more. Can also be a capture of a named function in another module that takes a resource and a type `:read \| :mutate` and returns the repo. An inline `fn` is not supported, because it cannot be called while the resource is compiling. | -| [`tenant_repo`](#sqlite-tenant_repo){: #sqlite-tenant_repo } | `(any, any -> any) \| module` | | Selects the repo instance a tenanted statement runs on. Required for `strategy :context`, where each tenant has its own database file. Either a function `(tenant, %{resource: resource, type: :read \| :mutate}) -> pid \| atom`, or a module implementing `AshSqlite.TenantRepo`. | | [`migrate?`](#sqlite-migrate?){: #sqlite-migrate? } | `boolean` | `true` | Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations` | | [`migration_types`](#sqlite-migration_types){: #sqlite-migration_types } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration type that should be used for that attribute. Only necessary if you need to override the defaults. | | [`migration_defaults`](#sqlite-migration_defaults){: #sqlite-migration_defaults } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration default that should be used for that attribute. The string you use will be placed verbatim in the migration. Use fragments like `fragment(\\"now()\\")`, or for `nil`, use `\\"nil\\"`. | diff --git a/lib/data_layer.ex b/lib/data_layer.ex index f065a7a..34db179 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -203,12 +203,6 @@ defmodule AshSqlite.DataLayer do doc: "The repo that will be used to fetch your data. See the `AshSqlite.Repo` documentation for more. Can also be a capture of a named function in another module that takes a resource and a type `:read | :mutate` and returns the repo. An inline `fn` is not supported, because it cannot be called while the resource is compiling." ], - tenant_repo: [ - type: - {:spark_function_behaviour, AshSqlite.TenantRepo, {AshSqlite.TenantRepo.Function, 2}}, - doc: - "Selects the repo instance a tenanted statement runs on. Required for `strategy :context`, where each tenant has its own database file. Either a function `(tenant, %{resource: resource, type: :read | :mutate}) -> pid | atom`, or a module implementing `AshSqlite.TenantRepo`." - ], migrate?: [ type: :boolean, default: true, @@ -553,7 +547,7 @@ defmodule AshSqlite.DataLayer do @doc """ A no-op on the query. Each tenant has its own database file, so the tenant is applied by choosing the connection rather than by changing the query. The - resource's `tenant_repo` chooses it, once per statement. + repo's `c:AshSqlite.Repo.tenant_repo/2` chooses it, once per statement. """ def set_tenant(_resource, query, _tenant) do {:ok, query} @@ -2206,8 +2200,13 @@ defmodule AshSqlite.DataLayer do @impl true def in_transaction?(resource) do - repo = AshSqlite.DataLayer.Info.repo(resource, :mutate) + resource + |> AshSqlite.DataLayer.Info.repo(:mutate) + |> repo_in_transaction?() + end + # A tenant repo's default instance is usually never started, so it can't be asked. + defp repo_in_transaction?(repo) do case repo.get_dynamic_repo() do pid when is_pid(pid) -> repo.in_transaction?() name when is_atom(name) -> !is_nil(GenServer.whereis(name)) and repo.in_transaction?() @@ -2229,16 +2228,13 @@ defmodule AshSqlite.DataLayer do if is_nil(tenant) and tenant_required?(resource) do raise ArgumentError, """ - #{inspect(resource)} has a tenant_repo and `strategy :context`, but the \ - transaction about to be opened for this action carried no tenant. + #{inspect(resource)} has `strategy :context`, but the transaction about to be \ + opened for it carried no tenant. - A transaction is opened on one connection, and for a database-per-tenant \ - layout that means one tenant's database. Ash forwards only the changeset's \ - data layer context to this callback, so the tenant has to travel in it: + A transaction is opened on one connection, which here means one tenant's \ + database. Pass the tenant: - Ash.Changeset.set_context(changeset, %{data_layer: %{tenant: tenant}}) - - A global change on the resource is the usual place to do that. + Ash.transaction(#{inspect(resource)}, fun, tenant: tenant) """ end @@ -2285,69 +2281,74 @@ defmodule AshSqlite.DataLayer do # Every callback that issues SQL goes through here, because a caller cannot bind # around a path it never sees -- aggregates and atomic writes give it nothing to # wrap. `type` is passed on so a tenant repo can route reads and writes differently. - defp with_tenant_repo(resource, nil, _type, fun), do: unbound(resource, fun) - + # + # Ecto binds an instance per repo module, so a tenant repo serves only + # `strategy :context` resources. That makes anything else on the module during a + # tenant's transaction tenant work too. `VerifyTenantRepo` enforces this at compile + # time; the raises below are for a function `repo` that resolves differently at + # runtime. defp with_tenant_repo(resource, tenant, type, fun) do - case AshSqlite.DataLayer.Info.tenant_repo(resource) do - # No tenant repo and a tenant means multitenancy this data layer does not - # resolve to a connection, such as `strategy :attribute`. `VerifyTenantRepo` - # covers `strategy :context` at compile time. - nil -> - fun.() - - {module, opts} -> - repo = AshSqlite.DataLayer.Info.repo(resource, :mutate) - - # Captured before the bind: if a transaction is already open it is open on - # *this* connection, so a statement that binds elsewhere leaves it. - enclosing = if in_transaction?(resource), do: repo.get_dynamic_repo() - - context = %{resource: resource, type: type} + repo = AshSqlite.DataLayer.Info.repo(resource, type) - bind_tenant_repo(module, tenant, context, opts, fn -> - if enclosing && repo.get_dynamic_repo() != enclosing do - raise ArgumentError, """ - #{inspect(resource)} tried to run a statement for tenant \ - #{inspect(tenant)} inside a transaction open on another tenant's \ - database. SQLite cannot commit across files atomically in WAL mode, so \ - this statement would commit on its own and survive a rollback. - - Open one transaction per tenant instead. - """ - end + case {tenant_required?(resource), tenant_repo?(repo)} do + {false, false} -> + fun.() - fun.() - end) - end - end + {true, true} -> + bind_tenant_repo(resource, repo, tenant, type, fun) - defp bind_tenant_repo(module, tenant, context, opts, fun) do - if Code.ensure_loaded?(module) and function_exported?(module, :with_repo, 4) do - module.with_repo(tenant, context, opts, fun) - else - repo = AshSqlite.DataLayer.Info.repo(context.resource, context.type) - previous = repo.put_dynamic_repo(module.repo(tenant, context, opts)) + {true, false} -> + raise ArgumentError, """ + #{inspect(resource)} has `strategy :context`, but its repo #{inspect(repo)} \ + does not implement `c:AshSqlite.Repo.tenant_repo/2`. + """ - try do - fun.() - after - repo.put_dynamic_repo(previous) - end + {false, true} -> + raise ArgumentError, """ + #{inspect(resource)} does not have `strategy :context`, but its repo \ + #{inspect(repo)} is a tenant repo. Put it on a repo module of its own. + """ end end # Without a tenant there is nothing to select a database, so this fails rather # than run against whichever connection the process happens to hold. Ash already # enforces this for actions; this catches the paths that bypass one. - defp unbound(resource, fun) do - if tenant_required?(resource) do + defp bind_tenant_repo(resource, _repo, nil, _type, _fun) do + raise ArgumentError, """ + #{inspect(resource)} has `strategy :context` but this statement carried no \ + tenant, so there is no database file to select. Pass a tenant. + """ + end + + defp bind_tenant_repo(resource, repo, tenant, type, fun) do + instance = repo.tenant_repo(tenant, %{resource: resource, type: type}) + + # An open transaction is open on the bound instance, so a statement for another + # one would leave it. + if repo_in_transaction?(repo) and repo.get_dynamic_repo() != instance do raise ArgumentError, """ - #{inspect(resource)} has `strategy :context` but this statement carried no \ - tenant, so there is no database file to select. Pass a tenant. + #{inspect(resource)} tried to run a statement for tenant #{inspect(tenant)} \ + inside a transaction open on another tenant's database. SQLite cannot commit \ + across files atomically in WAL mode, so this statement would commit on its \ + own and survive a rollback. + + Open one transaction per tenant instead. """ end - fun.() + previous = repo.put_dynamic_repo(instance) + + try do + fun.() + after + repo.put_dynamic_repo(previous) + end + end + + @doc false + def tenant_repo?(repo) do + Code.ensure_loaded?(repo) and function_exported?(repo, :tenant_repo, 2) end defp tenant_required?(resource) do diff --git a/lib/data_layer/info.ex b/lib/data_layer/info.ex index ae8f6d8..98ea859 100644 --- a/lib/data_layer/info.ex +++ b/lib/data_layer/info.ex @@ -66,17 +66,6 @@ defmodule AshSqlite.DataLayer.Info do defp resource_module(dsl_state), do: Spark.Dsl.Transformer.get_persisted(dsl_state, :module) - @doc """ - The tenant repo for a resource as `{module, opts}`, or nil. - - Only set for resources with context multitenancy. A function given to the - `tenant_repo` option is returned wrapped in a module implementing - `AshSqlite.TenantRepo`. - """ - def tenant_repo(resource) do - Extension.get_opt(resource, [:sqlite], :tenant_repo, nil, true) - end - @doc "The configured table for a resource" def table(resource) do Extension.get_opt(resource, [:sqlite], :table, nil, true) diff --git a/lib/repo.ex b/lib/repo.ex index b067efd..821e967 100644 --- a/lib/repo.ex +++ b/lib/repo.ex @@ -33,6 +33,24 @@ defmodule AshSqlite.Repo do @doc "Whether Ash may wrap write actions on this repo's resources in a transaction" @callback write_transactions?() :: boolean() + @doc """ + The repo instance, a pid or a registered name, that `tenant`'s statements run on. + + Implementing this makes the repo a tenant repo, for `strategy :context`: each + tenant has its own database file, and the tenant is applied by choosing the + instance. Every resource on a tenant repo must use `strategy :context`, so put + tables shared across tenants on a repo module of their own. + + `context.type` is `:read` for queries and aggregates, and `:mutate` for writes and + for opening a transaction. + """ + @callback tenant_repo( + tenant :: term(), + context :: %{resource: Ash.Resource.t(), type: :read | :mutate} + ) :: pid() | atom() + + @optional_callbacks tenant_repo: 2 + @doc "The path where your migrations are stored" @callback migrations_path() :: String.t() | nil @doc "Allows overriding a given migration type for *all* fields, for example if you wanted to always use :timestamptz for :utc_datetime fields" diff --git a/lib/tenant_repo.ex b/lib/tenant_repo.ex deleted file mode 100644 index a145519..0000000 --- a/lib/tenant_repo.ex +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: 2023 ash_sqlite contributors -# -# SPDX-License-Identifier: MIT - -defmodule AshSqlite.TenantRepo do - @moduledoc """ - Chooses the repo instance a tenanted statement runs on. - - Under `strategy :context` each tenant has its own database file. The SQL is the - same for every tenant, and the tenant is applied by choosing the repo instance. - Set with the `tenant_repo` option, as a function or as a module implementing - this behaviour. - """ - - @typedoc """ - What the statement is. - - * `:resource` — the resource the statement is for. - * `:type` — `:read` for queries and aggregates, `:mutate` for writes and for - the callback that opens a transaction. The same value the `repo` option is - called with. - """ - @type context :: %{resource: Ash.Resource.t(), type: :read | :mutate} - - @doc "The repo instance, a pid or a registered name, that `tenant`'s statement runs on." - @callback repo(tenant :: term(), context :: context(), opts :: Keyword.t()) :: pid() | atom() - - @doc """ - Runs `fun` with the repo instance for `tenant` bound, and returns its result. - - Optional. Without it, the instance from `c:repo/3` is bound with - `put_dynamic_repo/1` around `fun` and the previous binding is restored after. - Implement it to do work that has to bracket the statement. - """ - @callback with_repo( - tenant :: term(), - context :: context(), - opts :: Keyword.t(), - fun :: (-> result) - ) :: result - when result: var - - @optional_callbacks with_repo: 4 -end diff --git a/lib/tenant_repo/function.ex b/lib/tenant_repo/function.ex deleted file mode 100644 index ff0b1bd..0000000 --- a/lib/tenant_repo/function.ex +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-FileCopyrightText: 2023 ash_sqlite contributors -# -# SPDX-License-Identifier: MIT - -defmodule AshSqlite.TenantRepo.Function do - @moduledoc false - @behaviour AshSqlite.TenantRepo - - @impl true - def repo(tenant, context, fun: fun), do: fun.(tenant, context) -end diff --git a/lib/verifiers/verify_tenant_repo.ex b/lib/verifiers/verify_tenant_repo.ex index d8441b8..6810f2c 100644 --- a/lib/verifiers/verify_tenant_repo.ex +++ b/lib/verifiers/verify_tenant_repo.ex @@ -11,29 +11,59 @@ defmodule AshSqlite.Verifiers.VerifyTenantRepo do @impl true def verify(dsl) do - if Ash.Resource.Info.multitenancy_strategy(dsl) == :context and - is_nil(Verifier.get_option(dsl, [:sqlite], :tenant_repo)) do - {:error, - DslError.exception( - module: Verifier.get_persisted(dsl, :module), - path: [:sqlite, :tenant_repo], - message: """ - `strategy :context` needs a `tenant_repo`. - - Each tenant has its own database file, so a tenanted statement has to be \ - given the repo instance for its tenant. Only the application knows how to \ - map a tenant to a repo instance. - - sqlite do - repo MyApp.Repo - tenant_repo &MyApp.Tenants.repo/2 - end - - See `AshSqlite.TenantRepo`.\ - """ - )} - else - :ok - end + context? = Ash.Resource.Info.multitenancy_strategy(dsl) == :context + + # A function `repo` can name a different module for reads and writes. + [:read, :mutate] + |> Enum.map(&AshSqlite.DataLayer.Info.repo(dsl, &1)) + |> Enum.uniq() + |> Enum.filter(&match?({:module, _}, Code.ensure_compiled(&1))) + |> Enum.find_value(:ok, fn repo -> + case {context?, AshSqlite.DataLayer.tenant_repo?(repo)} do + {true, false} -> {:error, not_a_tenant_repo(dsl, repo)} + {false, true} -> {:error, not_context(dsl, repo)} + _ -> nil + end + end) + end + + defp not_a_tenant_repo(dsl, repo) do + DslError.exception( + module: Verifier.get_persisted(dsl, :module), + path: [:sqlite, :repo], + message: """ + `strategy :context` needs a tenant repo, but #{inspect(repo)} does not \ + implement `c:AshSqlite.Repo.tenant_repo/2`. + + Each tenant has its own database file, so a tenanted statement has to be \ + given the repo instance for its tenant. Only the application knows how to map \ + a tenant to a repo instance: + + defmodule MyApp.TenantRepo do + use AshSqlite.Repo, otp_app: :my_app + + def tenant_repo(tenant, _context), do: MyApp.Tenants.repo_for(tenant) + end + """ + ) + end + + defp not_context(dsl, repo) do + DslError.exception( + module: Verifier.get_persisted(dsl, :module), + path: [:sqlite, :repo], + message: """ + #{inspect(repo)} is a tenant repo, so every resource on it must use \ + `strategy :context`. + + Ecto binds a repo instance per repo module, not per resource. Inside a \ + tenant's transaction this resource would run on that tenant's database. Put \ + it on a repo module of its own: + + sqlite do + repo MyApp.SharedRepo + end + """ + ) end end diff --git a/mix.exs b/mix.exs index 120b48c..bdac2c1 100644 --- a/mix.exs +++ b/mix.exs @@ -119,9 +119,6 @@ defmodule AshSqlite.MixProject do AshSqlite.Repo, AshSqlite.DataLayer ], - Multitenancy: [ - AshSqlite.TenantRepo - ], Utilities: [ AshSqlite.ManualRelationship ], diff --git a/test/multitenancy_test.exs b/test/multitenancy_test.exs index 4b6a1ab..2851a16 100644 --- a/test/multitenancy_test.exs +++ b/test/multitenancy_test.exs @@ -10,7 +10,7 @@ defmodule AshSqlite.MultitenancyTest do import ExUnit.CaptureIO - alias AshSqlite.Test.{BracketedTenantPost, BracketingTenantRepo, TenantedPost, TenantRepos} + alias AshSqlite.Test.{TenantedPost, TenantRepos} require Ash.Query @@ -68,14 +68,7 @@ defmodule AshSqlite.MultitenancyTest do assert Ash.DataLayer.data_layer_can?(TenantedPost, :multitenancy) end - test "a function given as the tenant repo is wrapped in a module" do - assert {AshSqlite.TenantRepo.Function, [fun: fun]} = - AshSqlite.DataLayer.Info.tenant_repo(TenantedPost) - - assert fun == (&TenantRepos.repo/2) - end - - test "the tenant repo function is what actually runs" do + test "the repo's tenant_repo/2 is what actually runs" do TenantRepos.reset_calls() create!("acme", "one") @@ -146,22 +139,6 @@ defmodule AshSqlite.MultitenancyTest do assert Enum.all?(TenantRepos.calls(), &match?({"acme", :mutate}, &1)) end - test "a tenant repo module with with_repo/4 brackets each statement itself", %{repos: repos} do - BracketingTenantRepo.reset_events() - - BracketedTenantPost - |> Ash.Changeset.for_create(:create, %{title: "bracketed"}, tenant: "acme") - |> Ash.create!() - - events = BracketingTenantRepo.events() - - assert events != [] - assert List.first(events) == :entered - assert List.last(events) == :left - assert Enum.count(events, &(&1 == :entered)) == Enum.count(events, &(&1 == :left)) - assert titles_in_file(repos["acme"].path) == ["bracketed"] - end - test "Ash refuses a tenantless query before it reaches the data layer" do assert_raise Ash.Error.Invalid, ~r/require a tenant to be specified/, fn -> Ash.read!(TenantedPost) @@ -220,7 +197,6 @@ defmodule AshSqlite.MultitenancyTest do sqlite do table("refused_global_posts") repo(AshSqlite.TenantTestRepo) - tenant_repo(&AshSqlite.Test.TenantRepos.repo/2) migrate?(false) end end @@ -261,7 +237,7 @@ defmodule AshSqlite.MultitenancyTest do sqlite do table("no_tenant_repo_posts") - repo(AshSqlite.TenantTestRepo) + repo(AshSqlite.TestRepo) migrate?(false) end end @@ -271,6 +247,83 @@ defmodule AshSqlite.MultitenancyTest do end end) - assert message =~ "`strategy :context` needs a `tenant_repo`" + assert message =~ "`strategy :context` needs a tenant repo" + assert message =~ "AshSqlite.TestRepo does not implement" + end + + describe "a resource without strategy :context on a tenant repo" do + test "is refused at compile time" do + message = + capture_io(:stderr, fn -> + try do + Code.eval_string(""" + defmodule SharedOnTenantRepoPost do + use Ash.Resource, + domain: nil, + validate_domain_inclusion?: false, + data_layer: AshSqlite.DataLayer + + actions do + defaults([:read]) + end + + attributes do + uuid_primary_key(:id) + end + + sqlite do + table("shared_posts") + repo(AshSqlite.TenantTestRepo) + migrate?(false) + end + end + """) + rescue + _ -> :raised + end + end) + + assert message =~ "AshSqlite.TenantTestRepo is a tenant repo" + assert message =~ "repo MyApp.SharedRepo" + end + + test "is refused at compile time when the repo is a function" do + message = + capture_io(:stderr, fn -> + try do + Code.eval_string(""" + defmodule SharedOnTenantRepoRouting do + def repo(_resource, :read), do: AshSqlite.TestRepo + def repo(_resource, :mutate), do: AshSqlite.TenantTestRepo + end + + defmodule SharedOnRoutedTenantRepoPost do + use Ash.Resource, + domain: nil, + validate_domain_inclusion?: false, + data_layer: AshSqlite.DataLayer + + actions do + defaults([:read]) + end + + attributes do + uuid_primary_key(:id) + end + + sqlite do + table("shared_posts") + repo(&SharedOnTenantRepoRouting.repo/2) + migrate?(false) + end + end + """) + rescue + _ -> :raised + end + end) + + assert message =~ "AshSqlite.TenantTestRepo is a tenant repo" + end end end diff --git a/test/support/bracketing_tenant_repo.ex b/test/support/bracketing_tenant_repo.ex deleted file mode 100644 index 551c8c7..0000000 --- a/test/support/bracketing_tenant_repo.ex +++ /dev/null @@ -1,36 +0,0 @@ -# SPDX-FileCopyrightText: 2023 ash_sqlite contributors -# -# SPDX-License-Identifier: MIT - -defmodule AshSqlite.Test.BracketingTenantRepo do - @moduledoc """ - A tenant repo that binds the instance itself, and records entering and leaving. - """ - @behaviour AshSqlite.TenantRepo - - @doc "Every `:entered` and `:left`, oldest first." - def events, do: Enum.reverse(Process.get({__MODULE__, :events}, [])) - - @doc "Forgets what has been recorded." - def reset_events, do: Process.delete({__MODULE__, :events}) - - @impl true - def repo(tenant, context, _opts), do: AshSqlite.Test.TenantRepos.repo(tenant, context) - - @impl true - def with_repo(tenant, context, opts, fun) do - record(:entered) - previous = AshSqlite.TenantTestRepo.put_dynamic_repo(repo(tenant, context, opts)) - - try do - fun.() - after - AshSqlite.TenantTestRepo.put_dynamic_repo(previous) - record(:left) - end - end - - defp record(event) do - Process.put({__MODULE__, :events}, [event | Process.get({__MODULE__, :events}, [])]) - end -end diff --git a/test/support/domain.ex b/test/support/domain.ex index fe47b36..e5d8914 100644 --- a/test/support/domain.ex +++ b/test/support/domain.ex @@ -21,7 +21,6 @@ defmodule AshSqlite.Test.Domain do resource(AshSqlite.Test.TransactionalAccount) resource(AshSqlite.Test.NamedFnRepoAccount) resource(AshSqlite.Test.TenantedPost) - resource(AshSqlite.Test.BracketedTenantPost) resource(AshSqlite.Test.Organization) resource(AshSqlite.Test.Manager) resource(AshSqlite.Test.Device) diff --git a/test/support/resources/bracketed_tenant_post.ex b/test/support/resources/bracketed_tenant_post.ex deleted file mode 100644 index 972c762..0000000 --- a/test/support/resources/bracketed_tenant_post.ex +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: 2023 ash_sqlite contributors -# -# SPDX-License-Identifier: MIT - -defmodule AshSqlite.Test.BracketedTenantPost do - @moduledoc """ - `AshSqlite.Test.TenantedPost`'s table, reached through a tenant repo module that - implements `with_repo/4`. - """ - use Ash.Resource, domain: AshSqlite.Test.Domain, data_layer: AshSqlite.DataLayer - - actions do - default_accept(:*) - defaults([:create, :read]) - end - - multitenancy do - strategy(:context) - end - - attributes do - uuid_primary_key(:id) - attribute(:title, :string, public?: true) - end - - sqlite do - table("tenanted_posts") - repo(AshSqlite.TenantTestRepo) - tenant_repo(AshSqlite.Test.BracketingTenantRepo) - migrate?(false) - end -end diff --git a/test/support/resources/tenanted_post.ex b/test/support/resources/tenanted_post.ex index 3f4921a..c8e2a6c 100644 --- a/test/support/resources/tenanted_post.ex +++ b/test/support/resources/tenanted_post.ex @@ -29,7 +29,6 @@ defmodule AshSqlite.Test.TenantedPost do sqlite do table("tenanted_posts") repo(AshSqlite.TenantTestRepo) - tenant_repo(&AshSqlite.Test.TenantRepos.repo/2) migrate?(false) end end diff --git a/test/support/tenant_test_repo.ex b/test/support/tenant_test_repo.ex index a72fe3c..5fdd621 100644 --- a/test/support/tenant_test_repo.ex +++ b/test/support/tenant_test_repo.ex @@ -4,7 +4,7 @@ defmodule AshSqlite.TenantTestRepo do @moduledoc """ - A repo used as a template, started per tenant as an anonymous instance rather than under its own name. + A tenant repo, started per tenant as an anonymous instance rather than under its own name. """ use AshSqlite.Repo, otp_app: :ash_sqlite @@ -15,4 +15,6 @@ defmodule AshSqlite.TenantTestRepo do # Transactions are a repo decision rather than a resource one, so the tenanted # tests get them by asking here. def write_transactions?, do: true + + def tenant_repo(tenant, context), do: AshSqlite.Test.TenantRepos.repo(tenant, context) end