Seeking Advice on Structuring Multiple Contexts with Shared Schema

I’m currently working on a project, ClimateCollective, that involves managing various types of events such as OrgEvents, AdminEvents, ProposedEvents, etc. Each of these event types shares a common schema but has its own unique attributes and business logic.

Here is an example of how the modules are currently structured:

# Only Used for base of other events. Never used as Event unless for display purposes.
defmodule ClimateCollective.Events.Event do
  schema "events" do
    field(:title, :string)
  
    field(:status, Ecto.Enum,
      values: [:proposed, :published, :approved, :api_submitted, :rejected]
    )
  
    field(:hosted_by, :string)
    field(:starts_at, :utc_datetime_usec)
    field(:ends_at, :utc_datetime_usec)
    field(:event_url, :string)
    field(:place_name, :string)
    field(:is_paid, :boolean, default: false)
    field(:type, Ecto.Enum, values: [:action, :event, :other])
    field(:emoji, :string)
    field(:location_shortcode, :string, virtual: true)
    field(:via_api, :boolean, default: false)
    field(:can_expire, :boolean, default: true)
  end
end

(defmodule ClimateCollective.Events.OrgEvents.OrgEvent do
   @required_attrs [
    :title,
    :place_name,
    :event_url,
    :starts_at, 
    :emoji
  ]
  @castable_attrs [:type, :can_expire] ++ @required_attrs
  @event_attrs [:starts_at, :is_paid] ++ @required_attrs
  @action_attrs [:ends_at] ++ @required_attrs
end)

(defmodule ClimateCollective.Events.AdminEvents.AdminEvent do
   @castable_attrs [
      :hosted_by,
      :starts_at,
      :title,
      :place_name,
      :event_url,
      :is_paid,
      :emoji,
      :can_expire
    ]
end)

(defmodule ClimateCollective.Events.ProposedEvents.ProposedEvent do
   @castable_attrs [
    :hosted_by,
    :starts_at,
    :title,
    :place_name,
    :event_url,
    :is_paid,
    :emoji
  ]
end)

Only OrgEvents can have the type of [:event, :action, :other]. I’m struggling with organizing my code and not having to worry about managing all other events.

Is there a structure I can have to keep refactoring to a minimum?

The things I’m struggling with are

Most important: Making sure validations are working. I have 2 different changeset types for creation, one while filling out the form, the other while saving. So that’ll be a lot of changeset and validation handling for each one.

Right now I have this for each event type

def create_changeset(event, attrs) do
    event
    |> cast(attrs, @castable_attrs)
    |> validate_required(@castable_attrs)
    |> required_manipulations(attrs, :save)
  end

required_manipulations is imported from Event

and looks like this

def required_manipulations(changeset, attrs, action) do
    changeset
    |> base_validations
    |> action_based_manipulations(attrs, action)
  end

 def base_validations(changeset) do
    changeset
    |> validate_url(:event_url)
    |> validate_length(:title, max: 90)
  end

defp action_based_manipulations(changeset, attrs, :save) do
    location = Locations.get_location!(attrs["location_id"])

    # https://hexdocs.pm/ecto/Ecto.ParameterizedType.html implement
    # I’d probably use a parameterized ecto type and parameterize it with the user’s timezone
    # https://elixir-lang.slack.com/archives/C0HEX82NR/p1696925766802179

    changeset
    |> change_timezone(:starts_at, location)
    |> change_timezone(:ends_at, location)
  end

  defp action_based_manipulations(changeset, _attrs, _), do: changeset

In my opinion it’s a god awful mess. I was thinking maybe I just implement behaviors instead and just write things multiple times but I’m hoping there’s a way I don’t have to since it all belongs to the same schema since they really are just different types of events…

I’m not sure what the best approach is, but I’m getting intimidated by my own code base, haha.

Would love any help I can get. Apologies if there’s a blatantly obvious answer. This is the first time I’ve worked on such a thing. Would this be polymorphism?

Thanks!

If the types are inherently different, why not create separate schemas pointing to the same table?

4 Likes

Thanks for answering!

Great question. I have no idea why I didn’t! That makes a lot of sense.
Creating schemas and contexts of OrgAction, OrgEvent, OrgOtherEvent? (will figure out better naming convention) feels right doing so. That’s what you were thinking right?

2 Likes

Exactly. The great thing about ecto compared to ORMs from other languages is the fact that it is not coupled to your database.

1 Like