Skip to content
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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft

add scopes to generators #6102

wants to merge 7 commits into from

Conversation

SteffenDE
Copy link
Contributor

wip!

@@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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
Copy link
Member

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.

Copy link
Contributor Author

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"], [], []}

Copy link
Member

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
the current_scope:
the `current_scope`:

@doc """
Creates a scope for the given <%= schema.singular %>.
"""
def for_<%= schema.singular %>(nil), do: %__MODULE__{user: nil}
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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 %>")
Copy link
Member

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, ".") %>)
Copy link
Member

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?

Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants