diff --git a/backend/lib/ecto/json_variant.ex b/backend/lib/ecto/json_variant.ex deleted file mode 100644 index 77d4af0c7..000000000 --- a/backend/lib/ecto/json_variant.ex +++ /dev/null @@ -1,193 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2022 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 Ecto.JSONVariant do - use Ecto.Type - - alias __MODULE__ - - # TODO: add support for array values - @supported_types [ - :double, - :integer, - :boolean, - :longinteger, - :string, - :binaryblob, - :datetime - ] - - defstruct [:type, :value] - - @impl true - def type, do: :map - - @impl true - def cast(%{"type" => type, "value" => value}) when is_atom(type) and type in @supported_types do - do_cast(type, value) - end - - def cast(%{"type" => type, "value" => value}) when is_binary(type) do - with {:ok, type} <- type_string_to_atom(type) do - do_cast(type, value) - end - end - - def cast(%{type: type, value: value}) when is_atom(type) and type in @supported_types do - do_cast(type, value) - end - - def cast(%{type: type, value: value}) when is_binary(type) do - with {:ok, type} <- type_string_to_atom(type) do - do_cast(type, value) - end - end - - def cast(_) do - :error - end - - defp do_cast(type, value) do - with {:ok, value} <- cast_fun(type).(value) do - {:ok, struct!(__MODULE__, type: type, value: value)} - end - end - - defp cast_fun(:double), do: &Ecto.Type.cast(:float, &1) - defp cast_fun(:integer), do: &cast_integer/1 - defp cast_fun(:boolean), do: &Ecto.Type.cast(:boolean, &1) - defp cast_fun(:longinteger), do: &cast_longinteger/1 - defp cast_fun(:string), do: &cast_string/1 - defp cast_fun(:binaryblob), do: &cast_binaryblob/1 - defp cast_fun(:datetime), do: &Ecto.Type.cast(:utc_datetime_usec, &1) - - defp cast_integer(term) when is_binary(term) do - case Integer.parse(term) do - {integer, ""} when abs(integer) <= 0x7FFF_FFFF -> {:ok, integer} - _ -> :error - end - end - - defp cast_integer(term) when is_integer(term) and abs(term) <= 0x7FFF_FFFF, do: {:ok, term} - defp cast_integer(term) when is_integer(term), do: {:error, message: "is out of range"} - defp cast_integer(_), do: :error - - defp cast_longinteger(term) when is_binary(term) do - case Integer.parse(term) do - {integer, ""} when abs(integer) <= 0x7FFF_FFFF_FFFF_FFFF -> {:ok, integer} - _ -> :error - end - end - - defp cast_longinteger(term) when is_integer(term) and abs(term) <= 0x7FFF_FFFF_FFFF_FFFF do - {:ok, term} - end - - defp cast_longinteger(_), do: :error - - defp cast_string(term) when is_binary(term) do - if String.valid?(term) do - {:ok, term} - else - :error - end - end - - defp cast_string(_), do: :error - - defp cast_binaryblob(term) when is_binary(term) do - case Base.decode64(term) do - {:ok, value} -> {:ok, value} - _ -> :error - end - end - - defp cast_binaryblob(_), do: :error - - defp type_string_to_atom("double"), do: {:ok, :double} - defp type_string_to_atom("integer"), do: {:ok, :integer} - defp type_string_to_atom("boolean"), do: {:ok, :boolean} - defp type_string_to_atom("longinteger"), do: {:ok, :longinteger} - defp type_string_to_atom("string"), do: {:ok, :string} - defp type_string_to_atom("binaryblob"), do: {:ok, :binaryblob} - defp type_string_to_atom("datetime"), do: {:ok, :datetime} - defp type_string_to_atom(_), do: :error - - @impl true - def dump(%JSONVariant{type: type, value: value}) when type in @supported_types do - with {:ok, value} <- dump_fun(type).(value) do - {:ok, %{t: Atom.to_string(type), v: value}} - end - end - - def dump(_), do: :error - - defp dump_fun(:double), do: &Ecto.Type.dump(:float, &1) - defp dump_fun(:integer), do: &Ecto.Type.dump(:integer, &1) - defp dump_fun(:boolean), do: &Ecto.Type.dump(:boolean, &1) - defp dump_fun(:longinteger), do: &Ecto.Type.dump(:integer, &1) - defp dump_fun(:string), do: &Ecto.Type.dump(:string, &1) - defp dump_fun(:binaryblob), do: &dump_binaryblob/1 - defp dump_fun(:datetime), do: &dump_datetime/1 - - defp dump_binaryblob(value) do - with {:ok, binary} <- Ecto.Type.dump(:binary, value) do - {:ok, Base.encode64(binary)} - end - end - - defp dump_datetime(value) do - with {:ok, datetime} <- Ecto.Type.dump(:utc_datetime_usec, value) do - {:ok, DateTime.to_iso8601(datetime)} - end - end - - @impl true - def load(%{"t" => type_string, "v" => value}) do - with {:ok, type} <- type_string_to_atom(type_string), - {:ok, value} <- load_fun(type).(value) do - {:ok, struct!(__MODULE__, type: type, value: value)} - end - end - - def load(_), do: :error - - defp load_fun(:double), do: &Ecto.Type.load(:float, &1) - defp load_fun(:integer), do: &Ecto.Type.load(:integer, &1) - defp load_fun(:boolean), do: &Ecto.Type.load(:boolean, &1) - defp load_fun(:longinteger), do: &Ecto.Type.load(:integer, &1) - defp load_fun(:string), do: &Ecto.Type.load(:string, &1) - defp load_fun(:binaryblob), do: &load_binaryblob/1 - defp load_fun(:datetime), do: &load_datetime/1 - - defp load_binaryblob(value) do - case Base.decode64(value) do - {:ok, binary} -> Ecto.Type.load(:binary, binary) - _ -> :error - end - end - - defp load_datetime(value) do - case DateTime.from_iso8601(value) do - {:ok, datetime, 0} -> Ecto.Type.load(:utc_datetime_usec, datetime) - _ -> :error - end - end -end diff --git a/backend/lib/edgehog.ex b/backend/lib/edgehog.ex deleted file mode 100644 index 11e8cb2c7..000000000 --- a/backend/lib/edgehog.ex +++ /dev/null @@ -1,29 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2021 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 do - @moduledoc """ - Edgehog 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/backend/lib/edgehog/astarte/device/ota_request/v0.ex b/backend/lib/edgehog/astarte/device/ota_request/v0.ex deleted file mode 100644 index c0fd598d0..000000000 --- a/backend/lib/edgehog/astarte/device/ota_request/v0.ex +++ /dev/null @@ -1,33 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2022-2023 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.Astarte.Device.OTARequest.V0 do - @behaviour Edgehog.Astarte.Device.OTARequest.V0.Behaviour - - alias Astarte.Client.AppEngine - - @interface "io.edgehog.devicemanager.OTARequest" - - @impl true - def post(%AppEngine{} = client, device_id, uuid, url) do - data = %{uuid: uuid, url: url} - AppEngine.Devices.send_datastream(client, device_id, @interface, "/request", data) - end -end diff --git a/backend/lib/edgehog/astarte/device/ota_request/v0/behaviour.ex b/backend/lib/edgehog/astarte/device/ota_request/v0/behaviour.ex deleted file mode 100644 index 479840a98..000000000 --- a/backend/lib/edgehog/astarte/device/ota_request/v0/behaviour.ex +++ /dev/null @@ -1,31 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2022-2023 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.Astarte.Device.OTARequest.V0.Behaviour do - alias Astarte.Client.AppEngine - - @callback post( - client :: AppEngine.t(), - device_id :: String.t(), - uuid :: String.t(), - url :: String.t() - ) :: - :ok | {:error, term()} -end diff --git a/backend/lib/edgehog/changeset_validation.ex b/backend/lib/edgehog/changeset_validation.ex deleted file mode 100644 index ec7efd951..000000000 --- a/backend/lib/edgehog/changeset_validation.ex +++ /dev/null @@ -1,78 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2023 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.ChangesetValidation do - import Ecto.Changeset - - def validate_tenant_slug(changeset, field) do - validate_format(changeset, field, ~r/^[a-z\d\-]+$/, - message: "should only contain lower case ASCII letters (from a to z), digits and -" - ) - end - - def validate_realm_name(changeset, field) do - validate_format(changeset, field, ~r/^[a-z][a-z0-9]{0,47}$/, - message: - "should only contain lower case ASCII letters (from a to z) and digits, " <> - "and start with a lower case ASCII letter" - ) - end - - def validate_locale(changeset, field) do - validate_format(changeset, field, ~r/^[a-z]{2,3}-[A-Z]{2}$/, message: "is not a valid locale") - end - - def validate_pem_public_key(changeset, field) do - validate_change(changeset, field, fn field, pem_public_key -> - case X509.PublicKey.from_pem(pem_public_key) do - {:ok, _} -> [] - {:error, _reason} -> [{field, "is not a valid PEM public key"}] - end - end) - end - - def validate_pem_private_key(changeset, field) do - validate_change(changeset, field, fn field, pem_private_key -> - case X509.PrivateKey.from_pem(pem_private_key) do - {:ok, _} -> [] - {:error, _reason} -> [{field, "is not a valid PEM private key"}] - end - end) - end - - def validate_url(changeset, field) do - validate_change(changeset, field, fn field, url -> - %URI{scheme: scheme, host: maybe_host} = URI.parse(url) - - host = to_string(maybe_host) - empty_host? = host == "" - space_in_host? = host =~ " " - - valid_host? = not empty_host? and not space_in_host? - valid_scheme? = scheme in ["http", "https"] - - if valid_host? and valid_scheme? do - [] - else - [{field, "is not a valid URL"}] - end - end) - end -end diff --git a/backend/lib/edgehog/error.ex b/backend/lib/edgehog/error.ex deleted file mode 100644 index c67fe4a03..000000000 --- a/backend/lib/edgehog/error.ex +++ /dev/null @@ -1,148 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2021-2023 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.Error do - @moduledoc """ - Module used to normalize all errors in Edghog, so that they can be shown by the API. - """ - - # TODO: Remove this once it's not a compile dependency anymore - - require Logger - alias __MODULE__ - - defstruct [:code, :message, :status_code, :key] - - # Error Tuples - - # Regular errors - def normalize({:error, reason}) do - handle(reason) - end - - # Ecto transaction errors - def normalize({:error, _operation, reason, _changes}) do - handle(reason) - end - - # Unhandled errors - def normalize(other) do - handle(other) - end - - defp handle(code) when is_atom(code) do - {status, message} = metadata(code) - - %Error{ - code: code, - message: message, - status_code: status - } - end - - defp handle(errors) when is_list(errors) do - Enum.map(errors, &handle/1) - end - - defp handle(%Ecto.Changeset{} = changeset) do - changeset - |> Ecto.Changeset.traverse_errors(fn - {message, opts} when is_binary(message) -> - Regex.replace(~r"%{(\w+)}", message, fn _, key -> - opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() - end) - - {other, _opts} -> - other - end) - |> Enum.map(fn - {_k, v} when is_map(v) -> - # Nested changeset, inner errors are already a rendered map - from_rendered_changeset_errors(v) - - {k, v} -> - %Error{ - code: :validation, - message: String.capitalize("#{k} #{v}"), - status_code: 422 - } - end) - end - - defp handle(%{status: status, response: response} = error) - when is_struct(error, Astarte.Client.APIError) do - case response do - # detail already includes an error message - %{"errors" => %{"detail" => error_message}} -> - %Error{ - code: :astarte_api_error, - message: error_message, - status_code: status - } - - # This probably comes from a changeset error, translate it - %{"errors" => errors} when is_map(errors) and status == 422 -> - from_rendered_changeset_errors(errors) - - # If something else comes up, we return the status and print out an error - response -> - Logger.warning("Unhandled API Error: #{inspect(response)}") - - %Error{ - code: :astarte_api_error, - message: Jason.encode!(response), - status_code: status - } - end - end - - defp handle(other) do - Logger.warning("Unhandled error term: #{inspect(other)}") - handle(:unknown) - end - - defp from_rendered_changeset_errors(changeset_errors) do - Enum.map(changeset_errors, fn {k, error_messages} -> - # Emit an error struct for each error message on a key - Enum.map(error_messages, fn error_message -> - %Error{ - code: :astarte_api_error, - message: String.capitalize("#{k} #{error_message}"), - status_code: 422 - } - end) - end) - end - - defp metadata(:unauthenticated), do: {401, "Login required"} - defp metadata(:unauthorized), do: {403, "Unauthorized"} - defp metadata(:not_found), do: {404, "Resource not found"} - - defp metadata(:not_default_locale) do - {422, "The default tenant locale must be used when creating or updating this resource"} - end - - defp metadata(:unknown), do: {500, "Something went wrong"} - - defp metadata(code) do - Logger.warning("Unhandled error code: #{inspect(code)}") - {422, to_string(code)} - end -end diff --git a/backend/lib/edgehog/labeling/device_attribute.ex b/backend/lib/edgehog/labeling/device_attribute.ex deleted file mode 100644 index bdb927000..000000000 --- a/backend/lib/edgehog/labeling/device_attribute.ex +++ /dev/null @@ -1,52 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2022 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.Labeling.DeviceAttribute do - use Ecto.Schema - import Ecto.Changeset - - @primary_key false - schema "device_attributes" do - field :tenant_id, :integer, - autogenerate: {Edgehog.Repo, :get_tenant_id, []}, - primary_key: true - - field :device_id, :id, primary_key: true - field :namespace, Ecto.Enum, values: [:custom], primary_key: true - field :key, :string, primary_key: true - field :typed_value, Ecto.JSONVariant - - timestamps() - end - - @doc false - def changeset(attributes, attrs) do - attributes - |> cast(attrs, [:namespace, :key, :typed_value]) - |> validate_required([:namespace, :key, :typed_value]) - |> validate_format(:key, ~r/[a-z0-9-_]+/) - end - - @doc false - def custom_attribute_changeset(attributes, attrs) do - changeset(attributes, attrs) - |> validate_inclusion(:namespace, [:custom]) - end -end diff --git a/backend/lib/edgehog_web/middleware/error_handler.ex b/backend/lib/edgehog_web/middleware/error_handler.ex deleted file mode 100644 index cc2f7b138..000000000 --- a/backend/lib/edgehog_web/middleware/error_handler.ex +++ /dev/null @@ -1,39 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2021 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 EdgehogWeb.Middleware.ErrorHandler do - @behaviour Absinthe.Middleware - - alias Edgehog.Error - - @impl true - def call(resolution, _config) do - errors = - resolution.errors - |> Enum.map(&Error.normalize/1) - |> List.flatten() - |> Enum.map(&to_absinthe_format/1) - - %{resolution | errors: errors} - end - - defp to_absinthe_format(%Error{} = error), do: Map.from_struct(error) - defp to_absinthe_format(error), do: error -end diff --git a/backend/lib/edgehog_web/resolvers/base_images.ex b/backend/lib/edgehog_web/resolvers/base_images.ex deleted file mode 100644 index 0dd4a5ac5..000000000 --- a/backend/lib/edgehog_web/resolvers/base_images.ex +++ /dev/null @@ -1,129 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2022-2023 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 EdgehogWeb.Resolvers.BaseImages do - alias Edgehog.BaseImages - alias Edgehog.BaseImages.BaseImage - alias Edgehog.BaseImages.BaseImageCollection - alias Edgehog.Devices - alias Edgehog.Devices.SystemModel - - def find_base_image_collection(args, _resolution) do - BaseImages.fetch_base_image_collection(args.id) - end - - def list_base_image_collections(_args, _resolution) do - base_image_collections = BaseImages.list_base_image_collections() - - {:ok, base_image_collections} - end - - def create_base_image_collection(attrs, _resolution) do - with {:ok, %SystemModel{} = system_model} <- - Devices.fetch_system_model(attrs.system_model_id), - {:ok, base_image_collection} <- - BaseImages.create_base_image_collection(system_model, attrs) do - {:ok, %{base_image_collection: base_image_collection}} - end - end - - def update_base_image_collection(attrs, _resolution) do - with {:ok, %BaseImageCollection{} = base_image_collection} <- - BaseImages.fetch_base_image_collection(attrs.base_image_collection_id), - {:ok, %BaseImageCollection{} = base_image_collection} <- - BaseImages.update_base_image_collection(base_image_collection, attrs) do - {:ok, %{base_image_collection: base_image_collection}} - end - end - - def delete_base_image_collection(args, _resolution) do - with {:ok, %BaseImageCollection{} = base_image_collection} <- - BaseImages.fetch_base_image_collection(args.base_image_collection_id), - {:ok, %BaseImageCollection{} = base_image_collection} <- - BaseImages.delete_base_image_collection(base_image_collection) do - {:ok, %{base_image_collection: base_image_collection}} - end - end - - def find_base_image(args, _resolution) do - BaseImages.fetch_base_image(args.id) - end - - def list_base_images_for_collection(%BaseImageCollection{} = collection, _args, _resolution) do - base_images = BaseImages.list_base_images_for_collection(collection) - - {:ok, base_images} - end - - def create_base_image(args, resolution) do - default_locale = resolution.context.current_tenant.default_locale - - with {:ok, %BaseImageCollection{} = collection} <- - BaseImages.fetch_base_image_collection(args.base_image_collection_id), - :ok <- ensure_default_locale(args[:description], default_locale), - :ok <- ensure_default_locale(args[:release_display_name], default_locale), - args = wrap_localized_field(args, :description), - args = wrap_localized_field(args, :release_display_name), - {:ok, %BaseImage{} = base_image} <- BaseImages.create_base_image(collection, args) do - {:ok, %{base_image: base_image}} - end - end - - def update_base_image(args, resolution) do - default_locale = resolution.context.current_tenant.default_locale - - with {:ok, %BaseImage{} = base_image} <- BaseImages.fetch_base_image(args.base_image_id), - :ok <- ensure_default_locale(args[:description], default_locale), - :ok <- ensure_default_locale(args[:release_display_name], default_locale), - args = wrap_localized_field(args, :description), - args = wrap_localized_field(args, :release_display_name), - {:ok, %BaseImage{} = base_image} <- BaseImages.update_base_image(base_image, args) do - {:ok, %{base_image: base_image}} - end - end - - def delete_base_image(args, _resolution) do - with {:ok, %BaseImage{} = base_image} <- BaseImages.fetch_base_image(args.base_image_id), - {:ok, %BaseImage{} = base_image} <- BaseImages.delete_base_image(base_image) do - {:ok, %{base_image: base_image}} - end - end - - # TODO: consider extracting all this functions dealing with locale wrapping/unwrapping - # in a dedicated resolver/helper module - - # Only allow localized input text that uses the tenant default locale - defp ensure_default_locale(nil, _default_locale), do: :ok - defp ensure_default_locale(%{locale: default_locale}, default_locale), do: :ok - defp ensure_default_locale(%{locale: _other}, _default), do: {:error, :not_default_locale} - - # If it's there, wraps a localized field in a map, as the context expects a map - defp wrap_localized_field(args, field) when is_map_key(args, field) do - case Map.fetch!(args, field) do - %{locale: locale, text: text} -> - Map.put(args, field, %{locale => text}) - - _ -> - args - end - end - - defp wrap_localized_field(args, _field), do: args -end diff --git a/backend/lib/edgehog_web/schema.ex b/backend/lib/edgehog_web/schema.ex index 4f80ee15f..4fe5525e6 100644 --- a/backend/lib/edgehog_web/schema.ex +++ b/backend/lib/edgehog_web/schema.ex @@ -20,9 +20,7 @@ defmodule EdgehogWeb.Schema do use Absinthe.Schema - use Absinthe.Relay.Schema, :modern import_types EdgehogWeb.Schema.AstarteTypes - import_types EdgehogWeb.Schema.VariantTypes import_types Absinthe.Plug.Types import_types Absinthe.Type.Custom @@ -37,79 +35,11 @@ defmodule EdgehogWeb.Schema do Edgehog.UpdateCampaigns ] - # TODO: remove define_relay_types?: false once we convert everything to Ash use AshGraphql, domains: @domains, - define_relay_types?: false, relay_ids?: true - alias EdgehogWeb.Resolvers - - node interface do - resolve_type fn - %Edgehog.BaseImages.BaseImage{}, _ -> - :base_image - - %Edgehog.BaseImages.BaseImageCollection{}, _ -> - :base_image_collection - - %Edgehog.Devices.Device{}, _ -> - :device - - %Edgehog.Devices.HardwareType{}, _ -> - :hardware_type - - %Edgehog.Devices.SystemModel{}, _ -> - :system_model - - %Edgehog.Groups.DeviceGroup{}, _ -> - :device_group - - %Edgehog.OSManagement.OTAOperation{}, _ -> - :ota_operation - - %Edgehog.UpdateCampaigns.UpdateCampaign{}, _ -> - :update_campaign - - %Edgehog.UpdateCampaigns.UpdateTarget{}, _ -> - :update_target - - _, _ -> - nil - end - end - query do - node field do - resolve fn - %{type: :base_image, id: id}, context -> - Resolvers.BaseImages.find_base_image(%{id: id}, context) - - %{type: :base_image_collection, id: id}, context -> - Resolvers.BaseImages.find_base_image_collection(%{id: id}, context) - - %{type: :device, id: id}, context -> - Resolvers.Devices.find_device(%{id: id}, context) - - %{type: :hardware_type, id: id}, context -> - Resolvers.Devices.find_hardware_type(%{id: id}, context) - - %{type: :system_model, id: id}, context -> - Resolvers.Devices.find_system_model(%{id: id}, context) - - %{type: :device_group, id: id}, context -> - Resolvers.Groups.find_device_group(%{id: id}, context) - - %{type: :ota_operation, id: id}, context -> - Resolvers.OSManagement.find_ota_operation(%{id: id}, context) - - %{type: :update_campaign, id: id}, context -> - Resolvers.UpdateCampaigns.find_update_campaign(%{id: id}, context) - - %{type: :update_target, id: id}, context -> - Resolvers.UpdateCampaigns.find_target(%{id: id}, context) - end - end end mutation do diff --git a/backend/lib/edgehog_web/schema/variant_types.ex b/backend/lib/edgehog_web/schema/variant_types.ex deleted file mode 100644 index a517143dc..000000000 --- a/backend/lib/edgehog_web/schema/variant_types.ex +++ /dev/null @@ -1,92 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2022 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 EdgehogWeb.Schema.VariantTypes do - use Absinthe.Schema.Notation - - @supported_types [ - :double, - :integer, - :boolean, - :longinteger, - :string, - :binaryblob, - :datetime - ] - - enum :variant_type do - @desc "Double type" - value :double - @desc "32 bit integer type" - value :integer - @desc "Boolean type" - value :boolean - - @desc """ - 64 bit integer type. When this is the type, the value will be a string representing the number. - This is done to avoid representation errors when using JSON Numbers. - """ - value :longinteger - @desc "String type" - value :string - @desc "Binary blob type. When this is the type, the value will be Base64 encoded." - value :binaryblob - @desc "Datetime type. When this is the type, the value will be an ISO8601 timestamp." - value :datetime - end - - @desc """ - A variant value. It can contain any JSON value. The value will be checked together with the - type to verify whether it's valid. - """ - scalar :variant_value, name: "VariantValue" do - # We encode and decode values as-is, proper encoding/decoding and validation will be handled - # one level higher when both the type and the value are available at once. - # See encode/2 for encoding and the Ecto.JSONVariant module for decoding. - serialize &Function.identity/1 - parse &decode_variant_value/1 - end - - # Handle all scalar JSON types and decode them as-is - defp decode_variant_value(%Absinthe.Blueprint.Input.Float{value: value}), do: {:ok, value} - defp decode_variant_value(%Absinthe.Blueprint.Input.Integer{value: value}), do: {:ok, value} - defp decode_variant_value(%Absinthe.Blueprint.Input.String{value: value}), do: {:ok, value} - defp decode_variant_value(%Absinthe.Blueprint.Input.Null{}), do: {:ok, nil} - defp decode_variant_value(_), do: :error - - # Handle encoding with type + value - # :binaryblob gets converted to base64 - def encode_variant_value(:binaryblob, value) when is_binary(value) do - {:ok, Base.encode64(value)} - end - - # :datetime gets converted to ISO8601 - def encode_variant_value(:datetime, %DateTime{} = value) do - {:ok, DateTime.to_iso8601(value)} - end - - # :longinteger gets converted to string to avoid JSON representation problems - def encode_variant_value(:longinteger, value) when is_integer(value), - do: {:ok, to_string(value)} - - # Everything else is encoded as itself - def encode_variant_value(type, value) when type in @supported_types, do: {:ok, value} - def encode_variant_value(_type, _value), do: {:error, :unsupported_type} -end diff --git a/backend/test/support/mocks/astarte/device/ota_request/v0.ex b/backend/priv/repo/migrations/20240703090248_drop_device_attributes.exs similarity index 70% rename from backend/test/support/mocks/astarte/device/ota_request/v0.ex rename to backend/priv/repo/migrations/20240703090248_drop_device_attributes.exs index 08d91884f..3bc25973d 100644 --- a/backend/test/support/mocks/astarte/device/ota_request/v0.ex +++ b/backend/priv/repo/migrations/20240703090248_drop_device_attributes.exs @@ -1,7 +1,7 @@ # # This file is part of Edgehog. # -# Copyright 2023 SECO Mind Srl +# Copyright 2024 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. @@ -18,13 +18,10 @@ # SPDX-License-Identifier: Apache-2.0 # -defmodule Edgehog.Mocks.Astarte.Device.OTARequest.V0 do - @behaviour Edgehog.Astarte.Device.OTARequest.V0.Behaviour +defmodule Edgehog.Repo.Migrations.DropDeviceAttributes do + use Ecto.Migration - alias Astarte.Client.AppEngine - - @impl true - def post(%AppEngine{} = _client, _device_id, _uuid, _url) do - :ok + def change do + drop table(:device_attributes) end end diff --git a/backend/test/ecto/json_variant_test.exs b/backend/test/ecto/json_variant_test.exs deleted file mode 100644 index 8f0cb60c5..000000000 --- a/backend/test/ecto/json_variant_test.exs +++ /dev/null @@ -1,242 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2022 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 Ecto.JSONVariantTest do - use ExUnit.Case - alias Ecto.JSONVariant - - import Ecto.Changeset - - @types %{ - variant: JSONVariant - } - - describe "cast/1 with double type" do - test "correctly handles a double" do - assert {:ok, %{variant: %JSONVariant{type: :double, value: 42.0}}} == - cast_and_apply(%{"variant" => %{"type" => "double", "value" => 42.0}}) - end - - test "correctly handles an integer" do - assert {:ok, %{variant: %JSONVariant{type: :double, value: 42.0}}} === - cast_and_apply(%{"variant" => %{"type" => "double", "value" => 42}}) - end - - test "correctly handles a string" do - assert {:ok, %{variant: %JSONVariant{type: :double, value: 42.0}}} === - cast_and_apply(%{"variant" => %{"type" => "double", "value" => "42"}}) - end - - test "fails with invalid value" do - assert {:error, %Ecto.Changeset{}} = - cast_and_apply(%{"variant" => %{"type" => "double", "value" => "foobar"}}) - end - end - - describe "cast/1 with integer type" do - test "correctly handles an integer" do - assert {:ok, %{variant: %JSONVariant{type: :integer, value: 42}}} == - cast_and_apply(%{"variant" => %{"type" => "integer", "value" => 42}}) - end - - test "correctly handles a string" do - assert {:ok, %{variant: %JSONVariant{type: :integer, value: 42}}} == - cast_and_apply(%{"variant" => %{"type" => "integer", "value" => "42"}}) - end - - test "fails with invalid value" do - assert {:error, %Ecto.Changeset{}} = - cast_and_apply(%{"variant" => %{"type" => "integer", "value" => "foobar"}}) - end - - test "fails with out of range value" do - assert {:error, %Ecto.Changeset{}} = - cast_and_apply(%{"variant" => %{"type" => "integer", "value" => 2_000_000_000_000}}) - end - end - - describe "cast/1 with boolean type" do - test "correctly handles a boolean" do - assert {:ok, %{variant: %JSONVariant{type: :boolean, value: true}}} == - cast_and_apply(%{"variant" => %{"type" => "boolean", "value" => true}}) - end - - test "correctly handles a string" do - assert {:ok, %{variant: %JSONVariant{type: :boolean, value: true}}} == - cast_and_apply(%{"variant" => %{"type" => "boolean", "value" => "true"}}) - end - - test "correctly handles an integer string" do - assert {:ok, %{variant: %JSONVariant{type: :boolean, value: true}}} == - cast_and_apply(%{"variant" => %{"type" => "boolean", "value" => "1"}}) - end - - test "fails with invalid value" do - assert {:error, %Ecto.Changeset{}} = - cast_and_apply(%{"variant" => %{"type" => "boolean", "value" => "foobar"}}) - end - end - - describe "cast/1 with longinteger type" do - test "correctly handles an longinteger" do - assert {:ok, %{variant: %JSONVariant{type: :longinteger, value: 42}}} == - cast_and_apply(%{"variant" => %{"type" => "longinteger", "value" => 42}}) - end - - test "correctly handles a string" do - assert {:ok, %{variant: %JSONVariant{type: :longinteger, value: 42}}} == - cast_and_apply(%{"variant" => %{"type" => "longinteger", "value" => "42"}}) - end - - test "fails with invalid value" do - assert {:error, %Ecto.Changeset{}} = - cast_and_apply(%{"variant" => %{"type" => "longinteger", "value" => "foobar"}}) - end - - test "fails with out of range value" do - assert {:error, %Ecto.Changeset{}} = - cast_and_apply(%{ - "variant" => %{"type" => "longinteger", "value" => 0x1_FFFF_FFFF_FFFF_FFFF} - }) - end - end - - describe "cast/1 with string type" do - test "correctly handles a valid string" do - assert {:ok, %{variant: %JSONVariant{type: :string, value: "hello world"}}} == - cast_and_apply(%{"variant" => %{"type" => "string", "value" => "hello world"}}) - end - - test "fails with invalid string" do - assert {:error, %Ecto.Changeset{}} = - cast_and_apply(%{"variant" => %{"type" => "string", "value" => <<128>>}}) - end - - test "fails with invalid value" do - assert {:error, %Ecto.Changeset{}} = - cast_and_apply(%{"variant" => %{"type" => "string", "value" => 42}}) - end - end - - describe "cast/1 with binaryblob type" do - test "correctly handles base64" do - assert {:ok, %{variant: %JSONVariant{type: :binaryblob, value: <<128>>}}} == - cast_and_apply(%{"variant" => %{"type" => "binaryblob", "value" => "gA=="}}) - end - - test "fails with invalid value" do - assert {:error, %Ecto.Changeset{}} = - cast_and_apply(%{"variant" => %{"type" => "binaryblob", "value" => <<128>>}}) - end - end - - describe "cast/1 with datetime type" do - test "correctly handles an ISO8601 timestamp" do - assert {:ok, %{variant: %JSONVariant{type: :datetime, value: %DateTime{}}}} = - cast_and_apply(%{ - "variant" => %{"type" => "datetime", "value" => "2022-06-08T14:30:33.167352Z"} - }) - end - - test "correctly handles a DateTime" do - assert {:ok, %{variant: %JSONVariant{type: :datetime, value: %DateTime{}}}} = - cast_and_apply(%{ - "variant" => %{"type" => "datetime", "value" => DateTime.utc_now()} - }) - end - - test "fails with invalid value" do - assert {:error, %Ecto.Changeset{}} = - cast_and_apply(%{"variant" => %{"type" => "datetime", "value" => "foobar"}}) - end - end - - describe "dump and load" do - test "roundtrip for double" do - {:ok, %{variant: value}} = - cast_and_apply(%{"variant" => %{"type" => "double", "value" => 42.0}}) - - assert value == dump_load_roundtrip(value) - end - - test "roundtrip for integer" do - {:ok, %{variant: value}} = - cast_and_apply(%{"variant" => %{"type" => "integer", "value" => 42}}) - - assert value == dump_load_roundtrip(value) - end - - test "roundtrip for boolean" do - {:ok, %{variant: value}} = - cast_and_apply(%{"variant" => %{"type" => "boolean", "value" => true}}) - - assert value == dump_load_roundtrip(value) - end - - test "roundtrip for longinteger" do - {:ok, %{variant: value}} = - cast_and_apply(%{"variant" => %{"type" => "longinteger", "value" => 42}}) - - assert value == dump_load_roundtrip(value) - end - - test "roundtrip for string" do - {:ok, %{variant: value}} = - cast_and_apply(%{"variant" => %{"type" => "string", "value" => "hello"}}) - - assert value == dump_load_roundtrip(value) - end - - test "roundtrip for binaryblob" do - {:ok, %{variant: value}} = - cast_and_apply(%{"variant" => %{"type" => "binaryblob", "value" => "ZWRnZWhvZw=="}}) - - assert value == dump_load_roundtrip(value) - end - - test "roundtrip for datetime" do - {:ok, %{variant: value}} = - cast_and_apply(%{"variant" => %{"type" => "datetime", "value" => DateTime.utc_now()}}) - - assert value == dump_load_roundtrip(value) - end - end - - def cast_and_apply(params) do - {%{}, @types} - |> cast(params, Map.keys(@types)) - |> apply_action(:insert) - end - - def dump_load_roundtrip(value) do - {:ok, dumped_value} = JSONVariant.dump(value) - - {:ok, loaded_value} = - dumped_value - |> to_string_keys() - |> JSONVariant.load() - - loaded_value - end - - def to_string_keys(%{t: t, v: v} = _dumped_value) do - %{"t" => t, "v" => v} - end -end diff --git a/backend/test/support/astarte_mock_case.ex b/backend/test/support/astarte_mock_case.ex index 08c25bbfd..4e3624125 100644 --- a/backend/test/support/astarte_mock_case.ex +++ b/backend/test/support/astarte_mock_case.ex @@ -63,11 +63,6 @@ defmodule Edgehog.AstarteMockCase do Edgehog.Mocks.Astarte.Device.OSInfo ) - Mox.stub_with( - Edgehog.Astarte.Device.OTARequestV0Mock, - Edgehog.Mocks.Astarte.Device.OTARequest.V0 - ) - Mox.stub_with( Edgehog.Astarte.Device.OTARequestV1Mock, Edgehog.Mocks.Astarte.Device.OTARequest.V1 diff --git a/backend/test/support/mocks.ex b/backend/test/support/mocks.ex index 16c7f2813..145b2063a 100644 --- a/backend/test/support/mocks.ex +++ b/backend/test/support/mocks.ex @@ -34,10 +34,6 @@ Mox.defmock(Edgehog.Astarte.Device.OSInfoMock, for: Edgehog.Astarte.Device.OSInfo.Behaviour ) -Mox.defmock(Edgehog.Astarte.Device.OTARequestV0Mock, - for: Edgehog.Astarte.Device.OTARequest.V0.Behaviour -) - Mox.defmock(Edgehog.Astarte.Device.OTARequestV1Mock, for: Edgehog.Astarte.Device.OTARequest.V1.Behaviour )