Skip to content

Commit

Permalink
Add some EventStore Tests (#541)
Browse files Browse the repository at this point in the history
* update elixir & erlang

* add persist_event tests
plus cleanup, restructure

* creation of node sets prev nodes of optional next element now correctly

* enhance events with more data
  • Loading branch information
electronicbites authored Jun 18, 2024
1 parent 54427fe commit 8f43808
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 56 deletions.
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.16.2-otp-26
erlang 26.0.2
elixir 1.16.3-otp-26
erlang 26.2.5
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
# - https://pkgs.org/ - resource for finding needed packages
# - Ex: hexpm/elixir:1.15.7-erlang-26.0.2-debian-bullseye-20231009-slim
#
ARG ELIXIR_VERSION=1.16.2
ARG OTP_VERSION=26.0.2
ARG ELIXIR_VERSION=1.16.3
ARG OTP_VERSION=26.2.5
ARG DEBIAN_VERSION=bullseye-20240513-slim

ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
Expand Down
15 changes: 14 additions & 1 deletion assets/js/hooks/events/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
deleteItem,
focusItem,
moveItem,
getItemById,
} from "../item";
import { getNodeByItem } from "../node";

export function handleList({ nodes }: { nodes: Node[] }) {
const container: HTMLDivElement = this.el.querySelector(".children");
Expand Down Expand Up @@ -36,12 +38,23 @@ export function handleList({ nodes }: { nodes: Node[] }) {
focusItem(lastItem);
}

export function handleInsert({ node }: { node: Node }) {
export function handleInsert({
node,
next_id,
}: {
node: Node;
next_id: string | undefined;
}) {
const container: HTMLDivElement = this.el.querySelector(".children");

const item = createItem(node);
container.append(item);
moveItem(node, container);
const nextItem = getItemById(next_id) as HTMLDivElement;
const nextNode = getNodeByItem(nextItem);
nextNode.prev_id = node.uuid;
nextNode.dirty = false;
moveItem(nextNode, container);
}

export function handleContentChange({ node }: { node: Node }) {
Expand Down
10 changes: 10 additions & 0 deletions assets/js/hooks/events/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
getItemByNode,
moveItem,
setItemDirty,
setItemParent,
setItemPrev,
} from "../item";
import { getNodeByEvent, getNodeByItem } from "../node";

Expand Down Expand Up @@ -92,6 +94,14 @@ export function keydown(event: KeyboardEvent) {
const newItem = createItem(newNode);
item.after(newItem);
focusItem(newItem, false);

const oldNextItem = newItem.nextSibling as HTMLDivElement | null;
if (oldNextItem) {
const nextNode = getNodeByItem(oldNextItem);
nextNode.prev_id = newNode.uuid;
nextNode.dirty = true;
setItemPrev(oldNextItem, newNode.uuid);
}
break;

case "Backspace":
Expand Down
46 changes: 26 additions & 20 deletions assets/js/hooks/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,22 @@ export function createItem({ uuid, content, parent_id, prev_id, dirty }: Node) {

const item = document.createElement("div");
item.id = `outline-node-${uuid}`;
item.className = "my-1 bg-gray-100 data-[dirty=true]:bg-red-100";
item.className = "my-1 data-[dirty=true]:bg-red-100";
item.setAttribute("data-parent", parent_id || "");
item.setAttribute("data-prev", prev_id || "");

const link = document.createElement("a");
link.className = "block float-left my-0.5 bg-gray-200 rounded-full";
link.className = "block float-left my-0.5 rounded-full";
link.href = `#${uuid}`;
link.innerHTML =
'<svg viewBox="0 0 18 18" fill="currentColor" class="w-5 h-5"><circle cx="9" cy="9" r="3.5"></circle></svg>';
item.appendChild(link);

const contentWrap = document.createElement("div");
contentWrap.className = "ml-5 bg-gray-300 content";
contentWrap.contentEditable = "true";
item.appendChild(contentWrap);

const span = document.createElement("span");
span.className = "bg-gray-400 innerContent";
span.textContent = content || " ";
contentWrap.appendChild(span);
const input = document.createElement("div");
input.className = "ml-5 content";
input.contentEditable = "true";
input.textContent = content || "";
item.appendChild(input);

const childContainer = document.createElement("div");
childContainer.className = "ml-5 children";
Expand All @@ -51,8 +47,8 @@ export function changeItemContent({ uuid, content, dirty }: Node) {

const newContent = content || "";

const span = item.querySelector(".innerContent") as HTMLSpanElement;
if (span.textContent != newContent) span.textContent = newContent;
const input = item.querySelector(".content") as HTMLDivElement;
if (input.textContent != newContent) input.textContent = newContent;

setItemDirty(item, dirty);

Expand All @@ -62,7 +58,7 @@ export function changeItemContent({ uuid, content, dirty }: Node) {
export function moveItem(
{ uuid, parent_id, prev_id, dirty }: Node,
container: HTMLDivElement,
force: boolean = false
force: boolean = false,
) {
const item = getItemById(uuid);
if (!item) return;
Expand All @@ -75,8 +71,8 @@ export function moveItem(
)
return;

item.setAttribute("data-parent", parent_id || "");
item.setAttribute("data-prev", prev_id || "");
setItemParent(item, parent_id);
setItemPrev(item, prev_id);

const prevItem = getItemById(prev_id);
const parentItem = getItemById(parent_id);
Expand All @@ -94,6 +90,16 @@ export function moveItem(
return item;
}

export function setItemParent(
item: HTMLDivElement,
parent_id: string | undefined,
) {
item.setAttribute("data-parent", parent_id || "");
}
export function setItemPrev(item: HTMLDivElement, prev_id: string | undefined) {
item.setAttribute("data-prev", prev_id || "");
}

export function deleteItem({ uuid }: Node) {
const item = getItemById(uuid);
if (!item) return;
Expand All @@ -112,7 +118,7 @@ export function getItemByNode({ uuid }: Node) {
return getItemById(uuid);
}

function getItemById(uuid: string | undefined) {
export function getItemById(uuid: string | undefined) {
if (!uuid) return null;

return document.getElementById(`outline-node-${uuid}`) as HTMLDivElement;
Expand All @@ -126,12 +132,12 @@ export function getItemByEvent(event: Event): HTMLDivElement {
}

export function focusItem(item: HTMLDivElement, toEnd: boolean = true) {
const contentWrap = item.querySelector(".innerContent") as HTMLDivElement;
contentWrap.focus();
const input = item.querySelector(".content") as HTMLDivElement;
input.focus();

if (toEnd) {
const range = document.createRange();
range.setStart(contentWrap, 0);
range.setStart(input, 0);
range.collapse(true);

const selection = window.getSelection();
Expand Down
4 changes: 2 additions & 2 deletions assets/js/hooks/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export function getNodeByEvent(event: Event): Node {

export function getNodeByItem(item: HTMLDivElement): Node {
const uuid = item.id.split("outline-node-")[1] as UUID;
const span = item.querySelector(".innerContent") as HTMLSpanElement;
const content = span.textContent || "";
const input = item.querySelector(".content") as HTMLDivElement;
const content = input.textContent || "";

const parent_id = (item.getAttribute("data-parent") as UUID) || undefined;
const prev_id = (item.getAttribute("data-prev") as UUID) || undefined;
Expand Down
22 changes: 18 additions & 4 deletions lib/radiator/outline.ex
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
defmodule Radiator.Outline.NodeRepoResult do
@moduledoc """
Generic result structure for node operations.
"""
defstruct [
:node,
:old_prev_id,
:old_next_id,
:next_id,
:children
]
end

defmodule Radiator.Outline do
@moduledoc """
The Outline context.
Expand All @@ -6,6 +19,7 @@ defmodule Radiator.Outline do
import Ecto.Query, warn: false

alias Radiator.Outline.Node
alias Radiator.Outline.NodeRepoResult
alias Radiator.Outline.NodeRepository
alias Radiator.Repo

Expand All @@ -17,7 +31,7 @@ defmodule Radiator.Outline do
## Examples
iex> insert_node(%{content: 'foo'})
{:ok, %Node{}}
{:ok, %NodeRepoResult{}}
iex> insert_node(%{content: value})
{:error, :parent_and_prev_not_consistent}
Expand All @@ -34,7 +48,7 @@ defmodule Radiator.Outline do
parent_node_id = attrs["parent_id"]
episode_id = attrs["episode_id"]
# find Node which has been previously connected to prev_node
node_to_move =
next_node =
Node
|> where(episode_id: ^episode_id)
|> where_prev_node_equals(prev_node_id)
Expand All @@ -45,9 +59,9 @@ defmodule Radiator.Outline do
prev_node <- NodeRepository.get_node_if(prev_node_id),
true <- parent_and_prev_consistent?(parent_node, prev_node),
{:ok, node} <- NodeRepository.create_node(attrs),
{:ok, _node_to_move} <- move_node_if(node_to_move, nil, node.uuid),
{:ok, _node_to_move} <- move_node_if(next_node, nil, node.uuid),
{:ok, node} <- move_node_if(node, parent_node_id, prev_node_id) do
node
%NodeRepoResult{node: node, next_id: get_node_id(next_node)}
else
false ->
Repo.rollback("Insert node failed. Parent and prev node are not consistent.")
Expand Down
2 changes: 1 addition & 1 deletion lib/radiator/outline/event/node_deleted_event.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Radiator.Outline.Event.NodeDeletedEvent do
@moduledoc false
defstruct [:event_id, :node_id, :user_id]
defstruct [:event_id, :node_id, :user_id, :children]
end
2 changes: 1 addition & 1 deletion lib/radiator/outline/event/node_inserted_event.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule Radiator.Outline.Event.NodeInsertedEvent do
@moduledoc false

defstruct [:event_id, :node, :user_id]
defstruct [:event_id, :node, :user_id, :next_id]
end
11 changes: 10 additions & 1 deletion lib/radiator/outline/event/node_moved_event.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
defmodule Radiator.Outline.Event.NodeMovedEvent do
@moduledoc false
defstruct [:event_id, :node_id, :parent_id, :prev_id, :user_id]
defstruct [
:event_id,
:node_id,
:parent_id,
:prev_id,
:user_id,
:old_prev_id,
:old_next_id,
:next_id
]
end
10 changes: 8 additions & 2 deletions lib/radiator/outline/event_consumer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Radiator.Outline.EventConsumer do

alias Radiator.EventStore
alias Radiator.Outline
alias Radiator.Outline.NodeRepoResult

alias Radiator.Outline.Command.{
ChangeNodeContentCommand,
Expand Down Expand Up @@ -80,8 +81,13 @@ defmodule Radiator.Outline.EventConsumer do
:ok
end

defp handle_insert_node_result({:ok, node}, command) do
%NodeInsertedEvent{node: node, event_id: command.event_id, user_id: command.user_id}
defp handle_insert_node_result({:ok, %NodeRepoResult{node: node, next_id: next_id}}, command) do
%NodeInsertedEvent{
node: node,
event_id: command.event_id,
user_id: command.user_id,
next_id: next_id
}
|> EventStore.persist_event()
|> Dispatch.broadcast()

Expand Down
2 changes: 1 addition & 1 deletion lib/radiator_web/controllers/api/outline_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ defmodule RadiatorWeb.Api.OutlineController do
})
end

defp get_response({:ok, node}), do: {200, %{uuid: node.uuid}}
defp get_response({:ok, %{node: node}}), do: {200, %{uuid: node.uuid}}
defp get_response({:error, _}), do: {400, %{error: "params"}}
end
4 changes: 2 additions & 2 deletions lib/radiator_web/live/episode_live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ defmodule RadiatorWeb.EpisodeLive.Index do
|> reply(:noreply)
end

def handle_info(%NodeInsertedEvent{node: node}, socket) do
def handle_info(%NodeInsertedEvent{node: node, next_id: next_id}, socket) do
socket
|> push_event("insert", %{node: node})
|> push_event("insert", %{node: node, next_id: next_id})
|> reply(:noreply)
end

Expand Down
Loading

0 comments on commit 8f43808

Please sign in to comment.