Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit03510da

Browse files
authored
feat: multitenancy (ash-project#25)
1 parent7805b15 commit03510da

File tree

23 files changed

+1122
-111
lines changed

23 files changed

+1122
-111
lines changed

‎.credo.exs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
{Credo.Check.Refactor.MatchInCondition,[]},
124124
{Credo.Check.Refactor.NegatedConditionsInUnless,[]},
125125
{Credo.Check.Refactor.NegatedConditionsWithElse,[]},
126-
{Credo.Check.Refactor.Nesting,[]},
126+
{Credo.Check.Refactor.Nesting,[max_nesting:3]},
127127
{Credo.Check.Refactor.UnlessWithElse,[]},
128128
{Credo.Check.Refactor.WithClauses,[]},
129129

‎.formatter.exs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
# DONT MODIFY IT BY HAND
33
locals_without_parens=[
44
base_filter_sql:1,
5+
create?:1,
56
migrate?:1,
67
repo:1,
78
skip_unique_indexes:1,
8-
table:1
9+
table:1,
10+
template:1,
11+
update?:1
912
]
1013

1114
[

‎.github/workflows/elixir.yml‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
matrix:
1919
otp:["23", "22"]
2020
elixir:["1.10.0"]
21-
ash:["master", "1.15"]
21+
ash:["master", "1.22"]
2222
pg_version:["9.5", "9.6", "11"]
2323
env:
2424
GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}
@@ -39,13 +39,13 @@ jobs:
3939
with:
4040
otp-version:${{matrix.otp}}
4141
elixir-version:${{matrix.elixir}}
42-
-uses:actions/cache@v1
42+
-uses:actions/cache@v2
4343
id:cache-deps
4444
with:
4545
path:deps
4646
key:otp-${{matrix.otp}}-elixir-${{matrix.elixir}}-deps-2-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
4747
restore-keys:otp-${{matrix.otp}}-elixir-${{matrix.elixir}}-deps-2
48-
-uses:actions/cache@v1
48+
-uses:actions/cache@v2
4949
id:cache-build
5050
with:
5151
path:_build
@@ -54,6 +54,7 @@ jobs:
5454
-run:mix deps.get
5555
-run:MIX_ENV=test mix ecto.create
5656
-run:MIX_ENV=test mix ecto.migrate
57+
-run:MIX_ENV=test mix ecto.migrate --migrations-path priv/test_repo/tenant_migrations
5758
-run:mix check --except dialyzer
5859
if:startsWith(github.ref, 'refs/tags/v')
5960
-run:mix check
@@ -85,7 +86,7 @@ jobs:
8586
with:
8687
otp-version:${{matrix.otp}}
8788
elixir-version:${{matrix.elixir}}
88-
-uses:actions/cache@v1
89+
-uses:actions/cache@v2
8990
id:cache-deps
9091
with:
9192
path:deps
@@ -113,7 +114,7 @@ jobs:
113114
with:
114115
otp-version:${{matrix.otp}}
115116
elixir-version:${{matrix.elixir}}
116-
-uses:actions/cache@v1
117+
-uses:actions/cache@v2
117118
id:cache-deps
118119
with:
119120
path:deps

‎config/config.exs‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ if Mix.env() == :dev do
1515
end
1616

1717
ifMix.env()==:testdo
18-
# Configure your database
19-
#
20-
2118
config:ash_postgres,AshPostgres.TestRepo,
2219
username:"postgres",
2320
database:"ash_postgres_test",

‎lib/ash_postgres.ex‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,19 @@ defmodule AshPostgres do
3333
defskip_unique_indexes?(resource)do
3434
Extension.get_opt(resource,[:postgres],:skip_unique_indexes?,[])
3535
end
36+
37+
@doc"The template for a managed tenant"
38+
defmanage_tenant_template(resource)do
39+
Extension.get_opt(resource,[:postgres,:manage_tenant],:template,nil)
40+
end
41+
42+
@doc"Whether or not to create a tenant for a given resource"
43+
defmanage_tenant_create?(resource)do
44+
Extension.get_opt(resource,[:postgres,:manage_tenant],:create?,false)
45+
end
46+
47+
@doc"Whether or not to update a tenant for a given resource"
48+
defmanage_tenant_update?(resource)do
49+
Extension.get_opt(resource,[:postgres,:manage_tenant],:update?,false)
50+
end
3651
end

‎lib/data_layer.ex‎

Lines changed: 172 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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{
3973
name::postgres,
4074
describe:"""
4175
Postgres data layer configuration
4276
""",
77+
sections:[
78+
@manage_tenant
79+
],
4380
schema:[
4481
repo:[
4582
type:{:custom,AshPostgres.DataLayer,:validate_repo,[]},
@@ -101,6 +138,17 @@ defmodule AshPostgres.DataLayer do
101138
end
102139
end
103140

141+
@docfalse
142+
deftenant_template(value)do
143+
value=List.wrap(value)
144+
145+
ifEnum.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
@docfalse
105153
defvalidate_skip_unique_indexes(indexes)do
106154
indexes=List.wrap(indexes)
@@ -143,6 +191,7 @@ defmodule AshPostgres.DataLayer do
143191
defcan?(_,:filter),do:true
144192
defcan?(_,:limit),do:true
145193
defcan?(_,:offset),do:true
194+
defcan?(_,:multitenancy),do:true
146195
defcan?(_,{:filter_operator,%{right:%Ref{}}}),do:false
147196
defcan?(_,{:filter_operator,%Eq{left:%Ref{}}}),do:true
148197
defcan?(_,{:filter_operator,%In{left:%Ref{}}}),do:true
@@ -187,9 +236,23 @@ defmodule AshPostgres.DataLayer do
187236

188237
@impltrue
189238
defrun_query(query,resource)do
190-
{:ok,repo(resource).all(query)}
239+
{:ok,repo(resource).all(query,repo_opts(query))}
240+
end
241+
242+
defprepo_opts(%Ash.Changeset{tenant:tenant,resource:resource})do
243+
repo_opts(%{tenant:tenant,resource:resource})
244+
end
245+
246+
defprepo_opts(%{tenant:tenant,resource:resource})whennotis_nil(tenant)do
247+
ifAsh.Resource.multitenancy_strategy(resource)==:contextdo
248+
[prefix:tenant]
249+
else
250+
[]
251+
end
191252
end
192253

254+
defprepo_opts(_),do:[]
255+
193256
@impltrue
194257
deffunctions(resource)do
195258
config=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+
@impltrue
284+
defset_tenant(_resource,query,tenant)do
285+
{:ok,Ecto.Query.put_query_prefix(query,to_string(tenant))}
218286
end
219287

220288
@impltrue
@@ -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))}
249317
end
250318

251319
@impltrue
@@ -266,7 +334,7 @@ defmodule AshPostgres.DataLayer do
266334
destination_field
267335
)
268336

