feat(registry): add IoT device connection profile (iot_profiles table)

New optional 1:1 extension to services: providers can declare how IoT
devices connect to their hub (hub_url, protocols, provisioning endpoint,
device classes, firmware compatibility) via PATCH /services/{id}.

- New entity IotProfileEntity + Liquibase changeset 009 (GIN indexes for
  jsonb device_classes and protocols arrays)
- IotProtocol / CredentialFormat enums; IotProfileRequest / IotProfileResponse DTOs
- JsonStringListConverter for jsonb List<String> persistence
- GET /services/{id}/replacements extended with iotProfile per candidate
  and new filter params: iotReady=true, deviceClass=..., protocol=...
- 6 new BDD scenarios (IotProfileCucumberTest) covering profile creation,
  candidate enrichment, iotReady / deviceClass filtering, partial update,
  and missing-field 422 validation
- All 57 tests green (6 new + 27 existing transition + 24 unit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Carsten Rehfeld
2026-05-08 16:12:13 +02:00
parent b2a16a8be7
commit 5156089152
19 changed files with 537 additions and 12 deletions
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<changeSet id="009" author="apix">
<!-- Optional IoT device-connection profile attached to a registered service.
A service without an iot_profiles row is a generic API service with no
declared device-onboarding support. A service with a row is discoverable
by IoT devices polling for a cloud-sunset replacement target. -->
<createTable tableName="iot_profiles">
<column name="id" type="uuid" defaultValueComputed="gen_random_uuid()">
<constraints primaryKey="true" nullable="false"/>
</column>
<!-- Owning FK — one profile per service, deleted with the service -->
<column name="service_id" type="uuid">
<constraints nullable="false" unique="true"
foreignKeyName="fk_iot_profile_service"
references="services(id)"
deleteCascade="true"/>
</column>
<!-- Device connection -->
<!-- WebSocket / MQTT broker / CoAP endpoint the device connects to -->
<column name="hub_url" type="varchar(512)">
<constraints nullable="false"/>
</column>
<!-- JSONB array of IotProtocol enum values, e.g. ["MQTT_5_0","WEBSOCKET"] -->
<column name="protocols" type="jsonb">
<constraints nullable="false"/>
</column>
<column name="tls_min_version" type="varchar(10)" defaultValue="1.2">
<constraints nullable="false"/>
</column>
<!-- Provisioning — device calls this URL directly; APIX never sees device identity -->
<column name="provisioning_endpoint" type="varchar(512)"/>
<!-- CredentialFormat enum: JWT | PSK | X509 | NONE -->
<column name="credential_format" type="varchar(30)"/>
<!-- Which device field is presented at provisioning (serialNumber, mac, deviceEui) -->
<column name="device_id_field" type="varchar(100)"/>
<!-- Device class targeting — same capability taxonomy as GET /services?capability -->
<!-- JSONB array of class identifiers, e.g. ["device.class.smart-home-hub"] -->
<column name="device_classes" type="jsonb">
<constraints nullable="false"/>
</column>
<!-- Migration aid -->
<column name="min_firmware_version" type="varchar(50)"/>
<column name="firmware_update_url" type="varchar(512)"/>
<!-- Estimated wall-clock minutes to complete the migration (for UX display) -->
<column name="estimated_migration_minutes" type="integer"/>
<column name="created_at" type="timestamptz" defaultValueComputed="now()">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="timestamptz" defaultValueComputed="now()">
<constraints nullable="false"/>
</column>
</createTable>
<!-- Liquibase createIndex does not support PostgreSQL-specific index types;
GIN must be declared via raw SQL for jsonb containment queries. -->
<sql>CREATE INDEX idx_iot_device_classes ON iot_profiles USING gin(device_classes)</sql>
<sql>CREATE INDEX idx_iot_protocols ON iot_profiles USING gin(protocols)</sql>
</changeSet>
</databaseChangeLog>
@@ -13,5 +13,6 @@
<include file="changes/006-sunset-lock.xml" relativeToChangelogFile="true"/>
<include file="changes/007-service-replacements.xml" relativeToChangelogFile="true"/>
<include file="changes/008-sunset-at.xml" relativeToChangelogFile="true"/>
<include file="changes/009-iot-profiles.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>