From 4152cd95550a23484c7d50a4efde1e7d2af72cc0 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 3 Sep 2025 15:51:15 +0300 Subject: [PATCH 01/18] updated java rest client with missing methods --- .../msa/connectivity/RestClientTest.java | 78 ++++++ .../thingsboard/rest/client/RestClient.java | 265 +++++++++++++++++- 2 files changed, 337 insertions(+), 6 deletions(-) create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java new file mode 100644 index 0000000000..e7a9fa3acb --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java @@ -0,0 +1,78 @@ +package org.thingsboard.server.msa.connectivity; + +import org.springframework.web.client.RestTemplate; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.rest.client.RestClient; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.msa.AbstractContainerTest; +import org.thingsboard.server.msa.TestProperties; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; + +public class RestClientTest extends AbstractContainerTest { + + private static final RestClient restClient = new RestClient(new RestTemplate(), TestProperties.getBaseUrl()); + + @BeforeMethod + public void setUp() throws Exception { + restClient.login("tenant@thingsboard.org", "tenant"); + } + + @AfterMethod + public void tearDown() { + } + + @Test + public void testGetAlarmsV2() { + Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5))); + assertThat(device).isNotNull(); + + String type = "High temp" + RandomStringUtils.randomAlphabetic(5); + Alarm alarm = Alarm.builder() + .originator(device.getId()) + .severity(AlarmSeverity.CRITICAL) + .type(type) + .build(); + restClient.saveAlarm(alarm); + + // get /api/v2/alarm + PageData alarmsV2 = restClient.getAlarmsV2(device.getId(), null, null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(alarmsV2.getData()).hasSize(1); + + PageData activeAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(activeAlarms.getData()).hasSize(1); + + PageData cleared = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(cleared.getData()).hasSize(0); + + PageData activeAndClearedAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACTIVE), null, null, null, new TimePageLink(10, 0)); + assertThat(activeAndClearedAlarms.getData()).hasSize(1); + + // get /api/v2/alarms + PageData allAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(allAlarmsV2.getData()).hasSize(1); + + PageData allClearedAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(allClearedAlarmsV2.getData()).hasSize(0); + + // get /api/alarms + PageData allAlarms = restClient.getAllAlarms(AlarmSearchStatus.ACTIVE, null, new TimePageLink(10, 0), null); + assertThat(allAlarms.getData()).hasSize(1); + + PageData allClearedAlarms = restClient.getAllAlarms(AlarmSearchStatus.CLEARED, null, new TimePageLink(10, 0), null); + assertThat(allClearedAlarms.getData()).hasSize(0); + + } +} diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 0e559efd46..a80216c7ec 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -113,6 +113,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.NotificationId; +import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.id.OtaPackageId; @@ -132,6 +134,13 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestInfo; +import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; @@ -509,6 +518,99 @@ public class RestClient implements Closeable { params).getBody(); } + public PageData getAllAlarms(AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) { + String urlSecondPart = "/api/alarms?"; + Map params = new HashMap<>(); + if (fetchOriginator != null) { + params.put("fetchOriginator", String.valueOf(fetchOriginator)); + urlSecondPart += "&fetchOriginator={fetchOriginator}"; + } + if (searchStatus != null) { + params.put("searchStatus", searchStatus.name()); + urlSecondPart += "&searchStatus={searchStatus}"; + } + if (status != null) { + params.put("status", status.name()); + urlSecondPart += "&status={status}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public PageData getAlarmsV2(EntityId entityId, List statusList, List severityList, + List typeList, String assignedId, TimePageLink pageLink) { + String urlSecondPart = "/api/v2/alarm/{entityType}/{entityId}?"; + Map params = new HashMap<>(); + params.put("entityType", entityId.getEntityType().name()); + params.put("entityId", entityId.getId().toString()); + if (!CollectionUtils.isEmpty(statusList)) { + params.put("statusList", listEnumToString(statusList)); + urlSecondPart += "&statusList={statusList}"; + } + if (!CollectionUtils.isEmpty(severityList)) { + params.put("severityList", listEnumToString(severityList)); + urlSecondPart += "&severityList={severityList}"; + } + if (!CollectionUtils.isEmpty(typeList)) { + params.put("typeList", String.join(",", typeList)); + urlSecondPart += "&typeList={typeList}"; + } + if (assignedId != null) { + params.put("assignedId", assignedId); + urlSecondPart += "&assignedId={assignedId}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public PageData getAllAlarmsV2(List statusList, List severityList, + List typeList, String assignedId, TimePageLink pageLink) { + String urlSecondPart = "/api/v2/alarms?"; + Map params = new HashMap<>(); + if (!CollectionUtils.isEmpty(statusList)) { + params.put("statusList", listEnumToString(statusList)); + urlSecondPart += "&statusList={statusList}"; + } + if (!CollectionUtils.isEmpty(severityList)) { + params.put("severityList", listEnumToString(severityList)); + urlSecondPart += "&severityList={severityList}"; + } + if (!CollectionUtils.isEmpty(typeList)) { + params.put("typeList", String.join(",", typeList)); + urlSecondPart += "&typeList={typeList}"; + } + if (assignedId != null) { + params.put("assignedId", assignedId); + urlSecondPart += "&assignedId={assignedId}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + public Optional getHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); @@ -1710,6 +1812,14 @@ public class RestClient implements Closeable { }).getBody(); } + public JsonNode findEntityTimeseriesAndAttributesKeysByQuery(EntityDataQuery query) { + return restTemplate.exchange( + baseURL + "/api/entitiesQuery/find/keys", + HttpMethod.POST, new HttpEntity<>(query), + new ParameterizedTypeReference() { + }).getBody(); + } + public PageData findAlarmDataByQuery(AlarmDataQuery query) { return restTemplate.exchange( baseURL + "/api/alarmsQuery/find", @@ -2158,9 +2268,9 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId()); } - public PageData getTenantDomainInfos() { + public PageData getTenantDomainInfos(PageLink pageLink) { return restTemplate.exchange( - baseURL + "/api/domain/infos", + baseURL + "/api/domain/infos?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2192,9 +2302,9 @@ public class RestClient implements Closeable { restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId()); } - public PageData getTenantMobileApps() { + public PageData getTenantMobileApps(PageLink pageLink) { return restTemplate.exchange( - baseURL + "/api/mobile/app", + baseURL + "/api/mobile/app?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2222,9 +2332,9 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/mobile/app/{id}", mobileAppId.getId()); } - public PageData getTenantMobileBundleInfos() { + public PageData getTenantMobileBundleInfos(PageLink pageLink) { return restTemplate.exchange( - baseURL + "/api/mobile/bundle/infos", + baseURL + "/api/mobile/bundle/infos?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2846,6 +2956,17 @@ public class RestClient implements Closeable { }, params).getBody(); } + public PageData getUsersByQuery(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/users/info?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + public PageData getTenantAdmins(TenantId tenantId, PageLink pageLink) { Map params = new HashMap<>(); params.put("tenantId", tenantId.getId().toString()); @@ -4144,6 +4265,138 @@ public class RestClient implements Closeable { } } + public PageData getNotifications(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/notifications?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Integer getUnreadNotificationsCount(NotificationDeliveryMethod deliveryMethod) { + String uri = "/api/notifications/unread/count?"; + Map params = new HashMap<>(); + if (deliveryMethod != null) { + params.put("deliveryMethod", deliveryMethod.name()); + uri += "&deliveryMethod={deliveryMethod}"; + } + return restTemplate.exchange( + baseURL + uri, + HttpMethod.GET, + HttpEntity.EMPTY, + Integer.class, params).getBody(); + } + + public void markNotificationAsRead(NotificationId notificationId) { + restTemplate.exchange( + baseURL + "/api/notification/{id}/read", + HttpMethod.PUT, + HttpEntity.EMPTY, + Void.class, + notificationId.getId()); + } + + public void markAllNotificationsAsRead(NotificationDeliveryMethod deliveryMethod) { + String uri = "/api/notifications/read?"; + Map params = new HashMap<>(); + if (deliveryMethod != null) { + params.put("deliveryMethod", deliveryMethod.name()); + uri += "&deliveryMethod={deliveryMethod}"; + } + restTemplate.exchange( + baseURL + uri, + HttpMethod.PUT, + HttpEntity.EMPTY, + Void.class); + } + + + public void deleteNotification(NotificationId notificationId) { + restTemplate.delete(baseURL + "/api/notification/{id}", notificationId.getId()); + } + + public NotificationRequest createNotificationRequest(NotificationRequest notificationRequest) { + return restTemplate.postForEntity(baseURL + "/api/notification/request", notificationRequest, NotificationRequest.class).getBody(); + } + + public NotificationRequestPreview getNotificationRequestPreview(NotificationRequest notificationRequest, int recipientsPreviewSize) { + return restTemplate.postForEntity(baseURL + "/api/notification/request/preview?recipientsPreviewSize={recipientsPreviewSize}", notificationRequest, NotificationRequestPreview.class, recipientsPreviewSize).getBody(); + } + + public Optional getNotificationRequestById(NotificationRequestId notificationRequestId) { + try { + ResponseEntity notificationRequest = restTemplate.getForEntity(baseURL + "/api/notification/request/{id}", NotificationRequestInfo.class, notificationRequestId.getId()); + return Optional.ofNullable(notificationRequest.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getNotificationRequests(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/notification/requests?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public void deleteNotificationRequest(NotificationRequestId notificationRequestId) { + restTemplate.delete(baseURL + "/api/notification/request/{id}", notificationRequestId.getId()); + } + + public NotificationSettings saveNotificationSettings(NotificationSettings notificationSettings) { + return restTemplate.postForEntity(baseURL + "/api/notification/settings", notificationSettings, NotificationSettings.class).getBody(); + } + + public Optional getNotificationSettings() { + try { + ResponseEntity notificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings", NotificationSettings.class); + return Optional.ofNullable(notificationSettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public List getAvailableDeliveryMethods() { + return restTemplate.exchange(URI.create( + baseURL + "/api/notification/deliveryMethods"), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public UserNotificationSettings saveUserNotificationSettings(UserNotificationSettings userNotificationSettings) { + return restTemplate.postForEntity(baseURL + "/api/notification/settings/user", userNotificationSettings, UserNotificationSettings.class).getBody(); + } + + public Optional getUserNotificationSettings() { + try { + ResponseEntity userNotificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings/user", UserNotificationSettings.class); + return Optional.ofNullable(userNotificationSettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + private String getTimeUrlParams(TimePageLink pageLink) { String urlParams = getUrlParams(pageLink); if (pageLink.getStartTime() != null) { From a2afc6f1b206148b10da5a18f1c39003ebd9f954 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 3 Sep 2025 16:06:01 +0300 Subject: [PATCH 02/18] fixed methods missing page link params --- .../main/java/org/thingsboard/rest/client/RestClient.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index a80216c7ec..0725818f43 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -2269,6 +2269,8 @@ public class RestClient implements Closeable { } public PageData getTenantDomainInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/domain/infos?" + getUrlParams(pageLink), HttpMethod.GET, @@ -2303,6 +2305,8 @@ public class RestClient implements Closeable { } public PageData getTenantMobileApps(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/mobile/app?" + getUrlParams(pageLink), HttpMethod.GET, @@ -2333,6 +2337,8 @@ public class RestClient implements Closeable { } public PageData getTenantMobileBundleInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( baseURL + "/api/mobile/bundle/infos?" + getUrlParams(pageLink), HttpMethod.GET, From a66141308d2b9fa32467dfccb12721cb26e15d3f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 10 Sep 2025 18:01:40 +0300 Subject: [PATCH 03/18] fixed license --- .../server/msa/connectivity/RestClientTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java index e7a9fa3acb..6c470188c0 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * 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. + */ package org.thingsboard.server.msa.connectivity; import org.springframework.web.client.RestTemplate; From ccbf3a9707dc00788ca6d730590a437c3f026776 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 15 Sep 2025 16:28:55 +0300 Subject: [PATCH 04/18] fixed msa tests --- ...lientTest.java => JavaRestClientTest.java} | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) rename msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/{RestClientTest.java => JavaRestClientTest.java} (71%) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java similarity index 71% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java index 6c470188c0..636c80d5b3 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/RestClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -15,9 +15,19 @@ */ package org.thingsboard.server.msa.connectivity; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; +import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.core5.ssl.SSLContexts; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.thingsboard.rest.client.RestClient; @@ -31,14 +41,39 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.TestProperties; +import javax.net.ssl.SSLContext; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; -public class RestClientTest extends AbstractContainerTest { +public class JavaRestClientTest extends AbstractContainerTest { - private static final RestClient restClient = new RestClient(new RestTemplate(), TestProperties.getBaseUrl()); + private RestClient restClient; + + @BeforeClass + public void beforeClass() throws Exception { + SSLContext ssl = SSLContexts.custom() + .loadTrustMaterial((chain, authType) -> true) + .build(); + + var tls = new DefaultClientTlsStrategy( + ssl, + HostnameVerificationPolicy.CLIENT, + NoopHostnameVerifier.INSTANCE + ); + + HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() + .setTlsSocketStrategy(tls) + .build(); + + CloseableHttpClient httpClient = HttpClients.custom() + .setConnectionManager(cm) + .build(); + + RestTemplate rt = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); + restClient = new RestClient(rt, TestProperties.getBaseUrl()); + } @BeforeMethod public void setUp() throws Exception { From f427f3f058ecfb0fda9ddf90c7a0638a2bfddd26 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 6 Nov 2025 16:55:35 +0200 Subject: [PATCH 05/18] introduced new request param "key" for UI to deal with keys with comma --- .../controller/TelemetryController.java | 76 +++++++++++-------- .../server/controller/AbstractWebTest.java | 15 ++++ .../controller/TelemetryControllerTest.java | 60 +++++++++++++++ ui-ngx/src/app/core/http/attribute.service.ts | 4 +- 4 files changed, 121 insertions(+), 34 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index c98ae0dcb8..4e4d2256f3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -208,10 +209,12 @@ public class TelemetryController extends BaseController { public DeferredResult getAttributes( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keys)); } @@ -231,10 +234,12 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keys)); } @ApiOperation(value = "Get time series keys (getTimeseriesKeys)", @@ -270,10 +275,12 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) - @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, - (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes)); + (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes)); } @ApiOperation(value = "Get time series data (getTimeseries)", @@ -291,7 +298,7 @@ public class TelemetryController extends BaseController { public DeferredResult getTimeseries( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION, required = true) @RequestParam(name = "keys") String keys, + @Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = "A long value representing the start timestamp of the time range in milliseconds, UTC.") @RequestParam(name = "startTs") Long startTs, @Parameter(description = "A long value representing the end timestamp of the time range in milliseconds, UTC.") @@ -312,9 +319,11 @@ public class TelemetryController extends BaseController { @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) - @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); DeferredResult response = new DeferredResult<>(); - Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), toKeysList(keys), startTs, endTs, + Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs, intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()), getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor()); return response; @@ -466,7 +475,7 @@ public class TelemetryController extends BaseController { public DeferredResult deleteEntityTimeseries( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = TELEMETRY_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr, + @Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = "A boolean value to specify if should be deleted all data for selected keys or only data that are in the selected time range.") @RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys, @Parameter(description = "A long value representing the start timestamp of removal time range in milliseconds.") @@ -476,16 +485,17 @@ public class TelemetryController extends BaseController { @Parameter(description = "If the parameter is set to true, the latest telemetry can be removed, otherwise, in case that parameter is set to false the latest value will not removed.") @RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest, @Parameter(description = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.") - @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException { + @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest); + return deleteTimeseries(entityId, keys, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest); } - private DeferredResult deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys, + private DeferredResult deleteTimeseries(EntityId entityIdStr, List keys, boolean deleteAllDataForKeys, Long startTs, Long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) throws ThingsboardException { - List keys = toKeysList(keysStr); if (keys.isEmpty()) { - return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); + return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST); } SecurityUser user = getCurrentUser(); @@ -547,9 +557,11 @@ public class TelemetryController extends BaseController { public DeferredResult deleteDeviceAttributes( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); - return deleteAttributes(entityId, scope, keysStr); + return deleteAttributes(entityId, scope, keys); } @ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)", @@ -570,15 +582,16 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = params.get("key") != null ? params.get("key") : toKeysList(keysStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteAttributes(entityId, scope, keysStr); + return deleteAttributes(entityId, scope, keys); } - private DeferredResult deleteAttributes(EntityId entityIdSrc, AttributeScope scope, String keysStr) throws ThingsboardException { - List keys = toKeysList(keysStr); + private DeferredResult deleteAttributes(EntityId entityIdSrc, AttributeScope scope, List keys) throws ThingsboardException { if (keys.isEmpty()) { - return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); + return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST); } SecurityUser user = getCurrentUser(); @@ -709,30 +722,29 @@ public class TelemetryController extends BaseController { }); } - private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, String keys, Boolean useStrictDataTypes) { + private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, List keys, Boolean useStrictDataTypes) { ListenableFuture> future; - if (StringUtils.isEmpty(keys)) { + if (keys.isEmpty()) { future = tsService.findAllLatest(user.getTenantId(), entityId); } else { - future = tsService.findLatest(user.getTenantId(), entityId, toKeysList(keys)); + future = tsService.findLatest(user.getTenantId(), entityId, keys); } Futures.addCallback(future, getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor()); } - private void getAttributeValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, AttributeScope scope, String keys) { - List keyList = toKeysList(keys); - FutureCallback> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList); + private void getAttributeValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, AttributeScope scope, List keys) { + FutureCallback> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keys); if (scope != null) { - if (keyList != null && !keyList.isEmpty()) { - Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keyList), callback, MoreExecutors.directExecutor()); + if (keys != null && !keys.isEmpty()) { + Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keys), callback, MoreExecutors.directExecutor()); } else { Futures.addCallback(attributesService.findAll(user.getTenantId(), entityId, scope), callback, MoreExecutors.directExecutor()); } } else { List>> futures = new ArrayList<>(); for (AttributeScope tmpScope : AttributeScope.values()) { - if (keyList != null && !keyList.isEmpty()) { - futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keyList)); + if (keys != null && !keys.isEmpty()) { + futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keys)); } else { futures.add(attributesService.findAll(user.getTenantId(), entityId, tmpScope)); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index fcbe203e79..56447689c3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -738,6 +738,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doPost("/api/device-with-credentials", request, Device.class); } + protected ResultActions doGetAsync(String urlTemplate, MultiValueMap params) throws Exception { + MockHttpServletRequestBuilder getRequest = get(urlTemplate).params(params); + setJwtToken(getRequest); + return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); + } + protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); setJwtToken(getRequest); @@ -937,6 +943,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(deleteRequest); } + protected ResultActions doDeleteAsync(String urlTemplate, MultiValueMap params) throws Exception { + MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate) + .params(params); + setJwtToken(deleteRequest); + MvcResult result = mockMvc.perform(deleteRequest).andReturn(); + result.getAsyncResult(DEFAULT_TIMEOUT); + return mockMvc.perform(asyncDispatch(result)); + } + protected ResultActions doDeleteAsync(String urlTemplate, Long timeout, String... params) throws Exception { MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate, params); setJwtToken(deleteRequest); diff --git a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java index ad3c6d5312..21371cc316 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java @@ -19,7 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; import org.springframework.test.context.TestPropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -232,6 +235,63 @@ public class TelemetryControllerTest extends AbstractControllerTest { doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody2, String.class, status().isBadRequest()); } + @Test + public void testDeleteTelemetryByKeyWithComma() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + + String tsKey = "key1,key2"; + String testBody = JacksonUtil.newObjectNode() + .put(tsKey, "value") + .toString(); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk()); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("key", tsKey); + params.add("deleteAllDataForKeys", "true"); + + ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value"); + + doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params); + + ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull()); + } + + @Test + public void testDeleteTelemetryByKeysWithComma() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + + String keyWithComma = "key1,key2"; + String testBody = JacksonUtil.newObjectNode() + .put(keyWithComma, "value") + .toString(); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk()); + + String key = "key3"; + String testBody2 = JacksonUtil.newObjectNode() + .put(key, "value") + .toString(); + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("key", keyWithComma); + params.add("key", key); + params.add("deleteAllDataForKeys", "true"); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody2, String.class, status().isOk()); + + ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value"); + assertThat(tsData.get("key3").get(0).get("value").asText()).isEqualTo("value"); + + doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params); + + ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull()); + Assert.assertTrue(tsDataAfterDeletion.get("key3").get(0).get("value").isNull()); + } + private Device createDevice() throws Exception { String testToken = "TEST_TOKEN"; diff --git a/ui-ngx/src/app/core/http/attribute.service.ts b/ui-ngx/src/app/core/http/attribute.service.ts index 8687d3ec0a..589e429180 100644 --- a/ui-ngx/src/app/core/http/attribute.service.ts +++ b/ui-ngx/src/app/core/http/attribute.service.ts @@ -15,7 +15,7 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import {createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig} from './http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { EntityId } from '@shared/models/id/entity-id'; @@ -75,7 +75,7 @@ export class AttributeService { if (isDefinedAndNotNull(endTs)) { url += `&endTs=${endTs}`; } - return this.http.delete(url, defaultHttpOptionsFromConfig(config)); + return this.http.delete(url, createDefaultHttpOptions( {key: timeseries.map(key => key.key)}, config)); } public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, From d61e9a0cb35a0da5607084f25dbe17844c92706b Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 7 Nov 2025 16:19:46 +0200 Subject: [PATCH 06/18] fixes parse error --- .../widget/lib/rpc/led-indicator.component.ts | 13 +++++++++---- .../widget/lib/rpc/round-switch.component.ts | 13 +++++++++---- .../components/widget/lib/rpc/switch.component.ts | 13 +++++++++---- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts index d983a4ebfe..6cd91eadae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts @@ -332,11 +332,16 @@ export class LedIndicatorComponent extends PageComponent implements OnInit, OnDe if (keyData && keyData.data && keyData.data[0]) { const attrValue = keyData.data[0][1]; if (isDefined(attrValue)) { - let parsed = null; + let valueToParse = attrValue; try { - parsed = this.parseValueFunction(JSON.parse(attrValue)); - } catch (e){/**/} - value = !!parsed; + valueToParse = JSON.parse(attrValue); + } catch (e) { } + + try { + value = !!this.parseValueFunction(valueToParse); + } catch (e) { + value = false; + } } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts index 6990ff4264..3e483b498a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts @@ -330,11 +330,16 @@ export class RoundSwitchComponent extends PageComponent implements OnInit, OnDes if (keyData && keyData.data && keyData.data[0]) { const attrValue = keyData.data[0][1]; if (isDefined(attrValue)) { - let parsed = null; + let valueToParse = attrValue; try { - parsed = this.parseValueFunction(JSON.parse(attrValue)); - } catch (e){/**/} - value = !!parsed; + valueToParse = JSON.parse(attrValue); + } catch (e) { } + + try { + value = !!this.parseValueFunction(valueToParse); + } catch (e) { + value = false; + } } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts index c9807e5635..e9809cb61c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts @@ -373,11 +373,16 @@ export class SwitchComponent extends PageComponent implements AfterViewInit, OnD if (keyData && keyData.data && keyData.data[0]) { const attrValue = keyData.data[0][1]; if (isDefined(attrValue)) { - let parsed = null; + let valueToParse = attrValue; try { - parsed = this.parseValueFunction(JSON.parse(attrValue)); - } catch (e){/**/} - value = !!parsed; + valueToParse = JSON.parse(attrValue); + } catch (e) { } + + try { + value = !!this.parseValueFunction(valueToParse); + } catch (e) { + value = false; + } } } } From bb810d87376903cc7246e92a4aae7dd2a8a79c26 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 7 Nov 2025 17:25:05 +0200 Subject: [PATCH 07/18] UI: Implement delete attributes and telemetry with multi-value Query Param --- ui-ngx/src/app/core/http/attribute.service.ts | 81 ++++++++----------- ui-ngx/src/app/core/http/http-utils.ts | 31 ++++++- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/ui-ngx/src/app/core/http/attribute.service.ts b/ui-ngx/src/app/core/http/attribute.service.ts index 589e429180..fc87702628 100644 --- a/ui-ngx/src/app/core/http/attribute.service.ts +++ b/ui-ngx/src/app/core/http/attribute.service.ts @@ -15,7 +15,7 @@ /// import { Injectable } from '@angular/core'; -import {createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig} from './http-utils'; +import { defaultHttpOptionsFromConfig, defaultHttpOptionsFromParams, RequestConfig } from './http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { EntityId } from '@shared/models/id/entity-id'; @@ -35,47 +35,37 @@ export class AttributeService { public getEntityAttributes(entityId: EntityId, attributeScope?: AttributeScope, keys?: Array, config?: RequestConfig): Observable> { let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/attributes`; - + let queryParams: object = null; if (attributeScope) { url += `/${attributeScope}`; } if (keys && keys.length) { - url += `?keys=${keys.join(',')}`; + queryParams = {key: keys}; } - return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + return this.http.get>(url, defaultHttpOptionsFromParams(queryParams, config)); } public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, config?: RequestConfig): Observable { - const keys = attributes.map(attribute => encodeURIComponent(attribute.key)).join(','); - return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` + - `?keys=${keys}`, - defaultHttpOptionsFromConfig(config)); + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}`, + defaultHttpOptionsFromParams({key: attributes.map(attribute => attribute.key)}, config)); } public deleteEntityTimeseries(entityId: EntityId, timeseries: Array, deleteAllDataForKeys = false, startTs?: number, endTs?: number, rewriteLatestIfDeleted = false, deleteLatest = true, config?: RequestConfig): Observable { - const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(','); - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete?keys=${keys}`; - if (isDefinedAndNotNull(deleteAllDataForKeys)) { - url += `&deleteAllDataForKeys=${deleteAllDataForKeys}`; - } - if (isDefinedAndNotNull(rewriteLatestIfDeleted)) { - url += `&rewriteLatestIfDeleted=${rewriteLatestIfDeleted}`; - } - if (isDefinedAndNotNull(deleteLatest)) { - url += `&deleteLatest=${deleteLatest}`; - } - if (isDefinedAndNotNull(startTs)) { - url += `&startTs=${startTs}`; - } - if (isDefinedAndNotNull(endTs)) { - url += `&endTs=${endTs}`; - } - return this.http.delete(url, createDefaultHttpOptions( {key: timeseries.map(key => key.key)}, config)); + const queryParams = { + key: timeseries.map(key => key.key), + deleteAllDataForKeys: deleteAllDataForKeys, + rewriteLatestIfDeleted: rewriteLatestIfDeleted, + deleteLatest: deleteLatest, + startTs: startTs, + endTs: endTs + }; + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete`, + defaultHttpOptionsFromParams(queryParams, config)); } public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, @@ -138,32 +128,29 @@ export class AttributeService { limit: number = 100, agg: AggregationType = AggregationType.NONE, interval?: number, orderBy: DataSortOrder = DataSortOrder.DESC, useStrictDataTypes: boolean = false, config?: RequestConfig): Observable { - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?keys=${keys.join(',')}&startTs=${startTs}&endTs=${endTs}`; - if (isDefinedAndNotNull(limit)) { - url += `&limit=${limit}`; - } - if (isDefinedAndNotNull(agg)) { - url += `&agg=${agg}`; - } - if (isDefinedAndNotNull(interval)) { - url += `&interval=${interval}`; - } - if (isDefinedAndNotNull(orderBy)) { - url += `&orderBy=${orderBy}`; - } - if (isDefinedAndNotNull(useStrictDataTypes)) { - url += `&useStrictDataTypes=${useStrictDataTypes}`; - } - - return this.http.get(url, defaultHttpOptionsFromConfig(config)); + const queryParams = { + key: keys, + startTs: startTs, + endTs: endTs, + limit: limit, + agg: agg, + interval: interval, + orderBy: orderBy, + useStrictDataTypes: useStrictDataTypes + } + return this.http.get(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`, + defaultHttpOptionsFromParams(queryParams, config)); } public getEntityTimeseriesLatest(entityId: EntityId, keys?: Array, useStrictDataTypes = false, config?: RequestConfig): Observable { - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?useStrictDataTypes=${useStrictDataTypes}`; + const queryParams: Record = { + useStrictDataTypes: useStrictDataTypes + } if (isDefinedAndNotNull(keys) && keys.length) { - url += `&keys=${keys.join(',')}`; + queryParams.key = keys; } - return this.http.get(url, defaultHttpOptionsFromConfig(config)); + return this.http.get(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`, + defaultHttpOptionsFromParams(queryParams, config)); } } diff --git a/ui-ngx/src/app/core/http/http-utils.ts b/ui-ngx/src/app/core/http/http-utils.ts index ebd08f13fe..3d28f6ddf4 100644 --- a/ui-ngx/src/app/core/http/http-utils.ts +++ b/ui-ngx/src/app/core/http/http-utils.ts @@ -38,7 +38,10 @@ export function createDefaultHttpOptions(queryParamsOrConfig?: QueryParams | Req if (hasRequestConfig(queryParamsOrConfig)) { return defaultHttpOptionsFromConfig(queryParamsOrConfig as RequestConfig); } - const queryParams = queryParamsOrConfig as QueryParams; + return defaultHttpOptionsFromParams(queryParamsOrConfig as QueryParams, config); +} + +export function defaultHttpOptionsFromParams(queryParams?: QueryParams, config?: RequestConfig) { const finalConfig = { ...config, ...(queryParams && { queryParams }), @@ -57,9 +60,11 @@ export function defaultHttpOptions(ignoreLoading: boolean = false, ignoreErrors: boolean = false, resendRequest: boolean = false, queryParams?: QueryParams) { + const cleanedParams = cleanQueryParams(queryParams); + return { headers: new HttpHeaders({'Content-Type': 'application/json'}), - params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), queryParams) + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams) }; } @@ -67,7 +72,27 @@ export function defaultHttpUploadOptions(ignoreLoading: boolean = false, ignoreErrors: boolean = false, resendRequest: boolean = false, queryParams?: QueryParams) { + const cleanedParams = cleanQueryParams(queryParams); + return { - params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), queryParams) + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams) }; } + +function cleanQueryParams(params?: QueryParams): QueryParams | undefined { + if (!params) { + return undefined; + } + + const entries = Object.entries(params); + + const cleanedEntries = entries.filter( + ([_, value]) => value !== null && value !== undefined + ); + + if (!cleanedEntries.length) { + return undefined; + } + + return Object.fromEntries(cleanedEntries); +} From b5d3d9e05fe0dad30f7bc33cfcea6584ed79ab16 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Sun, 9 Nov 2025 12:30:22 +0200 Subject: [PATCH 08/18] fixed request error --- ui-ngx/src/app/core/http/queue.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/http/queue.service.ts b/ui-ngx/src/app/core/http/queue.service.ts index d9bc19ef62..651d26c9dc 100644 --- a/ui-ngx/src/app/core/http/queue.service.ts +++ b/ui-ngx/src/app/core/http/queue.service.ts @@ -75,7 +75,7 @@ export class QueueService { } public getQueueStatisticsByIds(queueStatIds: Array, config?: RequestConfig): Observable> { - return this.http.get>(`/api/queueStats?QueueStatsIds=${queueStatIds.join(',')}`, + return this.http.get>(`/api/queueStats?queueStatsIds=${queueStatIds.join(',')}`, defaultHttpOptionsFromConfig(config)).pipe( map(queueStats => queueStats.map(queueStat => this.parseQueueStatName(queueStat)) ) From 0905d5d2548c1cc8ea23c55c40af7b88ff0d9f7c Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 10 Nov 2025 13:14:34 +0200 Subject: [PATCH 09/18] fixed lint problems --- .../home/components/widget/lib/rpc/led-indicator.component.ts | 2 +- .../home/components/widget/lib/rpc/round-switch.component.ts | 2 +- .../modules/home/components/widget/lib/rpc/switch.component.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts index 6cd91eadae..a59bbca537 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts @@ -335,7 +335,7 @@ export class LedIndicatorComponent extends PageComponent implements OnInit, OnDe let valueToParse = attrValue; try { valueToParse = JSON.parse(attrValue); - } catch (e) { } + } catch (e) {/**/} try { value = !!this.parseValueFunction(valueToParse); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts index 3e483b498a..8e64cc7100 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts @@ -333,7 +333,7 @@ export class RoundSwitchComponent extends PageComponent implements OnInit, OnDes let valueToParse = attrValue; try { valueToParse = JSON.parse(attrValue); - } catch (e) { } + } catch (e) {/**/} try { value = !!this.parseValueFunction(valueToParse); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts index e9809cb61c..7a8f4e0dd6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts @@ -376,7 +376,7 @@ export class SwitchComponent extends PageComponent implements AfterViewInit, OnD let valueToParse = attrValue; try { valueToParse = JSON.parse(attrValue); - } catch (e) { } + } catch (e) {/**/} try { value = !!this.parseValueFunction(valueToParse); From 1e2c127e1cbf47d36c10fe22674a8545357e0ccd Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 10 Nov 2025 18:43:52 +0200 Subject: [PATCH 10/18] Remove redundant persistence of CF links # Conflicts: # application/src/main/data/upgrade/basic/schema_update.sql # common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java # common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java # common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java # common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java # dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java # dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java --- .../main/data/upgrade/basic/schema_update.sql | 7 ++ ...alculatedFieldManagerMessageProcessor.java | 23 +++-- .../controller/CalculatedFieldController.java | 4 +- .../cf/DefaultCalculatedFieldCache.java | 10 +- .../DefaultCalculatedFieldQueueService.java | 2 +- .../server/dao/cf/CalculatedFieldService.java | 14 --- .../server/common/data/EntityType.java | 3 +- .../common/data/cf/CalculatedFieldLink.java | 46 +-------- ...entsBasedCalculatedFieldConfiguration.java | 17 ++-- .../CalculatedFieldConfiguration.java | 12 +-- ...eofencingCalculatedFieldConfiguration.java | 12 ++- .../common/data/id/CalculatedFieldLinkId.java | 45 --------- .../common/data/id/EntityIdFactory.java | 1 - common/proto/src/main/proto/queue.proto | 2 +- .../dao/cf/BaseCalculatedFieldService.java | 62 ++---------- .../server/dao/cf/CalculatedFieldLinkDao.java | 42 --------- .../entity/DefaultEntityServiceRegistry.java | 3 - .../server/dao/model/ModelConstants.java | 12 +-- .../model/sql/CalculatedFieldLinkEntity.java | 79 ---------------- .../CalculatedFieldLinkDataValidator.java | 41 -------- .../sql/cf/CalculatedFieldLinkRepository.java | 36 ------- ...efaultNativeCalculatedFieldRepository.java | 38 -------- .../dao/sql/cf/JpaCalculatedFieldLinkDao.java | 94 ------------------- .../cf/NativeCalculatedFieldRepository.java | 3 - .../main/resources/sql/schema-entities.sql | 10 -- .../service/CalculatedFieldServiceTest.java | 23 +---- .../service/EntityServiceRegistryTest.java | 5 - .../CalculatedFieldDataValidatorTest.java | 10 +- .../CalculatedFieldLinkDataValidatorTest.java | 57 ----------- .../server/msa/cf/CalculatedFieldTest.java | 10 +- .../rule/engine/util/TenantIdLoader.java | 10 -- .../rule/engine/util/TenantIdLoaderTest.java | 11 +-- 32 files changed, 73 insertions(+), 671 deletions(-) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index e5fdba571b..17512ed64c 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -69,3 +69,10 @@ ALTER TABLE calculated_field DROP CONSTRAINT IF EXISTS calculated_field_unq_key; ALTER TABLE calculated_field ADD CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name); -- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE END + +-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE START + +DROP TABLE IF EXISTS calculated_field_link; +ANALYZE calculated_field; + +-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE END diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 3a19841cc5..ccdf719f6c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -611,7 +611,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var proto = msg.getProto(); List result = new ArrayList<>(); for (var link : getCalculatedFieldLinksByEntityId(entityId)) { - CalculatedFieldCtx ctx = calculatedFields.get(link.getCalculatedFieldId()); + CalculatedFieldCtx ctx = calculatedFields.get(link.calculatedFieldId()); if (ctx.linkMatches(entityId, proto)) { result.add(ctx.toCalculatedFieldEntityCtxId()); } @@ -738,13 +738,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void addLinks(CalculatedField newCf) { var newLinks = newCf.getConfiguration().buildCalculatedFieldLinks(tenantId, newCf.getEntityId(), newCf.getId()); - newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link)); + newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link)); } private void deleteLinks(CalculatedFieldCtx cfCtx) { var oldCf = cfCtx.getCalculatedField(); var oldLinks = oldCf.getConfiguration().buildCalculatedFieldLinks(tenantId, oldCf.getEntityId(), oldCf.getId()); - oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).remove(link)); + oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).remove(link)); } public void onPartitionChange(CalculatedFieldPartitionChangeMsg msg) { @@ -757,15 +757,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.trace("Processing calculated field record: {}", cf); try { initCalculatedField(cf); + initCalculatedFieldLinks(cf); } catch (CalculatedFieldException e) { log.error("Failed to process calculated field record: {}", cf, e); } }); - PageDataIterable cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); - cfls.forEach(link -> { - log.trace("Processing calculated field link record: {}", link); - initCalculatedFieldLink(link); - }); } private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException { @@ -782,10 +778,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } - private void initCalculatedFieldLink(CalculatedFieldLink link) { - // We use copy on write lists to safely pass the reference to another actor for the iteration. - // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) - entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link); + private void initCalculatedFieldLinks(CalculatedField cf) { + List links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId()); + for (CalculatedFieldLink link : links) { + // We use copy on write lists to safely pass the reference to another actor for the iteration. + // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) + entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link); + } } private void initEntitiesCache() { diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index 8c193a5b20..9a24ab91fc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -63,10 +63,10 @@ import org.thingsboard.server.service.security.permission.Operation; import java.util.ArrayList; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; @@ -287,7 +287,7 @@ public class CalculatedFieldController extends BaseController { } private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { - List referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); + Set referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); for (EntityId referencedEntityId : referencedEntityIds) { EntityType entityType = referencedEntityId.getEntityType(); switch (entityType) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index e802bd7677..4466b368b2 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -82,19 +82,17 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { cfs.forEach(cf -> { if (cf != null) { calculatedFields.putIfAbsent(cf.getId(), cf); + List links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId()); + calculatedFieldLinks.put(cf.getId(), new CopyOnWriteArrayList<>(links)); } }); calculatedFields.values().forEach(cf -> { entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf); }); - PageDataIterable cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize); - cfls.forEach(link -> { - calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link); - }); calculatedFieldLinks.values().stream() .flatMap(List::stream) .forEach(link -> - entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link) + entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link) ); } @@ -226,7 +224,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { log.debug("[{}] evict calculated field links from cache: {}", calculatedFieldId, oldCalculatedField); calculatedFieldsCtx.remove(calculatedFieldId); log.debug("[{}] evict calculated field ctx from cache: {}", calculatedFieldId, oldCalculatedField); - entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.getCalculatedFieldId().equals(calculatedFieldId))); + entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.calculatedFieldId().equals(calculatedFieldId))); log.debug("[{}] evict calculated field links from cached links by entity id: {}", calculatedFieldId, oldCalculatedField); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java index 2bf605140a..0236de552f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java @@ -172,7 +172,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS List links = calculatedFieldCache.getCalculatedFieldLinksByEntityId(entityId); for (CalculatedFieldLink link : links) { - CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.getCalculatedFieldId()); + CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.calculatedFieldId()); if (ctx != null && linkedEntityFilter.test(ctx)) { return true; } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 57c6df3c7f..bef4675e03 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -16,10 +16,8 @@ package org.thingsboard.server.dao.cf; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -52,18 +50,6 @@ public interface CalculatedFieldService extends EntityDaoService { int deleteAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId); - CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink); - - CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId); - - List findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId); - - List findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId); - - PageData findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink); - - PageData findAllCalculatedFieldLinks(PageLink pageLink); - boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 110052b57f..6a37f9fe1b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -23,6 +23,7 @@ import java.util.EnumSet; import java.util.List; public enum EntityType { + TENANT(1), CUSTOMER(2), USER(3, "tb_user"), @@ -61,7 +62,7 @@ public enum EntityType { MOBILE_APP(37), MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), - CALCULATED_FIELD_LINK(40), + // CALCULATED_FIELD_LINK(40), - was removed in 4.3 JOB(41), ADMIN_SETTINGS(42), AI_MODEL(43, "ai_model") { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java index 3f048815da..71f5917c34 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java @@ -15,52 +15,8 @@ */ package org.thingsboard.server.common.data.cf; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -@Schema -@Data -@EqualsAndHashCode(callSuper = true) -public class CalculatedFieldLink extends BaseData { - - private static final long serialVersionUID = 6492846246722091530L; - - private TenantId tenantId; - private EntityId entityId; - - @Schema(description = "JSON object with the Calculated Field Id. ", accessMode = Schema.AccessMode.READ_ONLY) - private CalculatedFieldId calculatedFieldId; - - public CalculatedFieldLink() { - super(); - } - - public CalculatedFieldLink(CalculatedFieldLinkId id) { - super(id); - } - - public CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) { - this.tenantId = tenantId; - this.entityId = entityId; - this.calculatedFieldId = calculatedFieldId; - } - - @Override - public String toString() { - return new StringBuilder() - .append("CalculatedFieldLink[") - .append("tenantId=").append(tenantId) - .append(", entityId=").append(entityId) - .append(", calculatedFieldId=").append(calculatedFieldId) - .append(", createdTime=").append(createdTime) - .append(", id=").append(id).append(']') - .toString(); - } - -} +public record CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java index f422869c95..294335e665 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java @@ -19,10 +19,12 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import org.thingsboard.server.common.data.id.EntityId; -import java.util.List; +import java.util.Collections; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { @@ -30,14 +32,15 @@ public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFi @NotEmpty Map getArguments(); - default List getReferencedEntities() { - if (getArguments() == null) { - return List.of(); + default Set getReferencedEntities() { + var args = getArguments(); + if (args == null) { + return Collections.emptySet(); } - return getArguments().values().stream() + return args.values().stream() .map(Argument::getRefEntityId) .filter(Objects::nonNull) - .collect(Collectors.toList()); + .collect(toSet()); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 3df9a32dcc..5313dea0bf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -28,7 +28,9 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @JsonTypeInfo( @@ -55,16 +57,12 @@ public interface CalculatedFieldConfiguration { default void validate() {} @JsonIgnore - default List getReferencedEntities() { - return List.of(); + default Set getReferencedEntities() { + return Collections.emptySet(); } default CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) { - CalculatedFieldLink link = new CalculatedFieldLink(); - link.setTenantId(tenantId); - link.setEntityId(referencedEntityId); - link.setCalculatedFieldId(calculatedFieldId); - return link; + return new CalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId); } default List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java index 47d344fe1b..acefd5ff26 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java @@ -26,10 +26,13 @@ import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.EntityId; +import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; @Data public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { @@ -62,8 +65,11 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal @Override - public List getReferencedEntities() { - return zoneGroups == null ? List.of() : zoneGroups.values().stream().map(ZoneGroupConfiguration::getRefEntityId).filter(Objects::nonNull).toList(); + public Set getReferencedEntities() { + return zoneGroups == null ? Collections.emptySet() : zoneGroups.values().stream() + .map(ZoneGroupConfiguration::getRefEntityId) + .filter(Objects::nonNull) + .collect(toSet()); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java deleted file mode 100644 index 6a0c680bb6..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * 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. - */ -package org.thingsboard.server.common.data.id; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import org.thingsboard.server.common.data.EntityType; - -import java.util.UUID; - -@Schema -public class CalculatedFieldLinkId extends UUIDBased implements EntityId { - - private static final long serialVersionUID = 1L; - - @JsonCreator - public CalculatedFieldLinkId(@JsonProperty("id") UUID id) { - super(id); - } - - public static CalculatedFieldLinkId fromString(String calculatedFieldLinkId) { - return new CalculatedFieldLinkId(UUID.fromString(calculatedFieldLinkId)); - } - - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD_LINK", allowableValues = "CALCULATED_FIELD_LINK") - @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD_LINK; - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 09d34ff10b..3b8959624d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -80,7 +80,6 @@ public class EntityIdFactory { case DOMAIN -> new DomainId(uuid); case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid); - case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); case JOB -> new JobId(uuid); case ADMIN_SETTINGS -> new AdminSettingsId(uuid); case AI_MODEL -> new AiModelId(uuid); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index bfdce15dbd..5f33524464 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -62,7 +62,7 @@ enum EntityTypeProto { MOBILE_APP = 37; MOBILE_APP_BUNDLE = 38; CALCULATED_FIELD = 39; - CALCULATED_FIELD_LINK = 40; + // CALCULATED_FIELD_LINK = 40; - was removed in 4.3 JOB = 41; ADMIN_SETTINGS = 42; AI_MODEL = 43; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 8e875bfae2..d3ca3ccff3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -21,11 +21,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; @@ -36,7 +34,6 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.validator.CalculatedFieldDataValidator; -import org.thingsboard.server.dao.service.validator.CalculatedFieldLinkDataValidator; import java.util.EnumSet; import java.util.List; @@ -57,9 +54,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements public static final String INCORRECT_ENTITY_ID = "Incorrect entityId "; private final CalculatedFieldDao calculatedFieldDao; - private final CalculatedFieldLinkDao calculatedFieldLinkDao; private final CalculatedFieldDataValidator calculatedFieldDataValidator; - private final CalculatedFieldLinkDataValidator calculatedFieldLinkDataValidator; @Override public CalculatedField save(CalculatedField calculatedField) { @@ -84,9 +79,13 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements log.trace("Executing save calculated field, [{}]", calculatedField); updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis()); CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); - createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) - .entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build()); + eventPublisher.publishEvent(SaveEntityEvent.builder() + .tenantId(savedCalculatedField.getTenantId()) + .entityId(savedCalculatedField.getId()) + .entity(savedCalculatedField) + .oldEntity(oldCalculatedField) + .created(calculatedField.getId() == null) + .build()); return savedCalculatedField; } catch (Exception e) { checkConstraintViolation(e, @@ -191,48 +190,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return calculatedFields.size(); } - @Override - public CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) { - calculatedFieldLinkDataValidator.validate(calculatedFieldLink, CalculatedFieldLink::getTenantId); - log.trace("Executing save calculated field link, [{}]", calculatedFieldLink); - return calculatedFieldLinkDao.save(tenantId, calculatedFieldLink); - } - - @Override - public CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId) { - log.trace("Executing findCalculatedFieldLinkById, tenantId [{}], calculatedFieldLinkId [{}]", tenantId, calculatedFieldLinkId); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateId(calculatedFieldLinkId, id -> "Incorrect calculatedFieldLinkId " + id); - return calculatedFieldLinkDao.findById(tenantId, calculatedFieldLinkId.getId()); - } - - @Override - public List findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId) { - log.trace("Executing findAllCalculatedFieldLinksById, calculatedFieldId [{}]", calculatedFieldId); - return calculatedFieldLinkDao.findCalculatedFieldLinksByCalculatedFieldId(tenantId, calculatedFieldId); - } - - @Override - public List findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) { - log.trace("Executing findAllCalculatedFieldLinksByEntityId, entityId [{}]", entityId); - return calculatedFieldLinkDao.findCalculatedFieldLinksByEntityId(tenantId, entityId); - } - - @Override - public PageData findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink) { - log.trace("Executing findAllCalculatedFieldLinksByTenantId, tenantId[{}] pageLink [{}]", tenantId, pageLink); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validatePageLink(pageLink); - return calculatedFieldLinkDao.findAllByTenantId(tenantId, pageLink); - } - - @Override - public PageData findAllCalculatedFieldLinks(PageLink pageLink) { - log.trace("Executing findAllCalculatedFieldLinks, pageLink [{}]", pageLink); - validatePageLink(pageLink); - return calculatedFieldLinkDao.findAll(pageLink); - } - @Override public boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId) { return calculatedFieldDao.findAllByTenantId(tenantId).stream() @@ -258,9 +215,4 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return EntityType.CALCULATED_FIELD; } - private void createOrUpdateCalculatedFieldLink(TenantId tenantId, CalculatedField calculatedField) { - List links = calculatedField.getConfiguration().buildCalculatedFieldLinks(tenantId, calculatedField.getEntityId(), calculatedField.getId()); - links.forEach(link -> saveCalculatedFieldLink(tenantId, link)); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java deleted file mode 100644 index dd184289ed..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * 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. - */ -package org.thingsboard.server.dao.cf; - -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.Dao; - -import java.util.List; - -public interface CalculatedFieldLinkDao extends Dao { - - List findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId); - - List findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId); - - List findCalculatedFieldLinksByTenantId(TenantId tenantId); - - List findAll(); - - PageData findAll(PageLink pageLink); - - PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java index 9c50ad621f..a795bddffa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java @@ -43,9 +43,6 @@ public class DefaultEntityServiceRegistry implements EntityServiceRegistry { if (EntityType.RULE_CHAIN.equals(entityType)) { entityDaoServicesMap.put(EntityType.RULE_NODE, entityDaoService); } - if (EntityType.CALCULATED_FIELD.equals(entityType)) { - entityDaoServicesMap.put(EntityType.CALCULATED_FIELD_LINK, entityDaoService); - } }); log.debug("Initialized EntityServiceRegistry total [{}] entries", entityDaoServicesMap.size()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 3960c67525..aee8356af4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -26,8 +26,7 @@ import java.util.UUID; public class ModelConstants { - private ModelConstants() { - } + private ModelConstants() {} public static final UUID NULL_UUID = Uuids.startOf(0); public static final TenantId SYSTEM_TENANT = TenantId.fromUUID(ModelConstants.NULL_UUID); @@ -731,15 +730,6 @@ public class ModelConstants { public static final String CALCULATED_FIELD_CONFIGURATION = "configuration"; public static final String CALCULATED_FIELD_VERSION = "version"; - /** - * Calculated field links constants. - */ - public static final String CALCULATED_FIELD_LINK_TABLE_NAME = "calculated_field_link"; - public static final String CALCULATED_FIELD_LINK_TENANT_ID_COLUMN = TENANT_ID_COLUMN; - public static final String CALCULATED_FIELD_LINK_ENTITY_TYPE = ENTITY_TYPE_COLUMN; - public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN; - public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id"; - /** * Tasks constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java deleted file mode 100644 index 0f2a6455ec..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * 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. - */ -package org.thingsboard.server.dao.model.sql; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.BaseSqlEntity; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_ID; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_TYPE; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TABLE_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TENANT_ID_COLUMN; - -@Data -@EqualsAndHashCode(callSuper = true) -@Entity -@Table(name = CALCULATED_FIELD_LINK_TABLE_NAME) -public class CalculatedFieldLinkEntity extends BaseSqlEntity implements BaseEntity { - - @Column(name = CALCULATED_FIELD_LINK_TENANT_ID_COLUMN) - private UUID tenantId; - - @Column(name = CALCULATED_FIELD_LINK_ENTITY_TYPE) - private String entityType; - - @Column(name = CALCULATED_FIELD_LINK_ENTITY_ID) - private UUID entityId; - - @Column(name = CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID) - private UUID calculatedFieldId; - - public CalculatedFieldLinkEntity() { - super(); - } - - public CalculatedFieldLinkEntity(CalculatedFieldLink calculatedFieldLink) { - super(calculatedFieldLink); - this.tenantId = calculatedFieldLink.getTenantId().getId(); - this.entityType = calculatedFieldLink.getEntityId().getEntityType().name(); - this.entityId = calculatedFieldLink.getEntityId().getId(); - this.calculatedFieldId = calculatedFieldLink.getCalculatedFieldId().getId(); - } - - @Override - public CalculatedFieldLink toData() { - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(new CalculatedFieldLinkId(id)); - calculatedFieldLink.setCreatedTime(createdTime); - calculatedFieldLink.setTenantId(TenantId.fromUUID(tenantId)); - calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId)); - return calculatedFieldLink; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java deleted file mode 100644 index aaba200c92..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * 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. - */ -package org.thingsboard.server.dao.service.validator; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.service.DataValidator; - -@Component -public class CalculatedFieldLinkDataValidator extends DataValidator { - - @Autowired - private CalculatedFieldLinkDao calculatedFieldLinkDao; - - @Override - protected CalculatedFieldLink validateUpdate(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) { - CalculatedFieldLink old = calculatedFieldLinkDao.findById(calculatedFieldLink.getTenantId(), calculatedFieldLink.getId().getId()); - if (old == null) { - throw new DataValidationException("Can't update non existing calculated field link!"); - } - return old; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java deleted file mode 100644 index 6f6a0775f3..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * 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. - */ -package org.thingsboard.server.dao.sql.cf; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity; - -import java.util.List; -import java.util.UUID; - -public interface CalculatedFieldLinkRepository extends JpaRepository { - - List findAllByTenantIdAndCalculatedFieldId(UUID tenantId, UUID calculatedFieldId); - - List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); - - List findAllByTenantId(UUID tenantId); - - Page findAllByTenantId(UUID tenantId, Pageable pageable); - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java index bbce4e2721..f2b565131e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java @@ -25,12 +25,10 @@ import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -49,9 +47,6 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF private final String CF_COUNT_QUERY = "SELECT count(id) FROM calculated_field;"; private final String CF_QUERY = "SELECT * FROM calculated_field ORDER BY created_time ASC LIMIT %s OFFSET %s"; - private final String CFL_COUNT_QUERY = "SELECT count(id) FROM calculated_field_link;"; - private final String CFL_QUERY = "SELECT * FROM calculated_field_link ORDER BY created_time ASC LIMIT %s OFFSET %s"; - private final NamedParameterJdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; @@ -103,37 +98,4 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF }); } - @Override - public PageData findCalculatedFieldLinks(Pageable pageable) { - return transactionTemplate.execute(status -> { - long startTs = System.currentTimeMillis(); - int totalElements = jdbcTemplate.queryForObject(CFL_COUNT_QUERY, Collections.emptyMap(), Integer.class); - log.debug("Count query took {} ms", System.currentTimeMillis() - startTs); - startTs = System.currentTimeMillis(); - List> rows = jdbcTemplate.queryForList(String.format(CFL_QUERY, pageable.getPageSize(), pageable.getOffset()), Collections.emptyMap()); - log.debug("Main query took {} ms", System.currentTimeMillis() - startTs); - int totalPages = pageable.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageable.getPageSize()) : 1; - boolean hasNext = pageable.getPageSize() > 0 && totalElements > pageable.getOffset() + rows.size(); - var data = rows.stream().map(row -> { - - UUID id = (UUID) row.get("id"); - long createdTime = (long) row.get("created_time"); - UUID tenantId = (UUID) row.get("tenant_id"); - EntityType entityType = EntityType.valueOf((String) row.get("entity_type")); - UUID entityId = (UUID) row.get("entity_id"); - UUID calculatedFieldId = (UUID) row.get("calculated_field_id"); - - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(); - calculatedFieldLink.setId(new CalculatedFieldLinkId(id)); - calculatedFieldLink.setCreatedTime(createdTime); - calculatedFieldLink.setTenantId(new TenantId(tenantId)); - calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId)); - - return calculatedFieldLink; - }).collect(Collectors.toList()); - return new PageData<>(data, totalPages, totalElements, hasNext); - }); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java deleted file mode 100644 index 38871f2fb8..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * 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. - */ -package org.thingsboard.server.dao.sql.cf; - -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity; -import org.thingsboard.server.dao.sql.JpaAbstractDao; -import org.thingsboard.server.dao.util.SqlDao; - -import java.util.List; -import java.util.UUID; - -@Slf4j -@Component -@AllArgsConstructor -@SqlDao -public class JpaCalculatedFieldLinkDao extends JpaAbstractDao implements CalculatedFieldLinkDao { - - private final CalculatedFieldLinkRepository calculatedFieldLinkRepository; - private final NativeCalculatedFieldRepository nativeCalculatedFieldRepository; - - @Override - public List findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndCalculatedFieldId(tenantId.getId(), calculatedFieldId.getId())); - } - - @Override - public List findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId())); - } - - @Override - public List findCalculatedFieldLinksByTenantId(TenantId tenantId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId())); - } - - @Override - public List findAll() { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAll()); - } - - @Override - public PageData findAll(PageLink pageLink) { - log.debug("Try to find calculated field links by pageLink [{}]", pageLink); - return nativeCalculatedFieldRepository.findCalculatedFieldLinks(DaoUtil.toPageable(pageLink)); - } - - @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - log.debug("Try to find calculated field links by tenantId [{}], pageLink [{}]", tenantId, pageLink); - return DaoUtil.toPageData(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); - } - - @Override - protected Class getEntityClass() { - return CalculatedFieldLinkEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return calculatedFieldLinkRepository; - } - - @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD_LINK; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java index f37a5764a0..7cc3507076 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java @@ -17,13 +17,10 @@ package org.thingsboard.server.dao.sql.cf; import org.springframework.data.domain.Pageable; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.page.PageData; public interface NativeCalculatedFieldRepository { PageData findCalculatedFields(Pageable pageable); - PageData findCalculatedFieldLinks(Pageable pageable); - } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 4ccc5c9a2b..4722d4dafa 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -926,16 +926,6 @@ CREATE TABLE IF NOT EXISTS calculated_field ( CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name) ); -CREATE TABLE IF NOT EXISTS calculated_field_link ( - id uuid NOT NULL CONSTRAINT calculated_field_link_pkey PRIMARY KEY, - created_time bigint NOT NULL, - tenant_id uuid NOT NULL, - entity_type VARCHAR(32), - entity_id uuid NOT NULL, - calculated_field_id uuid NOT NULL, - CONSTRAINT fk_calculated_field_id FOREIGN KEY (calculated_field_id) REFERENCES calculated_field(id) ON DELETE CASCADE -); - CREATE TABLE IF NOT EXISTS cf_debug_event ( id uuid NOT NULL, tenant_id uuid NOT NULL , diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 0e20f188b1..c904852fbe 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -15,13 +15,8 @@ */ package org.thingsboard.server.dao.service; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import org.junit.After; -import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -64,18 +59,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { @Autowired private TbTenantProfileCache tbTenantProfileCache; - private ListeningExecutorService executor; - - @Before - public void before() { - executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); - } - - @After - public void after() { - executor.shutdownNow(); - } - @Test public void testSaveCalculatedField() { Device device = createTestDevice(); @@ -144,7 +127,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { .isInstanceOf(DataValidationException.class) .hasCauseInstanceOf(IllegalArgumentException.class) .hasMessageStartingWith("Scheduled update interval is less than configured " + - "minimum allowed interval in tenant profile: "); + "minimum allowed interval in tenant profile: "); } @Test @@ -163,7 +146,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { .getMaxRelationLevelPerCfArgument(); // Zone-group argument (ATTRIBUTE) - ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration( "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); List levels = new ArrayList<>(); @@ -203,7 +186,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cfg.setEntityCoordinates(entityCoordinates); // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled - ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration( "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java index 9503f4e23f..0e7797ce56 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java @@ -20,7 +20,6 @@ import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.dao.rule.RuleChainService; @@ -45,8 +44,4 @@ public class EntityServiceRegistryTest extends AbstractServiceTest { Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.RULE_NODE) instanceof RuleChainService); } - @Test - public void givenCalculatedFieldLinkEntityType_whenGetServiceByEntityTypeCalled_thenReturnedCalculatedFieldService() { - Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.CALCULATED_FIELD_LINK) instanceof CalculatedFieldService); - } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java index 43fb44431e..0d52f999de 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java @@ -17,8 +17,8 @@ package org.thingsboard.server.dao.service.validator; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -38,11 +38,11 @@ public class CalculatedFieldDataValidatorTest { private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("7b5229e9-166e-41a9-a257-3b1dafad1b04")); private final CalculatedFieldId CALCULATED_FIELD_ID = new CalculatedFieldId(UUID.fromString("060fbe45-fbb2-4549-abf3-f72a6be3cb9f")); - @MockBean + @MockitoBean private CalculatedFieldDao calculatedFieldDao; - @MockBean + @MockitoBean private DefaultApiLimitService apiLimitService; - @SpyBean + @MockitoSpyBean private CalculatedFieldDataValidator validator; @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java deleted file mode 100644 index c477498602..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * 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. - */ -package org.thingsboard.server.dao.service.validator; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.exception.DataValidationException; - -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; - -@SpringBootTest(classes = CalculatedFieldLinkDataValidator.class) -public class CalculatedFieldLinkDataValidatorTest { - - private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("2ba09d99-6143-43dc-b645-381fc0c43ebe")); - private final CalculatedFieldLinkId CALCULATED_FIELD_LINK_ID = new CalculatedFieldLinkId(UUID.fromString("a5609ef4-cb42-43ce-9b23-e090a4878d1c")); - - @MockBean - private CalculatedFieldLinkDao calculatedFieldLinkDao; - @SpyBean - private CalculatedFieldLinkDataValidator validator; - - @Test - public void testUpdateNonExistingCalculatedField() { - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(CALCULATED_FIELD_LINK_ID); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(UUID.fromString("136477af-fd07-4498-b9c9-54fe50e82992"))); - - given(calculatedFieldLinkDao.findById(TENANT_ID, CALCULATED_FIELD_LINK_ID.getId())).willReturn(null); - - assertThatThrownBy(() -> validator.validateUpdate(TENANT_ID, calculatedFieldLink)) - .isInstanceOf(DataValidationException.class) - .hasMessage("Can't update non existing calculated field link!"); - } - -} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 68c2c9462f..9c043eee8d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.cf; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; @@ -600,7 +600,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(CalculatedFieldType.SIMPLE); - calculatedField.setName("C to F" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setName("C to F" + RandomStringUtils.insecure().nextAlphabetic(5)); calculatedField.setDebugSettings(DebugSettings.all()); SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); @@ -624,15 +624,11 @@ public class CalculatedFieldTest extends AbstractContainerTest { return testRestClient.postCalculatedField(calculatedField); } - private CalculatedField createScriptCalculatedField() { - return createScriptCalculatedField(device.getId(), asset.getId()); - } - private CalculatedField createScriptCalculatedField(EntityId entityId, EntityId refEntityId) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(CalculatedFieldType.SCRIPT); - calculatedField.setName("Air density" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setName("Air density" + RandomStringUtils.insecure().nextAlphabetic(5)); calculatedField.setDebugSettings(DebugSettings.all()); ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 8ea0f70a3f..cd0b11bb25 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -18,14 +18,12 @@ package org.thingsboard.rule.engine.util; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -170,14 +168,6 @@ public class TenantIdLoader { case CALCULATED_FIELD: tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, new CalculatedFieldId(id)); break; - case CALCULATED_FIELD_LINK: - CalculatedFieldLink calculatedFieldLink = ctx.getCalculatedFieldService().findCalculatedFieldLinkById(ctxTenantId, new CalculatedFieldLinkId(id)); - if (calculatedFieldLink != null) { - tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, calculatedFieldLink.getCalculatedFieldId()); - } else { - tenantEntity = null; - } - break; case JOB: tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id)); break; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 16698b2841..15d513790d 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -29,7 +29,6 @@ import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -46,14 +45,12 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.job.Job; @@ -80,6 +77,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.notification.NotificationRequestService; @@ -92,7 +90,6 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -422,12 +419,6 @@ public class TenantIdLoaderTest { when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); doReturn(calculatedField).when(calculatedFieldService).findById(eq(tenantId), any()); break; - case CALCULATED_FIELD_LINK: - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(); - calculatedFieldLink.setTenantId(tenantId); - when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); - doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any()); - break; case JOB: Job job = new Job(); job.setTenantId(tenantId); From 79b5250944d259352922f50d010a16b9b856cf01 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 12 Nov 2025 09:41:50 +0200 Subject: [PATCH 11/18] UI: Fixed icon color max level warning hint --- ...ulated-field-geofencing-zone-groups-panel.component.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss index 3768392229..4963e67a2b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss @@ -44,6 +44,12 @@ } } + .max-args-warning { + .mat-icon { + color: #FAA405; + } + } + .limit-field-row { @media screen and (max-width: 520px) { display: flex; From 171da3494688f53a909a2287dd2024fd75734f9f Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 12 Nov 2025 10:23:40 +0200 Subject: [PATCH 12/18] UI: Fixed max limit init value and make aliase required --- .../components/widget/lib/cards/api-usage-widget.component.ts | 2 +- .../lib/settings/cards/api-usage-widget-settings.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts index 69f328a006..b7f48d41f6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts @@ -94,7 +94,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy { const progress = data[0][key.maxLimit.key] !== 0 ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0; key.progress = isFinite(progress) ? progress : 0; key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled'; - key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞'; + key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 && data[0][key.maxLimit.key] !== '' ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞'; key.current.value = isFinite(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0; }); this.cd.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts index 1d42b1503c..3e909b0901 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts @@ -29,7 +29,7 @@ import { UntypedFormBuilder, UntypedFormGroup, ValidationErrors, - ValidatorFn + ValidatorFn, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -140,7 +140,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { protected onSettingsSet(settings: WidgetSettings) { this.apiUsageWidgetSettingsForm = this.fb.group({ - dsEntityAliasId: [settings?.dsEntityAliasId], + dsEntityAliasId: [settings?.dsEntityAliasId, Validators.required], apiUsageDataKeys: this.prepareDataKeysFormArray(settings?.apiUsageDataKeys), targetDashboardState: [settings?.targetDashboardState], background: [settings?.background, []], From 7f7ab7bff3e7b8eae0e955938190aba7a8ca3707 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 12 Nov 2025 10:50:04 +0200 Subject: [PATCH 13/18] UI: Fixed expansion panel for force 2fa settings --- .../home/pages/admin/two-factor-auth-settings.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index faf1904427..d1192cace4 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -96,7 +96,7 @@ {{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }} From b815aa02dca875b1293fe7d9dc9cdd127835e772 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 12 Nov 2025 11:47:32 +0200 Subject: [PATCH 14/18] UI: Remove button to doc --- .../home/components/alarm-rules/alarm-rule-dialog.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index 04423edea6..e0f1a052d5 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -19,7 +19,6 @@

{{ 'alarm-rule.alarm-rule' | translate}}

-