269-
{:ok,repo(source_resource).all(query)}
337+
{:ok,repo(source_resource).all(query,repo_opts(query))}
270338
end
271339

272340
defplateral_join_query(
@@ -307,25 +375,88 @@ defmodule AshPostgres.DataLayer do
307375
changeset.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+
|>casedo
380+
{:ok,result}->
381+
casemaybe_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
311392
rescue
312393
e->
313394
{:error,e}
314395
end
315396

397+
defpmaybe_create_tenant(resource,result)do
398+
ifAshPostgres.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+
defpmaybe_update_tenant(resource,changeset,result)do
408+
ifAshPostgres.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+
ifchanging_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+
defptenant_name(resource,result)do
427+
resource
428+
|>AshPostgres.manage_tenant_template()
429+
|>Enum.map_join(fnitem->
430+
ifis_binary(item)do
431+
item
432+
else
433+
result
434+
|>Map.get(item)
435+
|>to_string()
436+
end
437+
end)
438+
end
439+
316440
defpecto_changeset(record,changeset)do
317441
Ecto.Changeset.change(record,changeset.attributes)
318442
end
319443

320444
@impltrue
321445
defupsert(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+
ifAshPostgres.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
329460
rescue
330461
e->
331462
{:error,e}
@@ -336,17 +467,29 @@ defmodule AshPostgres.DataLayer do
336467
changeset.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+
|>casedo
472+
{:ok,result}->
473+
maybe_update_tenant(resource,changeset,result)
474+
475+
{:ok,result}
476+
477+
{:error,error}->
478+
{:error,error}
479+
end
340480
rescue
341481
e->
342482
{:error,e}
343483
end
344484

345485
@impltrue
346-
defdestroy(resource,%{data:record})do
347-
caserepo(resource).delete(record)do
348-
{:ok,_record}->:ok
349-
{:error,error}->{:error,error}
486+
defdestroy(resource,%{data:record}=changeset)do
487+
caserepo(resource).delete(record,repo_opts(changeset))do
488+
{:ok,_record}->
489+
:ok
490+
491+
{:error,error}->
492+
{:error,error}
350493
end
351494
rescue
352495
e->
@@ -624,11 +767,18 @@ defmodule AshPostgres.DataLayer do
624767
}
625768
end
626769

627-
defpaggregate_subquery(relationship,_aggregate)do
628-
from(rowinrelationship.destination,
629-
group_by:^relationship.destination_field,
630-
select:field(row,^relationship.destination_field)
631-
)
770+
defpaggregate_subquery(relationship,aggregate)do
771+
query=
772+
from(rowinrelationship.destination,
773+
group_by:^relationship.destination_field,
774+
select:field(row,^relationship.destination_field)
775+
)
776+
777+
ifaggregate.query&&aggregate.query.tenantdo
778+
Ecto.Query.put_query_prefix(query,aggregate.query.tenant)
779+
else
780+
query
781+
end
632782
end
633783

634784
defpadd_subquery_aggregate_select(query,%{kind::count}=aggregate,resource)do

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp