Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# This file is part of Edgehog.
#
# Copyright 2025 SECO Mind Srl
# Copyright 2025-2026 SECO Mind Srl
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -28,10 +28,13 @@ defmodule Edgehog.Campaigns.Channel.Calculations.DeployableDevices do
use Ash.Resource.Calculation

alias Ash.Resource.Calculation
alias Edgehog.Selector

require Ash.Query

@impl Calculation
def load(_query, _opts, _context) do
[target_groups: [devices: :system_model]]
[target_groups: [:selector]]
end

@impl Calculation
Expand All @@ -43,22 +46,42 @@ defmodule Edgehog.Campaigns.Channel.Calculations.DeployableDevices do
|> Ash.load!(:system_models)
|> Map.get(:system_models, [])

Enum.map(deployment_channels, fn deployment_channel ->
deployment_channel.target_groups
|> Enum.flat_map(fn target_group ->
Enum.filter(
target_group.devices,
&satisfies?(&1.system_model, system_model_requirements)
)
end)
|> Enum.uniq_by(& &1.id)
end)
deployment_channels
|> Ash.load!(target_groups: [:selector])
|> Enum.map(&resolve_deployment_channel(&1, system_model_requirements, context))
end

defp resolve_deployment_channel(deployment_channel, system_model_requirements, context) do
combined_expr = Enum.reduce(deployment_channel.target_groups, nil, &combine_selectors/2)

if combined_expr do
query =
Edgehog.Devices.Device
|> Ash.Query.filter(^combined_expr)
|> Ash.Query.set_tenant(context.tenant)

query =
if system_model_requirements != [] do
system_model_ids = Enum.map(system_model_requirements, & &1.id)
Ash.Query.filter(query, system_model_part_number.system_model_id in ^system_model_ids)
else
query
end

Ash.read!(query)
else
[]
end
end

defp satisfies?(_system_model, [] = _system_model_requirements), do: true
defp satisfies?(nil = _system_model, _system_model_requirements), do: false
defp combine_selectors(group, acc) do
case Selector.parse(group.selector) do
{:ok, ast} ->
expr = Selector.to_ash_expr(ast)
if acc, do: Ash.Expr.expr(^expr or ^acc), else: expr

defp satisfies?(system_model, system_model_requirements) do
Enum.any?(system_model_requirements, &(system_model.id == &1.id))
_ ->
acc
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,43 @@ defmodule Edgehog.Campaigns.Channel.Calculations.DownloadCapableDevices do
@moduledoc false
use Ash.Resource.Calculation

alias Edgehog.Selector

require Ash.Query

@impl Ash.Resource.Calculation
def calculate(channels, _opts, _context) do
def load(_query, _opts, _context) do
[target_groups: [:selector]]
end

@impl Ash.Resource.Calculation
def calculate(channels, _opts, context) do
channels
|> Ash.load!(target_groups: [:devices])
|> Enum.map(fn channel ->
channel.target_groups
|> Enum.flat_map(& &1.devices)
|> Enum.uniq_by(& &1.id)
end)
|> Ash.load!(target_groups: [:selector])
|> Enum.map(&resolve_channel(&1, context))
end

defp resolve_channel(channel, context) do
combined_expr = Enum.reduce(channel.target_groups, nil, &combine_selectors/2)

if combined_expr do
Edgehog.Devices.Device
|> Ash.Query.filter(^combined_expr)
|> Ash.Query.set_tenant(context.tenant)
|> Ash.read!()
else
[]
end
end

defp combine_selectors(group, acc) do
case Selector.parse(group.selector) do
{:ok, ast} ->
expr = Selector.to_ash_expr(ast)
if acc, do: Ash.Expr.expr(^expr or ^acc), else: expr

_ ->
acc
end
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# This file is part of Edgehog.
#
# Copyright 2024 SECO Mind Srl
# Copyright 2024-2026 SECO Mind Srl
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -22,8 +22,15 @@ defmodule Edgehog.Campaigns.Channel.Calculations.UpdatableDevices do
@moduledoc false
use Ash.Resource.Calculation

alias Edgehog.Selector

require Ash.Query

@impl Ash.Resource.Calculation
def load(_query, _opts, _context) do
[target_groups: [:selector]]
end

