defmodule Toolbox.Scenario.OutputAction do
@moduledoc """
Struct representing side effects produced by run. There are several constructor functions to
simplify and standardize side effect definition.
Instances of this struct are being parsed and then passed to output server.
"""
defmodule Asset do
@moduledoc "Body for Asset output actions"
defstruct [:id, :attributes]
@typedoc "Body structure of Asset output actions"
@type t :: %Asset{
id: String.t(),
attributes: map()
}
end
defmodule AssetPatch do
@moduledoc """
Body for asset attributes patch output action.
Describes a patch (a change) in attributes, some attributes can change
(`update`), some may be completely removed (`delete`).
"""
defstruct [:id, :update, :delete]
@typedoc "Body for asset attributes patch output action"
@type t :: %AssetPatch{
id: String.t(),
update: map() | nil,
delete: map() | nil
}
end
defmodule Edge do
@moduledoc "Body for Edge output actions"
defstruct [:from, :to, :type]
@typedoc "Body structure of Edge output actions"
@type t :: %Edge{
from: String.t(),
to: String.t(),
type: String.t()
}
end
defmodule IncidentState do
@moduledoc "Struct for Incident State"
defstruct [
:attributes,
:description,
:severity,
:status,
:timestamp
]
@type severity :: 1 | 2 | 3 | 4 | 5
@typedoc "Struct for Incident State"
@type t :: %IncidentState{
attributes: map() | nil,
description: String.t(),
severity: severity(),
status: String.t(),
timestamp: integer()
}
end
defmodule Incident do
@moduledoc "Body for new Incident action"
@enforce_keys [
:created_at,
:modified_at,
:closed_at,
:subject,
:status,
:severity,
:resolution,
:name,
:history,
:future
]
defstruct [
:created_at,
:modified_at,
:closed_at,
:subject,
:status,
:severity,
:resolution,
:name,
:history,
:future
]
@type severity :: 1 | 2 | 3 | 4 | 5
@type resolution :: :open | :closed
@typedoc "Body for new Incident action"
@type t :: %Incident{
created_at: integer(),
modified_at: integer(),
closed_at: integer() | nil,
subject: String.t(),
status: String.t(),
severity: severity(),
resolution: resolution(),
name: String.t(),
history: [IncidentState.t()],
future: [IncidentState.t()]
}
end
alias __MODULE__, as: OA
defstruct type: nil,
body: %{},
svector: [],
timestamp: 0
@type type ::
:notify
| :create_event
| :add_asset
| :edit_asset
| :upsert_asset
| :delete_asset
| :add_edge
| :upsert_edge
| :delete_edge
| :execute_sql
| :bad_output_action
@type t :: %OA{
type: type(),
body: map() | Asset.t() | Edge.t() | AssetPatch.t(),
svector: [any],
timestamp: integer()
}
@spec new(type(), map()) :: t()
def new(type, body), do: %OA{type: type, body: body}
@spec type(t()) :: type()
def type(%OA{type: type}), do: type
@spec set_type(t(), type) :: t()
def set_type(%OA{} = oa, type), do: %{oa | type: type}
@spec body(t()) :: map()
def body(%OA{body: body}), do: body
@spec set_body(t(), map()) :: t()
def set_body(%OA{} = oa, body), do: %{oa | body: body}
@doc """
Constructor for notification side effect.
Params describe the notification to be created. It has the following keys
- `:type` - string, type of the notification
- `:data` - map, data for the template evaluation
- `:primary_asset` - string, optional, id of an asset that corresponds to this notification
- `:ids` - list of asset ids, optional, to filter recipients, each recipient must have
sufficient privileges to see all listed IDs
- `:aqls` - map, AQLs which are evaluated, provide further context to the templates, and also can
serve the same purpose as `ids` - filter recipients to only those who can see the whole
result of the query. Key is the name of the AQL which can be used in the template. Value
is a map with `aql` (the AQL query) and `primary` (whether the AQL should also be used
to filter recipients) keys.
- `:priority` - optional, defaults to `:medium`, can be `:low`, `:medium`, `:high`, only
applicable to some channels
- `:metadata` - map, optional, additional metadata which can be used in notification group
subscriptions. Should adhere to the format specified in `notification/spec.exs`.
- `:direct_subscriptions` - list of maps, subscriptions to be used additionally to the usual
routing. Each map should have the following.
- `:user` - map, user object to be used instead of Altworx object, should have
`notification.email` and optionally even `notification.language`.
- `:templates` - list of ids, which templates should be used
- `:channels` - list of ids, which channels should be used
- `:attachments` - list of maps, optional, attachments that should be sent with the notification.
Each map has the following keys.
- `data` - binary, content of the attachment
- `filename` - string, name of the file
- `content_type` - string, optional, MIME type of the data
- `type` - whether the attachment is to inlined in the template (`:inline`) or not
(`:attachment`, this is the default)
"""
@spec new_notification_action(timestamp :: non_neg_integer(), params :: map()) :: t()
def new_notification_action(timestamp, %{type: type, data: data} = params) do
ids = Map.get(params, :ids, [])
aqls = Map.get(params, :aqls, %{})
pa = Map.get(params, :primary_asset, nil)
priority = Map.get(params, :priority, :medium)
metadata = Map.get(params, :metadata, %{})
direct_subs = Map.get(params, :direct_subscriptions)
attachments = Map.get(params, :attachments, nil)
body = %{
# scenario should be filled by runtime worker
"scenario" => nil,
"type" => type |> to_string(),
"primary_asset" => pa,
"data" => data,
"ids" => ids,
"aqls" => aqls,
"priority" => priority |> to_string(),
"metadata" => metadata,
"attachments" => attachments
}
# add direct subs conditionally
body =
if direct_subs do
Map.put(body, "direct_subscriptions", direct_subs)
else
body
end
%OA{type: :notify, timestamp: timestamp, body: body}
end
def new_notification_action(timestamp, params) do
body = %{type: :notification_action, timestamp: timestamp, params: params}
%OA{type: :bad_output_action, timestamp: timestamp, body: body}
end
@doc """
Constructor for event side effect.
First argument is unix `timestamp` and second is `body`, which is map with 3 required keys:
`actors`, `type` and `template`.
* `actors` - map of actors which are interpolated in the template.
* `params` - map of template params that are interpolated in the template.
* `origin_messages` - list of raw messages linked with the event itself.
* `template` - event's template. See `AssetMap.Events.Template`.
* `type` - type of the event. It's declared in Scenario's Manifest.
"""
@spec new_event_action(non_neg_integer(), map()) :: t()
def new_event_action(timestamp, %{type: type, template: template, actors: actors} = params) do
template_params = Map.get(params, :params, %{})
origin_messages = Map.get(params, :origin_messages, [])
body = %{
# run should be filled by runtime worker (the key has to exist)
"run_id" => nil,
"actors" => actors,
"params" => template_params,
"origin_messages" => origin_messages,
"template" => template,
"type" => type |> to_string()
}
%OA{type: :create_event, timestamp: timestamp, body: body}
end
def new_event_action(timestamp, params) do
body = %{type: :event_action, timestamp: timestamp, params: params}
%OA{type: :bad_output_action, timestamp: timestamp, body: body}
end
@doc """
Constructor for calling SQL query.
First argument is unix `timestamp` and second is `body`, which is map with 4 required keys:
`db_connection`, `sql_query`, `data` and `type`.
* `db_connection` - map with database connection info (`hostname`, `username`, `password`, `database`)
* `sql_query` - SQL query.
* `data` - list of parameterized data.
* `type` - database type, right now only PostgreSQL is supported. It's checked in `OutputProcessor` and
the value must be `:postgresql`.
If `:run_id` is present in `data`, it's replaced with actual run_id in `AssetMap.Api.SqlExecutor`.
"""
@spec execute_sql_action(non_neg_integer(), map()) :: t()
def execute_sql_action(timestamp, %{
db_connection: db_connection,
sql_query: sql_query,
data: data,
type: type
}) do
body = %{
# run should be filled by runtime worker (the key has to exist)
"run_id" => nil,
"db_connection" => db_connection,
"sql_query" => sql_query,
"data" => data,
"type" => type |> to_string()
}
%OA{type: :execute_sql, timestamp: timestamp, body: body}
end
def execute_sql_action(timestamp, params) do
body = %{type: :execute_sql, timestamp: timestamp, params: params}
%OA{type: :bad_output_action, timestamp: timestamp, body: body}
end
@doc """
Constructor for create new asset side effect.
"""
@spec new_create_asset_action(binary(), non_neg_integer(), map()) :: t()
def new_create_asset_action(id, timestamp, attributes) do
body = %Asset{id: id, attributes: attributes}
%OA{type: :add_asset, timestamp: timestamp, body: body, svector: [id]}
end
@doc """
Constructor for update existing asset side effect.
"""
@spec new_update_asset_action(binary(), non_neg_integer(), map() | nil, map() | nil) :: t()
def new_update_asset_action(id, timestamp, update, delete \\ nil) do
body = %AssetPatch{id: id, update: update, delete: delete}
%OA{type: :edit_asset, timestamp: timestamp, body: body, svector: [id]}
end
@doc """
Constructor for upsert asset side effect.
"""
@spec new_upsert_asset_action(binary(), non_neg_integer(), map()) :: t()
def new_upsert_asset_action(id, timestamp, attributes) do
body = %Asset{id: id, attributes: attributes}
%OA{type: :upsert_asset, timestamp: timestamp, body: body, svector: [id]}
end
@doc """
Constructor for delete asset side effect.
"""
@spec new_delete_asset_action(binary(), non_neg_integer()) :: t()
def new_delete_asset_action(id, timestamp) do
body = %Asset{id: id}
%OA{type: :delete_asset, timestamp: timestamp, body: body, svector: [id]}
end
@doc """
Constructor for create edge between assets side effect.
"""
@spec new_create_edge_action(binary(), non_neg_integer(), binary(), binary(), binary()) :: t()
def new_create_edge_action(id, timestamp, from_id, to_id, type) do
body = %Edge{from: from_id, to: to_id, type: type}
%OA{type: :add_edge, timestamp: timestamp, body: body, svector: [id]}
end
@doc """
Constructor for upsert edge between assets side effect.
"""
@spec new_upsert_edge_action(binary(), non_neg_integer(), binary(), binary(), binary()) :: t()
def new_upsert_edge_action(id, timestamp, from_id, to_id, type) do
body = %Edge{from: from_id, to: to_id, type: type}
%OA{type: :upsert_edge, timestamp: timestamp, body: body, svector: [id]}
end
@spec new_delete_edge_action(binary(), non_neg_integer(), binary(), binary(), binary()) :: t()
def new_delete_edge_action(id, timestamp, from_id, to_id, type) do
body = %Edge{from: from_id, to: to_id, type: type}
%OA{type: :delete_edge, timestamp: timestamp, body: body, svector: [id]}
end
@doc """
Constructor for new incident (add_asset) action
"""
@spec new_incident_action(binary(), Incident.t()) :: t()
def new_incident_action(id, %Incident{} = attributes) do
body = %Asset{
id: id,
attributes: attributes
}
%OA{type: :add_asset, timestamp: attributes.created_at, body: body, svector: ["svector"]}
end
end