Context for nested spaces under a Location — rooms, floors, zones, etc. forming a per-location tree.
Same-Location parent invariant
A space's parent_uuid (when set) must reference another space in
the same Location. The DB doesn't enforce this directly — a
composite FK on (parent_uuid, location_uuid) would, but it's
heavier than the consumer surface justifies. We guard at the
context boundary instead: create_space/2 and update_space/3
reject any cross-location parent with {:error, :parent_in_other_location}.
Cycle prevention
Direct self-loop is caught by the schema changeset. Indirect cycles
(A → B → A) are blocked here in validate_no_cycle/3 before any
parent_uuid change is persisted. Walk-up depth-limited to 64 hops —
generous for any realistic building hierarchy.
Activity logging
Mutating functions accept opts \ [] and forward :actor_uuid
for the activity log. Same wrapper shape as Locations —
guarded with Code.ensure_loaded?(PhoenixKit.Activity) and rescued
so logging never crashes the mutation.
Summary
Functions
Builds an empty changeset (for :new forms).
Creates a new space. Rejects parents that live in a different
Location with {:error, :parent_in_other_location}.
Hard-deletes a space. Children CASCADE via the DB FK — the entire subtree is removed. The activity log records the delete of the named root; children deletes aren't individually logged (would be noisy on deep trees).
Fetches a space by UUID. Returns nil if not found.
All spaces for a Location, ordered by (parent_uuid, position).
Returns a flat list; use list_tree/1 for a nested shape.
Nested tree of spaces for a Location. Each node carries a :children
key as a list (empty for leaves). Root-level nodes have parent_uuid == nil.
Reorders a sibling group under a single (location, parent) — accepts
the full ordered list of sibling UUIDs and rewrites their position
to match. Runs in a transaction; returns {:ok, :reordered} or
{:error, reason}.
Updates an existing space. Re-parenting is allowed but rejected if the new parent lives in another Location, or if the change would create a cycle.
Types
Functions
@spec change_space(PhoenixKitLocations.Schemas.Space.t(), map()) :: Ecto.Changeset.t()
Builds an empty changeset (for :new forms).
@spec create_space(map(), opts()) :: {:ok, PhoenixKitLocations.Schemas.Space.t()} | {:error, Ecto.Changeset.t() | :parent_in_other_location | :parent_not_found | :location_not_found}
Creates a new space. Rejects parents that live in a different
Location with {:error, :parent_in_other_location}.
@spec delete_space(PhoenixKitLocations.Schemas.Space.t(), opts()) :: {:ok, PhoenixKitLocations.Schemas.Space.t()} | {:error, Ecto.Changeset.t()}
Hard-deletes a space. Children CASCADE via the DB FK — the entire subtree is removed. The activity log records the delete of the named root; children deletes aren't individually logged (would be noisy on deep trees).
@spec get_space(uuid()) :: PhoenixKitLocations.Schemas.Space.t() | nil
Fetches a space by UUID. Returns nil if not found.
@spec list_for_location(uuid()) :: [PhoenixKitLocations.Schemas.Space.t()]
All spaces for a Location, ordered by (parent_uuid, position).
Returns a flat list; use list_tree/1 for a nested shape.
Nested tree of spaces for a Location. Each node carries a :children
key as a list (empty for leaves). Root-level nodes have parent_uuid == nil.
Single DB read — the tree is assembled in memory from the flat list.
@spec reorder_siblings(uuid(), uuid() | nil, [uuid()], opts()) :: {:ok, :reordered} | {:error, term()}
Reorders a sibling group under a single (location, parent) — accepts
the full ordered list of sibling UUIDs and rewrites their position
to match. Runs in a transaction; returns {:ok, :reordered} or
{:error, reason}.
@spec update_space(PhoenixKitLocations.Schemas.Space.t(), map(), opts()) :: {:ok, PhoenixKitLocations.Schemas.Space.t()} | {:error, Ecto.Changeset.t() | :parent_in_other_location | :parent_not_found | :location_not_found | :cycle}
Updates an existing space. Re-parenting is allowed but rejected if the new parent lives in another Location, or if the change would create a cycle.