Packages

phoenix_kit

1.7.39
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules entities mirror storage.ex
Raw

lib/modules/entities/mirror/storage.ex

defmodule PhoenixKit.Modules.Entities.Mirror.Storage do
@moduledoc """
Filesystem storage operations for entity mirror/export system.
Stores exported JSON files in the parent app's priv/entities/ directory.
Each entity is stored as a single file containing both the definition and all data records.
## Directory Structure
priv/entities/
brand.json # Contains definition + all data records
product.json # Contains definition + all data records
## File Format
{
"export_version": "1.0",
"exported_at": "2025-12-11T10:30:00Z",
"definition": { ... entity schema ... },
"data": [ ... array of data records ... ]
}
## Configuration
The export path can be configured via settings:
- `entities_mirror_path` - Custom path (empty = use default priv/entities/)
"""
alias PhoenixKit.Config
alias PhoenixKit.Settings
# ============================================================================
# Settings Helpers
# ============================================================================
@doc """
Checks if entity definitions mirroring is enabled.
"""
@spec definitions_enabled?() :: boolean()
def definitions_enabled? do
Settings.get_setting("entities_mirror_definitions_enabled", "false") == "true"
end
@doc """
Checks if entity data mirroring is enabled.
"""
@spec data_enabled?() :: boolean()
def data_enabled? do
Settings.get_setting("entities_mirror_data_enabled", "false") == "true"
end
@doc """
Enables entity definitions mirroring.
"""
@spec enable_definitions() :: {:ok, any()} | {:error, any()}
def enable_definitions do
Settings.update_setting("entities_mirror_definitions_enabled", "true")
end
@doc """
Disables entity definitions mirroring.
"""
@spec disable_definitions() :: {:ok, any()} | {:error, any()}
def disable_definitions do
Settings.update_setting("entities_mirror_definitions_enabled", "false")
end
@doc """
Enables entity data mirroring.
"""
@spec enable_data() :: {:ok, any()} | {:error, any()}
def enable_data do
Settings.update_setting("entities_mirror_data_enabled", "true")
end
@doc """
Disables entity data mirroring.
"""
@spec disable_data() :: {:ok, any()} | {:error, any()}
def disable_data do
Settings.update_setting("entities_mirror_data_enabled", "false")
end
# ============================================================================
# Path Resolution
# ============================================================================
@doc """
Returns the root path for entity mirror storage.
Uses custom path from settings if configured, otherwise defaults to
the parent app's priv/entities/ directory.
"""
@spec root_path() :: String.t()
def root_path do
case Settings.get_setting("entities_mirror_path", "") do
path when is_binary(path) and byte_size(path) > 0 -> path
_ -> default_path()
end
end
@doc """
Returns the default storage path in the parent app's priv directory.
"""
@spec default_path() :: String.t()
def default_path do
case Config.get_parent_app() do
nil -> Path.join([File.cwd!(), "priv", "entities"])
app -> Application.app_dir(app, Path.join("priv", "entities"))
end
end
@doc """
Returns the file path for a specific entity.
"""
@spec entity_path(String.t()) :: String.t()
def entity_path(entity_name) when is_binary(entity_name) do
Path.join(root_path(), "#{entity_name}.json")
end
# ============================================================================
# Directory Management
# ============================================================================
@doc """
Ensures the root directory exists.
"""
@spec ensure_directory() :: :ok | {:error, term()}
def ensure_directory do
path = root_path()
case File.mkdir_p(path) do
:ok -> :ok
{:error, reason} -> {:error, {:mkdir_failed, path, reason}}
end
end
# ============================================================================
# Write Operations
# ============================================================================
@doc """
Writes an entity file containing definition and optionally data.
## Parameters
- `entity_name` - The entity name (used as filename)
- `content` - The full content map with definition and data
## Returns
- `{:ok, file_path}` on success
- `{:error, reason}` on failure
"""
@spec write_entity(String.t(), map()) :: {:ok, String.t()} | {:error, term()}
def write_entity(entity_name, content) when is_binary(entity_name) and is_map(content) do
with :ok <- ensure_directory() do
file_path = entity_path(entity_name)
write_json_file(file_path, content)
end
end
defp write_json_file(file_path, content) when is_map(content) do
case Jason.encode(content, pretty: true) do
{:ok, json} ->
case File.write(file_path, json) do
:ok -> {:ok, file_path}
{:error, reason} -> {:error, {:write_failed, file_path, reason}}
end
{:error, reason} ->
{:error, {:encode_failed, reason}}
end
end
# ============================================================================
# Read Operations
# ============================================================================
@doc """
Reads an entity file containing definition and data.
## Parameters
- `entity_name` - The entity name
## Returns
- `{:ok, map}` with decoded JSON on success
- `{:error, reason}` on failure
"""
@spec read_entity(String.t()) :: {:ok, map()} | {:error, term()}
def read_entity(entity_name) when is_binary(entity_name) do
file_path = entity_path(entity_name)
read_json_file(file_path)
end
defp read_json_file(file_path) do
case File.read(file_path) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, data} -> {:ok, data}
{:error, reason} -> {:error, {:decode_failed, file_path, reason}}
end
{:error, :enoent} ->
{:error, :not_found}
{:error, reason} ->
{:error, {:read_failed, file_path, reason}}
end
end
# ============================================================================
# Delete Operations
# ============================================================================
@doc """
Deletes an entity file.
"""
@spec delete_entity(String.t()) :: :ok | {:error, term()}
def delete_entity(entity_name) when is_binary(entity_name) do
file_path = entity_path(entity_name)
case File.rm(file_path) do
:ok -> :ok
{:error, :enoent} -> :ok
{:error, reason} -> {:error, {:delete_failed, file_path, reason}}
end
end
# ============================================================================
# List Operations
# ============================================================================
@doc """
Lists all exported entity names.
Returns a list of entity names (without .json extension).
"""
@spec list_entities() :: [String.t()]
def list_entities do
path = root_path()
if File.exists?(path) do
path
|> File.ls!()
|> Enum.filter(&String.ends_with?(&1, ".json"))
|> Enum.map(&String.replace_trailing(&1, ".json", ""))
|> Enum.sort()
else
[]
end
end
# ============================================================================
# Stats
# ============================================================================
@doc """
Returns statistics about exported files.
## Returns
Map with:
- `definitions_count` - Number of exported entity files
- `data_count` - Total number of data records across all entities
- `entities_with_data` - List of entity names that have data records
- `last_export` - Timestamp of most recent export (nil if no files)
"""
@spec get_stats() :: map()
def get_stats do
entities = list_entities()
{data_count, entities_with_data} =
entities
|> Enum.reduce({0, []}, fn entity_name, {count, with_data} ->
case read_entity(entity_name) do
{:ok, %{"data" => data}} when is_list(data) and data != [] ->
{count + length(data), [entity_name | with_data]}
{:ok, _} ->
{count, with_data}
{:error, _} ->
{count, with_data}
end
end)
last_export = get_last_export_time(entities)
%{
definitions_count: length(entities),
data_count: data_count,
entities_with_data: Enum.reverse(entities_with_data),
last_export: last_export
}
end
defp get_last_export_time([]), do: nil
defp get_last_export_time(entities) do
entities
|> Enum.map(fn name -> entity_path(name) end)
|> Enum.map(&get_file_mtime/1)
|> Enum.reject(&is_nil/1)
|> Enum.max(fn -> nil end)
|> format_last_export()
end
defp get_file_mtime(path) do
case File.stat(path) do
{:ok, %{mtime: mtime}} -> mtime
_ -> nil
end
end
defp format_last_export(nil), do: nil
defp format_last_export({{year, month, day}, {hour, minute, _second}}) do
"#{year}-#{pad(month)}-#{pad(day)} #{pad(hour)}:#{pad(minute)}"
end
defp pad(num) when num < 10, do: "0#{num}"
defp pad(num), do: "#{num}"
@doc """
Checks if a file exists for the given entity.
"""
@spec entity_exists?(String.t()) :: boolean()
def entity_exists?(entity_name) do
file_path = entity_path(entity_name)
File.exists?(file_path)
end
# Legacy compatibility aliases
@doc false
def list_definitions, do: list_entities()
@doc false
def definition_exists?(name), do: entity_exists?(name)
end