From 3680cbdc03974bdbf9820dcb73953283a2e7e725 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 7 Oct 2025 17:00:01 +0300 Subject: [PATCH] Support of displayName entity field --- .../controller/EntityQueryControllerTest.java | 155 +++++++++++++++++- .../dao/sql/query/EntityKeyMapping.java | 27 ++- 2 files changed, 173 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 93d67ccd4b..0c40c2f868 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -77,6 +77,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -1068,6 +1069,119 @@ public class EntityQueryControllerTest extends AbstractControllerTest { countByQueryAndCheck(customerEntitiesQuery, 0); } + @Test + public void testFindDevicesByDisplayName() throws Exception { + loginTenantAdmin(); + int numOfDevices = 3; + + for (int i = 0; i < numOfDevices; i++) { + Device device = new Device(); + String name = "Device" + i; + device.setName(name); + device.setLabel("Device Label " + i); + device.setType("testFindDevicesByDisplayName"); + + Device savedDevice = doPost("/api/device?accessToken=" + name, device, Device.class); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("testFindDevicesByDisplayName")); + filter.setDeviceNameFilter(""); + + KeyFilter displayNameFilter = getEntityFieldEqualFilter("displayName", "Device Label " + 0); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName")); + + // all devices with ownerName = TEST TENANT + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), Collections.emptyList()); + checkEntitiesByQuery(query, numOfDevices, (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Device" + i, name); + Assert.assertEquals("Device Label " + i, displayName); + }); + + // all devices with ownerName = TEST TENANT + EntityDataQuery displayNameFilterQuery = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(displayNameFilter)); + checkEntitiesByQuery(displayNameFilterQuery, 1, (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Device" + i, name); + Assert.assertEquals("Device Label " + i, displayName); + }); + } + + @Test + public void testFindUsersByDisplayName() throws Exception { + loginTenantAdmin(); + + User userA = new User(); + userA.setAuthority(Authority.TENANT_ADMIN); + userA.setFirstName("John"); + userA.setLastName("Doe"); + userA.setEmail("john.doe@tb.org"); + userA = doPost("/api/user", userA, User.class); + var aId = userA.getId(); + + User userB = new User(); + userB.setAuthority(Authority.TENANT_ADMIN); + userB.setFirstName("John"); + userB.setEmail("john@tb.org"); + userB = doPost("/api/user", userB, User.class); + var bId = userB.getId(); + + User userC = new User(); + userC.setAuthority(Authority.TENANT_ADMIN); + userC.setLastName("Doe"); + userC.setEmail("doe@tb.org"); + userC = doPost("/api/user", userC, User.class); + var cId = userC.getId(); + + User userD = new User(); + userD.setAuthority(Authority.TENANT_ADMIN); + userD.setEmail("noname@tb.org"); + userD = doPost("/api/user", userD, User.class); + var dId = userD.getId(); + + EntityTypeFilter filter = new EntityTypeFilter(); + filter.setEntityType(EntityType.USER); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John Doe"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(aId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("John Doe", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(bId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("John", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "Doe"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(cId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Doe", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "noname@tb.org"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(dId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("noname@tb.org", displayName); + }); + } + @Test public void testFindDevicesByOwnerNameAndOwnerType() throws Exception { loginTenantAdmin(); @@ -1105,19 +1219,30 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // all devices with ownerName = TEST TENANT EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); - checkEntitiesByQuery(query, numOfDevices, TEST_TENANT_NAME, "TENANT"); + BiConsumer checkFunction = (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String ownerName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerName", new TsValue(0, "Invalid")).getValue(); + String ownerType = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerType", new TsValue(0, "Invalid")).getValue(); + String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue(); + + Assert.assertEquals("Device" + i, name); + Assert.assertEquals(TEST_TENANT_NAME, ownerName); + Assert.assertEquals("TENANT", ownerType); + Assert.assertEquals("1" + i, alarmActiveTime); + }; + checkEntitiesByQuery(query, numOfDevices, checkFunction); // all devices with wrong ownerName EntityDataQuery wrongTenantNameQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter)); - checkEntitiesByQuery(wrongTenantNameQuery, 0, null, null); + checkEntitiesByQuery(wrongTenantNameQuery, 0, null); // all devices with owner type = TENANT EntityDataQuery tenantEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter)); - checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, TEST_TENANT_NAME, "TENANT"); + checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, checkFunction); // all devices with owner type = CUSTOMER EntityDataQuery customerEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter)); - checkEntitiesByQuery(customerEntitiesQuery, 0, null, null); + checkEntitiesByQuery(customerEntitiesQuery, 0, null); } @Test @@ -1163,6 +1288,28 @@ public class EntityQueryControllerTest extends AbstractControllerTest { findByQueryAndCheck(query, 0); } + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer checkFunction) throws Exception { + await() + .alias("data by query") + .atMost(30, TimeUnit.SECONDS) + .until(() -> { + var data = findByQuery(query); + var loadedEntities = new ArrayList<>(data.getData()); + return loadedEntities.size() == expectedNumOfDevices; + }); + if (expectedNumOfDevices == 0) { + return; + } + var data = findByQuery(query); + var loadedEntities = new ArrayList<>(data.getData()); + + Assert.assertEquals(expectedNumOfDevices, loadedEntities.size()); + + for (int i = 0; i < expectedNumOfDevices; i++) { + checkFunction.accept(i, loadedEntities.get(i)); + } + } + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception { await() .alias("data by query") diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 9755201fe9..dda2cf127d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -65,6 +65,7 @@ public class EntityKeyMapping { public static final String NAME = "name"; public static final String TYPE = "type"; public static final String LABEL = "label"; + public static final String DISPLAY_NAME = "displayName"; public static final String FIRST_NAME = "firstName"; public static final String LAST_NAME = "lastName"; public static final String EMAIL = "email"; @@ -83,6 +84,8 @@ public class EntityKeyMapping { public static final String SERVICE_ID = "serviceId"; public static final String OWNER_NAME = "ownerName"; public static final String OWNER_TYPE = "ownerType"; + public static final String LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY = "COALESCE (e." + LABEL + ", e." + NAME+")"; + public static final String USER_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(TRIM(CONCAT_WS(' ', e.first_name, e.last_name)), ''), e.email)"; public static final String OWNER_NAME_SELECT_QUERY = "case when e.customer_id = '" + NULL_UUID + "' " + "then (select title from tenant where id = e.tenant_id) " + "else (select title from customer where id = e.customer_id) end"; @@ -94,6 +97,16 @@ public class EntityKeyMapping { OWNER_NAME, OWNER_NAME_SELECT_QUERY, OWNER_TYPE, OWNER_TYPE_SELECT_QUERY ); + public static final Map labeledPropertiesFunctions = Map.of( + OWNER_NAME, OWNER_NAME_SELECT_QUERY, + OWNER_TYPE, OWNER_TYPE_SELECT_QUERY, + DISPLAY_NAME, LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY + ); + public static final Map userPropertiesFunctions = Map.of( + OWNER_NAME, OWNER_NAME_SELECT_QUERY, + OWNER_TYPE, OWNER_TYPE_SELECT_QUERY, + DISPLAY_NAME, USER_DISPLAY_NAME_SELECT_QUERY + ); public static final Map queueStatsPropertiesFunctions = Map.of(NAME, QUEUE_STATS_NAME_QUERY); public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO); @@ -153,20 +166,24 @@ public class EntityKeyMapping { Map contactBasedAliases = new HashMap<>(); contactBasedAliases.put(NAME, TITLE); contactBasedAliases.put(LABEL, TITLE); + contactBasedAliases.put(DISPLAY_NAME, TITLE); aliases.put(EntityType.TENANT, contactBasedAliases); aliases.put(EntityType.CUSTOMER, contactBasedAliases); aliases.put(EntityType.DASHBOARD, contactBasedAliases); + Map deviceAndAssetAliases = new HashMap<>(); + deviceAndAssetAliases.put(TITLE, NAME); + aliases.put(EntityType.DEVICE, deviceAndAssetAliases); + aliases.put(EntityType.ASSET, deviceAndAssetAliases); Map commonEntityAliases = new HashMap<>(); commonEntityAliases.put(TITLE, NAME); - aliases.put(EntityType.DEVICE, commonEntityAliases); - aliases.put(EntityType.ASSET, commonEntityAliases); + commonEntityAliases.put(DISPLAY_NAME, NAME); aliases.put(EntityType.ENTITY_VIEW, commonEntityAliases); aliases.put(EntityType.WIDGETS_BUNDLE, commonEntityAliases); - propertiesFunctions.put(EntityType.DEVICE, ownerPropertiesFunctions); - propertiesFunctions.put(EntityType.ASSET, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.DEVICE, labeledPropertiesFunctions); + propertiesFunctions.put(EntityType.ASSET, labeledPropertiesFunctions); propertiesFunctions.put(EntityType.ENTITY_VIEW, ownerPropertiesFunctions); - propertiesFunctions.put(EntityType.USER, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.USER, userPropertiesFunctions); propertiesFunctions.put(EntityType.DASHBOARD, ownerPropertiesFunctions); propertiesFunctions.put(EntityType.QUEUE_STATS, queueStatsPropertiesFunctions);