defmodule Mv.Membership.CustomField do @moduledoc """ Ash resource defining the schema for custom member fields. ## Overview CustomFields define the "schema" for custom fields in the membership system. Each CustomField specifies the name, data type, and behavior of a custom field that can be attached to members via CustomFieldValue resources. ## Attributes - `name` - Unique identifier for the custom field (e.g., "phone_mobile", "birthday") - `slug` - URL-friendly, immutable identifier automatically generated from name (e.g., "phone-mobile") - `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`) - `description` - Optional human-readable description - `immutable` - If true, custom field values cannot be changed after creation - `required` - If true, all members must have this custom field (future feature) ## Supported Value Types - `:string` - Text data (max 10,000 characters) - `:integer` - Numeric data (64-bit integers) - `:boolean` - True/false flags - `:date` - Date values (no time component) - `:email` - Validated email addresses (max 254 characters) ## Relationships - `has_many :custom_field_values` - All custom field values of this type ## Constraints - Name must be unique across all custom fields - Name maximum length: 100 characters - Deleting a custom field will cascade delete all associated custom field values ## Calculations - `assigned_members_count` - Returns the number of distinct members with values for this custom field ## Examples # Create a new custom field CustomField.create!(%{ name: "phone_mobile", value_type: :string, description: "Mobile phone number" }) # Create a required custom field CustomField.create!(%{ name: "emergency_contact", value_type: :string, required: true }) """ use Ash.Resource, domain: Mv.Membership, data_layer: AshPostgres.DataLayer postgres do table "custom_fields" repo Mv.Repo end actions do defaults [:read, :update] default_accept [:name, :value_type, :description, :immutable, :required] create :create do accept [:name, :value_type, :description, :immutable, :required] change Mv.Membership.CustomField.Changes.GenerateSlug validate string_length(:slug, min: 1) end destroy :destroy_with_values do primary? true end read :prepare_deletion do argument :id, :uuid, allow_nil?: false filter expr(id == ^arg(:id)) prepare build(load: [:assigned_members_count]) end end attributes do uuid_primary_key :id attribute :name, :string, allow_nil?: false, public?: true, constraints: [ max_length: 100, trim?: true ] attribute :slug, :string, allow_nil?: false, public?: true, writable?: false, constraints: [ max_length: 100, trim?: true ] attribute :value_type, :atom, constraints: [one_of: [:string, :integer, :boolean, :date, :email]], allow_nil?: false, description: "Defines the datatype `CustomFieldValue.value` is interpreted as" attribute :description, :string, allow_nil?: true, public?: true, constraints: [ max_length: 500, trim?: true ] attribute :immutable, :boolean, default: false, allow_nil?: false attribute :required, :boolean, default: false, allow_nil?: false end relationships do has_many :custom_field_values, Mv.Membership.CustomFieldValue end calculations do calculate :assigned_members_count, :integer, expr( fragment( "(SELECT COUNT(DISTINCT member_id) FROM custom_field_values WHERE custom_field_id = ?)", id ) ) end identities do identity :unique_name, [:name] identity :unique_slug, [:slug] end end