Sow behaviour (Sow v0.1.4)

Copy Markdown View Source

A library for synchronizing code-defined fixtures with a database.

Usage

defmodule MyApp.Fixtures.Countries do
  use Sow,
    schema: MyApp.Country,
    keys: [:code]

  def records do
    [
      %{code: "NO", name: "Norway"},
      %{code: "SE", name: "Sweden"}
    ]
  end
end

belongs_to (sync dependency first)

defmodule MyApp.Fixtures.Organizations do
  use Sow,
    schema: MyApp.Organization,
    keys: [:slug]

  def records do
    [
      %{
        slug: "org-norway",
        country: Sow.belongs_to(MyApp.Fixtures.Countries, :code, "NO")
      }
    ]
  end
end

has_many (sync children after parent)

defmodule MyApp.Fixtures.Products do
  use Sow,
    schema: MyApp.Product,
    keys: [:org_id, :type]

  def records do
    %{
      type: :subscription,
      variants: Sow.has_many(MyApp.Fixtures.ProductVariants, foreign_key: :product_id)
    }
  end
end

many_to_many

For many_to_many, the related records are synced first, then passed to put_assoc:

defmodule MyApp.Fixtures.Products do
  use Sow,
    schema: MyApp.Product,
    keys: [:slug]

  def records do
    %{
      slug: "premium",
      tags: [
        Sow.many_to_many(MyApp.Fixtures.Tags, :slug, "featured"),
        Sow.many_to_many(MyApp.Fixtures.Tags, :slug, "new")
      ]
    }
  end
end

Auto-detect with assoc (shorthand)

Use Sow.assoc/1-3 to auto-detect the association type from the Ecto schema:

defmodule MyApp.Fixtures.Products do
  use Sow,
    schema: MyApp.Product,
    keys: [:slug]

  def records do
    %{
      slug: "premium",
      # Auto-detects belongs_to from schema
      organization: Sow.assoc(MyApp.Fixtures.Organizations, :slug, "org"),
      # Auto-detects many_to_many from schema
      tags: [
        Sow.assoc(MyApp.Fixtures.Tags, :slug, "featured")
      ],
      # Auto-detects has_many from schema
      variants: Sow.assoc(MyApp.Fixtures.ProductVariants)
    }
  end
end

Note: Your schema's changeset must handle many_to_many with put_assoc:

def changeset(product, attrs) do
  product
  |> cast(attrs, [:slug, :name])
  |> maybe_put_assoc(:tags, attrs)
end

defp maybe_put_assoc(changeset, key, attrs) do
  case Map.get(attrs, key) do
    nil -> changeset
    assoc -> put_assoc(changeset, key, assoc)
  end
end

Summary

Callbacks

Returns the fixture records to sync. Can return a single map or a list of maps.

Functions

Auto-detect association type from the Ecto schema.

Marks a belongs_to association. The referenced fixture is synced first, and its ID is assigned to {field_name}_id.

Returns the default repo configured in application env.

Marks a has_many association. Children are synced after the parent, with the parent's ID injected via the foreign_key.

Marks a has_many association with inline records (no separate fixture module needed).

Runtime database lookup to get a field value from an existing record.

Marks a many_to_many association. The referenced fixture is synced first, and the model is passed to put_assoc in the changeset.

Sync multiple fixture modules in dependency order.

Types

fixture_record()

@type fixture_record() :: map()

keys()

@type keys() :: [atom()]

Callbacks

records()

@callback records() :: fixture_record() | [fixture_record()]

Returns the fixture records to sync. Can return a single map or a list of maps.

The callback name can be customized with the :callback option:

use Sow, schema: MyApp.Country, keys: [:code], callback: :modify
def modify, do: [...]

Functions

assoc(fixture_module)

Auto-detect association type from the Ecto schema.

This is a shorthand that inspects the schema to determine whether the field is belongs_to, has_many, or many_to_many, and handles it appropriately.

Examples

# Auto-detect and sync
%{country: Sow.assoc(MyApp.Fixtures.Countries)}

# With lookup
%{country: Sow.assoc(MyApp.Fixtures.Countries, :code, "NO")}

# Works for all association types
%{
  organization: Sow.assoc(Organizations, :slug, "org"),  # belongs_to
  tags: [Sow.assoc(Tags, :slug, "featured")],            # many_to_many
  variants: Sow.assoc(ProductVariants)                   # has_many
}

assoc(fixture_module, lookup_key, lookup_value)

belongs_to(fixture_module)

Marks a belongs_to association. The referenced fixture is synced first, and its ID is assigned to {field_name}_id.

Examples

# Sync the Countries fixture, use the first record's ID
%{country: Sow.belongs_to(MyApp.Fixtures.Countries)}

# Sync Countries fixture, find by code "NO", use that record's ID
%{country: Sow.belongs_to(MyApp.Fixtures.Countries, :code, "NO")}

belongs_to(fixture_module, lookup_key, lookup_value)

default_repo()

Returns the default repo configured in application env.

has_many(fixture_module, opts)

Marks a has_many association. Children are synced after the parent, with the parent's ID injected via the foreign_key.

Examples

%{
  variants: Sow.has_many(MyApp.Fixtures.ProductVariants, foreign_key: :product_id)
}

has_many_inline(records, opts)

Marks a has_many association with inline records (no separate fixture module needed).

Children are synced after the parent, with the parent's ID injected via the foreign_key.

Options

  • :schema - (required) the Ecto schema module for the nested records
  • :foreign_key - (required) the foreign key field to inject parent's ID
  • :keys - (optional) search keys for upsert, defaults to schema's primary key

Examples

%{
  slug: "premium",
  variants: Sow.has_many_inline(
    [
      %{sku: "SMALL", name: "Small"},
      %{sku: "LARGE", name: "Large"}
    ],
    schema: MyApp.ProductVariant,
    foreign_key: :product_id,
    keys: [:product_id, :sku]
  )
}

Records can also contain relations:

flow_stages: Sow.has_many_inline(
  [
    %{position: 1, stage: Sow.belongs_to(StageFixture, :type, :select_client)},
    %{position: 2, stage: Sow.belongs_to(StageFixture, :type, :payment)}
  ],
  schema: MyApp.FlowStage,
  foreign_key: :flow_id,
  keys: [:flow_id, :stage_id]
)

lookup(schema, match)

Runtime database lookup to get a field value from an existing record.

Unlike belongs_to which syncs a fixture, lookup queries the database directly for an existing record and extracts a field (default: :id).

Examples

# Simple lookup - get country.id where code = "NO"
country_id: Sow.lookup(MyApp.Country, :code, "NO")

# With custom field extraction
country_name: Sow.lookup(MyApp.Country, :code, "NO", field: :name)

# Multiple match criteria
org_id: Sow.lookup(MyApp.Organization, %{country_id: 1, name: "ACME"})

# Chained lookups - resolve country_id first, then find organization
org_id: Sow.lookup(MyApp.Organization, %{
  country_id: Sow.lookup(MyApp.Country, :code, "NO"),
  name: "ACME"
})

lookup(schema, key, value)

lookup(schema, key, value, opts)

many_to_many(fixture_module)

Marks a many_to_many association. The referenced fixture is synced first, and the model is passed to put_assoc in the changeset.

Use as a list for multiple associations:

%{
  tags: [
    Sow.many_to_many(MyApp.Fixtures.Tags, :slug, "featured"),
    Sow.many_to_many(MyApp.Fixtures.Tags, :slug, "new")
  ]
}

many_to_many(fixture_module, lookup_key, lookup_value)

sync_all(modules, repo \\ default_repo())

Sync multiple fixture modules in dependency order.