PhoenixKitLocations.Spaces (PhoenixKitLocations v0.2.0)

Copy Markdown View Source

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

opts()

@type opts() :: keyword()

uuid()

@type uuid() :: String.t()

Functions

change_space(space, attrs \\ %{})

Builds an empty changeset (for :new forms).

create_space(attrs, opts \\ [])

@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}.

delete_space(space, opts \\ [])

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).

get_space(uuid)

@spec get_space(uuid()) :: PhoenixKitLocations.Schemas.Space.t() | nil

Fetches a space by UUID. Returns nil if not found.

list_for_location(location_uuid)

@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.

list_tree(location_uuid)

@spec list_tree(uuid()) :: [map()]

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.

reorder_siblings(location_uuid, parent_uuid, ordered_uuids, opts \\ [])

@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}.

update_space(space, attrs, opts \\ [])

@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.