mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-20 05:23:18 +12:00
fix: ensure that context multitenancy is properly applied to lateral many-to-many joins
This commit is contained in:
parent
2c31b3e167
commit
8e32e0ab9a
10 changed files with 235 additions and 13 deletions
|
@ -19,6 +19,7 @@ spark_locals_without_parens = [
|
||||||
include: 1,
|
include: 1,
|
||||||
index: 1,
|
index: 1,
|
||||||
index: 2,
|
index: 2,
|
||||||
|
index?: 1,
|
||||||
match_type: 1,
|
match_type: 1,
|
||||||
match_with: 1,
|
match_with: 1,
|
||||||
message: 1,
|
message: 1,
|
||||||
|
|
|
@ -301,10 +301,11 @@ reference :post, on_delete: :delete, on_update: :update, name: "comments_to_post
|
||||||
| [`ignore?`](#postgres-references-reference-ignore?){: #postgres-references-reference-ignore? } | `boolean` | | If set to true, no reference is created for the given relationship. This is useful if you need to define it in some custom way |
|
| [`ignore?`](#postgres-references-reference-ignore?){: #postgres-references-reference-ignore? } | `boolean` | | If set to true, no reference is created for the given relationship. This is useful if you need to define it in some custom way |
|
||||||
| [`on_delete`](#postgres-references-reference-on_delete){: #postgres-references-reference-on_delete } | `:delete \| :nilify \| :nothing \| :restrict \| {:nilify, atom \| list(atom)}` | | What should happen to records of this resource when the referenced record of the *destination* resource is deleted. |
|
| [`on_delete`](#postgres-references-reference-on_delete){: #postgres-references-reference-on_delete } | `:delete \| :nilify \| :nothing \| :restrict \| {:nilify, atom \| list(atom)}` | | What should happen to records of this resource when the referenced record of the *destination* resource is deleted. |
|
||||||
| [`on_update`](#postgres-references-reference-on_update){: #postgres-references-reference-on_update } | `:update \| :nilify \| :nothing \| :restrict` | | What should happen to records of this resource when the referenced destination_attribute of the *destination* record is update. |
|
| [`on_update`](#postgres-references-reference-on_update){: #postgres-references-reference-on_update } | `:update \| :nilify \| :nothing \| :restrict` | | What should happen to records of this resource when the referenced destination_attribute of the *destination* record is update. |
|
||||||
| [`deferrable`](#postgres-references-reference-deferrable){: #postgres-references-reference-deferrable } | `false \| true \| :initially` | `false` | Wether or not the constraint is deferrable. This only affects the migration generator. |
|
| [`deferrable`](#postgres-references-reference-deferrable){: #postgres-references-reference-deferrable } | `false \| true \| :initially` | `false` | Whether or not the constraint is deferrable. This only affects the migration generator. |
|
||||||
| [`name`](#postgres-references-reference-name){: #postgres-references-reference-name } | `String.t` | | The name of the foreign key to generate in the database. Defaults to <table>_<source_attribute>_fkey |
|
| [`name`](#postgres-references-reference-name){: #postgres-references-reference-name } | `String.t` | | The name of the foreign key to generate in the database. Defaults to <table>_<source_attribute>_fkey |
|
||||||
| [`match_with`](#postgres-references-reference-match_with){: #postgres-references-reference-match_with } | `keyword` | | Defines additional keys to the foreign key in order to build a composite foreign key. The key should be the name of the source attribute (in the current resource), the value the name of the destination attribute. |
|
| [`match_with`](#postgres-references-reference-match_with){: #postgres-references-reference-match_with } | `keyword` | | Defines additional keys to the foreign key in order to build a composite foreign key. The key should be the name of the source attribute (in the current resource), the value the name of the destination attribute. |
|
||||||
| [`match_type`](#postgres-references-reference-match_type){: #postgres-references-reference-match_type } | `:simple \| :partial \| :full` | | select if the match is `:simple`, `:partial`, or `:full` |
|
| [`match_type`](#postgres-references-reference-match_type){: #postgres-references-reference-match_type } | `:simple \| :partial \| :full` | | select if the match is `:simple`, `:partial`, or `:full` |
|
||||||
|
| [`index?`](#postgres-references-reference-index?){: #postgres-references-reference-index? } | `boolean` | `false` | Whether to create or not a corresponding index |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1270,11 +1270,17 @@ defmodule AshPostgres.DataLayer do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp lateral_join_source_query(
|
defp lateral_join_source_query(
|
||||||
%{__ash_bindings__: %{lateral_join_source_query: lateral_join_source_query}},
|
%{
|
||||||
_
|
__ash_bindings__: %{
|
||||||
|
lateral_join_source_query: lateral_join_source_query
|
||||||
|
}
|
||||||
|
},
|
||||||
|
source_query
|
||||||
)
|
)
|
||||||
when not is_nil(lateral_join_source_query) do
|
when not is_nil(lateral_join_source_query) do
|
||||||
{:ok, lateral_join_source_query}
|
{:ok,
|
||||||
|
lateral_join_source_query
|
||||||
|
|> set_subquery_prefix(source_query, lateral_join_source_query.__ash_bindings__.resource)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp lateral_join_source_query(query, source_query) do
|
defp lateral_join_source_query(query, source_query) do
|
||||||
|
|
|
@ -1777,8 +1777,10 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
attribute.source == old_attribute.source
|
attribute.source == old_attribute.source
|
||||||
end)
|
end)
|
||||||
|
|
||||||
has_removed_index? = attribute && !attribute[:index?] && old_attribute[:index?]
|
has_removed_index? =
|
||||||
attribute_doesnt_exist? = !attribute && old_attribute[:index?]
|
attribute && !attribute[:references][:index?] && old_attribute[:references][:index?]
|
||||||
|
|
||||||
|
attribute_doesnt_exist? = !attribute && old_attribute[:references][:index?]
|
||||||
|
|
||||||
has_removed_index? || attribute_doesnt_exist?
|
has_removed_index? || attribute_doesnt_exist?
|
||||||
end)
|
end)
|
||||||
|
@ -3173,6 +3175,7 @@ defmodule AshPostgres.MigrationGenerator do
|
||||||
|> Map.put_new(:on_update, nil)
|
|> Map.put_new(:on_update, nil)
|
||||||
|> Map.update!(:on_delete, &(&1 && load_references_on_delete(&1)))
|
|> Map.update!(:on_delete, &(&1 && load_references_on_delete(&1)))
|
||||||
|> Map.update!(:on_update, &(&1 && maybe_to_atom(&1)))
|
|> Map.update!(:on_update, &(&1 && maybe_to_atom(&1)))
|
||||||
|
|> Map.put_new(:index?, false)
|
||||||
|> Map.put_new(:match_with, nil)
|
|> Map.put_new(:match_with, nil)
|
||||||
|> Map.put_new(:match_type, nil)
|
|> Map.put_new(:match_type, nil)
|
||||||
|> Map.update!(
|
|> Map.update!(
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"default": "nil",
|
||||||
|
"size": null,
|
||||||
|
"type": "uuid",
|
||||||
|
"source": "source_id",
|
||||||
|
"references": {
|
||||||
|
"name": "friend_links_source_id_fkey",
|
||||||
|
"table": "multitenant_posts",
|
||||||
|
"multitenancy": {
|
||||||
|
"global": false,
|
||||||
|
"attribute": null,
|
||||||
|
"strategy": "context"
|
||||||
|
},
|
||||||
|
"destination_attribute": "id",
|
||||||
|
"primary_key?": true,
|
||||||
|
"schema": "public",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"deferrable": false,
|
||||||
|
"match_with": null,
|
||||||
|
"match_type": null,
|
||||||
|
"index?": false,
|
||||||
|
"destination_attribute_default": null,
|
||||||
|
"destination_attribute_generated": null
|
||||||
|
},
|
||||||
|
"primary_key?": true,
|
||||||
|
"allow_nil?": false,
|
||||||
|
"generated?": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "nil",
|
||||||
|
"size": null,
|
||||||
|
"type": "uuid",
|
||||||
|
"source": "dest_id",
|
||||||
|
"references": {
|
||||||
|
"name": "friend_links_dest_id_fkey",
|
||||||
|
"table": "multitenant_posts",
|
||||||
|
"multitenancy": {
|
||||||
|
"global": false,
|
||||||
|
"attribute": null,
|
||||||
|
"strategy": "context"
|
||||||
|
},
|
||||||
|
"destination_attribute": "id",
|
||||||
|
"primary_key?": true,
|
||||||
|
"schema": "public",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"deferrable": false,
|
||||||
|
"match_with": null,
|
||||||
|
"match_type": null,
|
||||||
|
"index?": false,
|
||||||
|
"destination_attribute_default": null,
|
||||||
|
"destination_attribute_generated": null
|
||||||
|
},
|
||||||
|
"primary_key?": true,
|
||||||
|
"allow_nil?": false,
|
||||||
|
"generated?": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"table": "friend_links",
|
||||||
|
"hash": "880BED202EB36FA2543D5DCC25DE1373676CE38CECFA2C8B5651757FFF3817EF",
|
||||||
|
"repo": "Elixir.AshPostgres.TestRepo",
|
||||||
|
"multitenancy": {
|
||||||
|
"global": false,
|
||||||
|
"attribute": null,
|
||||||
|
"strategy": "context"
|
||||||
|
},
|
||||||
|
"schema": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"identities": [],
|
||||||
|
"custom_indexes": [],
|
||||||
|
"base_filter": null,
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
defmodule AshPostgres.TestRepo.TenantMigrations.MigrateResources3 do
|
||||||
|
@moduledoc """
|
||||||
|
Updates resources based on their most recent snapshots.
|
||||||
|
|
||||||
|
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
create table(:friend_links, primary_key: false, prefix: prefix()) do
|
||||||
|
add(
|
||||||
|
:source_id,
|
||||||
|
references(:multitenant_posts,
|
||||||
|
column: :id,
|
||||||
|
name: "friend_links_source_id_fkey",
|
||||||
|
type: :uuid,
|
||||||
|
prefix: prefix()
|
||||||
|
),
|
||||||
|
primary_key: true,
|
||||||
|
null: false
|
||||||
|
)
|
||||||
|
|
||||||
|
add(
|
||||||
|
:dest_id,
|
||||||
|
references(:multitenant_posts,
|
||||||
|
column: :id,
|
||||||
|
name: "friend_links_dest_id_fkey",
|
||||||
|
type: :uuid,
|
||||||
|
prefix: prefix()
|
||||||
|
),
|
||||||
|
primary_key: true,
|
||||||
|
null: false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
drop(constraint(:friend_links, "friend_links_source_id_fkey"))
|
||||||
|
|
||||||
|
drop(constraint(:friend_links, "friend_links_dest_id_fkey"))
|
||||||
|
|
||||||
|
drop(table(:friend_links, prefix: prefix()))
|
||||||
|
end
|
||||||
|
end
|
|
@ -109,23 +109,74 @@ defmodule AshPostgres.Test.MultitenancyTest do
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
user1 =
|
user =
|
||||||
User
|
User
|
||||||
|> Ash.Changeset.for_create(:create, %{name: "a"})
|
|> Ash.Changeset.for_create(:create, %{name: "a"}, tenant: "org_#{org.id}")
|
||||||
|> Ash.Changeset.manage_relationship(:org, org, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:org, org, type: :append_and_remove)
|
||||||
|> Ash.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
user2 =
|
user2 =
|
||||||
User
|
User
|
||||||
|> Ash.Changeset.for_create(:create, %{name: "b"})
|
|> Ash.Changeset.for_create(:create, %{name: "a"}, tenant: "org_#{org.id}")
|
||||||
|> Ash.Changeset.manage_relationship(:org, org, type: :append_and_remove)
|
|> Ash.Changeset.manage_relationship(:org, org, type: :append_and_remove)
|
||||||
|> Ash.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
user1_id = user1.id
|
post =
|
||||||
user2_id = user2.id
|
Post
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "foobar"},
|
||||||
|
authorize?: false,
|
||||||
|
tenant: "org_#{org.id}"
|
||||||
|
)
|
||||||
|
|> Ash.Changeset.manage_relationship(:user, user, type: :append_and_remove)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
assert [%{id: ^user1_id}, %{id: ^user2_id}] =
|
post_id = post.id
|
||||||
Ash.load!(org, users: Ash.Query.sort(User, :name)).users
|
|
||||||
|
assert [%{posts: [%{id: ^post_id}]}, _] =
|
||||||
|
Ash.load!([user, user2], [posts: Ash.Query.limit(Post, 2)],
|
||||||
|
tenant: "org_#{org.id}",
|
||||||
|
authorize?: false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "loading context multitenant resources across a many-to-many with a limit works" do
|
||||||
|
org =
|
||||||
|
Org
|
||||||
|
|> Ash.Changeset.new()
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
user =
|
||||||
|
User
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "a"}, tenant: "org_#{org.id}")
|
||||||
|
|> Ash.Changeset.manage_relationship(:org, org, type: :append_and_remove)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
post =
|
||||||
|
Post
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "foobar"},
|
||||||
|
authorize?: false,
|
||||||
|
tenant: "org_#{org.id}"
|
||||||
|
)
|
||||||
|
|> Ash.Changeset.manage_relationship(:user, user, type: :append_and_remove)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
post2 =
|
||||||
|
Post
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "foobar"},
|
||||||
|
authorize?: false,
|
||||||
|
tenant: "org_#{org.id}"
|
||||||
|
)
|
||||||
|
|> Ash.Changeset.manage_relationship(:user, user, type: :append_and_remove)
|
||||||
|
|> Ash.Changeset.manage_relationship(:linked_posts, post, type: :append_and_remove)
|
||||||
|
|> Ash.create!()
|
||||||
|
|
||||||
|
post_id = post.id
|
||||||
|
|
||||||
|
assert [%{linked_posts: [%{id: ^post_id}]}, _] =
|
||||||
|
Ash.load!([post2, post], [linked_posts: Ash.Query.limit(Post, 2)],
|
||||||
|
tenant: "org_#{org.id}",
|
||||||
|
authorize?: false
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "manage_relationship from context multitenant resource to attribute multitenant resource doesn't raise an error" do
|
test "manage_relationship from context multitenant resource to attribute multitenant resource doesn't raise an error" do
|
||||||
|
|
|
@ -6,5 +6,6 @@ defmodule AshPostgres.MultitenancyTest.Domain do
|
||||||
resource(AshPostgres.MultitenancyTest.Org)
|
resource(AshPostgres.MultitenancyTest.Org)
|
||||||
resource(AshPostgres.MultitenancyTest.User)
|
resource(AshPostgres.MultitenancyTest.User)
|
||||||
resource(AshPostgres.MultitenancyTest.Post)
|
resource(AshPostgres.MultitenancyTest.Post)
|
||||||
|
resource(AshPostgres.MultitenancyTest.PostLink)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -50,6 +50,12 @@ defmodule AshPostgres.MultitenancyTest.Post do
|
||||||
end
|
end
|
||||||
|
|
||||||
has_one(:self, __MODULE__, destination_attribute: :id, source_attribute: :id, public?: true)
|
has_one(:self, __MODULE__, destination_attribute: :id, source_attribute: :id, public?: true)
|
||||||
|
|
||||||
|
many_to_many :linked_posts, __MODULE__ do
|
||||||
|
through(AshPostgres.MultitenancyTest.PostLink)
|
||||||
|
source_attribute_on_join_resource(:source_id)
|
||||||
|
destination_attribute_on_join_resource(:dest_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
calculations do
|
calculations do
|
||||||
|
|
31
test/support/multitenancy/resources/post_link.ex
Normal file
31
test/support/multitenancy/resources/post_link.ex
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
defmodule AshPostgres.MultitenancyTest.PostLink do
|
||||||
|
@moduledoc false
|
||||||
|
use Ash.Resource,
|
||||||
|
domain: AshPostgres.MultitenancyTest.Domain,
|
||||||
|
data_layer: AshPostgres.DataLayer
|
||||||
|
|
||||||
|
postgres do
|
||||||
|
table "friend_links"
|
||||||
|
repo AshPostgres.TestRepo
|
||||||
|
end
|
||||||
|
|
||||||
|
multitenancy do
|
||||||
|
strategy(:context)
|
||||||
|
end
|
||||||
|
|
||||||
|
actions do
|
||||||
|
defaults([:read, :destroy, create: :*, update: :*])
|
||||||
|
end
|
||||||
|
|
||||||
|
relationships do
|
||||||
|
belongs_to(:source, AshPostgres.MultitenancyTest.Post,
|
||||||
|
primary_key?: true,
|
||||||
|
allow_nil?: false
|
||||||
|
)
|
||||||
|
|
||||||
|
belongs_to(:dest, AshPostgres.MultitenancyTest.Post,
|
||||||
|
primary_key?: true,
|
||||||
|
allow_nil?: false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue