Change cf strategy for vc

This commit is contained in:
Andrii Landiak 2025-02-28 18:17:20 +02:00
parent 94ec82ea6d
commit 2b51451896
32 changed files with 525 additions and 222 deletions

View File

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -61,6 +62,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
private final Map<EntityType, EntityImportService<?, ?, ?>> importServices = new HashMap<>();
private final RelationService relationService;
private final CalculatedFieldService calculatedFieldService;
private final RateLimitService rateLimitService;
private final TbLogEntityActionService logEntityActionService;
@ -69,11 +71,9 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET,
EntityType.DEVICE_PROFILE, EntityType.DEVICE,
EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE,
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE,
EntityType.CALCULATED_FIELD
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE
);
@Override
public <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {
if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) {
@ -128,15 +128,18 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
logEntityActionService.logEntityRelationAction(ctx.getTenantId(), null,
relation, ctx.getUser(), ActionType.RELATION_ADD_OR_UPDATE, null, relation);
}
}
ctx.getCalculatedFields().forEach((calculatedField, created) -> {
var savedCalculatedField = calculatedFieldService.save(calculatedField);
logEntityActionService.logEntityAction(ctx.getTenantId(), savedCalculatedField.getId(), savedCalculatedField, created ? ActionType.ADDED : ActionType.UPDATED, ctx.getUser());
});
}
@Override
public Comparator<EntityType> getEntityTypeComparatorForImport() {
return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf);
}
@SuppressWarnings("unchecked")
private <I extends EntityId, E extends ExportableEntity<I>, D extends EntityExportData<E>> EntityExportService<I, E, D> getExportService(EntityType entityType) {
EntityExportService<?, ?, ?> exportService = exportServices.get(entityType);

View File

@ -32,7 +32,6 @@ public interface EntitiesExportImportService {
<E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(EntitiesImportCtx ctx, EntityExportData<E> exportData) throws ThingsboardException;
void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException;
Comparator<EntityType> getEntityTypeComparatorForImport();

View File

@ -19,7 +19,8 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.AssetExportData;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
@ -27,12 +28,22 @@ import java.util.Set;
@Service
@TbCoreComponent
public class AssetExportService extends BaseEntityExportService<AssetId, Asset, EntityExportData<Asset>> {
public class AssetExportService extends BaseCalculatedFieldsExportService<AssetId, Asset, AssetExportData> {
protected AssetExportService(CalculatedFieldService calculatedFieldService) {
super(calculatedFieldService);
}
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, Asset asset, EntityExportData<Asset> exportData) {
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, Asset asset, AssetExportData exportData) {
asset.setCustomerId(getExternalIdOrElseInternal(ctx, asset.getCustomerId()));
asset.setAssetProfileId(getExternalIdOrElseInternal(ctx, asset.getAssetProfileId()));
setCalculatedFields(ctx, asset, exportData);
}
@Override
protected AssetExportData newExportData() {
return new AssetExportData();
}
@Override

View File

@ -19,7 +19,8 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.AssetProfileExportData;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
@ -27,13 +28,23 @@ import java.util.Set;
@Service
@TbCoreComponent
public class AssetProfileExportService extends BaseEntityExportService<AssetProfileId, AssetProfile, EntityExportData<AssetProfile>> {
public class AssetProfileExportService extends BaseCalculatedFieldsExportService<AssetProfileId, AssetProfile, AssetProfileExportData> {
protected AssetProfileExportService(CalculatedFieldService calculatedFieldService) {
super(calculatedFieldService);
}
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData) {
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, AssetProfile assetProfile, AssetProfileExportData exportData) {
assetProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultDashboardId()));
assetProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultRuleChainId()));
assetProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultEdgeRuleChainId()));
setCalculatedFields(ctx, assetProfile, exportData);
}
@Override
protected AssetProfileExportData newExportData() {
return new AssetProfileExportData();
}
@Override

View File

