Resolves a raw label — text as it arrives from an external source such
as a Shopify option value ("Small", "Rouge") — to the value SLUG a
catalogue attribute set already uses for that same choice, creating a
new draft value when the set has none matching.
Built for Block 7 (the Shopify sync writing catalogue attribute-set selections instead of raw strings); nothing in this fork calls it yet.
Lookup order
- Slug match:
PhoenixKit.Utils.Slug.slugify(raw_label)against every value's stored:slugin the set (draft values included — onlyarchivedones are excluded byAttributeSets.list_values/2, so a value this resolver created asdrafton a previous call is found again rather than duplicated). - Exact-label match: the same whitespace-collapsed label against
each value's
:title. - Miss: a new value is created.
Why Slug.slugify/1 computes the slug handed to create_value/3
AttributeSets.create_value/3 derives its own slug from the label only
when no explicit :slug is given, via a hand-rolled ASCII-only
fallback in that module with no transliteration — a non-Latin label
there slugifies to "" and gets a random suffix instead of a stable,
readable slug. Passing the already-transliterated
PhoenixKit.Utils.Slug.slugify/1 result as the explicit :slug
sidesteps that, AND keeps the value locatable by the same slug next
time this resolver runs against the same label — a random suffix would
not be reproducible across calls.
Why the new value is created draft, and how
create_value/3 has no :status option — every value it creates is
hardcoded "published". EntityData.bulk_update_status/3 is the only
bulk alternative, and it skips per-record changeset validation and the
per-record activity log — the wrong trade for one new row. The
shortest correct path read from the API is: create (published), then
PhoenixKitEntities.EntityData.update/3 with %{status: "draft"} —
:status is a cast-and-validate_inclusion/3-checked field on
EntityData.changeset/2, so this is a normal, validated write, not a
raw SQL patch. (There is no PhoenixKitEntities.update_entity_data/2
— that name does not exist on the top-level module; the real API is
the EntityData.update/3 used here.) Both writes run inside one
PhoenixKit.RepoHelper.repo().transaction/1 — a failed status update
rolls the just-created (published) value back too, rather than
leaving a published, un-approved value behind with the caller seeing
only an error tuple.
Storefront facets and pickers already exclude non-published values
(Query.attribute_set_counts/2 joins on entity_data.status == "published"), so a draft value this resolver creates does not
appear on the storefront until an admin publishes it.
Set lookup never creates
Sets are admin-managed blueprints (AttributeSets.create_set/2, a
deliberate admin action) — set_slug must resolve to an EXISTING set
or this returns {:error, :set_not_found}; nothing here ever creates
a set.
Summary
Functions
Resolves raw_label to a value slug within the set named set_slug
(bare or "catalogue_set_"-prefixed — the same lookup
Query.filter_by_metadata/2 uses). opts is forwarded to
AttributeSets.list_values/2 and create_value/3 (e.g. :actor_uuid,
:mode for the activity log).
Same as resolve/3, but for MULTIPLE labels against the SAME set —
resolve/3 in a loop runs AttributeSets.list_sets/0 (a full
entities read, filtered in Elixir) and list_values/2 once PER
LABEL, an N+1 by construction for the exact caller this module was
built for (Block 7's Shopify sync, one label per variant option per
product). This resolves the set and reads its values once and reuses
both across every label — a :created value from an earlier label in
the same call is visible to a later label in the same call (a value
read once at the top would go stale the moment ANY label in the batch
creates one), so two labels needing the same NEW value in one call
create it only once.
Types
Functions
Resolves raw_label to a value slug within the set named set_slug
(bare or "catalogue_set_"-prefixed — the same lookup
Query.filter_by_metadata/2 uses). opts is forwarded to
AttributeSets.list_values/2 and create_value/3 (e.g. :actor_uuid,
:mode for the activity log).
Returns {:ok, slug} for an existing match, {:created, slug} when a
new draft value had to be made, {:error, :set_not_found} when
set_slug doesn't resolve to a set (or whatever error create_value/3
/ the follow-up status update returns, on the rare write failure).
Same as resolve/3, but for MULTIPLE labels against the SAME set —
resolve/3 in a loop runs AttributeSets.list_sets/0 (a full
entities read, filtered in Elixir) and list_values/2 once PER
LABEL, an N+1 by construction for the exact caller this module was
built for (Block 7's Shopify sync, one label per variant option per
product). This resolves the set and reads its values once and reuses
both across every label — a :created value from an earlier label in
the same call is visible to a later label in the same call (a value
read once at the top would go stale the moment ANY label in the batch
creates one), so two labels needing the same NEW value in one call
create it only once.
Returns %{raw_label => resolve/3's return}, one entry per DISTINCT
raw label (duplicates in raw_labels collapse to a single lookup).