;
+
+export const Default: Story = {
+ args: {},
+};
diff --git a/hok_ecs/frontend/src/stories/assets/code-brackets.svg b/hok_ecs/frontend/src/stories/assets/code-brackets.svg
new file mode 100644
index 0000000..73de947
--- /dev/null
+++ b/hok_ecs/frontend/src/stories/assets/code-brackets.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hok_ecs/frontend/src/stories/assets/colors.svg b/hok_ecs/frontend/src/stories/assets/colors.svg
new file mode 100644
index 0000000..17d58d5
--- /dev/null
+++ b/hok_ecs/frontend/src/stories/assets/colors.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hok_ecs/frontend/src/stories/assets/comments.svg b/hok_ecs/frontend/src/stories/assets/comments.svg
new file mode 100644
index 0000000..6493a13
--- /dev/null
+++ b/hok_ecs/frontend/src/stories/assets/comments.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hok_ecs/frontend/src/stories/assets/direction.svg b/hok_ecs/frontend/src/stories/assets/direction.svg
new file mode 100644
index 0000000..65676ac
--- /dev/null
+++ b/hok_ecs/frontend/src/stories/assets/direction.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hok_ecs/frontend/src/stories/assets/flow.svg b/hok_ecs/frontend/src/stories/assets/flow.svg
new file mode 100644
index 0000000..8ac27db
--- /dev/null
+++ b/hok_ecs/frontend/src/stories/assets/flow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hok_ecs/frontend/src/stories/assets/plugin.svg b/hok_ecs/frontend/src/stories/assets/plugin.svg
new file mode 100644
index 0000000..29e5c69
--- /dev/null
+++ b/hok_ecs/frontend/src/stories/assets/plugin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hok_ecs/frontend/src/stories/assets/repo.svg b/hok_ecs/frontend/src/stories/assets/repo.svg
new file mode 100644
index 0000000..f386ee9
--- /dev/null
+++ b/hok_ecs/frontend/src/stories/assets/repo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hok_ecs/frontend/src/stories/assets/stackalt.svg b/hok_ecs/frontend/src/stories/assets/stackalt.svg
new file mode 100644
index 0000000..9b7ad27
--- /dev/null
+++ b/hok_ecs/frontend/src/stories/assets/stackalt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/hok_ecs/frontend/src/stories/button.css b/hok_ecs/frontend/src/stories/button.css
new file mode 100644
index 0000000..dc91dc7
--- /dev/null
+++ b/hok_ecs/frontend/src/stories/button.css
@@ -0,0 +1,30 @@
+.storybook-button {
+ font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ font-weight: 700;
+ border: 0;
+ border-radius: 3em;
+ cursor: pointer;
+ display: inline-block;
+ line-height: 1;
+}
+.storybook-button--primary {
+ color: white;
+ background-color: #1ea7fd;
+}
+.storybook-button--secondary {
+ color: #333;
+ background-color: transparent;
+ box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
+}
+.storybook-button--small {
+ font-size: 12px;
+ padding: 10px 16px;
+}
+.storybook-button--medium {
+ font-size: 14px;
+ padding: 11px 20px;
+}
+.storybook-button--large {
+ font-size: 16px;
+ padding: 12px 24px;
+}
diff --git a/hok_ecs/frontend/src/urqlClient.tsx b/hok_ecs/frontend/src/urqlClient.tsx
new file mode 100644
index 0000000..5c487e3
--- /dev/null
+++ b/hok_ecs/frontend/src/urqlClient.tsx
@@ -0,0 +1,16 @@
+import React, { ReactNode } from 'react';
+import { cacheExchange, createClient, dedupExchange, fetchExchange, Provider } from 'urql';
+import { refocusExchange } from '@urql/exchange-refocus';
+import { devtoolsExchange } from '@urql/devtools';
+
+const client = createClient({
+ url: '/api/graphql',
+ requestPolicy: 'cache-and-network',
+ exchanges: [devtoolsExchange, dedupExchange, refocusExchange(), cacheExchange, fetchExchange],
+});
+
+export default client;
+
+export const UrqlProvider: React.FC<{ children: ReactNode }> = ({ children }) => (
+ {children}
+);
diff --git a/hok_ecs/frontend/src/vite-env.d.ts b/hok_ecs/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/hok_ecs/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/hok_ecs/frontend/src/withSuspense.tsx b/hok_ecs/frontend/src/withSuspense.tsx
new file mode 100644
index 0000000..fed0298
--- /dev/null
+++ b/hok_ecs/frontend/src/withSuspense.tsx
@@ -0,0 +1,16 @@
+import React, { Suspense } from 'react';
+import LoadingSpinner from './components/LoadingSpinner';
+
+/**
+ * HOC to wrap a component in a Suspense component.
+ */
+
+export default function withSuspense(Component: React.ComponentType
) {
+ return function WithSuspense(props: React.PropsWithChildren
) {
+ return (
+ }>
+
+
+ );
+ };
+}
diff --git a/hok_ecs/frontend/tsconfig.json b/hok_ecs/frontend/tsconfig.json
new file mode 100644
index 0000000..619dda4
--- /dev/null
+++ b/hok_ecs/frontend/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "noErrorTruncation": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/hok_ecs/frontend/tsconfig.node.json b/hok_ecs/frontend/tsconfig.node.json
new file mode 100644
index 0000000..9d31e2a
--- /dev/null
+++ b/hok_ecs/frontend/tsconfig.node.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/hok_ecs/frontend/vite.config.ts b/hok_ecs/frontend/vite.config.ts
new file mode 100644
index 0000000..29da679
--- /dev/null
+++ b/hok_ecs/frontend/vite.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import eslint from 'vite-plugin-eslint';
+import { visualizer } from 'rollup-plugin-visualizer';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ build: {
+ modulePreload: false,
+ },
+ plugins: [eslint({ cache: false }), react(), visualizer()],
+ // using the `webapp` base path for production builds
+ // So we can leverage Phoenix static assets plug to deliver
+ // our React app directly from our final Elixir app,
+ // Serving all files from the `priv/static/webapp` folder.
+ // NOTE: Remember to move the frontend build files to the
+ // `priv` folder during the application build process in CI
+ // @ts-ignore
+ base: process.env.NODE_ENV === 'production' ? '/webapp/' : '/',
+ // Forward all requests made by our React frontend to `localhost:3000/api`
+ // to our Phoenix backend running at `localhost:4000`.
+ // This is only necessary during development.
+ // In production, our Phoenix and React apps are served from the same
+ // domain and port, which makes this configuration unecessary.
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:4000',
+ secure: false,
+ ws: true,
+ },
+ },
+ },
+});
diff --git a/hok_ecs/lib/hok_ecs.ex b/hok_ecs/lib/hok_ecs.ex
new file mode 100644
index 0000000..9824232
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs.ex
@@ -0,0 +1,9 @@
+defmodule HokEcs do
+ @moduledoc """
+ HokEcs keeps the contexts that define your domain
+ and business logic.
+
+ Contexts are also responsible for managing your data, regardless
+ if it comes from the database, an external API or others.
+ """
+end
diff --git a/hok_ecs/lib/hok_ecs/application.ex b/hok_ecs/lib/hok_ecs/application.ex
new file mode 100644
index 0000000..9f381e4
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/application.ex
@@ -0,0 +1,37 @@
+defmodule HokEcs.Application do
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ @moduledoc false
+
+ use Application
+
+ @impl true
+ def start(_type, _args) do
+ children = [
+ # Start the Ecto repository
+ HokEcs.Repo,
+ # Start the Telemetry supervisor
+ HokEcsWeb.Telemetry,
+ # Start the PubSub system
+ {Phoenix.PubSub, name: HokEcs.PubSub},
+ {Cachex, name: :webapp_cache},
+ # Start the Endpoint (http/https)
+ HokEcsWeb.Endpoint
+ # Start a worker by calling: HokEcs.Worker.start_link(arg)
+ # {HokEcs.Worker, arg}
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: HokEcs.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ @impl true
+ def config_change(changed, _new, removed) do
+ HokEcsWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/components.ex b/hok_ecs/lib/hok_ecs/components.ex
new file mode 100644
index 0000000..cd34a8a
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/components.ex
@@ -0,0 +1,272 @@
+defmodule HokEcs.Components do
+ import Ecto.Query, warn: false
+
+ alias HokEcs.Repo
+ alias __MODULE__.{Component, ComponentSchema}
+ alias HokEcs.Events
+ alias HokEcs.Events.Event
+ alias HokEcs.Entities.Entity
+
+ def get_entity_component_types(entity_guid) do
+ query =
+ from c in Component,
+ where: c.entity_guid == ^entity_guid,
+ distinct: c.component_type,
+ order_by: [asc: c.component_type],
+ select: c.component_type
+
+ Repo.all(query)
+ end
+
+ @doc """
+ Returns the list of components.
+
+ ## Examples
+
+ iex> list_components()
+ [%Component{}, ...]
+
+ """
+ @spec list_components(map()) :: list(Component.t())
+ def list_components(args \\ %{}) do
+ args
+ |> Enum.reduce(from(Component), fn
+ {:entity_guid, entity_guid}, query ->
+ where(query, entity_guid: ^entity_guid)
+
+ {:component_type, component_type}, query ->
+ where(query, component_type: ^component_type)
+
+ {:entity_classification, entity_classification}, query ->
+ where(query, entity_classification: ^entity_classification)
+ end)
+ |> Repo.all()
+ end
+
+ @doc """
+ Gets a single component.
+
+ Returns `nil` if the Component does not exist.
+ """
+ @spec get_component(String.t()) :: Component.t() | nil
+ def get_component(component_guid), do: Repo.get(Component, component_guid)
+
+ @doc """
+ Gets a single component.
+
+ Raises `Ecto.NoResultsError` if the Component does not exist.
+
+ ## Examples
+
+ iex> get_component!(123)
+ %Component{}
+
+ iex> get_component!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ @spec get_component!(String.t()) :: Component.t()
+ def get_component!(id), do: Repo.get!(Component, id)
+
+ @doc """
+ Creates a component.
+
+ ## Examples
+
+ iex> create_component(%{field: value})
+ {:ok, %Component{}}
+
+ iex> create_component(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec create_component(map()) :: {:ok, Component.t()} | {:error, Ecto.Changeset.t()}
+ def create_component(attrs \\ %{}) do
+ entity_guid = Map.fetch!(attrs, :entity_guid)
+ component_type = Map.fetch!(attrs, :component_type)
+
+ Ecto.Multi.new()
+ |> Ecto.Multi.one(:entity, from(Entity, where: [entity_guid: ^entity_guid]))
+ |> Ecto.Multi.run(:ensure_entity, fn _repo, %{entity: entity} ->
+ case entity do
+ %Entity{} -> {:ok, true}
+ _ -> {:error, "Entity not found"}
+ end
+ end)
+ |> Ecto.Multi.one(:component_schema, from(ComponentSchema, where: [name: ^component_type]))
+ |> Ecto.Multi.run(:ensure_component_schema, fn _repo, %{component_schema: component_schema} ->
+ case component_schema do
+ %ComponentSchema{} -> {:ok, true}
+ _ -> {:error, "Schema for component type #{component_type} not found"}
+ end
+ end)
+ |> Ecto.Multi.run(:component, fn _repo,
+ %{
+ entity: %Entity{} = entity,
+ component_schema: %ComponentSchema{} = component_schema
+ } ->
+ attrs =
+ attrs
+ |> Map.put_new(:entity_classification, entity.entity_classification)
+ |> Map.put_new(:component_schema_guid, component_schema.component_schema_guid)
+
+ %Component{}
+ |> Component.changeset(attrs)
+ |> Repo.insert()
+ end)
+ |> Ecto.Multi.run(:event, fn _repo, %{component: component} ->
+ {:ok, %Event{}} = Events.create_component_created_event(component, attrs)
+ end)
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{component: %Component{} = component}} -> {:ok, component}
+ {:error, :ensure_entity, err, _} -> {:error, err}
+ {:error, :component, %Ecto.Changeset{} = changeset, _} -> {:error, changeset}
+ end
+ end
+
+ @doc """
+ Updates a component.
+
+ ## Examples
+
+ iex> update_component(component, %{field: new_value})
+ {:ok, %Component{}}
+
+ iex> update_component(component, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec update_component(Component.t(), map()) ::
+ {:ok, Component.t()} | {:error, Ecto.Changeset.t()}
+ def(update_component(%Component{} = component, attrs)) do
+ Ecto.Multi.new()
+ |> Ecto.Multi.update(:component, Component.changeset(component, attrs))
+ |> Ecto.Multi.run(:event, fn _repo, %{component: component} ->
+ {:ok, %Event{}} = Events.create_component_updated_event(component, attrs)
+ end)
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{component: %Component{} = component}} -> {:ok, component}
+ {:error, :component, %Ecto.Changeset{} = changeset, _} -> {:error, changeset}
+ end
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking component changes.
+
+ ## Examples
+
+ iex> change_component(component)
+ %Ecto.Changeset{data: %Component{}}
+
+ """
+ def change_component(%Component{} = component, attrs \\ %{}) do
+ Component.changeset(component, attrs)
+ end
+
+ alias HokEcs.Components.ComponentSchema
+
+ @doc """
+ Returns the list of component_schemas.
+
+ ## Examples
+
+ iex> list_component_schemas()
+ [%ComponentSchema{}, ...]
+
+ """
+ @spec list_component_schemas :: list(ComponentSchema.t())
+ def list_component_schemas do
+ Repo.all(ComponentSchema)
+ end
+
+ @doc """
+ Gets a single component_schema.
+
+ Raises `Ecto.NoResultsError` if the Component schema does not exist.
+
+ ## Examples
+
+ iex> get_component_schema!(123)
+ %ComponentSchema{}
+
+ iex> get_component_schema!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ @spec get_component_schema!(String.t()) :: ComponentSchema.t()
+ def get_component_schema!(id), do: Repo.get!(ComponentSchema, id)
+
+ @doc """
+ Creates a component_schema.
+
+ ## Examples
+
+ iex> create_component_schema(%{field: value})
+ {:ok, %ComponentSchema{}}
+
+ iex> create_component_schema(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec create_component_schema(map()) ::
+ {:ok, ComponentSchema.t()} | {:error, Ecto.Changeset.t()}
+ def create_component_schema(attrs \\ %{}) do
+ %ComponentSchema{}
+ |> ComponentSchema.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a component_schema.
+
+ ## Examples
+
+ iex> update_component_schema(component_schema, %{field: new_value})
+ {:ok, %ComponentSchema{}}
+
+ iex> update_component_schema(component_schema, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec update_component_schema(ComponentSchema.t(), map()) ::
+ {:ok, ComponentSchema.t()} | {:error, Ecto.Changeset.t()}
+ def update_component_schema(%ComponentSchema{} = component_schema, attrs) do
+ component_schema
+ |> ComponentSchema.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a component_schema.
+
+ ## Examples
+
+ iex> delete_component_schema(component_schema)
+ {:ok, %ComponentSchema{}}
+
+ iex> delete_component_schema(component_schema)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec delete_component_schema(ComponentSchema.t()) ::
+ {:ok, ComponentSchema.t()} | {:error, Ecto.Changeset.t()}
+ def delete_component_schema(%ComponentSchema{} = component_schema) do
+ Repo.delete(component_schema)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking component_schema changes.
+
+ ## Examples
+
+ iex> change_component_schema(component_schema)
+ %Ecto.Changeset{data: %ComponentSchema{}}
+
+ """
+ @spec change_component_schema(ComponentSchema.t(), map()) :: Ecto.Changeset.t()
+ def change_component_schema(%ComponentSchema{} = component_schema, attrs \\ %{}) do
+ ComponentSchema.changeset(component_schema, attrs)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/components/component.ex b/hok_ecs/lib/hok_ecs/components/component.ex
new file mode 100644
index 0000000..4bdfd2d
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/components/component.ex
@@ -0,0 +1,128 @@
+defmodule HokEcs.Components.Component do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ alias HokEcs.Components.ComponentSchema
+
+ @attrs [
+ :component_sequence_name,
+ :component_sequence_value,
+ :context,
+ :context_id,
+ :component_name,
+ :component_id,
+ :entity_classification,
+ :component_type,
+ :component_type_reference,
+ :component_type_payload,
+ :owner,
+ :version,
+ :status,
+ :active,
+ :creation_date,
+ :authoring_application,
+ :hash1,
+ :payload,
+ :component_schema_guid,
+ :entity_guid
+ ]
+
+ @required [
+ :entity_guid,
+ :context,
+ :context_id,
+ :component_type,
+ :component_type_reference,
+ :component_type_payload,
+ :owner,
+ :version,
+ :active,
+ :creation_date,
+ :payload
+ ]
+
+ @primary_key {:component_guid, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ @derive {Phoenix.Param, key: :component_guid}
+ typed_schema "components" do
+ field :component_sequence_name, :string
+ field :component_sequence_value, :string
+ field :active, :boolean, default: true
+ field :creation_date, :string
+ field :authoring_application, :string
+ field :component_id, :string
+ field :component_name, :string
+ field :component_type_payload, :string
+ field :component_type, :string
+ field :component_type_reference, :string
+ field :context, :string
+ field :context_id, :string
+ field :entity_classification, :string
+ field :hash1, :string
+ field :owner, :string
+ field :payload, :map
+ field :status, :string
+ field :version, :string
+ field :entity_guid, :binary_id
+
+ belongs_to :component_schema, ComponentSchema,
+ foreign_key: :component_schema_guid,
+ references: :component_schema_guid
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(component, attrs) do
+ component
+ |> cast(attrs, @attrs)
+ |> validate_required(@required)
+ |> foreign_key_constraint(:entity_guid)
+ |> foreign_key_constraint(:component_schema_guid)
+ |> update_change(:payload, fn payload_change ->
+ if is_nil(payload_change) do
+ nil
+ else
+ component
+ |> Map.get(:payload, %{})
+ |> case do
+ nil -> %{}
+ payload -> payload
+ end
+ |> Map.merge(payload_change)
+ end
+ end)
+ |> validate_payload()
+ end
+
+ def validate_payload(changeset) do
+ component_schema_guid = fetch_field!(changeset, :component_schema_guid)
+
+ if is_nil(component_schema_guid) do
+ changeset
+ else
+ # TODO: cache resolved schemas
+ component_schema =
+ component_schema_guid
+ |> HokEcs.Components.get_component_schema!()
+ |> Map.get(:schema)
+ |> ExJsonSchema.Schema.resolve()
+
+ validate_change(changeset, :payload, fn :payload, payload ->
+ component_schema
+ |> ExJsonSchema.Validator.validate(payload)
+ |> case do
+ :ok ->
+ []
+
+ {:error, errors} ->
+ errors
+ |> Enum.map(fn {err, path} ->
+ {:payload, {"#{err} Path: %{path}", [path: path]}}
+ end)
+ end
+ end)
+ end
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/components/component_schema.ex b/hok_ecs/lib/hok_ecs/components/component_schema.ex
new file mode 100644
index 0000000..61858a3
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/components/component_schema.ex
@@ -0,0 +1,25 @@
+defmodule HokEcs.Components.ComponentSchema do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ @attrs [:name, :schema]
+ @required_attrs [:name, :schema]
+
+ @primary_key {:component_schema_guid, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "component_schemas" do
+ field :name, :string
+ field :schema, :map
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(component_schema, attrs) do
+ component_schema
+ |> cast(attrs, @attrs)
+ |> validate_required(@required_attrs)
+ |> unique_constraint(:name)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/entities.ex b/hok_ecs/lib/hok_ecs/entities.ex
new file mode 100644
index 0000000..057a465
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/entities.ex
@@ -0,0 +1,126 @@
+defmodule HokEcs.Entities do
+ @moduledoc """
+ The ECS context.
+ """
+
+ import Ecto.Query, warn: false
+ alias HokEcs.Events.Event
+ alias HokEcs.Repo
+
+ alias HokEcs.Entities.Entity
+ alias HokEcs.Events
+ alias Events.Event
+
+ @doc """
+ Returns the list of entities.
+
+ ## Examples
+
+ iex> list_entities()
+ [%Entity{}, ...]
+
+ """
+ @spec list_entities(map()) :: list(Entity.t())
+ def list_entities(args \\ %{}) do
+ query = from(Entity)
+
+ query =
+ case Map.get(args, :entity_classification) do
+ nil -> query
+ classification -> where(query, entity_classification: ^classification)
+ end
+
+ Repo.all(query)
+ end
+
+ @doc """
+ Gets a single entity.
+
+ Returns `nil` if the Entity does not exist.
+ """
+ @spec get_entity(String.t()) :: Entity.t() | nil
+ def get_entity(entity_guid), do: Repo.get(Entity, entity_guid)
+
+ @doc """
+ Gets a single entity.
+
+ Raises `Ecto.NoResultsError` if the Entity does not exist.
+
+ ## Examples
+
+ iex> get_entity!(123)
+ %Entity{}
+
+ iex> get_entity!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ @spec get_entity!(String.t()) :: Entity.t()
+ def get_entity!(id), do: Repo.get!(Entity, id)
+
+ @doc """
+ Creates a entity.
+
+ ## Examples
+
+ iex> create_entity(%{field: value})
+ {:ok, %Entity{}}
+
+ iex> create_entity(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec create_entity(map()) :: {:ok, Entity.t()} | {:error, Ecto.Changeset.t()}
+ def create_entity(attrs \\ %{}) do
+ Ecto.Multi.new()
+ |> Ecto.Multi.insert(:entity, Entity.changeset(%Entity{}, attrs))
+ |> Ecto.Multi.run(:event, fn _repo, %{entity: entity} ->
+ {:ok, %Event{}} = Events.create_entity_created_event(entity, attrs)
+ end)
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{entity: %Entity{} = entity}} -> {:ok, entity}
+ {:error, :entity, %Ecto.Changeset{} = changeset, _} -> {:error, changeset}
+ end
+ end
+
+ @doc """
+ Updates a entity.
+
+ ## Examples
+
+ iex> update_entity(entity, %{field: new_value})
+ {:ok, %Entity{}}
+
+ iex> update_entity(entity, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec update_entity(Entity.t(), map()) :: {:ok, Entity.t()} | {:error, Ecto.Changeset.t()}
+ def update_entity(%Entity{} = entity, attrs) do
+ Ecto.Multi.new()
+ |> Ecto.Multi.update(:entity, Entity.changeset(entity, attrs))
+ |> Ecto.Multi.run(:event, fn _repo, %{entity: entity} ->
+ {:ok, %Event{}} = Events.create_entity_updated_event(entity, attrs)
+ end)
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{entity: %Entity{} = entity}} -> {:ok, entity}
+ {:error, :entity, %Ecto.Changeset{} = changeset, _} -> {:error, changeset}
+ end
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking entity changes.
+
+ ## Examples
+
+ iex> change_entity(entity)
+ %Ecto.Changeset{data: %Entity{}}
+
+ """
+ @spec change_entity(Entity.t(), map()) :: Ecto.Changeset.t()
+ def change_entity(%Entity{} = entity, attrs \\ %{}) do
+ Entity.changeset(entity, attrs)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/entities/entity.ex b/hok_ecs/lib/hok_ecs/entities/entity.ex
new file mode 100644
index 0000000..463cb91
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/entities/entity.ex
@@ -0,0 +1,34 @@
+defmodule HokEcs.Entities.Entity do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ @attrs [
+ :context,
+ :context_id,
+ :entity_classification,
+ :entity_classification_reference,
+ :creation_date
+ ]
+ @required [:entity_classification, :context, :context_id, :creation_date]
+
+ @primary_key {:entity_guid, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ @derive {Phoenix.Param, key: :entity_guid}
+ typed_schema "entities" do
+ field :context, :string
+ field :context_id, :string
+ field :entity_classification, :string
+ field :entity_classification_reference, :string
+ field :creation_date, :string
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(entity, attrs) do
+ entity
+ |> cast(attrs, @attrs)
+ |> validate_required(@required)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/events.ex b/hok_ecs/lib/hok_ecs/events.ex
new file mode 100644
index 0000000..e78e3c6
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/events.ex
@@ -0,0 +1,81 @@
+defmodule HokEcs.Events do
+ alias HokEcs.Entities.Entity
+ alias HokEcs.Components.Component
+ alias HokEcs.Repo
+ alias __MODULE__.Event
+
+ @doc """
+ Returns the list of events.
+
+ ## Examples
+
+ iex> list_events()
+ [%Event{}, ...]
+
+ """
+ @spec list_events :: list(Event.t())
+ def list_events do
+ Repo.all(Event)
+ end
+
+ @doc """
+ Gets a single event.
+
+ Raises `Ecto.NoResultsError` if the Event does not exist.
+
+ ## Examples
+
+ iex> get_event!(123)
+ %Event{}
+
+ iex> get_event!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ @spec get_event!(String.t()) :: Event.t()
+ def get_event!(id), do: Repo.get!(Event, id)
+
+ @spec create_entity_created_event(Entity.t(), map()) ::
+ {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
+ def create_entity_created_event(%Entity{} = entity, attrs \\ %{}) do
+ %Event{
+ type: "entity_created",
+ entity_guid: entity.entity_guid,
+ data: attrs
+ }
+ |> Repo.insert()
+ end
+
+ @spec create_entity_updated_event(Entity.t(), map()) ::
+ {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
+ def create_entity_updated_event(%Entity{} = entity, attrs \\ %{}) do
+ %Event{
+ type: "entity_updated",
+ entity_guid: entity.entity_guid,
+ data: attrs
+ }
+ |> Repo.insert()
+ end
+
+ @spec create_component_created_event(Component.t(), map()) ::
+ {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
+ def create_component_created_event(%Component{} = component, attrs \\ %{}) do
+ %Event{
+ type: "component_created",
+ component_guid: component.component_guid,
+ data: attrs
+ }
+ |> Repo.insert()
+ end
+
+ @spec create_component_updated_event(Component.t(), map()) ::
+ {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
+ def create_component_updated_event(%Component{} = component, attrs \\ %{}) do
+ %Event{
+ type: "component_updated",
+ component_guid: component.component_guid,
+ data: attrs
+ }
+ |> Repo.insert()
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/events/event.ex b/hok_ecs/lib/hok_ecs/events/event.ex
new file mode 100644
index 0000000..f7e92c8
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/events/event.ex
@@ -0,0 +1,36 @@
+defmodule HokEcs.Events.Event do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ alias HokEcs.Entities.Entity
+ alias HokEcs.Components.Component
+
+ @attrs [:type, :data, :component_guid, :entity_guid]
+ @required [:type, :data]
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "events" do
+ field :data, :map
+ field :type, :string
+
+ belongs_to :entity, Entity,
+ foreign_key: :entity_guid,
+ references: :entity_guid
+
+ belongs_to :component, Component,
+ foreign_key: :component_guid,
+ references: :component_guid
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(event, attrs) do
+ event
+ |> cast(attrs, @attrs)
+ |> validate_required(@required)
+ |> check_constraint(:entity_guid, name: :valid_fkey)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/graph.ex b/hok_ecs/lib/hok_ecs/graph.ex
new file mode 100644
index 0000000..0d78214
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/graph.ex
@@ -0,0 +1,48 @@
+defmodule HokEcs.Graph do
+ alias HokEcs.Repo
+ alias __MODULE__.{Node, Edge}
+
+ @spec get_nodes :: list(Node.t())
+ def get_nodes() do
+ query = ~S"""
+ select c.component_guid as node_id, 'component' as type, c.component_type as label
+ from components c
+ union
+ select e.entity_guid as node_id, 'entity' as type, e.entity_classification as label
+ from entities e
+ union
+ select r.relationship_guid as node_id, 'relationship' as type, r.relationship_type as label
+ from relationships r
+ """
+
+ %{rows: rows, columns: columns} = Repo.query!(query)
+
+ rows
+ |> Enum.map(&Repo.load(Node, {columns, &1}))
+ end
+
+ @spec get_edges :: list(Node.t())
+ def get_edges() do
+ query = ~S"""
+ select rsc.relationship_guid as to_id, rsc.component_guid as from_id
+ from relationship_source_components rsc
+ union
+ select rdc.component_guid as to_id, rdc.relationship_guid as from_id
+ from relationship_destination_components rdc
+ union
+ select rse.relationship_guid as to_id, rse.entity_guid as from_id
+ from relationship_source_entities rse
+ union
+ select rde.entity_guid as to_id, rde.relationship_guid as from_id
+ from relationship_destination_entities rde
+ union
+ select components.entity_guid as to_id, components.component_guid as from_id
+ from components
+ """
+
+ %{rows: rows, columns: columns} = Repo.query!(query)
+
+ rows
+ |> Enum.map(&Repo.load(Edge, {columns, &1}))
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/graph/edge.ex b/hok_ecs/lib/hok_ecs/graph/edge.ex
new file mode 100644
index 0000000..c0907f2
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/graph/edge.ex
@@ -0,0 +1,8 @@
+defmodule HokEcs.Graph.Edge do
+ use TypedEctoSchema
+
+ typed_embedded_schema do
+ field :from_id, :binary_id
+ field :to_id, :binary_id
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/graph/node.ex b/hok_ecs/lib/hok_ecs/graph/node.ex
new file mode 100644
index 0000000..c1df5fa
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/graph/node.ex
@@ -0,0 +1,9 @@
+defmodule HokEcs.Graph.Node do
+ use TypedEctoSchema
+
+ typed_embedded_schema do
+ field :node_id, :binary_id
+ field :type, :string
+ field :label, :string
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/helpers.ex b/hok_ecs/lib/hok_ecs/helpers.ex
new file mode 100644
index 0000000..0cbe037
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/helpers.ex
@@ -0,0 +1,35 @@
+defmodule HokEcs.Helpers do
+ @moduledoc """
+ Utility functions.
+ """
+
+ @doc """
+ Wraps a value in an `{:ok, value}` tuple.
+ If the value is already an ok tuple it will not be changed.
+
+ ## Examples
+ iex> "OK" |> ok()
+ {:ok, "OK"}
+
+ iex> {:ok, "OK"} |> ok()
+ {:ok, "OK"}
+ """
+ @spec ok(any) :: {:ok, any}
+ def ok({:ok, _} = val), do: val
+ def ok(val), do: {:ok, val}
+
+ @doc """
+ Wraps a value in an `{:error, value}` tuple.
+ If the value is already an error tuple it will not be changed.
+
+ ## Examples
+ iex> "Error" |> error()
+ {:error, "Error"}
+
+ iex> {:error, "Error"} |> error()
+ {:error, "Error"}
+ """
+ @spec error(any) :: {:error, any}
+ def error({:error, _} = error), do: error
+ def error(val), do: {:error, val}
+end
diff --git a/hok_ecs/lib/hok_ecs/layers.ex b/hok_ecs/lib/hok_ecs/layers.ex
new file mode 100644
index 0000000..c7c2233
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/layers.ex
@@ -0,0 +1,165 @@
+defmodule HokEcs.Layers do
+ @moduledoc """
+ The Layers context.
+ """
+
+ import Ecto.Query, warn: false
+
+ alias HokEcs.Repo
+ alias HokEcs.Layers.{Layer, LayerEntity, LayerComponent, LayerRelationship}
+
+ @doc """
+ Returns the list of layers.
+
+ ## Examples
+
+ iex> list_layers()
+ [%Layer{}, ...]
+
+ """
+ @spec list_layers :: list(Layer.t())
+ def list_layers do
+ Repo.all(Layer)
+ end
+
+ @doc """
+ Gets a single layer.
+
+ Raises `Ecto.NoResultsError` if the Layer does not exist.
+
+ ## Examples
+
+ iex> get_layer!(123)
+ %Layer{}
+
+ iex> get_layer!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ @spec get_layer!(String.t()) :: Layer.t()
+ def get_layer!(id), do: Repo.get!(Layer, id)
+
+ @doc """
+ Creates a layer.
+
+ ## Examples
+
+ iex> create_layer(%{field: value})
+ {:ok, %Layer{}}
+
+ iex> create_layer(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec create_layer(map()) :: {:ok, Layer.t()} | {:error, Ecto.Changeset.t()}
+ def create_layer(attrs \\ %{}) do
+ %Layer{}
+ |> Layer.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a layer.
+
+ ## Examples
+
+ iex> update_layer(layer, %{field: new_value})
+ {:ok, %Layer{}}
+
+ iex> update_layer(layer, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec update_layer(Layer.t(), map()) :: {:ok, Layer.t()} | {:error, Ecto.Changeset.t()}
+ def update_layer(%Layer{} = layer, attrs) do
+ layer
+ |> Layer.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a layer.
+
+ ## Examples
+
+ iex> delete_layer(layer)
+ {:ok, %Layer{}}
+
+ iex> delete_layer(layer)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec delete_layer(Layer.t()) :: {:ok, Layer.t()} | {:error, Ecto.Changeset.t()}
+ def delete_layer(%Layer{} = layer) do
+ Repo.delete(layer)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking layer changes.
+
+ ## Examples
+
+ iex> change_layer(layer)
+ %Ecto.Changeset{data: %Layer{}}
+
+ """
+ @spec change_layer(Layer.t(), map()) :: Ecto.Changeset.t()
+ def change_layer(%Layer{} = layer, attrs \\ %{}) do
+ Layer.changeset(layer, attrs)
+ end
+
+ @spec add_entity_to_layer(String.t(), Layer.t()) ::
+ {:ok, LayerEntity.t()} | {:error, Ecto.Changeset.t()}
+ def add_entity_to_layer(entity_guid, %Layer{} = layer) do
+ %LayerEntity{}
+ |> LayerEntity.changeset(%{
+ layer_guid: layer.layer_guid,
+ entity_guid: entity_guid
+ })
+ |> Repo.insert()
+ end
+
+ @spec add_component_to_layer(String.t(), Layer.t()) ::
+ {:ok, LayerComponent.t()} | {:error, Ecto.Changeset.t()}
+ def add_component_to_layer(component_guid, %Layer{} = layer) do
+ %LayerComponent{}
+ |> LayerComponent.changeset(%{
+ layer_guid: layer.layer_guid,
+ component_guid: component_guid
+ })
+ |> Repo.insert()
+ end
+
+ @spec add_relationship_to_layer(String.t(), Layer.t()) ::
+ {:ok, LayerRelationship.t()} | {:error, Ecto.Changeset.t()}
+ def add_relationship_to_layer(relationship_guid, %Layer{} = layer) do
+ %LayerRelationship{}
+ |> LayerRelationship.changeset(%{
+ layer_guid: layer.layer_guid,
+ relationship_guid: relationship_guid
+ })
+ |> Repo.insert()
+ end
+
+ @layer_data_preload [
+ :entities,
+ :relationships,
+ components: [:component_schema]
+ ]
+
+ @spec get_layer_data(Layer.t() | String.t()) :: Layer.t() | nil
+ def get_layer_data(%Layer{} = layer) do
+ layer
+ |> Map.get(:layer_guid)
+ |> get_layer_data()
+ end
+
+ def get_layer_data(layer_guid) do
+ query =
+ from l in Layer,
+ where: l.layer_guid == ^layer_guid,
+ preload: ^@layer_data_preload
+
+ query |> Repo.one()
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/layers/layer.ex b/hok_ecs/lib/hok_ecs/layers/layer.ex
new file mode 100644
index 0000000..4129561
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/layers/layer.ex
@@ -0,0 +1,68 @@
+defmodule HokEcs.Layers.Layer do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ alias HokEcs.Layers.{LayerEntity, LayerComponent, LayerRelationship}
+ alias HokEcs.Entities.Entity
+ alias HokEcs.Components.Component
+ alias HokEcs.Relationships.Relationship
+
+ @attrs [
+ :context,
+ :layer_name,
+ :layer_id,
+ :layer_owner,
+ :layer_description,
+ :layer_function,
+ :layer_update_method,
+ :active,
+ :version
+ ]
+
+ @primary_key {:layer_guid, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "layers" do
+ field :active, :boolean
+ field :context, :string
+ field :layer_description, :string
+ field :layer_function, :string
+ field :layer_id, :string
+ field :layer_name, :string
+ field :layer_owner, :string
+ field :layer_update_method, :string
+ field :version, :integer
+
+ has_many :layer_entities, LayerEntity,
+ foreign_key: :layer_guid,
+ references: :layer_guid
+
+ has_many :layer_components, LayerComponent,
+ foreign_key: :layer_guid,
+ references: :layer_guid
+
+ has_many :layer_relationships, LayerRelationship,
+ foreign_key: :layer_guid,
+ references: :layer_guid
+
+ many_to_many :entities, Entity,
+ join_through: LayerEntity,
+ join_keys: [layer_guid: :layer_guid, entity_guid: :entity_guid]
+
+ many_to_many :components, Component,
+ join_through: LayerComponent,
+ join_keys: [layer_guid: :layer_guid, component_guid: :component_guid]
+
+ many_to_many :relationships, Relationship,
+ join_through: LayerRelationship,
+ join_keys: [layer_guid: :layer_guid, relationship_guid: :relationship_guid]
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(layer, attrs) do
+ layer
+ |> cast(attrs, @attrs)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/layers/layer_component.ex b/hok_ecs/lib/hok_ecs/layers/layer_component.ex
new file mode 100644
index 0000000..632407e
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/layers/layer_component.ex
@@ -0,0 +1,33 @@
+defmodule HokEcs.Layers.LayerComponent do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ alias HokEcs.Components.Component
+ alias HokEcs.Layers.Layer
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "layer_components" do
+ # field :layer_guid, :binary_id
+ # field :component_guid, :binary_id
+
+ belongs_to :layer, Layer,
+ foreign_key: :layer_guid,
+ references: :layer_guid
+
+ belongs_to :component, Component,
+ foreign_key: :component_guid,
+ references: :component_guid
+
+ timestamps()
+ end
+
+ def changeset(layer_component, attrs) do
+ layer_component
+ |> cast(attrs, [:layer_guid, :component_guid])
+ |> foreign_key_constraint(:layer_guid)
+ |> foreign_key_constraint(:component_guid)
+ |> unique_constraint([:layer_guid, :component_guid])
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/layers/layer_entity.ex b/hok_ecs/lib/hok_ecs/layers/layer_entity.ex
new file mode 100644
index 0000000..5db44a0
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/layers/layer_entity.ex
@@ -0,0 +1,34 @@
+defmodule HokEcs.Layers.LayerEntity do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ alias HokEcs.Entities.Entity
+ alias HokEcs.Layers.Layer
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "layer_entities" do
+ # field :layer_guid, :binary_id
+ # field :entity_guid, :binary_id
+
+ belongs_to :layer, Layer,
+ foreign_key: :layer_guid,
+ references: :layer_guid
+
+ belongs_to :entity, Entity,
+ foreign_key: :entity_guid,
+ references: :entity_guid
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(layer_entity, attrs) do
+ layer_entity
+ |> cast(attrs, [:layer_guid, :entity_guid])
+ |> foreign_key_constraint(:layer_guid)
+ |> foreign_key_constraint(:entity_guid)
+ |> unique_constraint([:layer_guid, :entity_guid])
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/layers/layer_relationship.ex b/hok_ecs/lib/hok_ecs/layers/layer_relationship.ex
new file mode 100644
index 0000000..38017d8
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/layers/layer_relationship.ex
@@ -0,0 +1,34 @@
+defmodule HokEcs.Layers.LayerRelationship do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ alias HokEcs.Layers.Layer
+ alias HokEcs.Relationships.Relationship
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "layer_relationships" do
+ # field :layer_guid, :binary_id
+ # field :relationship_guid, :binary_id
+
+ belongs_to :layer, Layer,
+ foreign_key: :layer_guid,
+ references: :layer_guid
+
+ belongs_to :relationship, Relationship,
+ foreign_key: :relationship_guid,
+ references: :relationship_guid
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(layer_relationship, attrs) do
+ layer_relationship
+ |> cast(attrs, [:layer_guid, :relationship_guid])
+ |> foreign_key_constraint(:layer_guid)
+ |> foreign_key_constraint(:relationship_guid)
+ |> unique_constraint([:layer_guid, :relationship_guid])
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/relationships.ex b/hok_ecs/lib/hok_ecs/relationships.ex
new file mode 100644
index 0000000..e746640
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/relationships.ex
@@ -0,0 +1,265 @@
+defmodule HokEcs.Relationships do
+ alias HokEcs.Helpers
+ alias HokEcs.Repo
+
+ alias HokEcs.Relationships.{
+ Operations,
+ Relationship,
+ RelationshipSourceEntity,
+ RelationshipSourceComponent,
+ RelationshipDestinationEntity,
+ RelationshipDestinationComponent
+ }
+
+ import Ecto.Query, only: [from: 1, from: 2]
+
+ @spec get_relationships_by_member_ids(
+ list(String.t()),
+ list(String.t()),
+ list(String.t()),
+ list(String.t())
+ ) :: list(Relationship.t())
+ def get_relationships_by_member_ids(
+ source_entity_guids,
+ source_component_guids,
+ destination_entity_guids,
+ destination_component_guids
+ ) do
+ query = from(r in Relationship)
+
+ query
+ |> get_relationships_by_member_ids_source_entities_join(source_entity_guids)
+ |> get_relationships_by_member_ids_source_components_join(source_component_guids)
+ |> get_relationships_by_member_ids_destination_entities_join(destination_entity_guids)
+ |> get_relationships_by_member_ids_destination_components_join(destination_component_guids)
+ |> Repo.all()
+ end
+
+ @spec get_relationships_by_member_ids_source_entities_join(Ecto.Query.t(), list(String.t())) ::
+ Ecto.Query.t()
+ defp get_relationships_by_member_ids_source_entities_join(query, []), do: query
+
+ defp get_relationships_by_member_ids_source_entities_join(query, source_entity_guids) do
+ from r in query,
+ join: rse in RelationshipSourceEntity,
+ on:
+ rse.relationship_guid == r.relationship_guid and
+ rse.entity_guid in ^source_entity_guids
+ end
+
+ @spec get_relationships_by_member_ids_source_components_join(Ecto.Query.t(), list(String.t())) ::
+ Ecto.Query.t()
+ defp get_relationships_by_member_ids_source_components_join(query, []), do: query
+
+ defp get_relationships_by_member_ids_source_components_join(query, source_component_guids) do
+ from r in query,
+ join: rsc in RelationshipSourceComponent,
+ on:
+ rsc.relationship_guid == r.relationship_guid and
+ rsc.component_guid in ^source_component_guids
+ end
+
+ @spec get_relationships_by_member_ids_destination_entities_join(
+ Ecto.Query.t(),
+ list(String.t())
+ ) :: Ecto.Query.t()
+ defp get_relationships_by_member_ids_destination_entities_join(query, []), do: query
+
+ defp get_relationships_by_member_ids_destination_entities_join(query, destination_entity_guids) do
+ from r in query,
+ join: rde in RelationshipDestinationEntity,
+ on:
+ rde.relationship_guid == r.relationship_guid and
+ rde.entity_guid in ^destination_entity_guids
+ end
+
+ @spec get_relationships_by_member_ids_destination_components_join(
+ Ecto.Query.t(),
+ list(String.t())
+ ) :: Ecto.Query.t()
+ defp get_relationships_by_member_ids_destination_components_join(query, []), do: query
+
+ defp get_relationships_by_member_ids_destination_components_join(
+ query,
+ destination_component_guids
+ ) do
+ from r in query,
+ join: rse in RelationshipDestinationComponent,
+ on:
+ rse.relationship_guid == r.relationship_guid and
+ rse.component_guid in ^destination_component_guids
+ end
+
+ @doc """
+ Returns the list of relationships.
+
+ ## Examples
+
+ iex> list_relationships()
+ [%Relationship{}, ...]
+
+ """
+ @spec list_relationships :: list(Relationship.t())
+ def list_relationships do
+ Repo.all(Relationship)
+ end
+
+ @spec get_relationship(String.t()) :: Relationship.t() | nil
+ def get_relationship(relationship_guid), do: Repo.get(Relationship, relationship_guid)
+
+ @doc """
+ Gets a single relationship.
+
+ Raises `Ecto.NoResultsError` if the Relationship does not exist.
+
+ ## Examples
+
+ iex> get_relationship!(123)
+ %Relationship{}
+
+ iex> get_relationship!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ @spec get_relationship!(String.t()) :: Relationship.t()
+ def get_relationship!(id), do: Repo.get!(Relationship, id)
+
+ @doc """
+ Creates a relationship.
+
+ ## Examples
+
+ iex> create_relationship(%{field: value})
+ {:ok, %Relationship{}}
+
+ iex> create_relationship(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec create_relationship(map()) :: {:ok, Relationship.t()} | {:error, Ecto.Changeset.t()}
+ def create_relationship(attrs \\ %{}) do
+ %Relationship{}
+ |> Relationship.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a relationship.
+
+ ## Examples
+
+ iex> update_relationship(relationship, %{field: new_value})
+ {:ok, %Relationship{}}
+
+ iex> update_relationship(relationship, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec update_relationship(Relationship.t(), map()) ::
+ {:ok, Relationship.t()} | {:error, Ecto.Changeset.t()}
+ def update_relationship(%Relationship{} = relationship, attrs) do
+ relationship
+ |> Relationship.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a relationship.
+
+ ## Examples
+
+ iex> delete_relationship(relationship)
+ {:ok, %Relationship{}}
+
+ iex> delete_relationship(relationship)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ @spec delete_relationship(Relationship.t()) ::
+ {:ok, Relationship.t()} | {:error, Ecto.Changeset.t()}
+ def delete_relationship(%Relationship{} = relationship) do
+ Repo.delete(relationship)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking relationship changes.
+
+ ## Examples
+
+ iex> change_relationship(relationship)
+ %Ecto.Changeset{data: %Relationship{}}
+
+ """
+ @spec change_relationship(Relationship.t(), map()) :: Ecto.Changeset.t()
+ def change_relationship(%Relationship{} = relationship, attrs \\ %{}) do
+ Relationship.changeset(relationship, attrs)
+ end
+
+ @spec create_relationship(
+ map(),
+ list(String.t()),
+ list(String.t()),
+ list(String.t()),
+ list(String.t())
+ ) :: {:ok, Relationship.t()} | {:error, Ecto.Changeset.t()}
+ def create_relationship(
+ attrs \\ %{},
+ source_entity_guids,
+ source_component_guids,
+ destination_entity_guids,
+ destination_component_guids
+ ) do
+ case Ecto.Multi.new()
+ |> Ecto.Multi.insert(:relationship, Relationship.changeset(%Relationship{}, attrs))
+ |> Operations.create_relationship_joins_in_multi(
+ source_entity_guids,
+ source_component_guids,
+ destination_entity_guids,
+ destination_component_guids
+ )
+ |> Repo.transaction() do
+ {:ok, result} ->
+ result
+ |> Map.get(:relationship)
+ |> Operations.relationship_with_links()
+ |> Helpers.ok()
+
+ {:error, _, %Ecto.Changeset{} = changeset, _} ->
+ {:error, changeset}
+ end
+ end
+
+ @spec add_links_to_relationship(
+ Relationship.t(),
+ list(String.t()),
+ list(String.t()),
+ list(String.t()),
+ list(String.t())
+ ) :: {:ok, Relationship.t()} | {:error, Ecto.Changeset.t()}
+ def add_links_to_relationship(
+ %Relationship{} = relationship,
+ source_entity_guids,
+ source_component_guids,
+ destination_entity_guids,
+ destination_component_guids
+ ) do
+ case Ecto.Multi.new()
+ |> Ecto.Multi.put(:relationship, relationship)
+ |> Operations.create_relationship_joins_in_multi(
+ source_entity_guids,
+ source_component_guids,
+ destination_entity_guids,
+ destination_component_guids
+ )
+ |> Repo.transaction() do
+ {:ok, result} ->
+ result
+ |> Map.get(:relationship)
+ |> Operations.relationship_with_links()
+ |> Helpers.ok()
+
+ {:error, _, %Ecto.Changeset{} = changeset, _} ->
+ {:error, changeset}
+ end
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/relationships/operations.ex b/hok_ecs/lib/hok_ecs/relationships/operations.ex
new file mode 100644
index 0000000..8d1b981
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/relationships/operations.ex
@@ -0,0 +1,128 @@
+defmodule HokEcs.Relationships.Operations do
+ alias HokEcs.Repo
+
+ alias HokEcs.Relationships.{
+ Relationship,
+ RelationshipSourceEntity,
+ RelationshipSourceComponent,
+ RelationshipDestinationEntity,
+ RelationshipDestinationComponent
+ }
+
+ @spec relationship_with_links(Relationship.t()) :: Relationship.t()
+ def relationship_with_links(%Relationship{} = relationship) do
+ relationship
+ |> Repo.preload([
+ :relationship_source_entities,
+ :relationship_destination_entities,
+ :relationship_source_components,
+ :relationship_destination_components
+ ])
+ end
+
+ @spec create_relationship_joins_in_multi(
+ Ecto.Multi.t(),
+ list(String.t()),
+ list(String.t()),
+ list(String.t()),
+ list(String.t())
+ ) ::
+ Ecto.Multi.t()
+ def create_relationship_joins_in_multi(
+ %Ecto.Multi{} = multi,
+ source_entity_guids,
+ source_component_guids,
+ destination_entity_guids,
+ destination_component_guids
+ ) do
+ multi
+ |> Ecto.Multi.run(:source_entities, fn _repo, %{relationship: relationship} ->
+ create_relationship_source_entities(source_entity_guids, relationship)
+ end)
+ |> Ecto.Multi.run(:source_components, fn _repo, %{relationship: relationship} ->
+ create_relationship_source_components(
+ source_component_guids,
+ relationship
+ )
+ end)
+ |> Ecto.Multi.run(:destination_entitys, fn _repo, %{relationship: relationship} ->
+ create_relationship_destination_entities(
+ destination_entity_guids,
+ relationship
+ )
+ end)
+ |> Ecto.Multi.run(:destination_components, fn _repo, %{relationship: relationship} ->
+ create_relationship_destination_components(
+ destination_component_guids,
+ relationship
+ )
+ end)
+ end
+
+ def create_relationship_source_entities(source_entity_guids, %Relationship{} = relationship) do
+ create_relationship_joins(
+ source_entity_guids,
+ RelationshipSourceEntity,
+ :entity_guid,
+ relationship
+ )
+ end
+
+ def create_relationship_source_components(
+ source_component_guids,
+ %Relationship{} = relationship
+ ) do
+ create_relationship_joins(
+ source_component_guids,
+ RelationshipSourceComponent,
+ :component_guid,
+ relationship
+ )
+ end
+
+ def create_relationship_destination_entities(
+ destination_entity_guids,
+ %Relationship{} = relationship
+ ) do
+ create_relationship_joins(
+ destination_entity_guids,
+ RelationshipDestinationEntity,
+ :entity_guid,
+ relationship
+ )
+ end
+
+ def create_relationship_destination_components(
+ destination_component_guids,
+ %Relationship{} = relationship
+ ) do
+ create_relationship_joins(
+ destination_component_guids,
+ RelationshipDestinationComponent,
+ :component_guid,
+ relationship
+ )
+ end
+
+ defp create_relationship_joins(nil, _, _, _), do: {:ok, nil}
+ defp create_relationship_joins([], _, _, _), do: {:ok, []}
+
+ defp create_relationship_joins(guids, schema, join_key, %Relationship{} = relationship) do
+ guids
+ |> Enum.reduce_while({:ok, []}, fn guid, {:ok, acc} ->
+ attrs =
+ %{
+ relationship_guid: relationship.relationship_guid
+ }
+ |> Map.put(join_key, guid)
+
+ struct(schema)
+ |> schema.changeset(attrs)
+ |> Repo.insert()
+ |> case do
+ {:ok, result} -> {:cont, {:ok, [result | acc]}}
+ {:error, err} -> {:halt, {:error, err}}
+ end
+ end)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/relationships/relationship.ex b/hok_ecs/lib/hok_ecs/relationships/relationship.ex
new file mode 100644
index 0000000..c2cff0a
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/relationships/relationship.ex
@@ -0,0 +1,72 @@
+defmodule HokEcs.Relationships.Relationship do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ alias HokEcs.Relationships.{
+ RelationshipDestinationComponent,
+ RelationshipDestinationEntity,
+ RelationshipSourceComponent,
+ RelationshipSourceEntity
+ }
+
+ @attrs [
+ :context,
+ :relationship_name,
+ :relationship_type,
+ :relationship_type_reference,
+ :active,
+ :version
+ ]
+
+ @primary_key {:relationship_guid, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "relationships" do
+ field :active, :boolean, default: true
+ field :context, :string
+ field :relationship_name, :string
+ field :relationship_type, :string
+ field :relationship_type_reference, :string
+ field :version, :integer
+
+ has_many :relationship_source_entities, RelationshipSourceEntity,
+ foreign_key: :relationship_guid,
+ references: :relationship_guid
+
+ has_many :relationship_source_components, RelationshipSourceComponent,
+ foreign_key: :relationship_guid,
+ references: :relationship_guid
+
+ has_many :relationship_destination_entities, RelationshipDestinationEntity,
+ foreign_key: :relationship_guid,
+ references: :relationship_guid
+
+ has_many :relationship_destination_components, RelationshipDestinationComponent,
+ foreign_key: :relationship_guid,
+ references: :relationship_guid
+
+ # many_to_many :source_entities, HokEcs.ECS.Entity,
+ # join_through: HokEcs.ECS.RelationshipSourceEntity,
+ # join_keys: [relationship_guid: :relationship_guid, entity_guid: :entity_guid]
+
+ # many_to_many :source_components, HokEcs.ECS.Component,
+ # join_through: HokEcs.ECS.RelationshipSourceComponent,
+ # join_keys: [relationship_guid: :relationship_guid, component_guid: :component_guid]
+
+ # many_to_many :destination_entities, HokEcs.ECS.Entity,
+ # join_through: HokEcs.ECS.RelationshipDestinationEntity,
+ # join_keys: [relationship_guid: :relationship_guid, entity_guid: :entity_guid]
+
+ # many_to_many :destination_components, HokEcs.ECS.Component,
+ # join_through: HokEcs.ECS.RelationshipDestinationComponent,
+ # join_keys: [relationship_guid: :relationship_guid, component_guid: :component_guid]
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(relationship, attrs) do
+ relationship
+ |> cast(attrs, @attrs)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/relationships/relationship_destination_component.ex b/hok_ecs/lib/hok_ecs/relationships/relationship_destination_component.ex
new file mode 100644
index 0000000..36f6008
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/relationships/relationship_destination_component.ex
@@ -0,0 +1,23 @@
+defmodule HokEcs.Relationships.RelationshipDestinationComponent do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "relationship_destination_components" do
+ field :relationship_guid, :binary_id
+ field :component_guid, :binary_id
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(relationship_destination_component, attrs) do
+ relationship_destination_component
+ |> cast(attrs, [:relationship_guid, :component_guid])
+ |> validate_required([:relationship_guid, :component_guid])
+ |> foreign_key_constraint(:relationship_guid)
+ |> foreign_key_constraint(:component_guid)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/relationships/relationship_destination_entity.ex b/hok_ecs/lib/hok_ecs/relationships/relationship_destination_entity.ex
new file mode 100644
index 0000000..f6f0a2b
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/relationships/relationship_destination_entity.ex
@@ -0,0 +1,23 @@
+defmodule HokEcs.Relationships.RelationshipDestinationEntity do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "relationship_destination_entities" do
+ field :relationship_guid, :binary_id
+ field :entity_guid, :binary_id
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(relationship_destination_entity, attrs) do
+ relationship_destination_entity
+ |> cast(attrs, [:relationship_guid, :entity_guid])
+ |> validate_required([:relationship_guid, :entity_guid])
+ |> foreign_key_constraint(:relationship_guid)
+ |> foreign_key_constraint(:entity_guid)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/relationships/relationship_source_component.ex b/hok_ecs/lib/hok_ecs/relationships/relationship_source_component.ex
new file mode 100644
index 0000000..82f1cce
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/relationships/relationship_source_component.ex
@@ -0,0 +1,23 @@
+defmodule HokEcs.Relationships.RelationshipSourceComponent do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "relationship_source_components" do
+ field :relationship_guid, :binary_id
+ field :component_guid, :binary_id
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(relationship_source_component, attrs) do
+ relationship_source_component
+ |> cast(attrs, [:relationship_guid, :component_guid])
+ |> validate_required([:relationship_guid, :component_guid])
+ |> foreign_key_constraint(:relationship_guid)
+ |> foreign_key_constraint(:component_guid)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/relationships/relationship_source_entity.ex b/hok_ecs/lib/hok_ecs/relationships/relationship_source_entity.ex
new file mode 100644
index 0000000..a886e6d
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/relationships/relationship_source_entity.ex
@@ -0,0 +1,23 @@
+defmodule HokEcs.Relationships.RelationshipSourceEntity do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "relationship_source_entities" do
+ field :relationship_guid, :binary_id
+ field :entity_guid, :binary_id
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(relationship_source_entity, attrs) do
+ relationship_source_entity
+ |> cast(attrs, [:relationship_guid, :entity_guid])
+ |> validate_required([:relationship_guid, :entity_guid])
+ |> foreign_key_constraint(:relationship_guid)
+ |> foreign_key_constraint(:entity_guid)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/repo.ex b/hok_ecs/lib/hok_ecs/repo.ex
new file mode 100644
index 0000000..ebd0de3
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/repo.ex
@@ -0,0 +1,5 @@
+defmodule HokEcs.Repo do
+ use Ecto.Repo,
+ otp_app: :hok_ecs,
+ adapter: Ecto.Adapters.Postgres
+end
diff --git a/hok_ecs/lib/hok_ecs/scenes.ex b/hok_ecs/lib/hok_ecs/scenes.ex
new file mode 100644
index 0000000..7fbe73a
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/scenes.ex
@@ -0,0 +1,136 @@
+defmodule HokEcs.Scenes do
+ @moduledoc """
+ The Scenes context.
+ """
+
+ import Ecto.Query, warn: false
+ alias HokEcs.Scenes.SceneLayer
+ alias HokEcs.Repo
+
+ alias HokEcs.Scenes.Scene
+
+ @doc """
+ Returns the list of scenes.
+
+ ## Examples
+
+ iex> list_scenes()
+ [%Scene{}, ...]
+
+ """
+ def list_scenes do
+ Repo.all(Scene)
+ end
+
+ @doc """
+ Gets a single scene.
+
+ Raises `Ecto.NoResultsError` if the Scene does not exist.
+
+ ## Examples
+
+ iex> get_scene!(123)
+ %Scene{}
+
+ iex> get_scene!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_scene!(id), do: Repo.get!(Scene, id)
+
+ @doc """
+ Creates a scene.
+
+ ## Examples
+
+ iex> create_scene(%{field: value})
+ {:ok, %Scene{}}
+
+ iex> create_scene(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_scene(attrs \\ %{}) do
+ %Scene{}
+ |> Scene.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a scene.
+
+ ## Examples
+
+ iex> update_scene(scene, %{field: new_value})
+ {:ok, %Scene{}}
+
+ iex> update_scene(scene, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_scene(%Scene{} = scene, attrs) do
+ scene
+ |> Scene.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a scene.
+
+ ## Examples
+
+ iex> delete_scene(scene)
+ {:ok, %Scene{}}
+
+ iex> delete_scene(scene)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_scene(%Scene{} = scene) do
+ Repo.delete(scene)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking scene changes.
+
+ ## Examples
+
+ iex> change_scene(scene)
+ %Ecto.Changeset{data: %Scene{}}
+
+ """
+ def change_scene(%Scene{} = scene, attrs \\ %{}) do
+ Scene.changeset(scene, attrs)
+ end
+
+ def add_layer_to_scene(layer_guid, %Scene{} = scene) do
+ %SceneLayer{}
+ |> SceneLayer.changeset(%{
+ scene_guid: scene.scene_guid,
+ layer_guid: layer_guid
+ })
+ |> Repo.insert()
+ end
+
+ def get_scene_data(%Scene{} = scene) do
+ scene
+ |> Map.get(:scene_guid)
+ |> get_scene_data()
+ end
+
+ def get_scene_data(scene_guid) do
+ query =
+ from s in Scene,
+ where: s.scene_guid == ^scene_guid,
+ join: l in assoc(s, :layers),
+ left_join: e in assoc(l, :entities),
+ left_join: c in assoc(l, :components),
+ left_join: cs in assoc(c, :component_schema),
+ left_join: r in assoc(l, :relationships),
+ preload: [
+ layers: {l, entities: e, relationships: r, components: {c, component_schema: cs}}
+ ]
+
+ query |> Repo.one()
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/scenes/scene.ex b/hok_ecs/lib/hok_ecs/scenes/scene.ex
new file mode 100644
index 0000000..979f132
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/scenes/scene.ex
@@ -0,0 +1,31 @@
+defmodule HokEcs.Scenes.Scene do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ alias HokEcs.Layers.Layer
+ alias HokEcs.Scenes.SceneLayer
+
+ @primary_key {:scene_guid, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "scenes" do
+ field :active, :boolean, default: false
+ field :context, :string
+ field :date_created, :string
+ field :scene_id, :string
+ field :scene_name, :string
+ field :version, :integer
+
+ many_to_many :layers, Layer,
+ join_through: SceneLayer,
+ join_keys: [scene_guid: :scene_guid, layer_guid: :layer_guid]
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(scene, attrs) do
+ scene
+ |> cast(attrs, [:context, :scene_name, :scene_id, :active, :version, :date_created])
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs/scenes/scene_layer.ex b/hok_ecs/lib/hok_ecs/scenes/scene_layer.ex
new file mode 100644
index 0000000..f569ae9
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs/scenes/scene_layer.ex
@@ -0,0 +1,31 @@
+defmodule HokEcs.Scenes.SceneLayer do
+ use TypedEctoSchema
+
+ import Ecto.Changeset
+
+ alias HokEcs.Layers.Layer
+ alias HokEcs.Scenes.Scene
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ typed_schema "scene_layers" do
+ belongs_to :scene, Scene,
+ foreign_key: :scene_guid,
+ references: :scene_guid
+
+ belongs_to :layer, Layer,
+ foreign_key: :layer_guid,
+ references: :layer_guid
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(scene_layer, attrs) do
+ scene_layer
+ |> cast(attrs, [:scene_guid, :layer_guid])
+ |> foreign_key_constraint(:scene_guid)
+ |> foreign_key_constraint(:layer_guid)
+ |> unique_constraint([:scene_guid, :layer_guid])
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web.ex b/hok_ecs/lib/hok_ecs_web.ex
new file mode 100644
index 0000000..ee0491c
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web.ex
@@ -0,0 +1,107 @@
+defmodule HokEcsWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface, such
+ as controllers, views, channels and so on.
+
+ This can be used in your application as:
+
+ use HokEcsWeb, :controller
+ use HokEcsWeb, :view
+
+ The definitions below will be executed for every view,
+ controller, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below. Instead, define any helper function in modules
+ and import those modules here.
+ """
+
+ def controller do
+ quote do
+ use Phoenix.Controller, namespace: HokEcsWeb
+
+ import Plug.Conn
+ alias HokEcsWeb.Router.Helpers, as: Routes
+ end
+ end
+
+ def view do
+ quote do
+ use Phoenix.View,
+ root: "lib/hok_ecs_web/templates",
+ namespace: HokEcsWeb
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
+
+ # Include shared imports and aliases for views
+ unquote(view_helpers())
+ end
+ end
+
+ def live_view do
+ quote do
+ use Phoenix.LiveView,
+ layout: {HokEcsWeb.LayoutView, "live.html"}
+
+ unquote(view_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(view_helpers())
+ end
+ end
+
+ def component do
+ quote do
+ use Phoenix.Component
+
+ unquote(view_helpers())
+ end
+ end
+
+ def router do
+ quote do
+ use Phoenix.Router
+
+ import Plug.Conn
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ end
+ end
+
+ defp view_helpers do
+ quote do
+ # Use all HTML functionality (forms, tags, etc)
+ use Phoenix.HTML
+
+ # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
+ import Phoenix.LiveView.Helpers
+
+ # Import basic rendering functionality (render, render_layout, etc)
+ import Phoenix.View
+
+ import HokEcsWeb.ErrorHelpers
+ alias HokEcsWeb.Router.Helpers, as: Routes
+ end
+ end
+
+ @doc """
+ When used, dispatch to the appropriate controller/view/etc.
+ """
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/absinthe_schema/.formatter.exs b/hok_ecs/lib/hok_ecs_web/absinthe_schema/.formatter.exs
new file mode 100644
index 0000000..105fd40
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/absinthe_schema/.formatter.exs
@@ -0,0 +1,4 @@
+[
+ import_deps: [:absinthe],
+ inputs: ["*.ex"]
+]
diff --git a/hok_ecs/lib/hok_ecs_web/absinthe_schema/absinthe_schema.ex b/hok_ecs/lib/hok_ecs_web/absinthe_schema/absinthe_schema.ex
new file mode 100644
index 0000000..f33fcef
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/absinthe_schema/absinthe_schema.ex
@@ -0,0 +1,22 @@
+defmodule HokEcsWeb.AbsintheSchema do
+ use Absinthe.Schema
+
+ alias __MODULE__.{Types, Queries, Mutations}
+
+ import_types(Types)
+ import_types(Queries)
+ import_types(Mutations)
+
+ query do
+ import_fields(:entity_queries)
+ import_fields(:component_queries)
+ import_fields(:relationship_queries)
+ import_fields(:graph_queries)
+ end
+
+ mutation do
+ import_fields(:entity_mutations)
+ import_fields(:component_mutations)
+ import_fields(:relationship_mutations)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/absinthe_schema/absinthe_schema_helpers.ex b/hok_ecs/lib/hok_ecs_web/absinthe_schema/absinthe_schema_helpers.ex
new file mode 100644
index 0000000..22a6b4d
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/absinthe_schema/absinthe_schema_helpers.ex
@@ -0,0 +1,13 @@
+defmodule HokEcsWeb.AbsintheSchema.Helpers do
+ defmacro non_null_list(type) do
+ quote do
+ non_null(list_of(non_null(unquote(type))))
+ end
+ end
+
+ defmacro resolve_constant(value) do
+ quote do
+ resolve(fn _, _ -> {:ok, unquote(value)} end)
+ end
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/absinthe_schema/mutations.ex b/hok_ecs/lib/hok_ecs_web/absinthe_schema/mutations.ex
new file mode 100644
index 0000000..64ee3c1
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/absinthe_schema/mutations.ex
@@ -0,0 +1,105 @@
+defmodule HokEcsWeb.AbsintheSchema.Mutations do
+ use Absinthe.Schema.Notation
+
+ alias HokEcs.{Components, Entities, Relationships}
+
+ import AbsintheErrorPayload.Payload
+
+ import_types(AbsintheErrorPayload.ValidationMessageTypes)
+
+ payload_object(:entity_payload, :entity)
+ payload_object(:component_payload, :component)
+ payload_object(:relationship_payload, :relationship)
+
+ input_object :create_entity_input do
+ field(:classification_reference, :string)
+ field(:entity_classification, non_null(:string))
+ field(:context, non_null(:string))
+ field(:context_id, non_null(:string))
+ end
+
+ object :entity_mutations do
+ field :create_entity, non_null(:entity_payload) do
+ arg(:input, non_null(:create_entity_input))
+
+ resolve(fn %{input: input}, _ ->
+ input
+ |> Map.put_new(:creation_date, DateTime.utc_now() |> to_string())
+ |> Entities.create_entity()
+ end)
+
+ middleware(&build_payload/2)
+ end
+ end
+
+ input_object :create_component_input do
+ field(:entity_guid, non_null(:id))
+ field(:context, non_null(:string))
+ field(:context_id, non_null(:string))
+ field(:component_name, :string)
+ field(:component_id, :string)
+ field(:component_type, non_null(:string))
+ field(:component_type_reference, non_null(:string))
+ field(:component_type_payload, non_null(:string))
+ field(:owner, non_null(:string))
+ field(:version, non_null(:string))
+ field(:status, :string)
+ field(:payload, non_null(:json))
+ end
+
+ object :component_mutations do
+ field :create_component, non_null(:component_payload) do
+ arg(:input, non_null(:create_component_input))
+
+ resolve(fn %{input: input}, _ ->
+ input
+ |> Map.put_new(:creation_date, DateTime.utc_now() |> to_string())
+ |> Components.create_component()
+ end)
+
+ middleware(&build_payload/2)
+ end
+ end
+
+ input_object :create_relationship_input do
+ field(:relationship_type, :string)
+
+ field(:source_entity_guids, list_of(non_null(:id)))
+ field(:source_component_guids, list_of(non_null(:id)))
+
+ field(:destination_entity_guids, list_of(non_null(:id)))
+ field(:destination_component_guids, list_of(non_null(:id)))
+ end
+
+ object :relationship_mutations do
+ field :create_relationship, non_null(:relationship_payload) do
+ arg(:input, non_null(:create_relationship_input))
+
+ resolve(fn %{input: input}, _ ->
+ source_entity_guids = Map.get(input, :source_entity_guids, [])
+ source_component_guids = Map.get(input, :source_component_guids, [])
+
+ destination_entity_guids = Map.get(input, :destination_entity_guids, [])
+ destination_component_guids = Map.get(input, :destination_component_guids, [])
+
+ input =
+ Map.drop(input, [
+ :source_entity_guids,
+ :source_component_guids,
+ :destination_entity_guids,
+ :destination_component_guids
+ ])
+
+ Relationships.create_relationship(
+ input,
+ source_entity_guids,
+ source_component_guids,
+ destination_entity_guids,
+ destination_component_guids
+ )
+ end)
+
+ middleware(&build_payload/2)
+ end
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/absinthe_schema/queries.ex b/hok_ecs/lib/hok_ecs_web/absinthe_schema/queries.ex
new file mode 100644
index 0000000..9833ada
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/absinthe_schema/queries.ex
@@ -0,0 +1,106 @@
+defmodule HokEcsWeb.AbsintheSchema.Queries do
+ use Absinthe.Schema.Notation
+
+ alias HokEcs.Entities
+ alias HokEcs.Components
+ alias HokEcs.Relationships
+ alias HokEcs.Helpers
+
+ import HokEcsWeb.AbsintheSchema.Helpers
+
+ object :entity_queries do
+ field :entity, :entity do
+ arg(:entity_guid, non_null(:id))
+
+ resolve(fn %{entity_guid: entity_guid}, _ ->
+ entity_guid
+ |> Entities.get_entity()
+ |> Helpers.ok()
+ end)
+ end
+
+ field :entities, non_null_list(:entity) do
+ arg(:entity_classification, :string)
+
+ resolve(fn args, _ ->
+ args
+ |> Entities.list_entities()
+ |> Helpers.ok()
+ end)
+ end
+
+ field :entity_component_types, non_null_list(:string) do
+ arg(:entity_guid, non_null(:id))
+
+ resolve(fn %{entity_guid: entity_guid}, _ ->
+ entity_guid
+ |> Components.get_entity_component_types()
+ |> Helpers.ok()
+ end)
+ end
+ end
+
+ object :component_queries do
+ field :component, :component do
+ arg(:component_guid, non_null(:id))
+
+ resolve(fn %{component_guid: component_guid}, _ ->
+ component_guid
+ |> Components.get_component()
+ |> Helpers.ok()
+ end)
+ end
+
+ field :components, non_null_list(:component) do
+ arg(:component_type, :string)
+ arg(:entity_classification, :string)
+ arg(:entity_guid, :id)
+
+ resolve(fn args, _ ->
+ args
+ |> Components.list_components()
+ |> Helpers.ok()
+ end)
+ end
+ end
+
+ object :relationship_queries do
+ field :relationship, :relationship do
+ arg(:relationship_guid, non_null(:id))
+
+ resolve(fn %{relationship_guid: relationship_guid}, _ ->
+ relationship_guid
+ |> Relationships.get_relationship()
+ |> Helpers.ok()
+ end)
+ end
+
+ field :relationships, non_null_list(:relationship) do
+ arg(:source_entity_guids, list_of(non_null(:id)))
+ arg(:source_component_guids, list_of(non_null(:id)))
+ arg(:destination_entity_guids, list_of(non_null(:id)))
+ arg(:destination_component_guids, list_of(non_null(:id)))
+
+ resolve(fn args, _ ->
+ source_entity_guids = Map.get(args, :source_entity_guids, [])
+ source_component_guids = Map.get(args, :source_component_guids, [])
+ destination_entity_guids = Map.get(args, :destination_entity_guids, [])
+ destination_component_guids = Map.get(args, :destination_component_guids, [])
+
+ Relationships.get_relationships_by_member_ids(
+ source_entity_guids,
+ source_component_guids,
+ destination_entity_guids,
+ destination_component_guids
+ )
+ |> Helpers.ok()
+ end)
+ end
+ end
+
+ object :graph_queries do
+ field :graph, non_null(:graph) do
+ resolve_constant(%{})
+ end
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/absinthe_schema/types.ex b/hok_ecs/lib/hok_ecs_web/absinthe_schema/types.ex
new file mode 100644
index 0000000..e9f7b3b
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/absinthe_schema/types.ex
@@ -0,0 +1,85 @@
+defmodule HokEcsWeb.AbsintheSchema.Types do
+ use Absinthe.Schema.Notation
+
+ alias HokEcsWeb.AbsintheSchema
+
+ alias HokEcs.Graph
+ alias HokEcs.Helpers
+
+ import AbsintheSchema.Helpers
+
+ scalar :json do
+ parse(fn input ->
+ case Jason.decode(input.value) do
+ {:ok, result} -> {:ok, result}
+ _ -> :error
+ end
+ end)
+
+ serialize(& &1)
+ end
+
+ object :entity do
+ field :entity_guid, non_null(:id)
+ field :classification_reference, :string
+ field :entity_classification, non_null(:string)
+ field :context, non_null(:string)
+ field :context_id, non_null(:string)
+ field :creation_date, non_null(:string)
+ end
+
+ object :component do
+ field :component_guid, non_null(:id)
+ field :entity_guid, non_null(:id)
+ field :entity_classification, :string
+ field :context, non_null(:string)
+ field :component_name, :string
+ field :component_id, :string
+ field :component_type, non_null(:string)
+ field :component_type_reference, non_null(:string)
+ field :component_type_payload, non_null(:string)
+ field :owner, non_null(:string)
+ field :version, non_null(:string)
+ field :status, :string
+ field :payload, non_null(:json)
+ end
+
+ object :relationship do
+ field :relationship_guid, non_null(:id)
+ field :relationship_name, :string
+ field :relationship_type, :string
+ field :relationship_type_reference, :string
+ field :active, non_null(:boolean)
+ end
+
+ enum :node_type do
+ value(:entity, as: "entity")
+ value(:component, as: "component")
+ value(:relationship, as: "relationship")
+ end
+
+ object :graph do
+ field :nodes, non_null_list(:node),
+ resolve: fn _, _ ->
+ Graph.get_nodes()
+ |> Helpers.ok()
+ end
+
+ field :edges, non_null_list(:edge),
+ resolve: fn _, _ ->
+ Graph.get_edges()
+ |> Helpers.ok()
+ end
+ end
+
+ object :node do
+ field :id, non_null(:id), resolve: fn %{node_id: node_id}, _, _ -> {:ok, node_id} end
+ field :type, non_null(:node_type)
+ field :label, non_null(:string)
+ end
+
+ object :edge do
+ field :from, non_null(:id), resolve: fn %{from_id: from_id}, _, _ -> {:ok, from_id} end
+ field :to, non_null(:id), resolve: fn %{to_id: to_id}, _, _ -> {:ok, to_id} end
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/controllers/fallback_controller.ex b/hok_ecs/lib/hok_ecs_web/controllers/fallback_controller.ex
new file mode 100644
index 0000000..bb6628e
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/controllers/fallback_controller.ex
@@ -0,0 +1,24 @@
+defmodule HokEcsWeb.FallbackController do
+ @moduledoc """
+ Translates controller action results into valid `Plug.Conn` responses.
+
+ See `Phoenix.Controller.action_fallback/1` for more details.
+ """
+ use HokEcsWeb, :controller
+
+ # This clause is an example of how to handle resources that cannot be found.
+ def call(conn, {:error, :not_found}) do
+ conn
+ |> put_status(:not_found)
+ |> put_view(HokEcsWeb.ErrorView)
+ |> render(:"404")
+ end
+
+ def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
+ # FIXME
+ conn
+ |> put_status(:unprocessable_entity)
+ |> put_view(HokEcsWeb.ErrorView)
+ |> render("422.json", changeset: changeset)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/controllers/page_controller.ex b/hok_ecs/lib/hok_ecs_web/controllers/page_controller.ex
new file mode 100644
index 0000000..4d3399b
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/controllers/page_controller.ex
@@ -0,0 +1,7 @@
+defmodule HokEcsWeb.PageController do
+ use HokEcsWeb, :controller
+
+ def app_redirect(conn, _params) do
+ redirect(conn, to: "/app")
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/controllers/webapp_controller.ex b/hok_ecs/lib/hok_ecs_web/controllers/webapp_controller.ex
new file mode 100644
index 0000000..21af0e5
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/controllers/webapp_controller.ex
@@ -0,0 +1,28 @@
+defmodule HokEcsWeb.WebappController do
+ use HokEcsWeb, :controller
+
+ @cache :webapp_cache
+
+ def index(conn, _params) do
+ conn
+ |> send_resp(200, render_react_app())
+ end
+
+ defp render_react_app() do
+ case Cachex.get(@cache, "html") do
+ {:ok, nil} ->
+ html = read_html()
+ Cachex.put(@cache, "html", html)
+
+ html
+
+ {:ok, html} ->
+ html
+ end
+ end
+
+ defp read_html() do
+ Application.app_dir(:hok_ecs, "priv/static/webapp/index.html")
+ |> File.read!()
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/endpoint.ex b/hok_ecs/lib/hok_ecs_web/endpoint.ex
new file mode 100644
index 0000000..082fdcf
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/endpoint.ex
@@ -0,0 +1,50 @@
+defmodule HokEcsWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :hok_ecs
+
+ # The session will be stored in the cookie and signed,
+ # this means its contents can be read but not tampered with.
+ # Set :encryption_salt if you would also like to encrypt it.
+ @session_options [
+ store: :cookie,
+ key: "_hok_ecs_key",
+ signing_salt: "N8a1NRj9"
+ ]
+
+ socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
+
+ # Serve at "/" the static files from "priv/static" directory.
+ #
+ # You should set gzip to true if you are running phx.digest
+ # when deploying your static files in production.
+ plug Plug.Static,
+ at: "/",
+ from: :hok_ecs,
+ gzip: Mix.env() == :prod,
+ only: ~w(assets fonts images webapp favicon.ico robots.txt)
+
+ # Code reloading can be explicitly enabled under the
+ # :code_reloader configuration of your endpoint.
+ if code_reloading? do
+ socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
+ plug Phoenix.LiveReloader
+ plug Phoenix.CodeReloader
+ plug Phoenix.Ecto.CheckRepoStatus, otp_app: :hok_ecs
+ end
+
+ plug Phoenix.LiveDashboard.RequestLogger,
+ param_key: "request_logger",
+ cookie_key: "request_logger"
+
+ plug Plug.RequestId
+ plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
+
+ plug Plug.Parsers,
+ parsers: [:urlencoded, :multipart, :json],
+ pass: ["*/*"],
+ json_decoder: Phoenix.json_library()
+
+ plug Plug.MethodOverride
+ plug Plug.Head
+ plug Plug.Session, @session_options
+ plug HokEcsWeb.Router
+end
diff --git a/hok_ecs/lib/hok_ecs_web/router.ex b/hok_ecs/lib/hok_ecs_web/router.ex
new file mode 100644
index 0000000..c9bfc75
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/router.ex
@@ -0,0 +1,51 @@
+defmodule HokEcsWeb.Router do
+ use HokEcsWeb, :router
+
+ pipeline :browser do
+ plug :accepts, ["html"]
+ plug :fetch_session
+ plug :fetch_live_flash
+ plug :put_root_layout, {HokEcsWeb.LayoutView, :root}
+ plug :protect_from_forgery
+ plug :put_secure_browser_headers
+ end
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/", HokEcsWeb do
+ pipe_through :browser
+
+ get "/", PageController, :app_redirect
+ end
+
+ forward "/api/graphql", Absinthe.Plug, schema: HokEcsWeb.AbsintheSchema
+
+ forward "/api/graphiql",
+ Absinthe.Plug.GraphiQL,
+ schema: HokEcsWeb.AbsintheSchema,
+ interface: :advanced
+
+ scope "/app", HokEcsWeb do
+ get "/", WebappController, :index
+ get "/*path", WebappController, :index
+ end
+
+ # Enables LiveDashboard only for development
+ #
+ # If you want to use the LiveDashboard in production, you should put
+ # it behind authentication and allow only admins to access it.
+ # If your application does not have an admins-only section yet,
+ # you can use Plug.BasicAuth to set up some basic authentication
+ # as long as you are also using SSL (which you should anyway).
+ if Mix.env() in [:dev, :test] do
+ import Phoenix.LiveDashboard.Router
+
+ scope "/" do
+ pipe_through :browser
+
+ live_dashboard "/dashboard", metrics: HokEcsWeb.Telemetry
+ end
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/telemetry.ex b/hok_ecs/lib/hok_ecs_web/telemetry.ex
new file mode 100644
index 0000000..17cad18
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/telemetry.ex
@@ -0,0 +1,71 @@
+defmodule HokEcsWeb.Telemetry do
+ use Supervisor
+ import Telemetry.Metrics
+
+ def start_link(arg) do
+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
+ end
+
+ @impl true
+ def init(_arg) do
+ children = [
+ # Telemetry poller will execute the given period measurements
+ # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
+ {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
+ # Add reporters as children of your supervision tree.
+ # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
+ ]
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+
+ def metrics do
+ [
+ # Phoenix Metrics
+ summary("phoenix.endpoint.stop.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.stop.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+
+ # Database Metrics
+ summary("hok_ecs.repo.query.total_time",
+ unit: {:native, :millisecond},
+ description: "The sum of the other measurements"
+ ),
+ summary("hok_ecs.repo.query.decode_time",
+ unit: {:native, :millisecond},
+ description: "The time spent decoding the data received from the database"
+ ),
+ summary("hok_ecs.repo.query.query_time",
+ unit: {:native, :millisecond},
+ description: "The time spent executing the query"
+ ),
+ summary("hok_ecs.repo.query.queue_time",
+ unit: {:native, :millisecond},
+ description: "The time spent waiting for a database connection"
+ ),
+ summary("hok_ecs.repo.query.idle_time",
+ unit: {:native, :millisecond},
+ description:
+ "The time the connection spent waiting before being checked out for the query"
+ ),
+
+ # VM Metrics
+ summary("vm.memory.total", unit: {:byte, :kilobyte}),
+ summary("vm.total_run_queue_lengths.total"),
+ summary("vm.total_run_queue_lengths.cpu"),
+ summary("vm.total_run_queue_lengths.io")
+ ]
+ end
+
+ defp periodic_measurements do
+ [
+ # A module, function and arguments to be invoked periodically.
+ # This function must call :telemetry.execute/3 and a metric must be added above.
+ # {HokEcsWeb, :count_users, []}
+ ]
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/templates/layout/app.html.heex b/hok_ecs/lib/hok_ecs_web/templates/layout/app.html.heex
new file mode 100644
index 0000000..169aed9
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/templates/layout/app.html.heex
@@ -0,0 +1,5 @@
+
+ <%= get_flash(@conn, :info) %>
+ <%= get_flash(@conn, :error) %>
+ <%= @inner_content %>
+
diff --git a/hok_ecs/lib/hok_ecs_web/templates/layout/live.html.heex b/hok_ecs/lib/hok_ecs_web/templates/layout/live.html.heex
new file mode 100644
index 0000000..a29d604
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/templates/layout/live.html.heex
@@ -0,0 +1,11 @@
+
+ <%= live_flash(@flash, :info) %>
+
+ <%= live_flash(@flash, :error) %>
+
+ <%= @inner_content %>
+
diff --git a/hok_ecs/lib/hok_ecs_web/templates/layout/root.html.heex b/hok_ecs/lib/hok_ecs_web/templates/layout/root.html.heex
new file mode 100644
index 0000000..217ff6a
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/templates/layout/root.html.heex
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ <%= live_title_tag assigns[:page_title] || "HokEcs", suffix: " · Phoenix Framework" %>
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= @inner_content %>
+
+
diff --git a/hok_ecs/lib/hok_ecs_web/views/changeset_view.ex b/hok_ecs/lib/hok_ecs_web/views/changeset_view.ex
new file mode 100644
index 0000000..96b6f17
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/views/changeset_view.ex
@@ -0,0 +1,19 @@
+defmodule HokEcsWeb.ChangesetView do
+ use HokEcsWeb, :view
+
+ @doc """
+ Traverses and translates changeset errors.
+
+ See `Ecto.Changeset.traverse_errors/2` and
+ `HokEcsWeb.ErrorHelpers.translate_error/1` for more details.
+ """
+ def translate_errors(changeset) do
+ Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
+ end
+
+ def render("error.json", %{changeset: changeset}) do
+ # When encoded, the changeset returns its errors
+ # as a JSON object. So we just pass it forward.
+ %{errors: translate_errors(changeset)}
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/views/entity_view.ex b/hok_ecs/lib/hok_ecs_web/views/entity_view.ex
new file mode 100644
index 0000000..63f7f79
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/views/entity_view.ex
@@ -0,0 +1,22 @@
+defmodule HokEcsWeb.EntityView do
+ use HokEcsWeb, :view
+ alias HokEcsWeb.EntityView
+
+ def render("index.json", %{entities: entities}) do
+ %{data: render_many(entities, EntityView, "entity.json")}
+ end
+
+ def render("show.json", %{entity: entity}) do
+ %{data: render_one(entity, EntityView, "entity.json")}
+ end
+
+ def render("entity.json", %{entity: entity}) do
+ %{
+ entity_guid: entity.entity_guid,
+ classification: entity.classification,
+ classification_reference: entity.classification_reference,
+ context: entity.context,
+ creation_date: entity.creation_date
+ }
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/views/error_helpers.ex b/hok_ecs/lib/hok_ecs_web/views/error_helpers.ex
new file mode 100644
index 0000000..5a3c9df
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/views/error_helpers.ex
@@ -0,0 +1,30 @@
+defmodule HokEcsWeb.ErrorHelpers do
+ @moduledoc """
+ Conveniences for translating and building error messages.
+ """
+
+ use Phoenix.HTML
+
+ @doc """
+ Generates tag for inlined form input errors.
+ """
+ def error_tag(form, field) do
+ Enum.map(Keyword.get_values(form.errors, field), fn error ->
+ content_tag(:span, translate_error(error),
+ class: "invalid-feedback",
+ phx_feedback_for: input_name(form, field)
+ )
+ end)
+ end
+
+ @doc """
+ Translates an error message.
+ """
+ def translate_error({msg, opts}) do
+ # Because the error messages we show in our forms and APIs
+ # are defined inside Ecto, we need to translate them dynamically.
+ Enum.reduce(opts, msg, fn {key, value}, acc ->
+ String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
+ end)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/views/error_view.ex b/hok_ecs/lib/hok_ecs_web/views/error_view.ex
new file mode 100644
index 0000000..4d8ec87
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/views/error_view.ex
@@ -0,0 +1,26 @@
+defmodule HokEcsWeb.ErrorView do
+ alias HokEcsWeb.ChangesetView
+ use HokEcsWeb, :view
+
+ # If you want to customize a particular status code
+ # for a certain format, you may uncomment below.
+ # def render("500.html", _assigns) do
+ # "Internal Server Error"
+ # end
+
+ def render("422.json", %{changeset: changeset}) do
+ # errors =
+ # changeset
+ # |> Ecto.Changeset.traverse_errors(fn {msg, _} -> msg end)
+
+ # %{errors: errors}
+ render_one(changeset, ChangesetView, "error.json")
+ end
+
+ # By default, Phoenix returns the status message from
+ # the template name. For example, "404.html" becomes
+ # "Not Found".
+ def template_not_found(template, _assigns) do
+ Phoenix.Controller.status_message_from_template(template)
+ end
+end
diff --git a/hok_ecs/lib/hok_ecs_web/views/layout_view.ex b/hok_ecs/lib/hok_ecs_web/views/layout_view.ex
new file mode 100644
index 0000000..b6e2219
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/views/layout_view.ex
@@ -0,0 +1,7 @@
+defmodule HokEcsWeb.LayoutView do
+ use HokEcsWeb, :view
+
+ # Phoenix LiveDashboard is available only in development by default,
+ # so we instruct Elixir to not warn if the dashboard route is missing.
+ @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}}
+end
diff --git a/hok_ecs/lib/hok_ecs_web/views/page_view.ex b/hok_ecs/lib/hok_ecs_web/views/page_view.ex
new file mode 100644
index 0000000..dfc6f47
--- /dev/null
+++ b/hok_ecs/lib/hok_ecs_web/views/page_view.ex
@@ -0,0 +1,3 @@
+defmodule HokEcsWeb.PageView do
+ use HokEcsWeb, :view
+end
diff --git a/hok_ecs/lib/mix/tasks/webapp.ex b/hok_ecs/lib/mix/tasks/webapp.ex
new file mode 100644
index 0000000..cc7a83a
--- /dev/null
+++ b/hok_ecs/lib/mix/tasks/webapp.ex
@@ -0,0 +1,28 @@
+defmodule Mix.Tasks.Webapp do
+ @moduledoc """
+ React frontend compilation and bundling for production.
+ """
+
+ use Mix.Task
+ require Logger
+
+ # Path for the frontend static assets that are being served
+ # from our Phoenix router when accessing /app/* for the first time
+ @public_path "./priv/static/webapp"
+
+ @shortdoc "Compile and bundle React frontend for production"
+ def run(_) do
+ Logger.info("📦 - Installing NPM packages")
+ System.cmd("npm", ["install", "--quiet"], cd: "./frontend")
+
+ Logger.info("⚙️ - Compiling React frontend")
+ System.cmd("npm", ["run", "build"], cd: "./frontend")
+
+ Logger.info("🚛 - Moving dist folder to Phoenix at #{@public_path}")
+ # First clean up any stale files from previous builds if any
+ System.cmd("rm", ["-rf", @public_path])
+ System.cmd("cp", ["-R", "./frontend/dist", @public_path])
+
+ Logger.info("⚛️ - React frontend ready.")
+ end
+end
diff --git a/hok_ecs/mix.exs b/hok_ecs/mix.exs
new file mode 100644
index 0000000..fd2c5a7
--- /dev/null
+++ b/hok_ecs/mix.exs
@@ -0,0 +1,101 @@
+defmodule HokEcs.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :hok_ecs,
+ version: "0.1.0",
+ elixir: "~> 1.12",
+ elixirc_paths: elixirc_paths(Mix.env()),
+ compilers: Mix.compilers(),
+ start_permanent: Mix.env() == :prod,
+ aliases: aliases(),
+ deps: deps(),
+ docs: [
+ groups_for_modules: [
+ Entities: ~r/Entities/,
+ Components: ~r/Components/,
+ Relationships: ~r/Relationships/,
+ Layers: ~r/Layers/,
+ Scenes: ~r/Scenes/,
+ Events: ~r/Events/,
+ Web: ~r/Web/
+ ]
+ ],
+ test_coverage: [
+ tool: ExCoveralls
+ ],
+ preferred_cli_env: [
+ coveralls: :test,
+ "coveralls.detail": :test,
+ "coveralls.post": :test,
+ "coveralls.html": :test
+ ],
+ dialyzer: [plt_add_apps: [:mix]]
+ ]
+ end
+
+ # Configuration for the OTP application.
+ #
+ # Type `mix help compile.app` for more information.
+ def application do
+ [
+ mod: {HokEcs.Application, []},
+ extra_applications: [:logger, :runtime_tools, :os_mon]
+ ]
+ end
+
+ # Specifies which paths to compile per environment.
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ # Specifies your project dependencies.
+ #
+ # Type `mix help deps` for examples and options.
+ defp deps do
+ [
+ {:phoenix, "~> 1.6.15"},
+ {:phoenix_ecto, "~> 4.4"},
+ {:ecto_sql, "~> 3.6"},
+ {:postgrex, ">= 0.0.0"},
+ {:phoenix_html, "~> 3.0"},
+ {:phoenix_live_reload, "~> 1.2", only: :dev},
+ {:phoenix_live_view, "~> 0.17.5"},
+ {:floki, ">= 0.30.0", only: :test},
+ {:phoenix_live_dashboard, "~> 0.6"},
+ {:esbuild, "~> 0.4", runtime: Mix.env() == :dev},
+ {:telemetry_metrics, "~> 0.6"},
+ {:telemetry_poller, "~> 1.0"},
+ {:jason, "~> 1.2"},
+ {:plug_cowboy, "~> 2.5"},
+ {:ex_json_schema, "~> 0.9.2"},
+ {:ecto_psql_extras, "~> 0.6", only: :dev},
+ {:ex_doc, "~> 0.27", only: :dev, runtime: false},
+ {:excoveralls, "~> 0.10", only: :test},
+ {:dialyxir, "~> 1.0", only: [:dev], runtime: false},
+ {:typed_ecto_schema, "~> 0.4.1", runtime: false},
+ {:credo, "~> 1.6", only: [:dev, :test], runtime: false},
+ {:absinthe, "~> 1.7"},
+ {:absinthe_plug, "~> 1.5"},
+ {:absinthe_error_payload, "~> 1.0"},
+ {:cachex, "~> 3.6"}
+ ]
+ end
+
+ # Aliases are shortcuts or tasks specific to the current project.
+ # For example, to install project dependencies and perform other setup tasks, run:
+ #
+ # $ mix setup
+ #
+ # See the documentation for `Mix` for more info on aliases.
+ defp aliases do
+ [
+ setup: ["deps.get", "ecto.setup"],
+ "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
+ "ecto.reset": ["ecto.drop", "ecto.setup"],
+ test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
+ "assets.deploy": ["esbuild default --minify", "phx.digest"],
+ seed: ["run priv/repo/seeds.exs"]
+ ]
+ end
+end
diff --git a/hok_ecs/mix.lock b/hok_ecs/mix.lock
new file mode 100644
index 0000000..0303ec7
--- /dev/null
+++ b/hok_ecs/mix.lock
@@ -0,0 +1,69 @@
+%{
+ "absinthe": {:hex, :absinthe, "1.7.1", "aca6f64994f0914628429ddbdfbf24212747b51780dae189dd98909da911757b", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c0c4dbd93881fa3bfbad255608234b104b877c2a901850c1fe8c53b408a72a57"},
+ "absinthe_error_payload": {:hex, :absinthe_error_payload, "1.1.4", "502ff239148c8deaac028ddb600d6502d5be68d24fece0c93f4c3cf7e74c1a4d", [:make, :mix], [{:absinthe, "~> 1.3", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "9e262ef2fd4a2c644075e0cdde2573b1f713c0676ab905c8640eaa8a882b2aca"},
+ "absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"},
+ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
+ "bureaucrat": {:hex, :bureaucrat, "0.2.9", "d98e4d2b9bdbf22e4a45c2113ce8b38b5b63278506c6ff918e3b943a4355d85b", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "111c8dd84382a62e1026ae011d592ceee918553e5203fe8448d9ba6ccbdfff7d"},
+ "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
+ "castore": {:hex, :castore, "1.0.0", "c25cd0794c054ebe6908a86820c8b92b5695814479ec95eeff35192720b71eec", [:mix], [], "hexpm", "577d0e855983a97ca1dfa33cbb8a3b6ece6767397ffb4861514343b078fc284b"},
+ "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
+ "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
+ "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
+ "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
+ "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
+ "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"},
+ "db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
+ "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
+ "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.30", "0b938aa5b9bafd455056440cdaa2a79197ca5e693830b4a982beada840513c5f", [:mix], [], "hexpm", "3b5385c2d36b0473d0b206927b841343d25adb14f95f0110062506b300cd5a1b"},
+ "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"},
+ "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.10", "e14d400930f401ca9f541b3349212634e44027d7f919bbb71224d7ac0d0e8acd", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15.7 or ~> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "505e8cd81e4f17c090be0f99e92b1b3f0fd915f98e76965130b8ccfb891e7088"},
+ "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
+ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
+ "esbuild": {:hex, :esbuild, "0.6.0", "9ba6ead054abd43cb3d7b14946a0cdd1493698ccd8e054e0e5d6286d7f0f509c", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "30f9a05d4a5bab0d3e37398f312f80864e1ee1a081ca09149d06d474318fd040"},
+ "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
+ "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
+ "ex_json_schema": {:hex, :ex_json_schema, "0.9.2", "c9a42e04e70cd70eb11a8903a22e8ec344df16edef4cb8e6ec84ed0caffc9f0f", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "4854329cb352b6c01c4c4b8dbfb3be14dc5bea19ea13e0eafade4ff22ba55224"},
+ "excoveralls": {:hex, :excoveralls, "0.15.3", "54bb54043e1cf5fe431eb3db36b25e8fd62cf3976666bafe491e3fa5e29eba47", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f8eb5d8134d84c327685f7bb8f1db4147f1363c3c9533928234e496e3070114e"},
+ "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
+ "floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"},
+ "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
+ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
+ "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
+ "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
+ "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
+ "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
+ "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
+ "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
+ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
+ "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
+ "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
+ "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
+ "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
+ "phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
+ "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.12", "74f4c0ad02d7deac2d04f50b52827a5efdc5c6e7fac5cede145f5f0e4183aedc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "af6dd5e0aac16ff43571f527a8e0616d62cb80b10eb87aac82170243e50d99c8"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
+ "phoenix_swagger": {:hex, :phoenix_swagger, "0.8.2", "cc49d9641d7e7c87766ba800110ff67d2fb55379f83982ee33d85d1e0b39d100", [:mix], [{:ex_json_schema, "~> 0.6", [hex: :ex_json_schema, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "e6d177764d75d388b199a863c5f7502ac8c202cd3fca61220807cbdcb31efef2"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
+ "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
+ "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
+ "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
+ "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
+ "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
+ "redbug": {:hex, :redbug, "1.2.2", "366d8961770ddc7bb5d209fbadddfa7271005487f938c087a0e385a57abfee33", [:rebar3], [], "hexpm", "b5fe7b94e487be559cb0ec1c0e938c9761205d3e91a96bf263bdf1beaebea729"},
+ "rexbug": {:hex, :rexbug, "1.0.6", "024071c67d970151fbdc06f299faf8db3e1b2ac759a28623a9cc80a517fc74f2", [:mix], [{:mix_test_watch, ">= 0.5.0", [hex: :mix_test_watch, repo: "hexpm", optional: true]}, {:redbug, "~> 1.2", [hex: :redbug, repo: "hexpm", optional: false]}], "hexpm", "148ea724979413e9fd84ca3b4bb5d2d8b840ac481adfd645f5846fda409a642c"},
+ "sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"},
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
+ "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
+ "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
+ "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
+ "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.1", "a373ca6f693f4de84cde474a67467a9cb9051a8a7f3f615f1e23dc74b75237fa", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "85c6962f79d35bf543dd5659c6adc340fd2480cacc6f25d2cc2933ea6e8fcb3b"},
+ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
+ "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
+}
diff --git a/hok_ecs/priv/repo/migrations/.formatter.exs b/hok_ecs/priv/repo/migrations/.formatter.exs
new file mode 100644
index 0000000..49f9151
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/.formatter.exs
@@ -0,0 +1,4 @@
+[
+ import_deps: [:ecto_sql],
+ inputs: ["*.exs"]
+]
diff --git a/hok_ecs/priv/repo/migrations/20230206140427_create_entities.exs b/hok_ecs/priv/repo/migrations/20230206140427_create_entities.exs
new file mode 100644
index 0000000..0866d8f
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206140427_create_entities.exs
@@ -0,0 +1,16 @@
+defmodule HokEcs.Repo.Migrations.CreateEntities do
+ use Ecto.Migration
+
+ def change do
+ create table(:entities, primary_key: false) do
+ add :entity_guid, :binary_id, primary_key: true
+ add :context, :text, null: false
+ add :context_id, :text, null: false
+ add :entity_classification, :text, null: false
+ add :entity_classification_reference, :text
+ add :creation_date, :text, null: false
+
+ timestamps()
+ end
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206141203_create_components.exs b/hok_ecs/priv/repo/migrations/20230206141203_create_components.exs
new file mode 100644
index 0000000..70e58af
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206141203_create_components.exs
@@ -0,0 +1,35 @@
+defmodule HokEcs.Repo.Migrations.CreateComponents do
+ use Ecto.Migration
+
+ def change do
+ create table(:components, primary_key: false) do
+ add :component_guid, :binary_id, primary_key: true
+ add :component_sequence_name, :text
+ add :component_sequence_value, :text
+ add :context, :text, null: false
+ add :context_id, :text, null: false
+ add :component_name, :text
+ add :component_id, :text
+ add :entity_classification, :text
+ add :component_type, :text, null: false
+ add :component_type_reference, :text, null: false
+ add :component_type_payload, :text, null: false
+ add :owner, :text, null: false
+ add :version, :string, null: false
+ add :status, :text
+ add :active, :boolean, null: false, default: true
+ add :creation_date, :text, null: false
+ add :authoring_application, :text
+ add :hash1, :text
+ add :payload, :map, null: false, default: %{}
+
+ add :entity_guid,
+ references(:entities, column: :entity_guid, on_delete: :nothing, type: :binary_id),
+ null: false
+
+ timestamps()
+ end
+
+ create index(:components, [:entity_guid])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206142313_create_component_schemas.exs b/hok_ecs/priv/repo/migrations/20230206142313_create_component_schemas.exs
new file mode 100644
index 0000000..ea51199
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206142313_create_component_schemas.exs
@@ -0,0 +1,15 @@
+defmodule HokEcs.Repo.Migrations.CreateComponentSchemas do
+ use Ecto.Migration
+
+ def change do
+ create table(:component_schemas, primary_key: false) do
+ add :component_schema_guid, :binary_id, primary_key: true
+ add :name, :string, null: false
+ add :schema, :map, null: false
+
+ timestamps()
+ end
+
+ create unique_index(:component_schemas, [:name])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206142543_add_component_schema_to_components.exs b/hok_ecs/priv/repo/migrations/20230206142543_add_component_schema_to_components.exs
new file mode 100644
index 0000000..0f8c469
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206142543_add_component_schema_to_components.exs
@@ -0,0 +1,16 @@
+defmodule HokEcs.Repo.Migrations.AddComponentSchemaToComponents do
+ use Ecto.Migration
+
+ def change do
+ alter table(:components) do
+ add :component_schema_guid,
+ references(:component_schemas,
+ column: :component_schema_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ )
+ end
+
+ create index(:components, [:component_schema_guid])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206161817_create_events.exs b/hok_ecs/priv/repo/migrations/20230206161817_create_events.exs
new file mode 100644
index 0000000..839f236
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206161817_create_events.exs
@@ -0,0 +1,26 @@
+defmodule HokEcs.Repo.Migrations.CreateEvents do
+ use Ecto.Migration
+
+ def change do
+ create table(:events, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+ add :type, :string, null: false
+ add :data, :map, null: false
+
+ add :component_guid,
+ references(:components, column: :component_guid, on_delete: :nothing, type: :binary_id)
+
+ add :entity_guid,
+ references(:entities, column: :entity_guid, on_delete: :nothing, type: :binary_id)
+
+ timestamps()
+ end
+
+ create index(:events, [:component_guid])
+ create index(:events, [:entity_guid])
+
+ create constraint(:events, :valid_fkey,
+ check: "(component_guid IS NULL) != (entity_guid IS NULL)"
+ )
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206174903_create_relationships.exs b/hok_ecs/priv/repo/migrations/20230206174903_create_relationships.exs
new file mode 100644
index 0000000..ae467f9
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206174903_create_relationships.exs
@@ -0,0 +1,17 @@
+defmodule HokEcs.Repo.Migrations.CreateRelationships do
+ use Ecto.Migration
+
+ def change do
+ create table(:relationships, primary_key: false) do
+ add :relationship_guid, :binary_id, primary_key: true
+ add :context, :text
+ add :relationship_name, :text
+ add :relationship_type, :text
+ add :relationship_type_reference, :text
+ add :active, :boolean
+ add :version, :integer
+
+ timestamps()
+ end
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206175109_create_relationship_source_entities.exs b/hok_ecs/priv/repo/migrations/20230206175109_create_relationship_source_entities.exs
new file mode 100644
index 0000000..6c9cb97
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206175109_create_relationship_source_entities.exs
@@ -0,0 +1,28 @@
+defmodule HokEcs.Repo.Migrations.CreateRelationshipSourceEntities do
+ use Ecto.Migration
+
+ def change do
+ create table(:relationship_source_entities, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+
+ add :relationship_guid,
+ references(:relationships,
+ column: :relationship_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ add :entity_guid,
+ references(:entities, column: :entity_guid, on_delete: :nothing, type: :binary_id),
+ null: false
+
+ timestamps()
+ end
+
+ create index(:relationship_source_entities, [:relationship_guid])
+ create index(:relationship_source_entities, [:entity_guid])
+
+ create unique_index(:relationship_source_entities, [:relationship_guid, :entity_guid])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206175226_create_relationship_source_components.exs b/hok_ecs/priv/repo/migrations/20230206175226_create_relationship_source_components.exs
new file mode 100644
index 0000000..8f2a4f5
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206175226_create_relationship_source_components.exs
@@ -0,0 +1,28 @@
+defmodule HokEcs.Repo.Migrations.CreateRelationshipSourceComponents do
+ use Ecto.Migration
+
+ def change do
+ create table(:relationship_source_components, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+
+ add :relationship_guid,
+ references(:relationships,
+ column: :relationship_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ add :component_guid,
+ references(:components, column: :component_guid, on_delete: :nothing, type: :binary_id),
+ null: false
+
+ timestamps()
+ end
+
+ create index(:relationship_source_components, [:relationship_guid])
+ create index(:relationship_source_components, [:component_guid])
+
+ create unique_index(:relationship_source_components, [:relationship_guid, :component_guid])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206175253_create_relationship_destination_entities.exs b/hok_ecs/priv/repo/migrations/20230206175253_create_relationship_destination_entities.exs
new file mode 100644
index 0000000..82019dd
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206175253_create_relationship_destination_entities.exs
@@ -0,0 +1,28 @@
+defmodule HokEcs.Repo.Migrations.CreateRelationshipDestinationEntities do
+ use Ecto.Migration
+
+ def change do
+ create table(:relationship_destination_entities, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+
+ add :relationship_guid,
+ references(:relationships,
+ column: :relationship_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ add :entity_guid,
+ references(:entities, column: :entity_guid, on_delete: :nothing, type: :binary_id),
+ null: false
+
+ timestamps()
+ end
+
+ create index(:relationship_destination_entities, [:relationship_guid])
+ create index(:relationship_destination_entities, [:entity_guid])
+
+ create unique_index(:relationship_destination_entities, [:relationship_guid, :entity_guid])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206175329_create_relationship_destination_components.exs b/hok_ecs/priv/repo/migrations/20230206175329_create_relationship_destination_components.exs
new file mode 100644
index 0000000..a9221cf
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206175329_create_relationship_destination_components.exs
@@ -0,0 +1,31 @@
+defmodule HokEcs.Repo.Migrations.CreateRelationshipDestinationComponents do
+ use Ecto.Migration
+
+ def change do
+ create table(:relationship_destination_components, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+
+ add :relationship_guid,
+ references(:relationships,
+ column: :relationship_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ add :component_guid,
+ references(:components, column: :component_guid, on_delete: :nothing, type: :binary_id),
+ null: false
+
+ timestamps()
+ end
+
+ create index(:relationship_destination_components, [:relationship_guid])
+ create index(:relationship_destination_components, [:component_guid])
+
+ create unique_index(:relationship_destination_components, [
+ :relationship_guid,
+ :component_guid
+ ])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206222803_create_layers.exs b/hok_ecs/priv/repo/migrations/20230206222803_create_layers.exs
new file mode 100644
index 0000000..e1c27fe
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206222803_create_layers.exs
@@ -0,0 +1,20 @@
+defmodule HokEcs.Repo.Migrations.CreateLayer do
+ use Ecto.Migration
+
+ def change do
+ create table(:layers, primary_key: false) do
+ add :layer_guid, :binary_id, primary_key: true
+ add :context, :text
+ add :layer_name, :text
+ add :layer_id, :text
+ add :layer_owner, :text
+ add :layer_description, :text
+ add :layer_function, :text
+ add :layer_update_method, :text
+ add :active, :boolean
+ add :version, :integer
+
+ timestamps()
+ end
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206224059_create_layer_entities.exs b/hok_ecs/priv/repo/migrations/20230206224059_create_layer_entities.exs
new file mode 100644
index 0000000..241b4bd
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206224059_create_layer_entities.exs
@@ -0,0 +1,32 @@
+defmodule HokEcs.Repo.Migrations.CreateLayerEntities do
+ use Ecto.Migration
+
+ def change do
+ create table(:layer_entities, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+
+ add :layer_guid,
+ references(:layers,
+ column: :layer_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ add :entity_guid,
+ references(:entities,
+ column: :entity_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ timestamps()
+ end
+
+ create index(:layer_entities, [:layer_guid])
+ create index(:layer_entities, [:entity_guid])
+
+ create unique_index(:layer_entities, [:layer_guid, :entity_guid])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206224402_create_layer_components.exs b/hok_ecs/priv/repo/migrations/20230206224402_create_layer_components.exs
new file mode 100644
index 0000000..19198a3
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206224402_create_layer_components.exs
@@ -0,0 +1,32 @@
+defmodule HokEcs.Repo.Migrations.CreateLayerComponents do
+ use Ecto.Migration
+
+ def change do
+ create table(:layer_components, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+
+ add :layer_guid,
+ references(:layers,
+ column: :layer_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ add :component_guid,
+ references(:components,
+ column: :component_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ timestamps()
+ end
+
+ create index(:layer_components, [:layer_guid])
+ create index(:layer_components, [:component_guid])
+
+ create unique_index(:layer_components, [:layer_guid, :component_guid])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230206224516_create_layer_relationships.exs b/hok_ecs/priv/repo/migrations/20230206224516_create_layer_relationships.exs
new file mode 100644
index 0000000..cdfa5b5
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230206224516_create_layer_relationships.exs
@@ -0,0 +1,32 @@
+defmodule HokEcs.Repo.Migrations.CreateLayerRelationships do
+ use Ecto.Migration
+
+ def change do
+ create table(:layer_relationships, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+
+ add :layer_guid,
+ references(:layers,
+ column: :layer_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ add :relationship_guid,
+ references(:relationships,
+ column: :relationship_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ timestamps()
+ end
+
+ create index(:layer_relationships, [:layer_guid])
+ create index(:layer_relationships, [:relationship_guid])
+
+ create unique_index(:layer_relationships, [:layer_guid, :relationship_guid])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230207203308_create_scenes.exs b/hok_ecs/priv/repo/migrations/20230207203308_create_scenes.exs
new file mode 100644
index 0000000..8e7b5c6
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230207203308_create_scenes.exs
@@ -0,0 +1,17 @@
+defmodule HokEcs.Repo.Migrations.CreateScenes do
+ use Ecto.Migration
+
+ def change do
+ create table(:scenes, primary_key: false) do
+ add :scene_guid, :binary_id, primary_key: true
+ add :context, :text
+ add :scene_name, :text
+ add :scene_id, :text
+ add :active, :boolean
+ add :version, :integer
+ add :date_created, :text
+
+ timestamps()
+ end
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230207203347_create_scene_layers.exs b/hok_ecs/priv/repo/migrations/20230207203347_create_scene_layers.exs
new file mode 100644
index 0000000..a82b334
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230207203347_create_scene_layers.exs
@@ -0,0 +1,32 @@
+defmodule HokEcs.Repo.Migrations.CreateSceneLayers do
+ use Ecto.Migration
+
+ def change do
+ create table(:scene_layers, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+
+ add :scene_guid,
+ references(:scenes,
+ column: :scene_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ add :layer_guid,
+ references(:layers,
+ column: :layer_guid,
+ on_delete: :nothing,
+ type: :binary_id
+ ),
+ null: false
+
+ timestamps()
+ end
+
+ create index(:scene_layers, [:scene_guid])
+ create index(:scene_layers, [:layer_guid])
+
+ create unique_index(:scene_layers, [:scene_guid, :layer_guid])
+ end
+end
diff --git a/hok_ecs/priv/repo/migrations/20230307185138_modify_relationships_active_default.exs b/hok_ecs/priv/repo/migrations/20230307185138_modify_relationships_active_default.exs
new file mode 100644
index 0000000..447944d
--- /dev/null
+++ b/hok_ecs/priv/repo/migrations/20230307185138_modify_relationships_active_default.exs
@@ -0,0 +1,15 @@
+defmodule HokEcs.Repo.Migrations.ModifyRelationshipsActiveDefault do
+ use Ecto.Migration
+
+ def up do
+ alter table(:relationships) do
+ modify :active, :boolean, default: true, null: false
+ end
+ end
+
+ def down do
+ alter table(:relationships) do
+ modify :active, :boolean, default: nil, null: true
+ end
+ end
+end
diff --git a/hok_ecs/priv/repo/seeds.exs b/hok_ecs/priv/repo/seeds.exs
new file mode 100644
index 0000000..670a6fd
--- /dev/null
+++ b/hok_ecs/priv/repo/seeds.exs
@@ -0,0 +1,205 @@
+# Script for populating the database. You can run it as:
+#
+# mix run priv/repo/seeds.exs
+#
+# Inside the script, you can read and write to any of your
+# repositories directly:
+#
+# HokEcs.Repo.insert!(%HokEcs.SomeSchema{})
+#
+# We recommend using the bang functions (`insert!`, `update!`
+# and so on) as they will fail if something goes wrong.
+
+defmodule HokEcs.DatabaseSeeder do
+ alias HokEcs.Components.{Component, ComponentSchema}
+ alias HokEcs.Entities.Entity
+ # alias HokEcs.Relationships.Relationship
+
+ alias HokEcs.Repo
+
+ @component_schemas_path "../json_schema/Entity and Component Definitions/jsonschema"
+ @sample_data_path "../json_schema/Entity and Component Definitions/Sample Data"
+
+ def run do
+ # used to cache inserted component schema guids
+ {:ok, agent} = Agent.start_link(fn -> %{} end)
+
+ create_component_schemas(agent)
+ create_sample_data(agent)
+ end
+
+ def create_component_schemas(agent) do
+ @component_schemas_path
+ |> File.ls!()
+ |> Enum.each(&create_component_schema(&1, agent))
+ end
+
+ def create_component_schema(filename, agent) do
+ schema =
+ @component_schemas_path
+ |> Path.join(filename)
+ |> File.read!()
+ |> Jason.decode!()
+
+ name = Map.fetch!(schema, "title")
+
+ case name do
+ "entity" ->
+ :ok
+
+ "component" ->
+ :ok
+
+ name ->
+ schema =
+ %ComponentSchema{
+ name: name,
+ schema: schema
+ }
+ |> Repo.insert!()
+
+ Agent.update(agent, &Map.put_new(&1, name, schema.component_schema_guid))
+ end
+ end
+
+ def create_sample_data(agent) do
+ File.ls!(@sample_data_path)
+ |> Enum.each(&process_sample_data_dir(&1, agent))
+ end
+
+ def process_sample_data_dir(dir_name, agent) do
+ create_entities(dir_name)
+ create_components(dir_name, agent)
+ end
+
+ def create_entities(dir_name) do
+ [@sample_data_path, dir_name, "*.json"]
+ |> Path.join()
+ |> Path.wildcard()
+ |> Enum.each(&create_entity/1)
+ end
+
+ def create_entity(json_file_path) do
+ json_file_path
+ |> File.read!()
+ |> Jason.decode!()
+ |> create_entity_from_decoded_json()
+ end
+
+ def create_entity_from_decoded_json(entity) do
+ entity_guid = Map.fetch!(entity, "entity_guid")
+ context = Map.fetch!(entity, "context")
+ context_id = Map.fetch!(entity, "context")
+ entity_classification = Map.fetch!(entity, "entity_classification")
+ entity_classification_reference = Map.fetch!(entity, "entity_classification_reference")
+ creation_date = Map.fetch!(entity, "creation_date")
+
+ %Entity{
+ entity_guid: entity_guid,
+ context: context,
+ context_id: context_id,
+ entity_classification: entity_classification,
+ entity_classification_reference: entity_classification_reference,
+ creation_date: creation_date
+ }
+ |> Repo.insert!()
+ end
+
+ def get_component_suffix(filename) do
+ filename
+ |> String.slice(-13, 13)
+ |> String.slice(0, 8)
+ end
+
+ def create_components(dir_name, agent) do
+ [@sample_data_path, dir_name, "components"]
+ |> Path.join()
+ |> File.ls!()
+ |> Enum.group_by(&get_component_suffix/1)
+ |> Map.values()
+ |> Enum.map(&Enum.sort/1)
+ |> Enum.each(&create_component(&1, dir_name, agent))
+ end
+
+ def create_component(filenames, dir_name, agent) do
+ components_dir =
+ [@sample_data_path, dir_name, "components"]
+ |> Path.join()
+
+ [component | rest] =
+ filenames
+ |> Enum.map(fn filename ->
+ components_dir
+ |> Path.join(filename)
+ |> File.read!()
+ |> Jason.decode!()
+ end)
+
+ payload = List.first(rest)
+
+ create_component_from_decoded_json(component, agent, payload)
+ end
+
+ def create_component_from_decoded_json(component, agent, payload \\ %{}) do
+ # required
+ entity_guid = Map.fetch!(component, "entity_guid")
+ component_type = Map.fetch!(component, "component_type")
+ component_type_payload = Map.fetch!(component, "component_type_payload")
+ owner = Map.fetch!(component, "owner")
+ version = Map.fetch!(component, "version")
+ creation_date = Map.fetch!(component, "creation_date")
+ active = Map.fetch!(component, "active")
+
+ # FIXME: these should be required but are missing in the sample data
+ context = Map.get(component, "context", "")
+ context_id = Map.get(component, "context_id", "")
+ component_type_reference = Map.get(component, "component_type_reference", "")
+
+ # optional
+ component_sequence_name = Map.get(component, "component_sequence_name")
+ component_sequence_value = Map.get(component, "component_sequence_value")
+ authoring_application = Map.get(component, "authoring_application")
+ component_id = Map.get(component, "component_id")
+ component_name = Map.get(component, "component_name")
+ hash1 = Map.get(component, "hash1")
+ status = Map.get(component, "status")
+
+ # FIXME: this is missing in the sample data
+ entity_classification = component_type |> String.split(".") |> List.first()
+
+ # schema fkey
+ component_schema_name = component_type
+ component_schema_guid = Agent.get(agent, &Map.fetch!(&1, component_schema_name))
+
+ %Component{
+ entity_guid: entity_guid,
+ context: context,
+ context_id: context_id,
+ component_type: component_type,
+ component_type_reference: component_type_reference,
+ component_type_payload: component_type_payload,
+ owner: owner,
+ version: version,
+ active: active,
+ creation_date: creation_date,
+ component_sequence_name: component_sequence_name,
+ component_sequence_value: component_sequence_value,
+ authoring_application: authoring_application,
+ component_id: component_id,
+ component_name: component_name,
+ entity_classification: entity_classification,
+ hash1: hash1,
+ status: status,
+ component_schema_guid: component_schema_guid,
+ payload: payload
+ }
+ |> Repo.insert!()
+ end
+end
+
+alias HokEcs.DatabaseSeeder
+alias HokEcs.Repo
+
+Repo.transaction(fn ->
+ DatabaseSeeder.run()
+end)
diff --git a/hok_ecs/priv/static/favicon.ico b/hok_ecs/priv/static/favicon.ico
new file mode 100644
index 0000000..73de524
Binary files /dev/null and b/hok_ecs/priv/static/favicon.ico differ
diff --git a/hok_ecs/priv/static/images/phoenix.png b/hok_ecs/priv/static/images/phoenix.png
new file mode 100644
index 0000000..9c81075
Binary files /dev/null and b/hok_ecs/priv/static/images/phoenix.png differ
diff --git a/hok_ecs/priv/static/robots.txt b/hok_ecs/priv/static/robots.txt
new file mode 100644
index 0000000..26e06b5
--- /dev/null
+++ b/hok_ecs/priv/static/robots.txt
@@ -0,0 +1,5 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/hok_ecs/test/hok_ecs/components_test.exs b/hok_ecs/test/hok_ecs/components_test.exs
new file mode 100644
index 0000000..d371ec4
--- /dev/null
+++ b/hok_ecs/test/hok_ecs/components_test.exs
@@ -0,0 +1,336 @@
+defmodule HokEcs.ComponentsTest do
+ use HokEcs.DataCase, async: true
+
+ describe "components" do
+ alias HokEcs.Components
+ alias HokEcs.Components.Component
+ alias HokEcs.Events
+ alias HokEcs.Events.Event
+
+ import HokEcs.ECSFixtures
+
+ @invalid_attrs %{
+ context: nil
+ }
+
+ test "list_components/0 returns all components" do
+ entity = entity_fixture()
+ component = component_fixture(entity)
+ assert Components.list_components() == [component]
+ end
+
+ test "get_component!/1 returns the component with given id" do
+ entity = entity_fixture()
+ component = component_fixture(entity)
+ assert Components.get_component!(component.component_guid) == component
+ end
+
+ test "create_component/1 with valid data creates a component and component_created event" do
+ entity = entity_fixture()
+ component_schema = component_schema_fixture()
+
+ valid_attrs = %{
+ entity_guid: entity.entity_guid,
+ active: true,
+ authoring_application: "some authoring_application",
+ component_id: "some component_id",
+ component_name: "some component_name",
+ component_type_payload: "some component_type_payload",
+ component_type: component_schema.name,
+ component_type_reference: "some component_type_reference",
+ context: "some context",
+ context_id: "some context id",
+ entity_classification: "some entity_classification",
+ hash1: "some hash1",
+ owner: "some owner",
+ payload: %{},
+ status: "some status",
+ version: "42",
+ creation_date: DateTime.utc_now() |> to_string()
+ }
+
+ assert {:ok, %Component{} = component} = Components.create_component(valid_attrs)
+ assert component.active == true
+ assert component.authoring_application == "some authoring_application"
+ assert component.component_id == "some component_id"
+ assert component.component_name == "some component_name"
+ assert component.component_type_payload == "some component_type_payload"
+ assert component.component_type == component_schema.name
+ assert component.component_type_reference == "some component_type_reference"
+ assert component.context == "some context"
+ assert component.entity_classification == "some entity_classification"
+ assert component.hash1 == "some hash1"
+ assert component.owner == "some owner"
+ assert component.payload == %{}
+ assert component.status == "some status"
+ assert component.version == "42"
+
+ event = Repo.get_by(Event, component_guid: component.component_guid)
+
+ event_data =
+ valid_attrs |> Enum.map(fn {key, value} -> {to_string(key), value} end) |> Enum.into(%{})
+
+ assert %{
+ type: "component_created",
+ data: ^event_data
+ } = event
+ end
+
+ test "create_component/2 with valid payload creates the component and component_created event" do
+ entity = entity_fixture()
+ component_schema = component_schema_fixture()
+
+ attrs = %{
+ entity_guid: entity.entity_guid,
+ context: "some context",
+ context_id: "some context id",
+ component_type: component_schema.name,
+ component_type_reference: "some component type reference",
+ component_type_payload: "json",
+ payload: %{"FirstName" => "Bob"},
+ owner: "some owner",
+ version: "some version",
+ creation_date: DateTime.utc_now() |> to_string()
+ }
+
+ assert {:ok, %Component{} = component} = Components.create_component(attrs)
+ assert component.entity_guid == entity.entity_guid
+ assert component.component_schema_guid == component_schema.component_schema_guid
+
+ assert component.payload == %{
+ "FirstName" => "Bob"
+ }
+
+ event_data = attrs |> Enum.map(fn {key, val} -> {to_string(key), val} end) |> Enum.into(%{})
+ event = Repo.get_by(Event, component_guid: component.component_guid)
+
+ assert %{
+ type: "component_created",
+ data: ^event_data
+ } = event
+ end
+
+ test "create_component/2 with invalid payload returns error changeset and does not create an event" do
+ entity = entity_fixture()
+ component_schema = component_schema_fixture()
+
+ attrs = %{
+ entity_guid: entity.entity_guid,
+ component_type: component_schema.name,
+ payload: %{"FirstName" => 1234}
+ }
+
+ assert {:error, %Ecto.Changeset{} = changeset} = Components.create_component(attrs)
+
+ assert %{
+ payload: ["Type mismatch. Expected String but got Integer. Path: #/FirstName"]
+ } = errors_on(changeset)
+
+ assert [] = Events.list_events()
+ end
+
+ test "create_component/1 with invalid entity returns an error and does not create an event" do
+ assert {:error, "Entity not found"} =
+ Components.create_component(%{
+ entity_guid: Ecto.UUID.generate(),
+ component_type: "some_type"
+ })
+
+ assert [] = Events.list_events()
+ end
+
+ test "create_component/1 with valid entity but invalid component attrs returns an error changeset and does not create an event" do
+ entity = entity_fixture()
+
+ component_schema = component_schema_fixture()
+
+ attrs =
+ @invalid_attrs
+ |> Map.put_new(:entity_guid, entity.entity_guid)
+ |> Map.put_new(:component_type, component_schema.name)
+
+ assert {:error, %Ecto.Changeset{}} = Components.create_component(attrs)
+ assert [] = Events.list_events()
+ end
+
+ test "update_component/2 with valid data updates the component and creates a component_updated event" do
+ entity = entity_fixture()
+ component = component_fixture(entity)
+
+ update_attrs = %{
+ active: false,
+ authoring_application: "some updated authoring_application",
+ component_id: "some updated component_id",
+ component_name: "some updated component_name",
+ component_type_payload: "some updated component_type_payload",
+ component_type_reference: "some updated component_type_reference",
+ context: "some updated context",
+ entity_classification: "some updated entity_classification",
+ hash1: "some updated hash1",
+ owner: "some updated owner",
+ payload: %{},
+ status: "some updated status",
+ version: "43"
+ }
+
+ assert {:ok, %Component{} = component} =
+ Components.update_component(component, update_attrs)
+
+ assert component.active == false
+ assert component.authoring_application == "some updated authoring_application"
+ assert component.component_id == "some updated component_id"
+ assert component.component_name == "some updated component_name"
+ assert component.component_type_payload == "some updated component_type_payload"
+ assert component.component_type_reference == "some updated component_type_reference"
+ assert component.context == "some updated context"
+ assert component.entity_classification == "some updated entity_classification"
+ assert component.hash1 == "some updated hash1"
+ assert component.owner == "some updated owner"
+ assert component.payload == %{}
+ assert component.status == "some updated status"
+ assert component.version == "43"
+
+ event = Repo.get_by(Event, component_guid: component.component_guid)
+
+ event_data =
+ update_attrs |> Enum.map(fn {key, value} -> {to_string(key), value} end) |> Enum.into(%{})
+
+ assert %{
+ type: "component_updated",
+ data: ^event_data
+ } = event
+ end
+
+ test "update_component/2 with invalid payload returns error changeset and does not create an event" do
+ entity = entity_fixture()
+ component_schema = component_schema_fixture()
+
+ component =
+ component_fixture(entity, %{
+ component_schema_guid: component_schema.component_schema_guid,
+ payload: %{"FirstName" => "Bob"}
+ })
+
+ assert {:error, %Ecto.Changeset{} = changeset} =
+ Components.update_component(component, %{payload: %{"FirstName" => 1234}})
+
+ assert %{
+ payload: ["Type mismatch. Expected String but got Integer. Path: #/FirstName"]
+ } = errors_on(changeset)
+
+ assert [] = Events.list_events()
+ end
+
+ test "update_component/2 with valid payload updates the component and creates a component_updated event" do
+ entity = entity_fixture()
+ component_schema = component_schema_fixture()
+
+ component =
+ component_fixture(entity, %{
+ component_schema_guid: component_schema.component_schema_guid,
+ payload: %{"FirstName" => "Bob"}
+ })
+
+ assert {:ok, %Component{} = component} =
+ Components.update_component(component, %{payload: %{"FirstName" => "Alice"}})
+
+ assert %{"FirstName" => "Alice"} == component.payload
+
+ event = Repo.get_by(Event, component_guid: component.component_guid)
+
+ assert %{
+ type: "component_updated",
+ data: %{
+ "payload" => %{
+ "FirstName" => "Alice"
+ }
+ }
+ } = event
+ end
+
+ test "update_component/2 with invalid data returns error changeset and does not create an event" do
+ entity = entity_fixture()
+ component = component_fixture(entity)
+ assert {:error, %Ecto.Changeset{}} = Components.update_component(component, @invalid_attrs)
+ assert component == Components.get_component!(component.component_guid)
+
+ assert [] = Events.list_events()
+ end
+
+ test "change_component/1 returns a component changeset" do
+ entity = entity_fixture()
+ component = component_fixture(entity)
+ assert %Ecto.Changeset{} = Components.change_component(component)
+ end
+ end
+
+ describe "component_schemas" do
+ alias HokEcs.Components
+ alias HokEcs.Components.ComponentSchema
+
+ import HokEcs.ECSFixtures
+
+ @invalid_attrs %{name: nil, schema: nil}
+
+ test "list_component_schemas/0 returns all component_schemas" do
+ component_schema = component_schema_fixture()
+ assert Components.list_component_schemas() == [component_schema]
+ end
+
+ test "get_component_schema!/1 returns the component_schema with given id" do
+ component_schema = component_schema_fixture()
+
+ assert Components.get_component_schema!(component_schema.component_schema_guid) ==
+ component_schema
+ end
+
+ test "create_component_schema/1 with valid data creates a component_schema" do
+ valid_attrs = %{name: "some name", schema: %{}}
+
+ assert {:ok, %ComponentSchema{} = component_schema} =
+ Components.create_component_schema(valid_attrs)
+
+ assert component_schema.name == "some name"
+ assert component_schema.schema == %{}
+ end
+
+ test "create_component_schema/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Components.create_component_schema(@invalid_attrs)
+ end
+
+ test "update_component_schema/2 with valid data updates the component_schema" do
+ component_schema = component_schema_fixture()
+ update_attrs = %{name: "some updated name", schema: %{}}
+
+ assert {:ok, %ComponentSchema{} = component_schema} =
+ Components.update_component_schema(component_schema, update_attrs)
+
+ assert component_schema.name == "some updated name"
+ assert component_schema.schema == %{}
+ end
+
+ test "update_component_schema/2 with invalid data returns error changeset" do
+ component_schema = component_schema_fixture()
+
+ assert {:error, %Ecto.Changeset{}} =
+ Components.update_component_schema(component_schema, @invalid_attrs)
+
+ assert component_schema ==
+ Components.get_component_schema!(component_schema.component_schema_guid)
+ end
+
+ test "delete_component_schema/1 deletes the component_schema" do
+ component_schema = component_schema_fixture()
+ assert {:ok, %ComponentSchema{}} = Components.delete_component_schema(component_schema)
+
+ assert_raise Ecto.NoResultsError, fn ->
+ Components.get_component_schema!(component_schema.component_schema_guid)
+ end
+ end
+
+ test "change_component_schema/1 returns a component_schema changeset" do
+ component_schema = component_schema_fixture()
+ assert %Ecto.Changeset{} = Components.change_component_schema(component_schema)
+ end
+ end
+end
diff --git a/hok_ecs/test/hok_ecs/entities_test.exs b/hok_ecs/test/hok_ecs/entities_test.exs
new file mode 100644
index 0000000..7d61211
--- /dev/null
+++ b/hok_ecs/test/hok_ecs/entities_test.exs
@@ -0,0 +1,135 @@
+defmodule HokEcs.EntitiesTest do
+ use HokEcs.DataCase, async: true
+
+ import HokEcs.ECSFixtures
+
+ alias HokEcs.Entities
+ alias HokEcs.Entities.Entity
+ alias HokEcs.Events
+ alias HokEcs.Events.Event
+
+ describe "entities" do
+ @invalid_attrs %{entity_classification_reference: 1234}
+
+ test "list_entities/1 with no args returns all entities" do
+ entity = entity_fixture()
+ assert Entities.list_entities() == [entity]
+ end
+
+ test "list_entities/1 with classification returns all entities with the given classification" do
+ matching_entity_1 = entity_fixture(%{entity_classification: "my classification"})
+ matching_entity_1_guid = matching_entity_1.entity_guid
+
+ matching_entity_2 = entity_fixture(%{entity_classification: "my classification"})
+ matching_entity_2_guid = matching_entity_2.entity_guid
+
+ _other_entity_1 = entity_fixture()
+ _other_entity_2 = entity_fixture()
+
+ result = Entities.list_entities(%{entity_classification: "my classification"})
+
+ assert [
+ %Entity{
+ entity_guid: ^matching_entity_1_guid
+ },
+ %Entity{
+ entity_guid: ^matching_entity_2_guid
+ }
+ ] = result
+ end
+
+ test "list_entities/1 with classification returns an empty list if no entities match the given classification" do
+ entity_fixture(%{entity_classification: "one"})
+ entity_fixture(%{entity_classification: "two"})
+ entity_fixture(%{entity_classification: "three"})
+
+ assert [] == Entities.list_entities(%{entity_classification: "four"})
+ end
+
+ test "get_entity!/1 returns the entity with given id" do
+ entity = entity_fixture()
+ assert Entities.get_entity!(entity.entity_guid) == entity
+ end
+
+ test "create_entity/1 with valid data creates an entity and entity_created event" do
+ valid_attrs = %{
+ entity_classification_reference: "some classification_reference",
+ entity_classification: "some classification",
+ context: "some context",
+ context_id: "some context id",
+ creation_date: DateTime.utc_now() |> to_string()
+ }
+
+ assert {:ok, %Entity{} = entity} = Entities.create_entity(valid_attrs)
+ assert entity.entity_classification_reference == "some classification_reference"
+ assert entity.entity_classification == "some classification"
+ assert entity.context == "some context"
+
+ event = Repo.get_by(Event, entity_guid: entity.entity_guid)
+
+ assert %{
+ type: "entity_created",
+ data: %{
+ "entity_classification_reference" => "some classification_reference",
+ "entity_classification" => "some classification",
+ "context" => "some context"
+ }
+ } = event
+ end
+
+ test "create_entity/1 with invalid data returns error changeset and does not create an event" do
+ assert {:error, %Ecto.Changeset{} = changeset} = Entities.create_entity(@invalid_attrs)
+
+ assert %{
+ errors: [
+ entity_classification: {"can't be blank", [validation: :required]},
+ context: {"can't be blank", [validation: :required]},
+ context_id: {"can't be blank", [validation: :required]},
+ creation_date: {"can't be blank", [validation: :required]},
+ entity_classification_reference:
+ {"is invalid", [type: :string, validation: :cast]}
+ ]
+ } = changeset
+
+ assert [] = Events.list_events()
+ end
+
+ test "update_entity/2 with valid data updates the entity and creates an entity_updated event" do
+ entity = entity_fixture()
+
+ update_attrs = %{
+ entity_classification_reference: "some updated classification_reference",
+ entity_classification: "some updated classification",
+ context: "some updated context"
+ }
+
+ assert {:ok, %Entity{} = entity} = Entities.update_entity(entity, update_attrs)
+ assert entity.entity_classification_reference == "some updated classification_reference"
+ assert entity.entity_classification == "some updated classification"
+ assert entity.context == "some updated context"
+
+ event = Repo.get_by(Event, entity_guid: entity.entity_guid, type: "entity_updated")
+
+ event_data =
+ update_attrs |> Enum.map(fn {key, value} -> {to_string(key), value} end) |> Enum.into(%{})
+
+ assert %{
+ type: "entity_updated",
+ data: ^event_data
+ } = event
+ end
+
+ test "update_entity/2 with invalid data returns error changeset and does not create an event" do
+ entity = entity_fixture()
+ assert {:error, %Ecto.Changeset{}} = Entities.update_entity(entity, @invalid_attrs)
+ assert entity == Entities.get_entity!(entity.entity_guid)
+
+ assert [] = Events.list_events()
+ end
+
+ test "change_entity/1 returns a entity changeset" do
+ entity = entity_fixture()
+ assert %Ecto.Changeset{} = Entities.change_entity(entity)
+ end
+ end
+end
diff --git a/hok_ecs/test/hok_ecs/events_test.exs b/hok_ecs/test/hok_ecs/events_test.exs
new file mode 100644
index 0000000..64b298c
--- /dev/null
+++ b/hok_ecs/test/hok_ecs/events_test.exs
@@ -0,0 +1,72 @@
+defmodule HokEcs.EventsTest do
+ use HokEcs.DataCase, async: true
+
+ alias HokEcs.Events
+ alias Events.Event
+
+ import HokEcs.ECSFixtures
+
+ describe "events" do
+ test "list_events/0 returns all events" do
+ entity = entity_fixture()
+ event = event_fixture(%{entity_guid: entity.entity_guid})
+ assert Events.list_events() == [event]
+ end
+
+ test "get_event!/1 returns the event with given id" do
+ entity = entity_fixture()
+ event = event_fixture(%{entity_guid: entity.entity_guid})
+ assert Events.get_event!(event.id) == event
+ end
+
+ test "create_entity_created_event/1 creates an event" do
+ entity = entity_fixture()
+
+ valid_attrs = %{classification: "Person"}
+
+ assert {:ok, %Event{} = event} = Events.create_entity_created_event(entity, valid_attrs)
+ assert event.data == %{classification: "Person"}
+ assert event.type == "entity_created"
+ assert event.entity_guid == entity.entity_guid
+ end
+
+ test "create_component_created_event/1 creates an event" do
+ entity = entity_fixture()
+ component = component_fixture(entity)
+
+ valid_attrs = %{context: "my_context"}
+
+ assert {:ok, %Event{} = event} =
+ Events.create_component_created_event(component, valid_attrs)
+
+ assert event.data == %{context: "my_context"}
+ assert event.type == "component_created"
+ assert event.component_guid == component.component_guid
+ end
+
+ test "create_entity_updated_event/1 creates an event" do
+ entity = entity_fixture()
+
+ valid_attrs = %{classification: "Person"}
+
+ assert {:ok, %Event{} = event} = Events.create_entity_updated_event(entity, valid_attrs)
+ assert event.data == %{classification: "Person"}
+ assert event.type == "entity_updated"
+ assert event.entity_guid == entity.entity_guid
+ end
+
+ test "create_component_updated_event/1 creates an event" do
+ entity = entity_fixture()
+ component = component_fixture(entity)
+
+ valid_attrs = %{context: "my_context"}
+
+ assert {:ok, %Event{} = event} =
+ Events.create_component_updated_event(component, valid_attrs)
+
+ assert event.data == %{context: "my_context"}
+ assert event.type == "component_updated"
+ assert event.component_guid == component.component_guid
+ end
+ end
+end
diff --git a/hok_ecs/test/hok_ecs/helpers_test.exs b/hok_ecs/test/hok_ecs/helpers_test.exs
new file mode 100644
index 0000000..c84a352
--- /dev/null
+++ b/hok_ecs/test/hok_ecs/helpers_test.exs
@@ -0,0 +1,4 @@
+defmodule HokEcs.HelpersTest do
+ use ExUnit.Case, async: true
+ doctest HokEcs.Helpers, import: true
+end
diff --git a/hok_ecs/test/hok_ecs/layers_test.exs b/hok_ecs/test/hok_ecs/layers_test.exs
new file mode 100644
index 0000000..2530904
--- /dev/null
+++ b/hok_ecs/test/hok_ecs/layers_test.exs
@@ -0,0 +1,218 @@
+defmodule HokEcs.LayersTest do
+ use HokEcs.DataCase, async: true
+
+ alias HokEcs.Layers
+ alias HokEcs.Layers.{LayerEntity, LayerComponent, LayerRelationship}
+
+ describe "layer" do
+ alias HokEcs.Layers.Layer
+
+ import HokEcs.ECSFixtures
+ import HokEcs.LayersFixtures
+
+ @invalid_attrs %{context: 1234}
+
+ test "list_layers/0 returns all layers" do
+ layer = layer_fixture()
+ assert Layers.list_layers() == [layer]
+ end
+
+ test "get_layer!/1 returns the layer with given id" do
+ layer = layer_fixture()
+ assert Layers.get_layer!(layer.layer_guid) == layer
+ end
+
+ test "create_layer/1 with valid data creates a layer" do
+ valid_attrs = %{
+ active: true,
+ context: "some context",
+ layer_description: "some layer_description",
+ layer_function: "some layer_function",
+ layer_id: "some layer_id",
+ layer_name: "some layer_name",
+ layer_owner: "some layer_owner",
+ layer_update_method: "some layer_update_method",
+ version: 42
+ }
+
+ assert {:ok, %Layer{} = layer} = Layers.create_layer(valid_attrs)
+ assert layer.active == true
+ assert layer.context == "some context"
+ assert layer.layer_description == "some layer_description"
+ assert layer.layer_function == "some layer_function"
+ assert layer.layer_id == "some layer_id"
+ assert layer.layer_name == "some layer_name"
+ assert layer.layer_owner == "some layer_owner"
+ assert layer.layer_update_method == "some layer_update_method"
+ assert layer.version == 42
+ end
+
+ test "create_layer/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Layers.create_layer(@invalid_attrs)
+ end
+
+ test "update_layer/2 with valid data updates the layer" do
+ layer = layer_fixture()
+
+ update_attrs = %{
+ active: false,
+ context: "some updated context",
+ layer_description: "some updated layer_description",
+ layer_function: "some updated layer_function",
+ layer_id: "some updated layer_id",
+ layer_name: "some updated layer_name",
+ layer_owner: "some updated layer_owner",
+ layer_update_method: "some updated layer_update_method",
+ version: 43
+ }
+
+ assert {:ok, %Layer{} = layer} = Layers.update_layer(layer, update_attrs)
+ assert layer.active == false
+ assert layer.context == "some updated context"
+ assert layer.layer_description == "some updated layer_description"
+ assert layer.layer_function == "some updated layer_function"
+ assert layer.layer_id == "some updated layer_id"
+ assert layer.layer_name == "some updated layer_name"
+ assert layer.layer_owner == "some updated layer_owner"
+ assert layer.layer_update_method == "some updated layer_update_method"
+ assert layer.version == 43
+ end
+
+ test "update_layer/2 with invalid data returns error changeset" do
+ layer = layer_fixture()
+ assert {:error, %Ecto.Changeset{}} = Layers.update_layer(layer, @invalid_attrs)
+ assert layer == Layers.get_layer!(layer.layer_guid)
+ end
+
+ test "delete_layer/1 deletes the layer" do
+ layer = layer_fixture()
+ assert {:ok, %Layer{}} = Layers.delete_layer(layer)
+ assert_raise Ecto.NoResultsError, fn -> Layers.get_layer!(layer.layer_guid) end
+ end
+
+ test "change_layer/1 returns a layer changeset" do
+ layer = layer_fixture()
+ assert %Ecto.Changeset{} = Layers.change_layer(layer)
+ end
+
+ test "add_entity_to_layer/2 adds an entity to a layer" do
+ layer = layer_fixture()
+ entity = entity_fixture()
+
+ assert {:ok, %LayerEntity{} = layer_entity} =
+ Layers.add_entity_to_layer(entity.entity_guid, layer)
+
+ assert layer_entity.layer_guid == layer.layer_guid
+ assert layer_entity.entity_guid == entity.entity_guid
+ end
+
+ test "add_entity_to_layer/2 returns an error changeset when adding an entity already associated with a layer" do
+ layer = layer_fixture()
+ layer_guid = layer.layer_guid
+
+ entity = entity_fixture()
+ entity_guid = entity.entity_guid
+
+ {:ok, %LayerEntity{}} = Layers.add_entity_to_layer(entity_guid, layer)
+
+ assert {:error, %Ecto.Changeset{} = changeset} =
+ Layers.add_entity_to_layer(entity_guid, layer)
+
+ assert %{
+ changes: %{
+ entity_guid: ^entity_guid,
+ layer_guid: ^layer_guid
+ },
+ errors: [
+ layer_guid: {"has already been taken", _}
+ ]
+ } = changeset
+ end
+
+ test "add_component_to_layer/2 adds a component to a layer" do
+ layer = layer_fixture()
+ entity = entity_fixture()
+ component = component_fixture(entity)
+
+ assert {:ok, %LayerComponent{} = layer_component} =
+ Layers.add_component_to_layer(component.component_guid, layer)
+
+ assert layer_component.layer_guid == layer.layer_guid
+ assert layer_component.component_guid == component.component_guid
+ end
+
+ test "add_relationship_to_layer/2 adds a relationship to a layer" do
+ layer = layer_fixture()
+ relationship = relationship_fixture()
+
+ assert {:ok, %LayerRelationship{} = layer_relationship} =
+ Layers.add_relationship_to_layer(relationship.relationship_guid, layer)
+
+ assert layer_relationship.layer_guid == layer.layer_guid
+ assert layer_relationship.relationship_guid == relationship.relationship_guid
+ end
+
+ test "get_layer_data/1 gets components (with schema), entities, and relationships for a layer" do
+ layer = layer_fixture()
+ entity = entity_fixture()
+ component_schema = component_schema_fixture()
+
+ component =
+ component_fixture(entity, %{
+ component_schema_guid: component_schema.component_schema_guid
+ })
+
+ relationship = relationship_fixture()
+
+ {:ok, %LayerEntity{}} = Layers.add_entity_to_layer(entity.entity_guid, layer)
+ {:ok, %LayerComponent{}} = Layers.add_component_to_layer(component.component_guid, layer)
+
+ {:ok, %LayerRelationship{}} =
+ Layers.add_relationship_to_layer(relationship.relationship_guid, layer)
+
+ layer_data = Layers.get_layer_data(layer)
+
+ layer_guid = layer.layer_guid
+
+ component = Repo.preload(component, :component_schema)
+
+ assert %Layer{
+ layer_guid: ^layer_guid,
+ entities: [^entity],
+ components: [^component],
+ relationships: [^relationship]
+ } = layer_data
+ end
+
+ test "get_layer_data/1 gets components (with schema), entities, and relationships for a layer by layer guid" do
+ layer = layer_fixture()
+ entity = entity_fixture()
+ component_schema = component_schema_fixture()
+
+ component =
+ component_fixture(entity, %{
+ component_schema_guid: component_schema.component_schema_guid
+ })
+
+ relationship = relationship_fixture()
+
+ {:ok, %LayerEntity{}} = Layers.add_entity_to_layer(entity.entity_guid, layer)
+ {:ok, %LayerComponent{}} = Layers.add_component_to_layer(component.component_guid, layer)
+
+ {:ok, %LayerRelationship{}} =
+ Layers.add_relationship_to_layer(relationship.relationship_guid, layer)
+
+ layer_guid = layer.layer_guid
+ layer_data = Layers.get_layer_data(layer_guid)
+
+ component = Repo.preload(component, :component_schema)
+
+ assert %Layer{
+ layer_guid: ^layer_guid,
+ entities: [^entity],
+ components: [^component],
+ relationships: [^relationship]
+ } = layer_data
+ end
+ end
+end
diff --git a/hok_ecs/test/hok_ecs/relationships/relationship_test.exs b/hok_ecs/test/hok_ecs/relationships/relationship_test.exs
new file mode 100644
index 0000000..ab36775
--- /dev/null
+++ b/hok_ecs/test/hok_ecs/relationships/relationship_test.exs
@@ -0,0 +1,62 @@
+defmodule HokEcs.RelationshipsTest.RelationshipTest do
+ use HokEcs.DataCase, async: true
+
+ import HokEcs.ECSFixtures
+
+ alias HokEcs.Relationships.Relationship
+
+ describe "relationship schema" do
+ test "includes associated source/destination entity/component joins" do
+ entity_1 = entity_fixture()
+ entity_2 = entity_fixture()
+
+ component_1 = component_fixture(entity_1)
+ component_2 = component_fixture(entity_2)
+
+ relationship = relationship_fixture()
+
+ source_entity =
+ relationship_source_entity_fixture(%{
+ relationship_guid: relationship.relationship_guid,
+ entity_guid: entity_1.entity_guid
+ })
+
+ source_component =
+ relationship_source_component_fixture(%{
+ relationship_guid: relationship.relationship_guid,
+ component_guid: component_1.component_guid
+ })
+
+ destination_entity =
+ relationship_destination_entity_fixture(%{
+ relationship_guid: relationship.relationship_guid,
+ entity_guid: entity_2.entity_guid
+ })
+
+ destination_component =
+ relationship_destination_component_fixture(%{
+ relationship_guid: relationship.relationship_guid,
+ component_guid: component_2.component_guid
+ })
+
+ relationship_with_assocs =
+ relationship
+ |> Repo.preload([
+ :relationship_source_entities,
+ :relationship_source_components,
+ :relationship_destination_entities,
+ :relationship_destination_components
+ ])
+
+ relationship_guid = relationship.relationship_guid
+
+ assert %Relationship{
+ relationship_guid: ^relationship_guid,
+ relationship_source_entities: [^source_entity],
+ relationship_source_components: [^source_component],
+ relationship_destination_entities: [^destination_entity],
+ relationship_destination_components: [^destination_component]
+ } = relationship_with_assocs
+ end
+ end
+end
diff --git a/hok_ecs/test/hok_ecs/relationships_test.exs b/hok_ecs/test/hok_ecs/relationships_test.exs
new file mode 100644
index 0000000..279f949
--- /dev/null
+++ b/hok_ecs/test/hok_ecs/relationships_test.exs
@@ -0,0 +1,341 @@
+defmodule HokEcs.RelationshipsTest do
+ use HokEcs.DataCase, async: true
+
+ alias HokEcs.Relationships
+
+ alias HokEcs.Relationships.{
+ Relationship,
+ RelationshipSourceEntity,
+ RelationshipSourceComponent,
+ RelationshipDestinationEntity,
+ RelationshipDestinationComponent
+ }
+
+ import HokEcs.ECSFixtures
+
+ describe "relationships" do
+ @invalid_attrs %{
+ relationship_name: 1234
+ }
+
+ test "get_relationships_by_member_ids/2 returns all relationships for empty lists" do
+ relationship_1 = relationship_fixture()
+ relationship_2 = relationship_fixture()
+ relationship_3 = relationship_fixture()
+
+ result =
+ Relationships.get_relationships_by_member_ids([], [], [], [])
+ |> Enum.sort_by(& &1.relationship_guid)
+
+ expected =
+ [relationship_1, relationship_2, relationship_3] |> Enum.sort_by(& &1.relationship_guid)
+
+ assert result == expected
+ end
+
+ test "get_relationships_by_member_ids/2 filters relationships by source entity guids" do
+ entity = entity_fixture()
+
+ matching_relationship = relationship_fixture()
+
+ _relationship_source_entity =
+ relationship_source_entity_fixture(%{
+ entity_guid: entity.entity_guid,
+ relationship_guid: matching_relationship.relationship_guid
+ })
+
+ _relationship_2 = relationship_fixture()
+
+ result = Relationships.get_relationships_by_member_ids([entity.entity_guid], [], [], [])
+
+ assert length(result) == 1
+ assert List.first(result) == matching_relationship
+ end
+
+ test "get_relationships_by_member_ids/2 filters relationships by both source and destination guids" do
+ source_entity = entity_fixture()
+
+ entity_2 = entity_fixture()
+ destination_component = component_fixture(entity_2)
+
+ matching_relationship = relationship_fixture()
+
+ _relationship_source_entity =
+ relationship_source_entity_fixture(%{
+ entity_guid: source_entity.entity_guid,
+ relationship_guid: matching_relationship.relationship_guid
+ })
+
+ _relationship_destination_component =
+ relationship_destination_component_fixture(%{
+ component_guid: destination_component.component_guid,
+ relationship_guid: matching_relationship.relationship_guid
+ })
+
+ _relationship_2 = relationship_fixture()
+
+ result =
+ Relationships.get_relationships_by_member_ids([source_entity.entity_guid], [], [], [
+ destination_component.component_guid
+ ])
+
+ assert length(result) == 1
+ assert List.first(result) == matching_relationship
+ end
+
+ test "list_relationships/0 returns all relationships" do
+ relationship = relationship_fixture()
+ assert Relationships.list_relationships() == [relationship]
+ end
+
+ test "get_relationship!/1 returns the relationship with given id" do
+ relationship = relationship_fixture()
+ assert Relationships.get_relationship!(relationship.relationship_guid) == relationship
+ end
+
+ test "create_relationship/1 with valid data creates a relationship" do
+ valid_attrs = %{
+ active: true,
+ context: "some context",
+ relationship_name: "some relationship_name",
+ relationship_type: "some relationship_type",
+ relationship_type_reference: "some relationship_type_reference",
+ version: 42
+ }
+
+ assert {:ok, %Relationship{} = relationship} =
+ Relationships.create_relationship(valid_attrs)
+
+ assert relationship.active == true
+ assert relationship.context == "some context"
+ assert relationship.relationship_name == "some relationship_name"
+ assert relationship.relationship_type == "some relationship_type"
+ assert relationship.relationship_type_reference == "some relationship_type_reference"
+ assert relationship.version == 42
+ end
+
+ test "create_relationship/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Relationships.create_relationship(@invalid_attrs)
+ end
+
+ test "update_relationship/2 with valid data updates the relationship" do
+ relationship = relationship_fixture()
+
+ update_attrs = %{
+ active: false,
+ context: "some updated context",
+ relationship_name: "some updated relationship_name",
+ relationship_type: "some updated relationship_type",
+ relationship_type_reference: "some updated relationship_type_reference",
+ version: 43
+ }
+
+ assert {:ok, %Relationship{} = relationship} =
+ Relationships.update_relationship(relationship, update_attrs)
+
+ assert relationship.active == false
+ assert relationship.context == "some updated context"
+ assert relationship.relationship_name == "some updated relationship_name"
+ assert relationship.relationship_type == "some updated relationship_type"
+
+ assert relationship.relationship_type_reference ==
+ "some updated relationship_type_reference"
+
+ assert relationship.version == 43
+ end
+
+ test "update_relationship/2 with invalid data returns error changeset" do
+ relationship = relationship_fixture()
+
+ assert {:error, %Ecto.Changeset{}} =
+ Relationships.update_relationship(relationship, @invalid_attrs)
+
+ assert relationship == Relationships.get_relationship!(relationship.relationship_guid)
+ end
+
+ test "delete_relationship/1 deletes the relationship" do
+ relationship = relationship_fixture()
+ assert {:ok, %Relationship{}} = Relationships.delete_relationship(relationship)
+
+ assert_raise Ecto.NoResultsError, fn ->
+ Relationships.get_relationship!(relationship.relationship_guid)
+ end
+ end
+
+ test "change_relationship/1 returns a relationship changeset" do
+ relationship = relationship_fixture()
+ assert %Ecto.Changeset{} = Relationships.change_relationship(relationship)
+ end
+
+ test "create_relationship/5 creates a relationship between two entities" do
+ source_entity = entity_fixture()
+ destination_entity = entity_fixture()
+
+ source_entity_guid = source_entity.entity_guid
+ destination_entity_guid = destination_entity.entity_guid
+
+ assert {:ok, %Relationship{} = relationship} =
+ Relationships.create_relationship(
+ [source_entity.entity_guid],
+ nil,
+ [destination_entity.entity_guid],
+ nil
+ )
+
+ assert relationship.relationship_source_components == []
+ assert relationship.relationship_destination_components == []
+
+ relationship_guid = relationship.relationship_guid
+
+ assert [
+ %RelationshipSourceEntity{
+ relationship_guid: ^relationship_guid,
+ entity_guid: ^source_entity_guid
+ }
+ ] = relationship.relationship_source_entities
+
+ assert [
+ %RelationshipDestinationEntity{
+ relationship_guid: ^relationship_guid,
+ entity_guid: ^destination_entity_guid
+ }
+ ] = relationship.relationship_destination_entities
+ end
+
+ test "create_relationship/5 adds multiple components to the relationship" do
+ entity_1 = entity_fixture()
+ entity_2 = entity_fixture()
+
+ source_component_1 = component_fixture(entity_1)
+ source_component_1_guid = source_component_1.component_guid
+
+ source_component_2 = component_fixture(entity_2)
+ source_component_2_guid = source_component_2.component_guid
+
+ assert {:ok, %Relationship{} = relationship} =
+ Relationships.create_relationship(
+ nil,
+ [source_component_1_guid, source_component_2_guid],
+ nil,
+ nil
+ )
+
+ relationship_guid = relationship.relationship_guid
+ relationship_source_components = relationship.relationship_source_components
+
+ assert [
+ %RelationshipSourceComponent{
+ relationship_guid: ^relationship_guid,
+ component_guid: ^source_component_1_guid
+ },
+ %RelationshipSourceComponent{
+ relationship_guid: ^relationship_guid,
+ component_guid: ^source_component_2_guid
+ }
+ ] = relationship_source_components
+ end
+
+ test "create_relationship/5 returns error changeset with invalid source entity id" do
+ source_entity_1 = entity_fixture()
+ source_entity_1_guid = source_entity_1.entity_guid
+
+ source_entity_2_guid = Ecto.UUID.generate()
+
+ assert {:error, %Ecto.Changeset{} = changeset} =
+ Relationships.create_relationship(
+ [source_entity_1_guid, source_entity_2_guid],
+ nil,
+ nil,
+ nil
+ )
+
+ assert %{
+ changes: %{
+ entity_guid: ^source_entity_2_guid
+ },
+ errors: [
+ entity_guid: {"does not exist", _}
+ ]
+ } = changeset
+
+ assert [] = Relationships.list_relationships()
+ end
+
+ test "create_relationship/5 returns error changeset with invalid destination component id" do
+ destination_component_guid = Ecto.UUID.generate()
+
+ assert {:error, %Ecto.Changeset{} = changeset} =
+ Relationships.create_relationship(
+ nil,
+ nil,
+ nil,
+ [destination_component_guid]
+ )
+
+ assert %{
+ changes: %{
+ component_guid: ^destination_component_guid
+ },
+ errors: [
+ component_guid: {"does not exist", _}
+ ]
+ } = changeset
+
+ assert [] = Relationships.list_relationships()
+ end
+
+ test "add_links_to_relationship/5 adds entities and components to an existing relationship" do
+ relationship = relationship_fixture()
+ relationship_guid = relationship.relationship_guid
+
+ source_entity = entity_fixture()
+ source_entity_guid = source_entity.entity_guid
+
+ source_component = component_fixture(source_entity)
+ source_component_guid = source_component.component_guid
+
+ destination_entity = entity_fixture()
+ destination_entity_guid = destination_entity.entity_guid
+
+ destination_component = component_fixture(destination_entity)
+ destination_component_guid = destination_component.component_guid
+
+ assert {:ok, %Relationship{} = relationship} =
+ Relationships.add_links_to_relationship(
+ relationship,
+ [source_entity_guid],
+ [source_component_guid],
+ [destination_entity_guid],
+ [destination_component_guid]
+ )
+
+ assert %Relationship{
+ relationship_guid: ^relationship_guid,
+ relationship_source_entities: [
+ %RelationshipSourceEntity{
+ relationship_guid: ^relationship_guid,
+ entity_guid: ^source_entity_guid
+ }
+ ],
+ relationship_source_components: [
+ %RelationshipSourceComponent{
+ relationship_guid: ^relationship_guid,
+ component_guid: ^source_component_guid
+ }
+ ],
+ relationship_destination_entities: [
+ %RelationshipDestinationEntity{
+ relationship_guid: ^relationship_guid,
+ entity_guid: ^destination_entity_guid
+ }
+ ],
+ relationship_destination_components: [
+ %RelationshipDestinationComponent{
+ relationship_guid: ^relationship_guid,
+ component_guid: ^destination_component_guid
+ }
+ ]
+ } = relationship
+ end
+ end
+end
diff --git a/hok_ecs/test/hok_ecs/scenes_test.exs b/hok_ecs/test/hok_ecs/scenes_test.exs
new file mode 100644
index 0000000..e173e4a
--- /dev/null
+++ b/hok_ecs/test/hok_ecs/scenes_test.exs
@@ -0,0 +1,157 @@
+defmodule HokEcs.ScenesTest do
+ use HokEcs.DataCase, async: true
+
+ alias HokEcs.Scenes
+ alias HokEcs.Scenes.{Scene, SceneLayer}
+ alias HokEcs.Layers
+ alias HokEcs.Layers.Layer
+ alias HokEcs.Relationships.Relationship
+ alias HokEcs.Entities.Entity
+ alias HokEcs.Components.{Component, ComponentSchema}
+
+ import HokEcs.ScenesFixtures
+ import HokEcs.LayersFixtures
+ import HokEcs.ECSFixtures
+
+ describe "scenes" do
+ @invalid_attrs %{
+ context: 1234
+ }
+
+ test "list_scenes/0 returns all scenes" do
+ scene = scene_fixture()
+ assert Scenes.list_scenes() == [scene]
+ end
+
+ test "get_scene!/1 returns the scene with given id" do
+ scene = scene_fixture()
+ assert Scenes.get_scene!(scene.scene_guid) == scene
+ end
+
+ test "create_scene/1 with valid data creates a scene" do
+ valid_attrs = %{
+ active: true,
+ context: "some context",
+ date_created: "some date_created",
+ scene_id: "some scene_id",
+ scene_name: "some scene_name",
+ version: 42
+ }
+
+ assert {:ok, %Scene{} = scene} = Scenes.create_scene(valid_attrs)
+ assert scene.active == true
+ assert scene.context == "some context"
+ assert scene.date_created == "some date_created"
+ assert scene.scene_id == "some scene_id"
+ assert scene.scene_name == "some scene_name"
+ assert scene.version == 42
+ end
+
+ test "create_scene/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Scenes.create_scene(@invalid_attrs)
+ end
+
+ test "update_scene/2 with valid data updates the scene" do
+ scene = scene_fixture()
+
+ update_attrs = %{
+ active: false,
+ context: "some updated context",
+ date_created: "some updated date_created",
+ scene_id: "some updated scene_id",
+ scene_name: "some updated scene_name",
+ version: 43
+ }
+
+ assert {:ok, %Scene{} = scene} = Scenes.update_scene(scene, update_attrs)
+ assert scene.active == false
+ assert scene.context == "some updated context"
+ assert scene.date_created == "some updated date_created"
+ assert scene.scene_id == "some updated scene_id"
+ assert scene.scene_name == "some updated scene_name"
+ assert scene.version == 43
+ end
+
+ test "update_scene/2 with invalid data returns error changeset" do
+ scene = scene_fixture()
+ assert {:error, %Ecto.Changeset{}} = Scenes.update_scene(scene, @invalid_attrs)
+ assert scene == Scenes.get_scene!(scene.scene_guid)
+ end
+
+ test "delete_scene/1 deletes the scene" do
+ scene = scene_fixture()
+ assert {:ok, %Scene{}} = Scenes.delete_scene(scene)
+ assert_raise Ecto.NoResultsError, fn -> Scenes.get_scene!(scene.scene_guid) end
+ end
+
+ test "change_scene/1 returns a scene changeset" do
+ scene = scene_fixture()
+ assert %Ecto.Changeset{} = Scenes.change_scene(scene)
+ end
+
+ test "add_layer_to_scene/2 adds a layer by guid to an existing scene" do
+ scene = scene_fixture()
+ layer = layer_fixture()
+
+ assert {:ok, %SceneLayer{} = scene_layer} =
+ Scenes.add_layer_to_scene(layer.layer_guid, scene)
+
+ assert scene_layer.scene_guid == scene.scene_guid
+ assert scene_layer.layer_guid == layer.layer_guid
+ end
+
+ test "get_scene_data/1 gets all entities, relationships, and components for a scene" do
+ scene = scene_fixture()
+ scene_guid = scene.scene_guid
+
+ layer = layer_fixture()
+ layer_guid = layer.layer_guid
+
+ entity = entity_fixture()
+ entity_guid = entity.entity_guid
+
+ component_schema = component_schema_fixture()
+ component_schema_guid = component_schema.component_schema_guid
+
+ component = component_fixture(entity, %{component_schema_guid: component_schema_guid})
+ component_guid = component.component_guid
+
+ relationship = relationship_fixture()
+ relationship_guid = relationship.relationship_guid
+
+ {:ok, _} = Scenes.add_layer_to_scene(layer_guid, scene)
+ {:ok, _} = Layers.add_entity_to_layer(entity_guid, layer)
+ {:ok, _} = Layers.add_component_to_layer(component_guid, layer)
+ {:ok, _} = Layers.add_relationship_to_layer(relationship_guid, layer)
+
+ scene = Scenes.get_scene_data(scene)
+
+ assert %Scene{
+ scene_guid: ^scene_guid,
+ layers: [
+ %Layer{
+ layer_guid: ^layer_guid,
+ entities: [
+ %Entity{
+ entity_guid: ^entity_guid
+ }
+ ],
+ components: [
+ %Component{
+ component_guid: ^component_guid,
+ component_schema: %ComponentSchema{
+ component_schema_guid: ^component_schema_guid
+ }
+ }
+ ],
+ relationships: [
+ %Relationship{
+ relationship_guid: ^relationship_guid
+ }
+ ]
+ }
+ ]
+ } = scene
+ end
+ end
+end
diff --git a/hok_ecs/test/hok_ecs_web/controllers/page_controller_test.exs b/hok_ecs/test/hok_ecs_web/controllers/page_controller_test.exs
new file mode 100644
index 0000000..1ab0bda
--- /dev/null
+++ b/hok_ecs/test/hok_ecs_web/controllers/page_controller_test.exs
@@ -0,0 +1,8 @@
+defmodule HokEcsWeb.PageControllerTest do
+ use HokEcsWeb.ConnCase, async: true
+
+ test "GET /", %{conn: conn} do
+ conn = get(conn, "/")
+ assert redirected_to(conn) =~ "/app"
+ end
+end
diff --git a/hok_ecs/test/hok_ecs_web/views/error_view_test.exs b/hok_ecs/test/hok_ecs_web/views/error_view_test.exs
new file mode 100644
index 0000000..af2c031
--- /dev/null
+++ b/hok_ecs/test/hok_ecs_web/views/error_view_test.exs
@@ -0,0 +1,14 @@
+defmodule HokEcsWeb.ErrorViewTest do
+ use HokEcsWeb.ConnCase, async: true
+
+ # Bring render/3 and render_to_string/3 for testing custom views
+ import Phoenix.View
+
+ test "renders 404.html" do
+ assert render_to_string(HokEcsWeb.ErrorView, "404.html", []) == "Not Found"
+ end
+
+ test "renders 500.html" do
+ assert render_to_string(HokEcsWeb.ErrorView, "500.html", []) == "Internal Server Error"
+ end
+end
diff --git a/hok_ecs/test/hok_ecs_web/views/layout_view_test.exs b/hok_ecs/test/hok_ecs_web/views/layout_view_test.exs
new file mode 100644
index 0000000..8407277
--- /dev/null
+++ b/hok_ecs/test/hok_ecs_web/views/layout_view_test.exs
@@ -0,0 +1,8 @@
+defmodule HokEcsWeb.LayoutViewTest do
+ use HokEcsWeb.ConnCase, async: true
+
+ # When testing helpers, you may want to import Phoenix.HTML and
+ # use functions such as safe_to_string() to convert the helper
+ # result into an HTML string.
+ # import Phoenix.HTML
+end
diff --git a/hok_ecs/test/hok_ecs_web/views/page_view_test.exs b/hok_ecs/test/hok_ecs_web/views/page_view_test.exs
new file mode 100644
index 0000000..940f357
--- /dev/null
+++ b/hok_ecs/test/hok_ecs_web/views/page_view_test.exs
@@ -0,0 +1,3 @@
+defmodule HokEcsWeb.PageViewTest do
+ use HokEcsWeb.ConnCase, async: true
+end
diff --git a/hok_ecs/test/support/conn_case.ex b/hok_ecs/test/support/conn_case.ex
new file mode 100644
index 0000000..252f50d
--- /dev/null
+++ b/hok_ecs/test/support/conn_case.ex
@@ -0,0 +1,39 @@
+defmodule HokEcsWeb.ConnCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ tests that require setting up a connection.
+
+ Such tests rely on `Phoenix.ConnTest` and also
+ import other functionality to make it easier
+ to build common data structures and query the data layer.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use HokEcsWeb.ConnCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # Import conveniences for testing with connections
+ import Plug.Conn
+
+ import Phoenix.ConnTest
+ import HokEcsWeb.ConnCase
+
+ alias HokEcsWeb.Router.Helpers, as: Routes
+
+ # The default endpoint for testing
+ @endpoint HokEcsWeb.Endpoint
+ end
+ end
+
+ setup tags do
+ HokEcs.DataCase.setup_sandbox(tags)
+ {:ok, conn: Phoenix.ConnTest.build_conn()}
+ end
+end
diff --git a/hok_ecs/test/support/data_case.ex b/hok_ecs/test/support/data_case.ex
new file mode 100644
index 0000000..d6ae8ba
--- /dev/null
+++ b/hok_ecs/test/support/data_case.ex
@@ -0,0 +1,58 @@
+defmodule HokEcs.DataCase do
+ @moduledoc """
+ This module defines the setup for tests requiring
+ access to the application's data layer.
+
+ You may define functions here to be used as helpers in
+ your tests.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use HokEcs.DataCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ alias HokEcs.Repo
+
+ import Ecto
+ import Ecto.Changeset
+ import Ecto.Query
+ import HokEcs.DataCase
+ end
+ end
+
+ setup tags do
+ HokEcs.DataCase.setup_sandbox(tags)
+ :ok
+ end
+
+ @doc """
+ Sets up the sandbox based on the test tags.
+ """
+ def setup_sandbox(tags) do
+ pid = Ecto.Adapters.SQL.Sandbox.start_owner!(HokEcs.Repo, shared: not tags[:async])
+ on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
+ end
+
+ @doc """
+ A helper that transforms changeset errors into a map of messages.
+
+ assert {:error, changeset} = Accounts.create_user(%{password: "short"})
+ assert "password is too short" in errors_on(changeset).password
+ assert %{password: ["password is too short"]} = errors_on(changeset)
+
+ """
+ def errors_on(changeset) do
+ Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
+ Regex.replace(~r"%{(\w+)}", message, fn _, key ->
+ opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
+ end)
+ end)
+ end
+end
diff --git a/hok_ecs/test/support/fixtures/ecs_fixtures.ex b/hok_ecs/test/support/fixtures/ecs_fixtures.ex
new file mode 100644
index 0000000..1de832e
--- /dev/null
+++ b/hok_ecs/test/support/fixtures/ecs_fixtures.ex
@@ -0,0 +1,177 @@
+defmodule HokEcs.ECSFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `HokEcs.ECS` context.
+ """
+
+ @doc """
+ Generate a entity.
+ """
+ alias HokEcs.Repo
+ alias HokEcs.Events.Event
+ alias HokEcs.Entities.Entity
+ alias HokEcs.Components
+ alias HokEcs.Components.Component
+ alias HokEcs.Relationships
+
+ alias HokEcs.Relationships.{
+ RelationshipDestinationComponent,
+ RelationshipDestinationEntity,
+ RelationshipSourceComponent,
+ RelationshipSourceEntity
+ }
+
+ def entity_fixture(attrs \\ %{}) do
+ attrs =
+ attrs
+ |> Enum.into(%{
+ context: "some_context",
+ context_id: "some_context_id",
+ creation_date: DateTime.utc_now() |> to_string(),
+ entity_classification: "some_entity_classification"
+ })
+
+ %Entity{}
+ |> Entity.changeset(attrs)
+ |> Repo.insert!()
+ end
+
+ @doc """
+ Generate a component.
+ """
+ def component_fixture(%Entity{} = entity, attrs \\ %{}) do
+ attrs =
+ attrs
+ |> Enum.into(%{
+ entity_guid: entity.entity_guid,
+ active: true,
+ authoring_application: "some authoring_application",
+ component_id: "some component_id",
+ component_name: "some component_name",
+ component_type_payload: "some component_payload_type",
+ component_type: "some component_type",
+ component_type_reference: "some component_type_reference",
+ context: "some context",
+ context_id: "some_context_id",
+ entity_classification: "some entity_classification",
+ hash1: "some hash1",
+ owner: "some owner",
+ payload: %{},
+ status: "some status",
+ version: "42",
+ creation_date: DateTime.utc_now() |> to_string()
+ })
+
+ %Component{}
+ |> Component.changeset(attrs)
+ |> Repo.insert!()
+ end
+
+ @doc """
+ Generate a component_schema.
+ """
+ def component_schema_fixture(attrs \\ %{}) do
+ {:ok, component_schema} =
+ attrs
+ |> Enum.into(%{
+ name: "Person.Details",
+ schema: %{
+ "$schema" => "http://json-schema.org/schema#",
+ "title" => "Person.Details",
+ "version" => "V0.02",
+ "type" => "object",
+ "properties" => %{
+ "FirstName" => %{
+ "type" => "string",
+ "description" => "Persons First Name"
+ },
+ "LastName" => %{
+ "type" => "string",
+ "description" => "Persons last name"
+ },
+ "EmailAddress" => %{
+ "type" => "string",
+ "description" => "Persons email address"
+ },
+ "UniqueID" => %{
+ "type" => "string",
+ "description" => "Entity Specific Identifier"
+ }
+ }
+ }
+ })
+ |> Components.create_component_schema()
+
+ component_schema
+ end
+
+ @doc """
+ Generate a event.
+ """
+ def event_fixture(attrs \\ %{}) do
+ {:ok, event} =
+ %Event{
+ type: "some type",
+ data: %{}
+ }
+ |> Event.changeset(attrs)
+ |> Repo.insert()
+
+ event
+ end
+
+ @doc """
+ Generate a relationship.
+ """
+ def relationship_fixture(attrs \\ %{}) do
+ {:ok, relationship} =
+ attrs
+ |> Enum.into(%{
+ active: true,
+ context: "some context",
+ relationship_name: "some relationship_name",
+ relationship_type: "some relationship_type",
+ relationship_type_reference: "some relationship_type_reference",
+ version: 42
+ })
+ |> Relationships.create_relationship()
+
+ relationship
+ end
+
+ @doc """
+ Generate a relationship_source_entity.
+ """
+ def relationship_source_entity_fixture(attrs \\ %{}) do
+ %RelationshipSourceEntity{}
+ |> RelationshipSourceEntity.changeset(attrs)
+ |> Repo.insert!()
+ end
+
+ @doc """
+ Generate a relationship_source_component.
+ """
+ def relationship_source_component_fixture(attrs \\ %{}) do
+ %RelationshipSourceComponent{}
+ |> RelationshipSourceComponent.changeset(attrs)
+ |> Repo.insert!()
+ end
+
+ @doc """
+ Generate a relationship_destination_entity.
+ """
+ def relationship_destination_entity_fixture(attrs \\ %{}) do
+ %RelationshipDestinationEntity{}
+ |> RelationshipDestinationEntity.changeset(attrs)
+ |> Repo.insert!()
+ end
+
+ @doc """
+ Generate a relationship_destination_component.
+ """
+ def relationship_destination_component_fixture(attrs \\ %{}) do
+ %RelationshipDestinationComponent{}
+ |> RelationshipDestinationComponent.changeset(attrs)
+ |> Repo.insert!()
+ end
+end
diff --git a/hok_ecs/test/support/fixtures/layers_fixtures.ex b/hok_ecs/test/support/fixtures/layers_fixtures.ex
new file mode 100644
index 0000000..ba4f2ce
--- /dev/null
+++ b/hok_ecs/test/support/fixtures/layers_fixtures.ex
@@ -0,0 +1,28 @@
+defmodule HokEcs.LayersFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `HokEcs.Layers` context.
+ """
+
+ @doc """
+ Generate a layer.
+ """
+ def layer_fixture(attrs \\ %{}) do
+ {:ok, layer} =
+ attrs
+ |> Enum.into(%{
+ active: true,
+ context: "some context",
+ layer_description: "some layer_description",
+ layer_function: "some layer_function",
+ layer_id: "some layer_id",
+ layer_name: "some layer_name",
+ layer_owner: "some layer_owner",
+ layer_update_method: "some layer_update_method",
+ version: 42
+ })
+ |> HokEcs.Layers.create_layer()
+
+ layer
+ end
+end
diff --git a/hok_ecs/test/support/fixtures/scenes_fixtures.ex b/hok_ecs/test/support/fixtures/scenes_fixtures.ex
new file mode 100644
index 0000000..0f66186
--- /dev/null
+++ b/hok_ecs/test/support/fixtures/scenes_fixtures.ex
@@ -0,0 +1,25 @@
+defmodule HokEcs.ScenesFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `HokEcs.Scenes` context.
+ """
+
+ @doc """
+ Generate a scene.
+ """
+ def scene_fixture(attrs \\ %{}) do
+ {:ok, scene} =
+ attrs
+ |> Enum.into(%{
+ active: true,
+ context: "some context",
+ date_created: "some date_created",
+ scene_id: "some scene_id",
+ scene_name: "some scene_name",
+ version: 42
+ })
+ |> HokEcs.Scenes.create_scene()
+
+ scene
+ end
+end
diff --git a/hok_ecs/test/test_helper.exs b/hok_ecs/test/test_helper.exs
new file mode 100644
index 0000000..38ce7e8
--- /dev/null
+++ b/hok_ecs/test/test_helper.exs
@@ -0,0 +1,2 @@
+ExUnit.start()
+Ecto.Adapters.SQL.Sandbox.mode(HokEcs.Repo, :manual)