@ -0,0 +1,51 @@
/**
* 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.service.sync.ie.exporting.impl;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.sync.ie.CalculatedFieldExportData;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
import java.util.List;
public abstract class BaseCalculatedFieldsExportService<ID extends EntityId, E extends ExportableEntity<ID> & HasTenantId, D extends CalculatedFieldExportData<E>> extends BaseEntityExportService<ID, E, D> {
protected final CalculatedFieldService calculatedFieldService;
protected BaseCalculatedFieldsExportService(CalculatedFieldService calculatedFieldService) {
this.calculatedFieldService = calculatedFieldService;
}
protected void setCalculatedFields(EntitiesExportCtx<?> ctx, E entity, D exportData) {
if (ctx.getSettings().isExportCalculatedFields()) {
List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entity.getId());
calculatedFields.forEach(calculatedField -> {
calculatedField.getConfiguration().getArguments().values().forEach(argument -> {
if (argument.getRefEntityId() != null) {
EntityId externalId = getExternalIdOrElseInternal(ctx, argument.getRefEntityId());
argument.setRefEntityId(externalId);
}
});
});
exportData.setCalculatedFields(calculatedFields);
}
}
}

View File

@ -1,49 +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.service.sync.ie.exporting.impl;
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.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
import java.util.Set;
@Service
@TbCoreComponent
public class CalculatedFieldExportService extends BaseEntityExportService<CalculatedFieldId, CalculatedField, EntityExportData<CalculatedField>> {
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, CalculatedField calculatedField, EntityExportData<CalculatedField> exportData) {
calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, calculatedField.getEntityId()));
calculatedField.getConfiguration().getArguments().values().forEach(argument -> {
if (argument.getRefEntityId() != null) {
EntityId internalEntityId = getExternalIdOrElseInternal(ctx, argument.getRefEntityId());
argument.setRefEntityId(internalEntityId);
}
});
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.CALCULATED_FIELD);
}
}

View File

@ -15,12 +15,12 @@
*/
package org.thingsboard.server.service.sync.ie.exporting.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.sync.ie.DeviceExportData;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
@ -29,11 +29,15 @@ import java.util.Set;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class DeviceExportService extends BaseEntityExportService<DeviceId, Device, DeviceExportData> {
public class DeviceExportService extends BaseCalculatedFieldsExportService<DeviceId, Device, DeviceExportData> {
private final DeviceCredentialsService deviceCredentialsService;
public DeviceExportService(CalculatedFieldService calculatedFieldService, DeviceCredentialsService deviceCredentialsService) {
super(calculatedFieldService);
this.deviceCredentialsService = deviceCredentialsService;
}
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, Device device, DeviceExportData exportData) {
device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId()));
@ -44,6 +48,7 @@ public class DeviceExportService extends BaseEntityExportService<DeviceId, Devic
credentials.setDeviceId(null);
exportData.setCredentials(credentials);
}
setCalculatedFields(ctx, device, exportData);
}
@Override

View File

@ -19,7 +19,8 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.DeviceProfileExportData;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
@ -27,13 +28,23 @@ import java.util.Set;
@Service
@TbCoreComponent
public class DeviceProfileExportService extends BaseEntityExportService<DeviceProfileId, DeviceProfile, EntityExportData<DeviceProfile>> {
public class DeviceProfileExportService extends BaseCalculatedFieldsExportService<DeviceProfileId, DeviceProfile, DeviceProfileExportData> {
protected DeviceProfileExportService(CalculatedFieldService calculatedFieldService) {
super(calculatedFieldService);
}
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData) {
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, DeviceProfile deviceProfile, DeviceProfileExportData exportData) {
deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId()));
deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId()));
setCalculatedFields(ctx, deviceProfile, exportData);
}
@Override
protected DeviceProfileExportData newExportData() {
return new DeviceProfileExportData();
}
@Override

View File

