-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add scopes to generators #6102
base: main
Are you sure you want to change the base?
add scopes to generators #6102
Conversation
@@ -59,7 +59,7 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithNoOptionsTest do | |||
[ | |||
"--no-halt", | |||
"-e", | |||
"spawn fn -> IO.gets('') && System.halt(0) end", | |||
"spawn fn -> IO.gets(~c[]) && System.halt(0) end", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"spawn fn -> IO.gets(~c[]) && System.halt(0) end", | |
"spawn fn -> IO.gets([]) && System.halt(0) end", |
migration: :boolean | ||
migration: :boolean, | ||
scope: :string, | ||
no_scope: :boolean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you don't need no_scope
. Since you have --scope
, you can pass --no-scope
, and it will come with scope: nil
. You can distinguish between them verifying if the option exists or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm
iex(1)> OptionParser.parse(["--no-scope"], switches: [scope: :string])
{[], [], []}
iex(2)> OptionParser.parse(["--scope"], switches: [scope: :string])
{[], [], [{"--scope", nil}]}
iex(3)> OptionParser.parse(["--scope", "foo"], switches: [scope: :string])
{[scope: "foo"], [], []}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sad trombone. You are right. You could explicitly check for "--no-scope" in args
and act accordingly, but then I guess it would end-up the same.
on <%= schema.singular %>_token. | ||
Redirects to login page if there's no logged <%= schema.singular %>. | ||
|
||
## Examples | ||
|
||
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate | ||
the current_<%= schema.singular %>: | ||
the current_scope: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the current_scope: | |
the `current_scope`: |
priv/templates/phx.gen.auth/scope.ex
Outdated
@doc """ | ||
Creates a scope for the given <%= schema.singular %>. | ||
""" | ||
def for_<%= schema.singular %>(nil), do: %__MODULE__{user: nil} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly, I would prefer to not have this pattern. If we are thinking about the type system, it means that now we need to handle about the possibility of user being nil everywhere. Or we have a scope with user, or we have no scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it depends if we think that it is common to want scoped functionality for unauthenticated requests. I think it would be common to have one scope struct with fields that can either be nil or not instead of different scope structs.
The alternative is to have a for_user(nil), do: nil
or the need to change all UserAuth functions to something like
defp mount_current_scope(socket, session) do
Phoenix.Component.assign_new(socket, :current_scope, fn ->
with user_token when is_binary(user_token) <- session["user_token"],
user when not is_nil(user) <- Accounts.get_user_by_session_token(user_token) do
AuthScope.for_user(user)
end
end)
end
and only call the scope function if we have a user in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be common to have one scope struct with fields that can either be nil or not instead of different scope structs.
I agree, but I would prefer to not assume and start with the most streamlined version. For example, in teams, I believe we always have org and user.
""" | ||
def for_<%= schema.singular %>(nil), do: %__MODULE__{user: nil} | ||
|
||
def for_<%= schema.singular %>(%<%= inspect schema.alias %>{} = <%= schema.singular %>) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean we will have UserScope.for_user
? Maybe we should have AccountsScope
or just AuthScope
? I also don't like calling it UserScope
because in many cases you will have an org field too, and you will likely want to customize it to be per org. We should probably have some docs/guides on that. Perhaps under the mix phx.gen.auth
section of the guides.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well yes, we could have the module as MyApp.Accounts.AuthScope.for_user, but I think the scope itself should still be called :user
, because if I create a new application, resources are scoped by user. If I then add a tenant / org, I'd do
config :my_app, :scopes,
user: [
default: false,
module: MyApp.Accounts.AuthScope,
assign_key: :current_scope,
access_path: [:user, :id],
schema_key: :user_id,
schema_type: :id,
schema_table: :users
],
org: [
default: true,
module: MyApp.Accounts.AuthScope,
assign_key: :current_scope,
access_path: [:user, :org_id],
schema_key: :org_id,
schema_type: :id,
schema_table: :organization
]
And then I can decide with a single scope module if I want to scope a new resource by user or organization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, which is why I think it should be called AuthScope or something more generic. 👍
def subscribe_<%= schema.plural %>(%<%= inspect scope.alias %>{} = <%= scope.name %>_scope) do | ||
key = <%= scope.name %>_scope.<%= Enum.join(scope.access_path, ".") %> | ||
|
||
Phoenix.PubSub.subscribe(<%= inspect context.base_module %>.PubSub, "<%= scope.name %>:#{key}:<%= schema.plural %>") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering if we should have this under the scope somehow. subscribe(current_scope, topic)
and broadcast(current_scope, topic)
. But this also means a basic scope will need additional functions in order to work with generators.
|
||
""" | ||
def list_<%= schema.plural %>(%<%= inspect scope.alias %>{} = <%= scope.name %>_scope) do | ||
Repo.all(from <%= schema.singular %> in <%= inspect schema.alias %>, where: <%= schema.singular %>.<%= scope.schema_key %> == ^<%= scope.name %>_scope.<%= Enum.join(scope.access_path, ".") %>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We just added all_by to Ecto. Perhaps we should do a new release?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway, not the topic of this PR, we can update this and the auth generators to use Repo.transact once Ecto is actually out. I will start working on that front.
wip!