@impl Ash.Resource.Calculation
def calculate(channels, _opts, context) do
%{arguments: %{base_image: base_image}} = context
Expand All @@ -33,16 +40,32 @@ defmodule Edgehog.Campaigns.Channel.Calculations.UpdatableDevices do
system_model_id = base_image.base_image_collection.system_model_id

channels
|> Ash.load!(target_groups: [devices: :system_model])
|> Enum.map(fn channel ->
channel.target_groups
|> Enum.flat_map(fn target_group ->
Enum.filter(
target_group.devices,
&(&1.system_model != nil && &1.system_model.id == system_model_id)
)
end)
|> Enum.uniq_by(& &1.id)
end)
|> Ash.load!(target_groups: [:selector])
|> Enum.map(&resolve_channel(&1, system_model_id, context))
end

defp resolve_channel(channel, system_model_id, context) do
combined_expr = Enum.reduce(channel.target_groups, nil, &combine_selectors/2)

if combined_expr do
Edgehog.Devices.Device
|> Ash.Query.filter(^combined_expr)
|> Ash.Query.filter(system_model_part_number.system_model_id == ^system_model_id)
|> Ash.Query.set_tenant(context.tenant)
|> Ash.read!()
else
[]
end
end

defp combine_selectors(group, acc) do
case Selector.parse(group.selector) do
{:ok, ast} ->
expr = Selector.to_ash_expr(ast)
if acc, do: Ash.Expr.expr(^expr or ^acc), else: expr

_ ->
acc
end
end
end
29 changes: 24 additions & 5 deletions backend/lib/edgehog/devices/device/device.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
defmodule Edgehog.Devices.Device do
@moduledoc false
use Edgehog.MultitenantResource,
primary_read_warning?: false,
domain: Edgehog.Devices,
extensions: [
AshGraphql.Resource
Expand Down Expand Up @@ -75,7 +76,24 @@ defmodule Edgehog.Devices.Device do
end

actions do
defaults [:read, :destroy]
defaults [:destroy]

read :read do
primary? true
pagination keyset?: true, offset?: true, required?: false

argument :matching_group_id, :id do
description "Filters devices dynamically based on a DeviceGroup's selector"
allow_nil? true
end

argument :matching_selector, :string do
description "Optional raw selector for dry-runs"
allow_nil? true
end

prepare Edgehog.Devices.Device.Preparations.FilterBySelector
end

create :create do
primary? true
Expand Down Expand Up @@ -692,20 +710,21 @@ defmodule Edgehog.Devices.Device do
end

postgres do
table "devices"
repo Edgehog.Repo
table("devices")
repo(Edgehog.Repo)

references do
reference :realm,
reference(:realm,
index?: true,
on_delete: :nothing,
match_with: [tenant_id: :tenant_id],
match_type: :full
)

# We don't generate a foreign key for the system model part number since we want the device
# to be able to declare its part number even _before_ we add the relative system model to
# Edgehog
reference :system_model_part_number, ignore?: true
reference(:system_model_part_number, ignore?: true)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#
# This file is part of Edgehog.
#
# Copyright 2026 SECO Mind Srl
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

defmodule Edgehog.Devices.Device.Preparations.FilterBySelector do
@moduledoc false
use Ash.Resource.Preparation

alias Edgehog.Selector

require Ash.Query

@impl Ash.Resource.Preparation
def prepare(query, _opts, context) do
selector_string = get_selector(query, context)

if selector_string do
{:ok, ast} = Selector.parse(selector_string)
ash_expr = Selector.to_ash_expr(ast)
Ash.Query.filter(query, ^ash_expr)
else
query
end
end

defp get_selector(query, context) do
selector = Ash.Query.get_argument(query, :matching_selector)
group_id = Ash.Query.get_argument(query, :matching_group_id)

cond do
is_binary(selector) ->
selector

group_id != nil ->
group =
Edgehog.Groups.DeviceGroup
|> Ash.Query.filter(id == ^group_id)
|> Ash.Query.set_tenant(context.tenant)
|> Ash.read_first!()

if group, do: group.selector, else: nil

true ->
nil
end
end
end
Loading
Loading