@ -15,24 +15,28 @@
*/
package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.AssetExportData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class AssetImportService extends BaseEntityImportService<AssetId, Asset, EntityExportData<Asset>> {
public class AssetImportService extends BaseCalculatedFieldsImportService<AssetId, Asset, AssetExportData> {
private final AssetService assetService;
public AssetImportService(CalculatedFieldService calculatedFieldService, AssetService assetService) {
super(calculatedFieldService);
this.assetService = assetService;
}
@Override
protected void setOwner(TenantId tenantId, Asset asset, IdProvider idProvider) {
asset.setTenantId(tenantId);
@ -40,14 +44,14 @@ public class AssetImportService extends BaseEntityImportService<AssetId, Asset,
}
@Override
protected Asset prepare(EntitiesImportCtx ctx, Asset asset, Asset old, EntityExportData<Asset> exportData, IdProvider idProvider) {
protected Asset prepare(EntitiesImportCtx ctx, Asset asset, Asset old, AssetExportData exportData, IdProvider idProvider) {
asset.setAssetProfileId(idProvider.getInternalId(asset.getAssetProfileId()));
return asset;
}
@Override
protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData<Asset> exportData, IdProvider idProvider) {
return assetService.saveAsset(asset);
protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, AssetExportData exportData, IdProvider idProvider) {
return saveOrUpdateEntity(ctx, asset, exportData, idProvider, assetService::saveAsset);
}
@Override

View File

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
@ -23,25 +22,30 @@ import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.AssetProfileExportData;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class AssetProfileImportService extends BaseEntityImportService<AssetProfileId, AssetProfile, EntityExportData<AssetProfile>> {
public class AssetProfileImportService extends BaseCalculatedFieldsImportService<AssetProfileId, AssetProfile, AssetProfileExportData> {
private final AssetProfileService assetProfileService;
public AssetProfileImportService(CalculatedFieldService calculatedFieldService, AssetProfileService assetProfileService) {
super(calculatedFieldService);
this.assetProfileService = assetProfileService;
}
@Override
protected void setOwner(TenantId tenantId, AssetProfile assetProfile, IdProvider idProvider) {
assetProfile.setTenantId(tenantId);
}
@Override
protected AssetProfile prepare(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfile old, EntityExportData<AssetProfile> exportData, IdProvider idProvider) {
protected AssetProfile prepare(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfile old, AssetProfileExportData exportData, IdProvider idProvider) {
assetProfile.setDefaultRuleChainId(idProvider.getInternalId(assetProfile.getDefaultRuleChainId()));
assetProfile.setDefaultDashboardId(idProvider.getInternalId(assetProfile.getDefaultDashboardId()));
assetProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(assetProfile.getDefaultEdgeRuleChainId()));
@ -49,8 +53,8 @@ public class AssetProfileImportService extends BaseEntityImportService<AssetProf
}
@Override
protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData, IdProvider idProvider) {
return assetProfileService.saveAssetProfile(assetProfile);
protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfileExportData exportData, IdProvider idProvider) {
return saveOrUpdateEntity(ctx, assetProfile, exportData, idProvider, assetProfileService::saveAssetProfile);
}
@Override

View File

@ -0,0 +1,123 @@
/**
* 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.service.sync.ie.importing.impl;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.sync.ie.CalculatedFieldExportData;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public abstract class BaseCalculatedFieldsImportService<ID extends EntityId, E extends ExportableEntity<ID> & HasTenantId, D extends CalculatedFieldExportData<E>> extends BaseEntityImportService<ID, E, D> {
private final CalculatedFieldService calculatedFieldService;
protected BaseCalculatedFieldsImportService(CalculatedFieldService calculatedFieldService) {
this.calculatedFieldService = calculatedFieldService;
}
protected E saveOrUpdateEntity(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider, Function<E, E> saveFunction) {
E savedEntity = saveFunction.apply(entity);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
saveCalculatedFields(ctx, savedEntity, exportData, idProvider);
}
return savedEntity;
}
protected void saveCalculatedFields(EntitiesImportCtx ctx, E savedEntity, D exportData, IdProvider idProvider) {
if (exportData.getCalculatedFields() == null || !ctx.isSaveCalculatedFields()) {
return;
}
exportData.getCalculatedFields().forEach(calculatedField -> {
calculatedField.setTenantId(savedEntity.getTenantId());
calculatedField.setExternalId(calculatedField.getId());
calculatedField.setId(idProvider.getInternalId(calculatedField.getId(), false));
calculatedField.setEntityId(savedEntity.getId());
calculatedField.getConfiguration().getArguments().values().forEach(argument -> {
if (argument.getRefEntityId() != null) {
argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), false));
}
});
calculatedFieldService.save(calculatedField);
});
}
@Override
protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
boolean updated = super.updateRelatedEntitiesIfUnmodified(ctx, prepared, exportData, idProvider);
updated |= updateCalculatedFields(ctx, prepared, exportData, idProvider);
return updated;
}
private boolean updateCalculatedFields(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
var calculatedFields = exportData.getCalculatedFields();
if (calculatedFields == null || !ctx.isSaveCalculatedFields()) {
return false;
}
Map<CalculatedFieldId, CalculatedField> calculatedFieldMap = calculatedFields.stream()
.peek(newField -> {
newField.setTenantId(ctx.getTenantId());
newField.setExternalId(newField.getId());
newField.setId(idProvider.getInternalId(newField.getId(), false));
newField.setEntityId(prepared.getId());
newField.getConfiguration().getArguments().values().forEach(argument -> {
argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), false));
});
})
.collect(Collectors.toMap(CalculatedField::getId, field -> field));
List<CalculatedField> existingFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), prepared.getId());
boolean updated = false;
Map<CalculatedField, Boolean> result = new LinkedHashMap<>();
for (CalculatedField existingField : existingFields) {
if (calculatedFieldMap.containsKey(existingField.getId())) {
CalculatedField newField = calculatedFieldMap.get(existingField.getId());
if (!newField.equals(existingField)) {
result.put(newField, false);
}
calculatedFieldMap.remove(existingField.getId());
} else {
updated = true;
calculatedFieldService.deleteCalculatedField(ctx.getTenantId(), existingField.getId());
}
}
for (CalculatedField newField : calculatedFieldMap.values()) {
result.put(newField, true);
}
if (!result.isEmpty()) {
updated = true;
ctx.addCalculatedFields(result);
}
return updated;
}
}

View File

@ -1,85 +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.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.exception.ThingsboardException;
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.sync.ie.EntityExportData;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class CalculatedFieldImportService extends BaseEntityImportService<CalculatedFieldId, CalculatedField, EntityExportData<CalculatedField>> {
private final CalculatedFieldService calculatedFieldService;
@Override
protected void setOwner(TenantId tenantId, CalculatedField calculatedField, IdProvider idProvider) {
calculatedField.setTenantId(tenantId);
}
@Override
protected CalculatedField prepare(EntitiesImportCtx ctx, CalculatedField calculatedField, CalculatedField oldEntity, EntityExportData<CalculatedField> exportData, IdProvider idProvider) {
calculatedField.setEntityId(idProvider.getInternalId(calculatedField.getEntityId()));
calculatedField.getConfiguration().getArguments().values().forEach(argument -> {
if (argument.getRefEntityId() != null) {
EntityId internalEntityId = idProvider.getInternalId(argument.getRefEntityId());
argument.setRefEntityId(internalEntityId);
}
});
return calculatedField;
}
@Override
protected CalculatedField saveOrUpdate(EntitiesImportCtx ctx, CalculatedField calculatedField, EntityExportData<CalculatedField> exportData, IdProvider idProvider) {
ConstraintValidator.validateFields(calculatedField);
return calculatedFieldService.save(calculatedField);
}
@Override
protected CalculatedField deepCopy(CalculatedField calculatedField) {
return new CalculatedField(calculatedField);
}
@Override
protected void onEntitySaved(User user, CalculatedField savedEntity, CalculatedField oldEntity) throws ThingsboardException {
entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity, null,
oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null);
}
@Override
protected void cleanupForComparison(CalculatedField e) {
super.cleanupForComparison(e);
}
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD;
}
}

View File

@ -15,13 +15,13 @@
*/
package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.DeviceExportData;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -29,12 +29,17 @@ import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class DeviceImportService extends BaseEntityImportService<DeviceId, Device, DeviceExportData> {
public class DeviceImportService extends BaseCalculatedFieldsImportService<DeviceId, Device, DeviceExportData> {
private final DeviceService deviceService;
private final DeviceCredentialsService credentialsService;
public DeviceImportService(CalculatedFieldService calculatedFieldService, DeviceService deviceService, DeviceCredentialsService credentialsService) {
super(calculatedFieldService);
this.deviceService = deviceService;
this.credentialsService = credentialsService;
}
@Override
protected void setOwner(TenantId tenantId, Device device, IdProvider idProvider) {
device.setTenantId(tenantId);
@ -64,31 +69,44 @@ public class DeviceImportService extends BaseEntityImportService<DeviceId, Devic
@Override
protected Device saveOrUpdate(EntitiesImportCtx ctx, Device device, DeviceExportData exportData, IdProvider idProvider) {
Device savedDevice;
if (exportData.getCredentials() != null && ctx.isSaveCredentials()) {
exportData.getCredentials().setId(null);
exportData.getCredentials().setDeviceId(null);
return deviceService.saveDeviceWithCredentials(device, exportData.getCredentials());
savedDevice = deviceService.saveDeviceWithCredentials(device, exportData.getCredentials());
} else {
return deviceService.saveDevice(device);
savedDevice = deviceService.saveDevice(device);
}
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
saveCalculatedFields(ctx, savedDevice, exportData, idProvider);
}
return savedDevice;
}
@Override
protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, Device prepared, DeviceExportData exportData, IdProvider idProvider) {
boolean updated = super.updateRelatedEntitiesIfUnmodified(ctx, prepared, exportData, idProvider);
var credentials = exportData.getCredentials();
if (credentials != null && ctx.isSaveCredentials()) {
var existing = credentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), prepared.getId());
credentials.setId(existing.getId());
credentials.setDeviceId(prepared.getId());
if (!existing.equals(credentials)) {
credentialsService.updateDeviceCredentials(ctx.getTenantId(), credentials);
updated = true;
}
}
updated |= updateCredentials(ctx, prepared, exportData);
return updated;
}
private boolean updateCredentials(EntitiesImportCtx ctx, Device prepared, DeviceExportData exportData) {
var credentials = exportData.getCredentials();
if (credentials == null || !ctx.isSaveCredentials()) {
return false;
}
var existing = credentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), prepared.getId());
credentials.setId(existing.getId());
credentials.setDeviceId(prepared.getId());
if (!existing.equals(credentials)) {
credentialsService.updateDeviceCredentials(ctx.getTenantId(), credentials);
return true;
}
return false;
}
@Override
public EntityType getEntityType() {
return EntityType.DEVICE;

View File

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
@ -23,25 +22,30 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.DeviceProfileExportData;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class DeviceProfileImportService extends BaseEntityImportService<DeviceProfileId, DeviceProfile, EntityExportData<DeviceProfile>> {
public class DeviceProfileImportService extends BaseCalculatedFieldsImportService<DeviceProfileId, DeviceProfile, DeviceProfileExportData> {
private final DeviceProfileService deviceProfileService;
public DeviceProfileImportService(CalculatedFieldService calculatedFieldService, DeviceProfileService deviceProfileService) {
super(calculatedFieldService);
this.deviceProfileService = deviceProfileService;
}
@Override
protected void setOwner(TenantId tenantId, DeviceProfile deviceProfile, IdProvider idProvider) {
deviceProfile.setTenantId(tenantId);
}
@Override
protected DeviceProfile prepare(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfile old, EntityExportData<DeviceProfile> exportData, IdProvider idProvider) {
protected DeviceProfile prepare(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfile old, DeviceProfileExportData exportData, IdProvider idProvider) {
deviceProfile.setDefaultRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultEdgeRuleChainId()));
deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId()));
@ -51,8 +55,8 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
}
@Override
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider) {
return deviceProfileService.saveDeviceProfile(deviceProfile);
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfileExportData exportData, IdProvider idProvider) {
return saveOrUpdateEntity(ctx, deviceProfile, exportData, idProvider, deviceProfileService::saveDeviceProfile);
}
@Override

