@@ -35,11 +35,48 @@ defmodule AshPostgres.DataLayer do
3535 See the documentation for `Mix.Tasks.AshPostgres.GenerateMigrations` for how to generate
3636 migrations from your resources
3737 """
38+
39+ @ manage_tenant % Ash.Dsl.Section {
40+ name: :manage_tenant ,
41+ describe: """
42+ Configuration for the behavior of a resource that manages a tenant
43+ """ ,
44+ schema: [
45+ template: [
46+ type: { :custom , __MODULE__ , :tenant_template , [ ] } ,
47+ required: true ,
48+ doc: """
49+ A template that will cause the resource to create/manage the specified schema.
50+
51+ Use this if you have a resource that, when created, it should create a new tenant
52+ for you. For example, if you have a `customer` resource, and you want to create
53+ a schema for each customer based on their id, e.g `customer_10` set this option
54+ to `["customer_", :id]`. Then, when this is created, it will create a schema called
55+ `["customer_", :id]`, and run your tenant migrations on it. Then, if you were to change
56+ that customer's id to `20`, it would rename the schema to `customer_20`. Generally speaking
57+ you should avoid changing the tenant id.
58+ """
59+ ] ,
60+ create?: [
61+ type: :boolean ,
62+ default: true ,
63+ doc: "Whether or not to automatically create a tenant when a record is created"
64+ ] ,
65+ update?: [
66+ type: :boolean ,
67+ default: true ,
68+ doc: "Whether or not to automatically update the tenant name if the record is udpated"
69+ ]
70+ ]
71+ }
3872@ postgres % Ash.Dsl.Section {
3973name: :postgres ,
4074describe: """
4175 Postgres data layer configuration
4276 """ ,
77+ sections: [
78+ @ manage_tenant
79+ ] ,
4380schema: [
4481repo: [
4582type: { :custom , AshPostgres.DataLayer , :validate_repo , [ ] } ,
@@ -101,6 +138,17 @@ defmodule AshPostgres.DataLayer do
101138end
102139end
103140
141+ @ doc false
142+ def tenant_template ( value ) do
143+ value = List . wrap ( value )
144+
145+ if Enum . all? ( value , & ( is_binary ( & 1 ) || is_atom ( & 1 ) ) ) do
146+ { :ok , value }
147+ else
148+ { :error , "Expected all values for `manages_tenant` to be strings or atoms" }
149+ end
150+ end
151+
104152@ doc false
105153def validate_skip_unique_indexes ( indexes ) do
106154indexes = List . wrap ( indexes )
@@ -143,6 +191,7 @@ defmodule AshPostgres.DataLayer do
143191def can? ( _ , :filter ) , do: true
144192def can? ( _ , :limit ) , do: true
145193def can? ( _ , :offset ) , do: true
194+ def can? ( _ , :multitenancy ) , do: true
146195def can? ( _ , { :filter_operator , % { right: % Ref { } } } ) , do: false
147196def can? ( _ , { :filter_operator , % Eq { left: % Ref { } } } ) , do: true
148197def can? ( _ , { :filter_operator , % In { left: % Ref { } } } ) , do: true
@@ -187,9 +236,23 @@ defmodule AshPostgres.DataLayer do
187236
188237@ impl true
189238def run_query ( query , resource ) do
190- { :ok , repo ( resource ) . all ( query ) }
239+ { :ok , repo ( resource ) . all ( query , repo_opts ( query ) ) }
240+ end
241+
242+ defp repo_opts ( % Ash.Changeset { tenant: tenant , resource: resource } ) do
243+ repo_opts ( % { tenant: tenant , resource: resource } )
244+ end
245+
246+ defp repo_opts ( % { tenant: tenant , resource: resource } ) when not is_nil ( tenant ) do
247+ if Ash.Resource . multitenancy_strategy ( resource ) == :context do
248+ [ prefix: tenant ]
249+ else
250+ [ ]
251+ end
191252end
192253
254+ defp repo_opts ( _ ) , do: [ ]
255+
193256@ impl true
194257def functions ( resource ) do
195258config = repo ( resource ) . config ( )
@@ -214,7 +277,12 @@ defmodule AshPostgres.DataLayer do
214277& add_subquery_aggregate_select ( & 2 , & 1 , resource )
215278)
216279
217- { :ok , repo ( resource ) . one ( query ) }
280+ { :ok , repo ( resource ) . one ( query , repo_opts ( query ) ) }
281+ end
282+
283+ @ impl true
284+ def set_tenant ( _resource , query , tenant ) do
285+ { :ok , Ecto.Query . put_query_prefix ( query , to_string ( tenant ) ) }
218286end
219287
220288@ impl true
@@ -245,7 +313,7 @@ defmodule AshPostgres.DataLayer do
245313& add_subquery_aggregate_select ( & 2 , & 1 , destination_resource )
246314)
247315
248- { :ok , repo ( source_resource ) . one ( query ) }
316+ { :ok , repo ( source_resource ) . one ( query , repo_opts ( :query ) ) }
249317end
250318
251319@ impl true
@@ -266,7 +334,7 @@ defmodule AshPostgres.DataLayer do
266334destination_field
267335)
268336
269- { :ok , repo ( source_resource ) . all ( query ) }
337+ { :ok , repo ( source_resource ) . all ( query , repo_opts ( query ) ) }
270338end
271339
272340defp lateral_join_query (
@@ -307,25 +375,88 @@ defmodule AshPostgres.DataLayer do
307375changeset . data
308376|> Map . update! ( :__meta__ , & Map . put ( & 1 , :source , table ( resource ) ) )
309377|> ecto_changeset ( changeset )
310- |> repo ( resource ) . insert ( )
378+ |> repo ( resource ) . insert ( repo_opts ( changeset ) )
379+ |> case do
380+ { :ok , result } ->
381+ case maybe_create_tenant ( resource , result ) do
382+ :ok ->
383+ { :ok , result }
384+
385+ { :error , error } ->
386+ { :error , error }
387+ end
388+
389+ { :error , error } ->
390+ { :error , error }
391+ end
311392rescue
312393e ->
313394{ :error , e }
314395end
315396
397+ defp maybe_create_tenant ( resource , result ) do
398+ if AshPostgres . manage_tenant_create? ( resource ) do
399+ tenant_name = tenant_name ( resource , result )
400+
401+ AshPostgres.MultiTenancy . create_tenant ( tenant_name , repo ( resource ) )
402+ else
403+ :ok
404+ end
405+ end
406+
407+ defp maybe_update_tenant ( resource , changeset , result ) do
408+ if AshPostgres . manage_tenant_update? ( resource ) do
409+ changing_tenant_name? =
410+ resource
411+ |> AshPostgres . manage_tenant_template ( )
412+ |> Enum . filter ( & is_atom / 1 )
413+ |> Enum . any? ( & Ash.Changeset . changing_attribute? ( changeset , & 1 ) )
414+
415+ if changing_tenant_name? do
416+ old_tenant_name = tenant_name ( resource , changeset . data )
417+
418+ new_tenant_name = tenant_name ( resource , result )
419+ AshPostgres.MultiTenancy . rename_tenant ( repo ( resource ) , old_tenant_name , new_tenant_name )
420+ end
421+ end
422+
423+ :ok
424+ end
425+
426+ defp tenant_name ( resource , result ) do
427+ resource
428+ |> AshPostgres . manage_tenant_template ( )
429+ |> Enum . map_join ( fn item ->
430+ if is_binary ( item ) do
431+ item
432+ else
433+ result
434+ |> Map . get ( item )
435+ |> to_string ( )
436+ end
437+ end )
438+ end
439+
316440defp ecto_changeset ( record , changeset ) do
317441Ecto.Changeset . change ( record , changeset . attributes )
318442end
319443
320444@ impl true
321445def upsert ( resource , changeset ) do
322- changeset . data
323- |> Map . update! ( :__meta__ , & Map . put ( & 1 , :source , table ( resource ) ) )
324- |> ecto_changeset ( changeset )
325- |> repo ( resource ) . insert (
326- on_conflict: :replace_all ,
327- conflict_target: Ash.Resource . primary_key ( resource )
328- )
446+ repo_opts =
447+ changeset
448+ |> repo_opts ( )
449+ |> Keyword . put ( :on_conflict , { :replace , Map . keys ( changeset . attributes ) } )
450+ |> Keyword . put ( :conflict_target , Ash.Resource . primary_key ( resource ) )
451+
452+ if AshPostgres . manage_tenant_update? ( resource ) do
453+ { :error , "Cannot currently upsert a resource that owns a tenant" }
454+ else
455+ changeset . data
456+ |> Map . update! ( :__meta__ , & Map . put ( & 1 , :source , table ( resource ) ) )
457+ |> ecto_changeset ( changeset )
458+ |> repo ( resource ) . insert ( repo_opts )
459+ end
329460rescue
330461e ->
331462{ :error , e }
@@ -336,17 +467,29 @@ defmodule AshPostgres.DataLayer do
336467changeset . data
337468|> Map . update! ( :__meta__ , & Map . put ( & 1 , :source , table ( resource ) ) )
338469|> ecto_changeset ( changeset )
339- |> repo ( resource ) . update ( )
470+ |> repo ( resource ) . update ( repo_opts ( changeset ) )
471+ |> case do
472+ { :ok , result } ->
473+ maybe_update_tenant ( resource , changeset , result )
474+
475+ { :ok , result }
476+
477+ { :error , error } ->
478+ { :error , error }
479+ end
340480rescue
341481e ->
342482{ :error , e }
343483end
344484
345485@ impl true
346- def destroy ( resource , % { data: record } ) do
347- case repo ( resource ) . delete ( record ) do
348- { :ok , _record } -> :ok
349- { :error , error } -> { :error , error }
486+ def destroy ( resource , % { data: record } = changeset ) do
487+ case repo ( resource ) . delete ( record , repo_opts ( changeset ) ) do
488+ { :ok , _record } ->
489+ :ok
490+
491+ { :error , error } ->
492+ { :error , error }
350493end
351494rescue
352495e ->
@@ -624,11 +767,18 @@ defmodule AshPostgres.DataLayer do
624767}
625768end
626769
627- defp aggregate_subquery ( relationship , _aggregate ) do
628- from ( row in relationship . destination ,
629- group_by: ^ relationship . destination_field ,
630- select: field ( row , ^ relationship . destination_field )
631- )
770+ defp aggregate_subquery ( relationship , aggregate ) do
771+ query =
772+ from ( row in relationship . destination ,
773+ group_by: ^ relationship . destination_field ,
774+ select: field ( row , ^ relationship . destination_field )
775+ )
776+
777+ if aggregate . query && aggregate . query . tenant do
778+ Ecto.Query . put_query_prefix ( query , aggregate . query . tenant )
779+ else
780+ query
781+ end
632782end
633783
634784defp add_subquery_aggregate_select ( query , % { kind: :count } = aggregate , resource ) do