diff --git a/application/src/main/java/org/thingsboard/server/controller/EntitiesExportImportController.java b/application/src/main/java/org/thingsboard/server/controller/EntitiesExportImportController.java index a6ad9f1ca6..c91fd0c292 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntitiesExportImportController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesExportImportController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; @@ -39,6 +40,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; + @RestController @RequestMapping("/api/entities") @TbCoreComponent @@ -50,6 +53,42 @@ public class EntitiesExportImportController extends BaseController { private final ExportableEntitiesService exportableEntitiesService; + @ApiOperation(value = "Export entities by request", notes = "" + + "Takes export request and returns list of export data for each entity found by request. " + + "Supported entity types for export, hence for import, are **DEVICE**, **DEVICE_PROFILE**, **ASSET**, " + + "**CUSTOMER**, **RULE_CHAIN** and **DASHBOARD**." + NEW_LINE + + "For each type of export request, you can set some export settings: \n" + + "- **exportRelations** - whether to export inbound and outbound relations for an entity " + + "(only relations of type group COMMON can be exported)" + NEW_LINE + + "Supported export requests:\n" + + "- **SINGLE_ENTITY**:" + NEW_LINE + + " To export a single entity by id. Example:" + NEW_LINE + + "```\n{\n \"type\": \"SINGLE_ENTITY\",\n \"entityId\": {\n \"entityType\": \"DEVICE\",\n \"id\": \"2eb16d70-989d-11ec-93b5-6de6c2b68078\"\n },\n \"exportSettings\": {\n \"exportRelations\": false\n }\n}\n```" + NEW_LINE + + "- **ENTITY_LIST**:" + NEW_LINE + + " To export a list of entities by their ids. Example:" + NEW_LINE + + "```\n{\n \"type\": \"ENTITY_LIST\",\n \"entitiesIds\": [\n {\n \"entityType\": \"DEVICE\",\n \"id\": \"2eb16d70-989d-11ec-93b5-6de6c2b68078\"\n },\n {\n \"entityType\": \"ASSET\",\n \"id\": \"2f0a3bd0-989d-11ec-93b5-6de6c2b68078\"\n }\n ],\n \"exportSettings\": {\n \"exportRelations\": true\n }\n}\n```" + NEW_LINE + + "- **ENTITY_TYPE**:" + NEW_LINE + + " To export entities of specified entity type. You need to specify page size, " + + "and may specify page index and customer id (to limit the list of entities to the ones owned by a customer). " + + "Entities are ordered by created time descendingly. Example:" + NEW_LINE + + "```\n{\n \"type\": \"ENTITY_TYPE\",\n \"entityType\": \"ASSET\",\n \"page\": 0,\n \"pageSize\": 100,\n \"customerId\": \"2eb16d70-989d-11ec-93b5-6de6c2b68078\"\n}\n```" + NEW_LINE + + "- **CUSTOM_ENTITY_FILTER**:" + NEW_LINE + + " To export entities by custom entity filter. The order used is the same as for ENTITY_TYPE export request. Example:" + NEW_LINE + + "```\n{\n \"type\": \"CUSTOM_ENTITY_FILTER\",\n \"filter\": {\n \"type\": \"deviceType\",\n \"deviceType\": \"Thermostats\",\n \"deviceNameFilter\": \"\"\n },\n \"page\": 0,\n \"pageSize\": 100,\n \"customerId\": null,\n \"exportSettings\": {\n \"exportRelations\": false\n }\n}\n```" + NEW_LINE + + "- **CUSTOM_ENTITY_QUERY**:" + NEW_LINE + + " To export entities by custom entity query. Example: " + NEW_LINE + + "```\n{\n \"type\": \"CUSTOM_ENTITY_QUERY\",\n \"query\": {\n \"entityFilter\": {\n \"type\": \"entityType\",\n \"entityType\": \"DEVICE\"\n },\n \"pageLink\": {\n \"page\": 0,\n \"pageSize\": 200,\n \"textSearch\": \"THB_\",\n \"sortOrder\": {\n \"key\": {\n \"type\": \"ENTITY_FIELD\",\n \"key\": \"name\"\n },\n \"direction\": \"DESC\"\n }\n },\n \"entityFields\": [\n {\n \"type\": \"ENTITY_FIELD\",\n \"key\": \"name\"\n }\n ],\n \"latestValues\": [\n {\n \"type\": \"SERVER_ATTRIBUTE\",\n \"key\": \"lastActivityTime\"\n }\n ],\n \"keyFilters\": [\n {\n \"key\": {\n \"type\": \"SERVER_ATTRIBUTE\",\n \"key\": \"lastActivityTime\"\n },\n \"valueType\": \"NUMERIC\",\n \"predicate\": {\n \"type\": \"NUMERIC\",\n \"operation\": \"GREATER\",\n \"value\": {\n \"defaultValue\": 0\n }\n }\n }\n ]\n },\n \"customerId\": null,\n \"exportSettings\": {\n \"exportRelations\": false\n }\n}\n```" + NEW_LINE + + "Mostly, export data of an entity contains the whole entity itself and its relations " + + "(if option to export relations was enabled):" + NEW_LINE + + "```\n[\n {\n \"entityType\": \"ASSET\",\n \"entity\": {\n \"id\": { ... },\n \"createdTime\": 1648204424029,\n \"additionalInfo\": {\n \"description\": \"\"\n },\n \"tenantId\": { ... },\n \"customerId\": { ... },\n \"name\": \"Asset 1\",\n \"type\": \"A\",\n ...\n },\n \"relations\": [\n {\n \"from\": {\n \"entityType\": \"ASSET\",\n \"id\": ...\n },\n \"to\": {\n \"entityType\": \"DEVICE\",\n \"id\": ...\n },\n \"type\": \"Contains\",\n \"typeGroup\": \"COMMON\",\n \"additionalInfo\": {\n \"a\": \"b\"\n }\n }\n ]\n }\n]\n```" + NEW_LINE + + "For devices, export data will additionally contain device's credentials; for rule chains - its metadata:" + NEW_LINE + + "```\n[\n {\n \"entityType\": \"DEVICE\",\n \"entity\": { ... },\n \"credentials\": {\n \"id\": { ... },\n \"createdTime\": 1648829321209,\n \"deviceId\": { ... },\n \"credentialsType\": \"ACCESS_TOKEN\",\n \"credentialsId\": \"5cZEDo45KGW7JgVNv4Ko\",\n \"credentialsValue\": null\n }\n }\n]\n```" + NEW_LINE + + "```\n[\n {\n \"entityType\": \"RULE_CHAIN\",\n \"entity\": {\n \"id\": { ... },\n \"createdTime\": 1646056614257,\n \"additionalInfo\": null,\n \"tenantId\": { ... },\n \"name\": \"Rule Chain 2\",\n \"type\": \"CORE\",\n \"firstRuleNodeId\": { ... },\n \"root\": false,\n ...\n },\n \"metaData\": {\n \"ruleChainId\": { ... },\n \"firstNodeIndex\": 7,\n \"nodes\": [ ... ],\n \"connections\": [ ... ],\n \"ruleChainConnections\": null\n }\n }\n]\n```" + NEW_LINE + + "Returned export data is to be used later for import request." + NEW_LINE + + "If any entity found by request is of unsupported type - an error will be returned.\n" + + "Also, if a user does not have a READ permission for an entity (or, if relations are exported, for a bounded entity), " + + "access will be denied." + + ControllerConstants.TENANT_AUTHORITY_PARAGRAPH) @PostMapping("/export") @PreAuthorize("hasAuthority('TENANT_ADMIN')") public List> exportEntities(@RequestBody ExportRequest exportRequest) throws ThingsboardException { @@ -62,6 +101,12 @@ public class EntitiesExportImportController extends BaseController { } } + @ApiOperation(value = "Export entities by multiple requests", notes = "" + + "The API behaviour is the same as for exporting entities by single request, " + + "except that this method takes an array of export requests as a request body." + NEW_LINE + + "Example:" + NEW_LINE + + "```\n[\n {\n \"type\": \"SINGLE_ENTITY\",\n \"entityId\": {\n \"entityType\": \"DEVICE_PROFILE\",\n \"id\": \"5f9eda10-b442-11ec-bbf5-adec34031568\"\n }\n },\n {\n \"type\": \"CUSTOM_ENTITY_FILTER\",\n \"filter\": {\n \"type\": \"deviceType\",\n \"deviceType\": \"thermostat\",\n \"deviceNameFilter\": \"\"\n },\n \"pageSize\": 1000\n },\n {\n \"type\": \"ENTITY_TYPE\",\n \"entityType\": \"ASSET\",\n \"pageSize\": 1000,\n \"exportSettings\": {\n \"exportRelations\": true\n }\n },\n {\n \"type\": \"ENTITY_LIST\",\n \"entitiesIds\": [\n {\n \"entityType\": \"RULE_CHAIN\",\n \"id\": \"2ef13590-989d-11ec-93b5-6de6c2b68078\"\n },\n {\n \"entityType\": \"RULE_CHAIN\",\n \"id\": \"e7311ec0-b442-11ec-bbf5-adec34031568\"\n }\n ]\n }\n]\n```" + + ControllerConstants.TENANT_AUTHORITY_PARAGRAPH) @PostMapping(value = "/export", params = {"multiple"}) @PreAuthorize("hasAuthority('TENANT_ADMIN')") public List> exportEntities(@RequestBody List exportRequests) throws ThingsboardException { @@ -98,6 +143,34 @@ public class EntitiesExportImportController extends BaseController { } + @ApiOperation(value = "Import entities by request", notes = "" + + "Takes import request and returns the list of import results. " + + "Import request must contain the list of export data and might contain import settings. " + NEW_LINE + + "The method creates an entity if it is new in the scope of a tenant, or otherwise updates an existing one. " + + "On entity import request, we first try to find an entity within a tenant that has externalId equal " + + "to the id in the export data. If the platform fails to do that, we then search for an entity with " + + "regular (internal) id like the one in the export data (this is useful in case we are exporting and " + + "importing entities within the same tenant). Then, if we still haven't found any entity, if findExistingByName " + + "option of the EntityImportSettings is enabled, we will search for the one by its name (this is also useful " + + "for avoiding conflicts with default device profile or Root Rule Chain when importing all entities from another " + + "tenant). After, if the exported entity is new for this tenant, we simply save it with external id " + + "from the export data, and also create relations if any (if updateRelations option is enabled). " + + "Otherwise, we will reset all fields of the existing entity to the ones from the export data and save it, " + + "and also will update the list of relations (remove the ones that aren't present in the export data, " + + "and update or create others), if updateRelations option is enabled." + NEW_LINE + + "If an entity contains references to some other entities, like device references certain device profile, " + + "we will find this other entity within the tenant by this principle: look for an entity with " + + "such external id, or otherwise, internal id. This requires referenced entities to be imported " + + "before the referencing entity (if we are importing to another tenant). So, when receiving the list " + + "of entities' export data for import, we first try to fix the data order to import 'standalone' entities first." + NEW_LINE + + "As for relations importing, they are processed after all entities in the import batch are already saved, " + + "and the internal id of a bounded entity is found with the regular principle." + NEW_LINE + + "Import of all entities and their relations from the import request is processed in the single transaction, " + + "and so everything will be rolled back if the platform fails to e.g. find internal entity by external id. \n" + + "Example of import request:\n" + + "```\n{\n \"importSettings\": {\n \"findExistingByName\": false,\n \"updateRelations\": false\n },\n \"exportDataList\": [\n {\n \"entityType\": \"DEVICE_PROFILE\",\n \"entity\": {\n \"id\": {\n \"entityType\": \"DEVICE_PROFILE\",\n \"id\": \"f84363d0-b442-11ec-bbf5-adec34031568\"\n },\n \"createdTime\": 1649096026765,\n \"tenantId\": {\n \"entityType\": \"TENANT\",\n \"id\": \"4c9001b0-b442-11ec-bbf5-adec34031568\"\n },\n \"name\": \"Profile 1\",\n ...\n }\n },\n {\n \"entityType\": \"DEVICE\",\n \"entity\": {\n \"id\": {\n \"entityType\": \"DEVICE\",\n \"id\": \"98161420-b4ca-11ec-ab0c-e7744c90d468\"\n },\n \"createdTime\": 1649154276962,\n \"tenantId\": {\n \"entityType\": \"TENANT\",\n \"id\": \"4c9001b0-b442-11ec-bbf5-adec34031568\"\n },\n \"customerId\": {\n \"entityType\": \"CUSTOMER\",\n \"id\": \"13814000-1dd2-11b2-8080-808080808080\"\n },\n \"name\": \"Device 1\",\n \"type\": \"Profile 1\",\n \"label\": \"v1.0\",\n \"deviceProfileId\": {\n \"entityType\": \"DEVICE_PROFILE\",\n \"id\": \"f84363d0-b442-11ec-bbf5-adec34031568\"\n },\n ...\n },\n \"credentials\": {\n \"id\": {\n \"id\": \"981e0360-b4ca-11ec-ab0c-e7744c90d468\"\n },\n \"createdTime\": 1649154277014,\n \"deviceId\": {\n \"entityType\": \"DEVICE\",\n \"id\": \"98161420-b4ca-11ec-ab0c-e7744c90d468\"\n },\n \"credentialsType\": \"ACCESS_TOKEN\",\n \"credentialsId\": \"sGExNdnl71uKmkNvtNdp\",\n \"credentialsValue\": null\n }\n }\n ]\n}\n```" + NEW_LINE + + "The response contains a list of EntityImportResult which has values of savedEntity and oldEntity:\n" + + "```\n[\n {\n \"savedEntity\": {\n \"id\": {\n \"entityType\": \"ASSET\",\n \"id\": \"d73d7690-b4e6-11ec-b9eb-0562e1a20a1b\"\n },\n \"createdTime\": 1649166408825,\n \"additionalInfo\": {\n \"description\": \"\"\n },\n \"tenantId\": {\n \"entityType\": \"TENANT\",\n \"id\": \"c0b2e4f0-b4e6-11ec-b9eb-0562e1a20a1b\"\n },\n \"name\": \"Asset 1\",\n \"type\": \"A\",\n \"label\": \"v2.0\",\n ...\n \"externalId\": {\n \"entityType\": \"ASSET\",\n \"id\": \"6b03ab20-989e-11ec-b446-89df822d7fa2\"\n }\n },\n \"oldEntity\": {\n \"id\": {\n \"entityType\": \"ASSET\",\n \"id\": \"d73d7690-b4e6-11ec-b9eb-0562e1a20a1b\"\n },\n \"createdTime\": 1649166408825,\n \"tenantId\": {\n \"entityType\": \"TENANT\",\n \"id\": \"c0b2e4f0-b4e6-11ec-b9eb-0562e1a20a1b\"\n },\n \"name\": \"Asset 1\",\n \"type\": \"A\",\n \"label\": \"v1.0\",\n ...\n \"externalId\": null\n },\n \"entityType\": \"ASSET\"\n },\n {\n \"savedEntity\": {\n \"id\": {\n \"entityType\": \"ASSET\",\n \"id\": \"387213f0-b4ea-11ec-abfc-6dcc6508d0b5\"\n },\n \"createdTime\": 1649167860399,\n \"tenantId\": {\n \"entityType\": \"TENANT\",\n \"id\": \"c0b2e4f0-b4e6-11ec-b9eb-0562e1a20a1b\"\n },\n \"name\": \"Asset 2\",\n \"type\": \"B\",\n ...\n \"externalId\": {\n \"entityType\": \"ASSET\",\n \"id\": \"0b9ea0d0-ac27-11ec-a2a6-89d15eae3b21\"\n }\n },\n \"oldEntity\": null,\n \"entityType\": \"ASSET\"\n }\n]\n```") @PostMapping("/import") public List> importEntities(@RequestBody ImportRequest importRequest) throws ThingsboardException { SecurityUser user = getCurrentUser();