View File

@ -94,7 +94,6 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.Futures.transform;
import static org.thingsboard.server.common.data.sync.vc.VcUtils.checkBranchName;
@ -304,6 +303,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.updateRelations(config.isLoadRelations())
.saveAttributes(config.isLoadAttributes())
.saveCredentials(config.isLoadCredentials())
.saveCalculatedFields(config.isLoadCalculatedFields())
.findExistingByName(false)
.build());
ctx.setFinalImportAttempt(true);
@ -327,7 +327,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
var sw = TbStopWatch.create("before");
List<EntityType> entityTypes = request.getEntityTypes().keySet().stream()
.sorted(exportImportService.getEntityTypeComparatorForImport()).collect(Collectors.toList());
.sorted(exportImportService.getEntityTypeComparatorForImport()).toList();
for (EntityType entityType : entityTypes) {
log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType);
sw.startNew("Entities " + entityType.name());
@ -362,6 +362,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.updateRelations(config.isLoadRelations())
.saveAttributes(config.isLoadAttributes())
.saveCredentials(config.isLoadCredentials())
.saveCalculatedFields(config.isLoadCalculatedFields())
.findExistingByName(config.isFindExistingEntityByName())
.build();
}
@ -471,7 +472,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
}
@Override
public ListenableFuture<EntityDataDiff> compareEntityDataToVersion(User user, EntityId entityId, String versionId) throws Exception {
public ListenableFuture<EntityDataDiff> compareEntityDataToVersion(User user, EntityId entityId, String versionId) {
HasId<EntityId> entity = exportableEntitiesService.findEntityByTenantIdAndId(user.getTenantId(), entityId);
if (!(entity instanceof ExportableEntity)) throw new IllegalArgumentException("Unsupported entity type");
@ -484,6 +485,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.exportRelations(otherVersion.hasRelations())
.exportAttributes(otherVersion.hasAttributes())
.exportCredentials(otherVersion.hasCredentials())
.exportCalculatedFields(otherVersion.hasCalculatedFields())
.build());
EntityExportData<?> currentVersion;
try {
@ -503,7 +505,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override
public ListenableFuture<List<BranchInfo>> listBranches(TenantId tenantId) throws Exception {
public ListenableFuture<List<BranchInfo>> listBranches(TenantId tenantId) {
return gitServiceQueue.listBranches(tenantId);
}

View File

@ -69,6 +69,7 @@ public abstract class EntitiesExportCtx<R extends VersionCreateRequest> {
.exportRelations(config.isSaveRelations())
.exportAttributes(config.isSaveAttributes())
.exportCredentials(config.isSaveCredentials())
.exportCalculatedFields(config.isSaveCalculatedFields())
.build();
}
@ -85,4 +86,5 @@ public abstract class EntitiesExportCtx<R extends VersionCreateRequest> {
log.debug("[{}][{}] Local cache put: {}", internalId.getEntityType(), internalId.getId(), externalId);
externalIdMap.put(internalId, externalId != null ? externalId : internalId);
}
}

View File

@ -19,6 +19,7 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
@ -31,6 +32,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@ -54,6 +56,7 @@ public class EntitiesImportCtx {
private final Set<EntityId> notFoundIds = new HashSet<>();
private final Set<EntityRelation> relations = new LinkedHashSet<>();
private final Map<CalculatedField, Boolean> calculatedFields = new LinkedHashMap<>();
private boolean finalImportAttempt = false;
private EntityImportSettings settings;
@ -91,6 +94,10 @@ public class EntitiesImportCtx {
return getSettings().isSaveCredentials();
}
public boolean isSaveCalculatedFields() {
return getSettings().isSaveCalculatedFields();
}
public EntityId getInternalId(EntityId externalId) {
var result = externalToInternalIdMap.get(externalId);
log.debug("[{}][{}] Local cache {} for id", externalId.getEntityType(), externalId.getId(), result != null ? "hit" : "miss");
@ -120,6 +127,10 @@ public class EntitiesImportCtx {
relations.addAll(values);
}
public void addCalculatedFields(Map<CalculatedField, Boolean> calculatedFieldMap) {
calculatedFields.putAll(calculatedFieldMap);
}
public void addReferenceCallback(EntityId externalId, ThrowingRunnable tr) {
if (tr != null) {
referenceCallbacks.put(externalId, tr);
@ -140,5 +151,4 @@ public class EntitiesImportCtx {
return notFoundIds.contains(externalId);
}
}

View File

@ -39,6 +39,7 @@ public class EntityTypeExportCtx extends EntitiesExportCtx<VersionCreateRequest>
.exportRelations(config.isSaveRelations())
.exportAttributes(config.isSaveAttributes())
.exportCredentials(config.isSaveCredentials())
.exportCalculatedFields(config.isSaveCalculatedFields())
.build();
this.overwrite = ObjectUtils.defaultIfNull(config.getSyncStrategy(), defaultSyncStrategy) == SyncStrategy.OVERWRITE;
}

View File

@ -80,6 +80,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.sync.ie.DeviceExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
@ -207,11 +208,12 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId());
Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(),
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId(), calculatedField.getId())
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId())
.map(entityId -> {
try {
return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder()
.exportCredentials(false)
.exportCalculatedFields(true)
.build());
} catch (Exception e) {
throw new RuntimeException(e);
@ -281,23 +283,28 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE));
verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice));
// calculated field of imported device:
List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId());
assertThat(calculatedFields.size()).isOne();
var importedCalculatedField = calculatedFields.get(0);
assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(importedCalculatedField.getExternalId()).isEqualTo(calculatedField.getId());
EntityExportData<Device> updatedDeviceEntity = getAndClone(entitiesExportData, EntityType.DEVICE);
updatedDeviceEntity.getEntity().setLabel("t" + updatedDeviceEntity.getEntity().getLabel());
Device updatedDevice = importEntity(tenantAdmin2, updatedDeviceEntity).getSavedEntity();
verify(tbClusterService).onDeviceUpdated(eq(updatedDevice), eq(importedDevice));
CalculatedField importedCalculatedField = (CalculatedField) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedCalculatedField.getId()), eq(importedCalculatedField),
any(), eq(ActionType.ADDED), isNull());
importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD));
verify(entityActionService, Mockito.never()).logEntityAction(any(), eq(importedCalculatedField.getId()), eq(importedCalculatedField),
any(), eq(ActionType.UPDATED), isNull());
// update calculated field:
DeviceExportData deviceExportData = (DeviceExportData) getAndClone(entitiesExportData, EntityType.DEVICE);
deviceExportData.setCalculatedFields(deviceExportData.getCalculatedFields().stream().peek(field -> field.setName("t_" + field.getName())).toList());
importEntity(tenantAdmin2, deviceExportData).getSavedEntity();
EntityExportData<CalculatedField> updatedCalculatedFieldEntity = getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD);
updatedCalculatedFieldEntity.getEntity().setName("t" + updatedCalculatedFieldEntity.getEntity().getName());
CalculatedField updatedCalculatedField = importEntity(tenantAdmin2, updatedCalculatedFieldEntity).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(updatedCalculatedField.getId()), eq(updatedCalculatedField),
any(), eq(ActionType.UPDATED), isNull());
calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId());
assertThat(calculatedFields.size()).isOne();
importedCalculatedField = calculatedFields.get(0);
assertThat(importedCalculatedField.getExternalId()).isEqualTo(calculatedField.getId());
assertThat(importedCalculatedField.getName()).startsWith("t_");
}
@Test
@ -320,10 +327,11 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
Map<EntityId, EntityId> ids = new HashMap<>();
for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(),
deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId(), calculatedField.getId())) {
deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) {
EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId);
EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder()
.saveCredentials(false)
.saveCalculatedFields(true)
.build());
ids.put(entityId, (EntityId) importResult.getSavedEntity().getId());
}
@ -353,19 +361,20 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId());
assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId());
Device exportedDevice = (Device) exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())).getEntity();
EntityExportData<Device> entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId()));
Device exportedDevice = entityExportData.getEntity();
assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId());
assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId());
List<CalculatedField> calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields();
assertThat(calculatedFields.size()).isOne();
CalculatedField field = calculatedFields.get(0);
assertThat(field.getName()).isEqualTo(calculatedField.getName());
EntityView exportedEntityView = (EntityView) exportEntity(tenantAdmin2, (EntityViewId) ids.get(entityView.getId())).getEntity();
assertThat(exportedEntityView.getCustomerId()).isEqualTo(customer.getId());
assertThat(exportedEntityView.getEntityId()).isEqualTo(device.getId());
CalculatedField exportedCalculatedField = (CalculatedField) exportEntity(tenantAdmin2, (CalculatedFieldId) ids.get(calculatedField.getId())).getEntity();
assertThat(exportedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(exportedCalculatedField.getEntityId()).isEqualTo(device.getId());
assertThat(exportedCalculatedField.getConfiguration().getReferencedEntities()).isEqualTo(calculatedField.getConfiguration().getReferencedEntities());
deviceProfile.setDefaultDashboardId(null);
deviceProfileService.saveDeviceProfile(deviceProfile);
DeviceProfile importedDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId2, (DeviceProfileId) ids.get(deviceProfile.getId()));
@ -373,7 +382,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
deviceProfileService.saveDeviceProfile(importedDeviceProfile);
}
protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) {
Device device = new Device();
device.setTenantId(tenantId);
@ -618,6 +626,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
protected <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(User user, I entityId) throws Exception {
return exportEntity(user, entityId, EntityExportSettings.builder()
.exportCredentials(true)
.exportCalculatedFields(true)
.build());
}
@ -628,6 +637,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
protected <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(User user, EntityExportData<E> exportData) throws Exception {
return importEntity(user, exportData, EntityImportSettings.builder()
.saveCredentials(true)
.saveCalculatedFields(true)
.build());
}

View File

@ -576,12 +576,12 @@ public class VersionControlTest extends AbstractControllerTest {
Asset asset = createAsset(null, null, "Asset 1");
Device device = createDevice(null, null, "Device 1", "test1");
CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId());
String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.CALCULATED_FIELD);
String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
loginTenant2();
loadVersion(versionId, config -> {
config.setLoadCredentials(false);
}, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.CALCULATED_FIELD);
}, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
Asset importedAsset = findAsset(asset.getName());
Device importedDevice = findDevice(device.getName());
@ -602,9 +602,9 @@ public class VersionControlTest extends AbstractControllerTest {
public void testVcWithCalculatedFields_sameTenant() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
CalculatedField calculatedField = createCalculatedField("CalculatedField", asset.getId(), asset.getId());
String versionId = createVersion("asset and field", EntityType.ASSET, EntityType.CALCULATED_FIELD);
String versionId = createVersion("asset and field", EntityType.ASSET);
loadVersion(versionId, EntityType.ASSET, EntityType.CALCULATED_FIELD);
loadVersion(versionId, EntityType.ASSET);
CalculatedField importedCalculatedField = findCalculatedFieldByEntityId(asset.getId());
checkImportedEntity(tenantId1, calculatedField, tenantId1, importedCalculatedField);
assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName());
@ -676,10 +676,10 @@ public class VersionControlTest extends AbstractControllerTest {
request.setEntityTypes(Arrays.stream(entityTypes).collect(Collectors.toMap(t -> t, entityType -> {
EntityTypeVersionCreateConfig config = new EntityTypeVersionCreateConfig();
config.setAllEntities(true);
config.setSaveRelations(true);
config.setSaveAttributes(true);
config.setSaveCredentials(true);
config.setSaveCalculatedFields(true);
return config;
})));
@ -745,6 +745,7 @@ public class VersionControlTest extends AbstractControllerTest {
config.setLoadAttributes(true);
config.setLoadRelations(true);
config.setLoadCredentials(true);
config.setLoadCalculatedFields(true);
config.setRemoveOtherEntities(false);
config.setFindExistingEntityByName(true);
configModifier.accept(config);

View File

@ -0,0 +1,28 @@
/**
* 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.sync.ie;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.thingsboard.server.common.data.asset.Asset;
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Data
public class AssetExportData extends CalculatedFieldExportData<Asset> {
}

View File

@ -0,0 +1,28 @@
/**
* 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.sync.ie;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.thingsboard.server.common.data.asset.AssetProfile;
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Data
public class AssetProfileExportData extends CalculatedFieldExportData<AssetProfile> {
}

View File

@ -0,0 +1,56 @@
/**
* 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.sync.ie;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.cf.CalculatedField;
import java.util.Comparator;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class CalculatedFieldExportData<E extends ExportableEntity<?>> extends EntityExportData<E> {
public static final Comparator<CalculatedField> calculatedFieldsComparator = Comparator.comparing(CalculatedField::getName);
@JsonProperty(index = 102)
@JsonIgnoreProperties({"entityId", "createdTime", "version"})
private List<CalculatedField> calculatedFields;
@JsonIgnore
@Override
public boolean hasCalculatedFields() {
return calculatedFields != null;
}
@Override
public CalculatedFieldExportData<E> sort() {
super.sort();
if (calculatedFields != null && !calculatedFields.isEmpty()) {
calculatedFields.sort(calculatedFieldsComparator);
}
return this;
}
}

View File

@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Data
public class DeviceExportData extends EntityExportData<Device> {
public class DeviceExportData extends CalculatedFieldExportData<Device> {
@JsonProperty(index = 3)
@JsonIgnoreProperties({"id", "deviceId", "createdTime", "version"})
@ -38,4 +38,5 @@ public class DeviceExportData extends EntityExportData<Device> {
public boolean hasCredentials() {
return credentials != null;
}
}

View File

@ -0,0 +1,28 @@
/**
* 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.sync.ie;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.thingsboard.server.common.data.DeviceProfile;
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Data
public class DeviceProfileExportData extends CalculatedFieldExportData<DeviceProfile> {
}

View File

@ -37,7 +37,10 @@ import java.util.Map;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "entityType", include = As.EXISTING_PROPERTY, visible = true, defaultImpl = EntityExportData.class)
@JsonSubTypes({
@Type(name = "DEVICE_PROFILE", value = DeviceProfileExportData.class),
@Type(name = "ASSET_PROFILE", value = AssetProfileExportData.class),
@Type(name = "DEVICE", value = DeviceExportData.class),
@Type(name = "ASSET", value = AssetExportData.class),
@Type(name = "RULE_CHAIN", value = RuleChainExportData.class),
@Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class),
@Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class)
@ -96,4 +99,9 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
return relations != null;
}
@JsonIgnore
public boolean hasCalculatedFields() {
return false;
}
}

View File

@ -25,7 +25,10 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@Builder
public class EntityExportSettings {
private boolean exportRelations;
private boolean exportAttributes;
private boolean exportCredentials;
private boolean exportCalculatedFields;
}

View File

@ -25,8 +25,11 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@Builder
public class EntityImportSettings {
private boolean findExistingByName;
private boolean updateRelations;
private boolean saveAttributes;
private boolean saveCredentials;
private boolean saveCalculatedFields;
}

View File

@ -17,13 +17,18 @@ package org.thingsboard.server.common.data.sync.vc.request.create;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class VersionCreateConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1223723167716612772L;
private boolean saveRelations;
private boolean saveAttributes;
private boolean saveCredentials;
private boolean saveCalculatedFields;
}

View File

@ -23,5 +23,6 @@ public class VersionLoadConfig {
private boolean loadRelations;
private boolean loadAttributes;
private boolean loadCredentials;
private boolean loadCalculatedFields;
}

View File

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.cf;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

View File

@ -15,9 +15,7 @@
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
@ -60,6 +58,15 @@ public class CalculatedFieldLinkEntity extends BaseSqlEntity<CalculatedFieldLink
super();
}
public CalculatedFieldLinkEntity(CalculatedFieldLink calculatedFieldLink) {
this.setUuid(calculatedFieldLink.getUuidId());
this.createdTime = calculatedFieldLink.getCreatedTime();
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));