diff --git a/application/pom.xml b/application/pom.xml index 9a02714efd..1a5665143c 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -289,6 +289,11 @@ rest-client test + + org.thingsboard.client + thingsboard-ce-client + test + org.springframework.security spring-security-test @@ -502,6 +507,82 @@ + + + openapi-spec + + true + none + none + + + + + org.springframework.boot + spring-boot-maven-plugin + + org.thingsboard.server.ThingsboardServerApplication + 9001 + + + + build-info + build-info + + false + + + + openapi-start + start + + false + true + -Xmx1024m + + --spring.config.name=thingsboard + --spring.profiles.active=openapi + + 180 + 2000 + + + + openapi-stop + stop + + false + + + + + + org.springdoc + springdoc-openapi-maven-plugin + 1.4 + + + generate-openapi-spec + generate + + + + http://localhost:8080/v3/api-docs/thingsboard + openapi.json + ${project.build.directory} + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + jenkins diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 1a1a409c1a..6bdb1ae758 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -15,11 +15,15 @@ */ package org.thingsboard.server.config; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverter; import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.jackson.ModelResolver; import io.swagger.v3.core.util.Json; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; @@ -31,6 +35,7 @@ import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.IntegerSchema; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.parameters.RequestBody; @@ -39,11 +44,12 @@ import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.tags.Tag; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springdoc.core.customizers.OpenApiCustomizer; import org.springdoc.core.customizers.OperationCustomizer; -import org.springdoc.core.customizers.RouterOperationCustomizer; import org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer; +import org.springdoc.core.utils.SpringDocUtils; import org.springdoc.core.models.GroupedOpenApi; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.properties.SwaggerUiConfigProperties; @@ -54,26 +60,35 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; -import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.RequestParam; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; -import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse; import org.thingsboard.server.exception.ThingsboardErrorResponse; import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginResponse; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.Deque; +import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -83,16 +98,30 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @Profile("!test") public class SwaggerConfiguration { + @PostConstruct + public void configureModelResolver() { + ModelResolver.enumsAsRef = true; + SpringDocUtils.getConfig().replaceWithSchema(ByteBuffer.class, + new Schema().type("string").format("byte")); + } + public static final String LOGIN_ENDPOINT = "/api/auth/login"; public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token"; - private static final String LOGIN_PASSWORD_SCHEME = "HTTP login form"; - private static final String API_KEY_SCHEME = "API key form"; + private static final String LOGIN_PASSWORD_SCHEME = "HttpLoginForm"; + private static final String API_KEY_SCHEME = "ApiKeyForm"; private static final ApiResponses loginResponses = loginResponses(); private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false); private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true); + // Populated by mapAwareConverter, consumed by customOpenApiCustomizer. + // Keyed by the schema name that swagger-core generates (see resolveSchemaName). + private final Map> schemaPropertyOrders = new ConcurrentHashMap<>(); + private final Map> schemaOwnProps = new ConcurrentHashMap<>(); + // Tracks schema name → fully-qualified class names to detect collisions. + private final Map> schemaNameToClasses = new ConcurrentHashMap<>(); + @Value("${swagger.api_path:/api/**}") private String apiPath; @Value("${swagger.security_path_regex}") @@ -157,9 +186,9 @@ public class SwaggerConfiguration { .in(SecurityScheme.In.HEADER) .description(""" Enter the API key value with 'ApiKey' prefix in format: **ApiKey ** - + Example: **ApiKey tb_5te51SkLRYpjGrujUGwqkjFvooWBlQpVe2An2Dr3w13wjfxDW** - +
**NOTE**: Use only ONE authentication method at a time. If both are authorized, JWT auth takes the priority.
"""); @@ -213,11 +242,12 @@ public class SwaggerConfiguration { private void addLoginOperation(OpenAPI openAPI) { var operation = new Operation(); operation.summary("Login method to get user JWT token data"); + operation.operationId("login"); operation.description(""" Login method used to authenticate user and get JWT token data. - + Value of the response **token** field can be used as **X-Authorization** header value: - + `X-Authorization: Bearer $JWT_TOKEN_VALUE`."""); var requestBody = new RequestBody().description("Login request") @@ -235,11 +265,12 @@ public class SwaggerConfiguration { private void addRefreshTokenOperation(OpenAPI openAPI) { var operation = new Operation(); operation.summary("Refresh user JWT token data"); + operation.operationId("refreshToken"); operation.description(""" Method to refresh JWT token. Provide a valid refresh token to get a new JWT token. - + The response contains a new token that can be used for authorization. - + `X-Authorization: Bearer $JWT_TOKEN_VALUE`"""); var requestBody = new RequestBody().description("Refresh token request") @@ -260,7 +291,6 @@ public class SwaggerConfiguration { return GroupedOpenApi.builder() .group(groupName) .pathsToMatch(apiPath) - .addRouterOperationCustomizer(routerOperationCustomizer(localSpringDocParameterNameDiscoverer)) .addOperationCustomizer(operationCustomizer()) .addOpenApiCustomizer(customOpenApiCustomizer()) .build(); @@ -270,9 +300,34 @@ public class SwaggerConfiguration { @Lazy(false) ModelConverter mapAwareConverter() { return (type, context, chain) -> { + // Strip field-level @JsonIgnoreProperties from context annotations so it + // doesn't pollute the global schema. The OpenAPI schema should show all + // properties; field-level ignore is a serialization concern only. + Annotation[] ctxAnnotations = type.getCtxAnnotations(); + if (ctxAnnotations != null) { + Annotation[] filtered = Arrays.stream(ctxAnnotations) + .filter(a -> !(a instanceof JsonIgnoreProperties)) + .toArray(Annotation[]::new); + if (filtered.length != ctxAnnotations.length) { + type.ctxAnnotations(filtered); + } + } + + JavaType javaType = Json.mapper().constructType(type.getType()); + if (javaType != null) { + Class cls = javaType.getRawClass(); + Schema atomicSchema = switch (cls.getName()) { + case "java.util.concurrent.atomic.AtomicInteger" -> new IntegerSchema().format("int32"); + case "java.util.concurrent.atomic.AtomicLong" -> new IntegerSchema().format("int64"); + case "com.google.common.util.concurrent.AtomicDouble" -> new IntegerSchema().format("double"); + default -> null; + }; + if (atomicSchema != null) { + return atomicSchema; + } + } if (chain.hasNext()) { Schema schema = chain.next().resolve(type, context, chain); - JavaType javaType = Json.mapper().constructType(type.getType()); if (javaType != null) { Class cls = javaType.getRawClass(); if (Map.class.isAssignableFrom(cls)) { @@ -282,6 +337,26 @@ public class SwaggerConfiguration { schema.setProperties(null); } } + } else { + // Precompute property order and own-prop names for this class. + // The actual reordering happens later in the OpenApiCustomizer, + // which has access to the final state of all component schemas + // (including ones where the ModelConverter only sees a $ref). + try { + var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType); + String schemaName = resolveSchemaName(javaType); + Set classes = schemaNameToClasses.computeIfAbsent(schemaName, k -> ConcurrentHashMap.newKeySet()); + if (classes.add(cls.getName()) && classes.size() > 1) { + log.error("Duplicate OpenAPI schema name '{}' mapped by: {}. Use @Schema(name = ...) to disambiguate.", schemaName, classes); + } + schemaPropertyOrders.put(schemaName, resolvePropertyOrder(cls, beanDesc)); + Set ownProps = computeOwnPropNames(cls, beanDesc); + if (!ownProps.isEmpty()) { + schemaOwnProps.put(schemaName, ownProps); + } + } catch (Exception e) { + log.debug("Failed to resolve property order for {}: {}", cls.getName(), e.getMessage()); + } } } return schema; @@ -292,45 +367,19 @@ public class SwaggerConfiguration { } private void addDefaultSchemas(OpenAPI openAPI) { - var jsonNodeSchema = ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(JsonNode.class)).schema; - jsonNodeSchema.setType("any"); - //noinspection unchecked - jsonNodeSchema.setExamples(List.of(JacksonUtil.newObjectNode())); - jsonNodeSchema.setDescription("A value representing the any type (object or primitive)"); + Schema errorCodeSchema = new Schema<>() + .type("integer") + .description("Platform error code") + ._enum(Arrays.stream(ThingsboardErrorCode.values()) + .map(ThingsboardErrorCode::getErrorCode) + .collect(Collectors.toList())); openAPI.getComponents() - .addSchemas("JsonNode", jsonNodeSchema) .addSchemas("LoginRequest", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginRequest.class)).schema) .addSchemas("LoginResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginResponse.class)).schema) .addSchemas("ThingsboardErrorResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardErrorResponse.class)).schema) - .addSchemas("ThingsboardCredentialsExpiredResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardCredentialsExpiredResponse.class)).schema); - } - - private RouterOperationCustomizer routerOperationCustomizer(SpringDocParameterNameDiscoverer localSpringDocParameterNameDiscoverer) { - return (routerOperation, handlerMethod) -> { - String[] pNames = localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod()); - String[] reflectionParametersNames = Arrays.stream(handlerMethod.getMethod().getParameters()).map(java.lang.reflect.Parameter::getName).toArray(String[]::new); - if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) { - pNames = reflectionParametersNames; - } - MethodParameter[] parameters = handlerMethod.getMethodParameters(); - List requestParams = new ArrayList<>(); - for (var i = 0; i < parameters.length; i++) { - var methodParameter = parameters[i]; - RequestParam requestParam = methodParameter.getParameterAnnotation(RequestParam.class); - if (requestParam != null) { - String pName = StringUtils.isNotBlank(requestParam.value()) ? requestParam.value() : - pNames[i]; - if (StringUtils.isNotBlank(pName)) { - requestParams.add(pName); - } - } - } - if (!requestParams.isEmpty()) { - var path = routerOperation.getPath() + "{?" + String.join(",", requestParams) + "}"; - routerOperation.setPath(path); - } - return routerOperation; - }; + .addSchemas("ThingsboardCredentialsExpiredResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardCredentialsExpiredResponse.class)).schema) + .addSchemas("ThingsboardErrorCode", errorCodeSchema) + .addSchemas("AiChatModelConfig", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(AiChatModelConfig.class)).schema); } private OperationCustomizer operationCustomizer() { @@ -347,6 +396,19 @@ public class SwaggerConfiguration { var apiKeyRequirement = createSecurityRequirement(API_KEY_SCHEME); return openAPI -> { + // Fail fast on duplicate schema names — two different classes resolving to the same + // OpenAPI schema name causes one to silently overwrite the other. + List duplicates = schemaNameToClasses.entrySet().stream() + .filter(e -> e.getValue().size() > 1) + .map(e -> "'" + e.getKey() + "' mapped by: " + e.getValue()) + .sorted() + .toList(); + if (!duplicates.isEmpty()) { + throw new IllegalStateException( + "Duplicate OpenAPI schema names detected. Use @Schema(name = ...) to disambiguate:\n " + + String.join("\n ", duplicates)); + } + var paths = openAPI.getPaths(); paths.entrySet().stream() .peek(entry -> { @@ -370,17 +432,131 @@ public class SwaggerConfiguration { }); sortedPaths.setExtensions(paths.getExtensions()); openAPI.setPaths(sortedPaths); + + if (openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) { + Map schemas = openAPI.getComponents().getSchemas(); + + // Fix all schemas: if they have additionalProperties but no type, set type to object + schemas.forEach((schemaName, schema) -> { + if (schema.getAdditionalProperties() != null && schema.getType() == null) { + schema.setType("object"); + log.debug("Added type 'object' to schema: {}", schemaName); + } + }); + + // Springdoc creates duplicate schemas with an "Object" suffix when a type is + // resolved through multiple inheritance paths or via generic type resolution. + // Remove the "*Object" duplicate when the base schema exists (either + // pre-registered in addDefaultSchemas or generated by springdoc). + for (String name : new ArrayList<>(schemas.keySet())) { + if (!name.endsWith("Object")) continue; + String baseName = name.substring(0, name.length() - "Object".length()); + if (!schemas.containsKey(baseName)) continue; + + schemas.remove(name); + String refToRemove = "#/components/schemas/" + name; + schemas.values().forEach(s -> { + if (s.getAllOf() != null) { + s.getAllOf().removeIf(allOfEntry -> refToRemove.equals(((Schema) allOfEntry).get$ref())); + } + }); + log.debug("Removed duplicate schema '{}' (base '{}' exists)", name, baseName); + } + + // Remove duplicate or redundant inline entries in allOf. Springdoc can + // generate multiple inline property blocks when resolving a type through + // multiple parent paths (e.g. record + sealed interface). One block may be + // a strict subset of another (same properties, but the superset has extras + // like "modelType"). Keep only the superset in that case. + schemas.values().forEach(schema -> { + if (schema.getAllOf() != null && schema.getAllOf().size() > 1) { + List allOf = schema.getAllOf(); + Set redundant = new HashSet<>(); + for (int i = 0; i < allOf.size(); i++) { + if (redundant.contains(i)) continue; + Schema a = allOf.get(i); + if (a.get$ref() != null || a.getProperties() == null) continue; + for (int j = i + 1; j < allOf.size(); j++) { + if (redundant.contains(j)) continue; + Schema b = allOf.get(j); + if (b.get$ref() != null || b.getProperties() == null) continue; + if (a.getProperties().entrySet().containsAll(b.getProperties().entrySet())) { + redundant.add(j); // b is a subset of a + } else if (b.getProperties().entrySet().containsAll(a.getProperties().entrySet())) { + redundant.add(i); // a is a subset of b + break; + } + } + } + if (!redundant.isEmpty()) { + List filtered = new ArrayList<>(); + for (int i = 0; i < allOf.size(); i++) { + if (!redundant.contains(i)) { + filtered.add(allOf.get(i)); + } + } + allOf.clear(); + allOf.addAll(filtered); + } + } + }); + + + // Fix polymorphic properties: replace inline oneOf with base type $ref + schemas.values().forEach(schema -> { + replaceInlineOneOfProperties(schema, schemas); + if (schema.getAllOf() != null) { + List allOf = schema.getAllOf(); + for (Schema allOfElement : allOf) { + replaceInlineOneOfProperties(allOfElement, schemas); + } + } + }); + + // Deduplicate allOf child schemas: remove properties that are already defined + // in the referenced parent schema to avoid duplication (e.g. EntityId children). + schemas.forEach((schemaName, schema) -> { + Set ownProps = schemaOwnProps.getOrDefault(schemaName, Set.of()); + deduplicateAllOfProperties(schema, schemas, ownProps); + }); + + // Reorder properties for all component schemas. This runs after all + // schemas are finalized so it covers schemas the ModelConverter only + // saw as a $ref (e.g. interface-based discriminator types like EntityId). + schemas.forEach((schemaName, schema) -> { + List propOrder = schemaPropertyOrders.getOrDefault(schemaName, List.of()); + reorderSchemaProperties(schema, propOrder); + }); + + // Fix polymorphic request/response bodies: replace inline oneOf with base type $ref + paths.values().stream() + .flatMap(pathItem -> pathItem.readOperationsMap().values().stream()) + .forEach(operation -> { + // Request bodies + if (operation.getRequestBody() != null && operation.getRequestBody().getContent() != null) { + replaceInlineOneOfInContent(operation.getRequestBody().getContent(), schemas); + } + // Response bodies + if (operation.getResponses() != null) { + operation.getResponses().values().stream() + .filter(response -> response.getContent() != null) + .forEach(response -> replaceInlineOneOfInContent(response.getContent(), schemas)); + } + }); + } + + // Set JsonNode schema last so model scanning cannot overwrite it + openAPI.getComponents().addSchemas("JsonNode", new Schema<>() + .description("A value representing the any type (object or primitive)") + .example(JacksonUtil.newObjectNode())); + var sortedSchemas = new TreeMap<>(openAPI.getComponents().getSchemas()); openAPI.getComponents().setSchemas(new LinkedHashMap<>(sortedSchemas)); }; } private SecurityRequirement createSecurityRequirement(String schemeName) { - return new SecurityRequirement().addList(schemeName, Arrays.asList( - Authority.SYS_ADMIN.name(), - Authority.TENANT_ADMIN.name(), - Authority.CUSTOMER_USER.name() - )); + return new SecurityRequirement().addList(schemeName, List.of()); } private Tag extractTagFromPath(Map.Entry entry) { @@ -388,9 +564,134 @@ public class SwaggerConfiguration { return tagName != null ? tagFromTagItem(tagName) : null; } + private String findBaseTypeForOneOf(Map schemas, List oneOfSchemas) { + if (oneOfSchemas.isEmpty()) { + return null; + } + + for (Schema oneOfSchema : oneOfSchemas) { + String ref = oneOfSchema.get$ref(); + if (ref == null) { + continue; + } + String refName = ref.substring(ref.lastIndexOf('/') + 1); + + // Check if this entry is itself a base type with discriminator + Schema candidate = schemas.get(refName); + if (candidate != null && candidate.getDiscriminator() != null + && candidate.getDiscriminator().getMapping() != null) { + return refName; + } + + // Check if this subtype is in another schema's discriminator mapping + String baseType = schemas.entrySet().stream() + .filter(entry -> { + Schema schema = entry.getValue(); + if (schema.getDiscriminator() != null && schema.getDiscriminator().getMapping() != null) { + return schema.getDiscriminator().getMapping().values().stream() + .anyMatch(r -> r.endsWith("/" + refName)); + } + return false; + }) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + + if (baseType != null) { + return baseType; + } + + // Check if other oneOf items extend this candidate via allOf (parent-child without discriminator) + if (candidate != null) { + boolean isParent = oneOfSchemas.stream() + .filter(s -> s.get$ref() != null && !s.get$ref().equals(ref)) + .anyMatch(s -> { + String otherName = s.get$ref().substring(s.get$ref().lastIndexOf('/') + 1); + Schema otherSchema = schemas.get(otherName); + return otherSchema != null && otherSchema.getAllOf() != null && + otherSchema.getAllOf().stream().anyMatch( + a -> a.get$ref() != null && a.get$ref().endsWith("/" + refName)); + }); + if (isParent) { + return refName; + } + } + } + return null; + } + + + private void replaceInlineOneOfInContent(Content content, Map schemas) { + content.values().forEach(mediaType -> { + Schema schema = mediaType.getSchema(); + if (schema != null && schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { + String baseType = findBaseTypeForOneOf(schemas, schema.getOneOf()); + if (baseType != null) { + Schema refSchema = new Schema<>(); + refSchema.set$ref("#/components/schemas/" + baseType); + mediaType.setSchema(refSchema); + log.debug("Replaced oneOf in content with $ref to {}", baseType); + } + } + }); + } + + @SuppressWarnings("unchecked") + private void replaceInlineOneOfProperties(Schema schema, Map allSchemas) { + if (schema == null || schema.getProperties() == null) { + return; + } + schema.getProperties().forEach((propName, propSchema) -> { + if (propSchema instanceof Schema) { + Schema prop = (Schema) propSchema; + + // Check if additionalProperties has oneOf (polymorphic map values) + if (prop.getAdditionalProperties() instanceof Schema) { + Schema additionalProps = (Schema) prop.getAdditionalProperties(); + if (additionalProps.getOneOf() != null && !additionalProps.getOneOf().isEmpty()) { + String baseType = findBaseTypeForOneOf(allSchemas, additionalProps.getOneOf()); + if (baseType != null) { + Schema refSchema = new Schema<>(); + refSchema.set$ref("#/components/schemas/" + baseType); + prop.setAdditionalProperties(refSchema); + log.debug("Replaced oneOf in additionalProperties with $ref to {} in property {}", baseType, propName); + } + } + // Check if additionalProperties is an array whose items has oneOf (e.g. Map>) + if (additionalProps.getItems() != null && additionalProps.getItems().getOneOf() != null && !additionalProps.getItems().getOneOf().isEmpty()) { + String baseType = findBaseTypeForOneOf(allSchemas, additionalProps.getItems().getOneOf()); + if (baseType != null) { + Schema refSchema = new Schema<>(); + refSchema.set$ref("#/components/schemas/" + baseType); + additionalProps.setItems(refSchema); + log.debug("Replaced oneOf in additionalProperties.items with $ref to {} in property {}", baseType, propName); + } + } + } + + // If property has oneOf, try to find the base discriminated type + if (prop.getOneOf() != null && !prop.getOneOf().isEmpty()) { + String baseType = findBaseTypeForOneOf(allSchemas, prop.getOneOf()); + if (baseType != null) { + Schema refSchema = new Schema<>(); + refSchema.set$ref("#/components/schemas/" + baseType); + if (prop.getDescription() != null) { + refSchema.setDescription(prop.getDescription()); + } + if (prop.getReadOnly() != null) { + refSchema.setReadOnly(prop.getReadOnly()); + } + schema.getProperties().put(propName, refSchema); + log.debug("Replaced oneOf with $ref to {} in property {}", baseType, propName); + } + } + } + }); + } + private String tagItemFromPathItem(PathItem item) { var operations = item.readOperationsMap().values(); - var operation = operations.stream().findAny(); + var operation = operations.stream().findFirst(); if (operation.isPresent()) { var tags = operation.get().getTags(); if (tags != null && !tags.isEmpty()) { @@ -480,28 +781,34 @@ public class SwaggerConfiguration { private static ApiResponses loginErrorResponses() { ApiResponses apiResponses = new ApiResponses(); - apiResponses.addApiResponse("401", errorResponse("Unauthorized", - Map.of( - "bad-credentials", errorExample("Bad credentials", - ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), - "token-expired", errorExample("JWT token expired", - ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)), - "account-disabled", errorExample("Disabled account", - ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), - "account-locked", errorExample("Locked account", - ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), - "authentication-failed", errorExample("General authentication error", - ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)) - ) - )); - var credentialsExpiredSchema = new Schema().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse"); - apiResponses.addApiResponse("401 ", errorResponse("Unauthorized (**Expired credentials**)", - Map.of( - "credentials-expired", errorExample("Expired credentials", - ThingsboardCredentialsExpiredResponse.of("User password expired!", StringUtils.randomAlphanumeric(30))) - ), - credentialsExpiredSchema + Map unauthorizedExamples = new LinkedHashMap<>(); + + unauthorizedExamples.put("bad-credentials", errorExample("Bad credentials", + ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))); + + unauthorizedExamples.put("token-expired", errorExample("JWT token expired", + ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED))); + + unauthorizedExamples.put("account-disabled", errorExample("Disabled account", + ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))); + + unauthorizedExamples.put("account-locked", errorExample("Locked account", + ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))); + + unauthorizedExamples.put("authentication-failed", errorExample("General authentication error", + ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))); + + unauthorizedExamples.put("credentials-expired", errorExample("Expired credentials", + ThingsboardCredentialsExpiredResponse.of("User password expired!", "udgDQOpS1Q4ZFEL8qHF9s8cSKQ7d1h"))); + + Schema unauthorizedSchema = new Schema<>(); + unauthorizedSchema.oneOf(List.of( + new Schema().$ref("#/components/schemas/ThingsboardErrorResponse"), + new Schema().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse") )); + + apiResponses.addApiResponse("401", errorResponse("Unauthorized", unauthorizedExamples, unauthorizedSchema)); + return apiResponses; } @@ -520,10 +827,250 @@ public class SwaggerConfiguration { return new ApiResponse().description(description).content(content); } + /** + * Recursively collects all property names reachable from {@code schemaName}, walking the + * ancestor chain through allOf $ref entries (to handle multi-level inheritance). + * {@code visited} prevents infinite loops in case of circular references. + */ + @SuppressWarnings("unchecked") + private void collectAllProperties(String schemaName, Map allSchemas, + Set result, Set visited) { + if (!visited.add(schemaName)) { + return; + } + Schema schema = allSchemas.get(schemaName); + if (schema == null) { + return; + } + if (schema.getProperties() != null) { + result.addAll(schema.getProperties().keySet()); + } + if (schema.getAllOf() != null) { + for (Schema allOfElement : schema.getAllOf()) { + String ref = allOfElement.get$ref(); + if (ref != null) { + String refName = ref.substring(ref.lastIndexOf('/') + 1); + collectAllProperties(refName, allSchemas, result, visited); + } else if (allOfElement.getProperties() != null) { + result.addAll(allOfElement.getProperties().keySet()); + } + } + } + } + + @SuppressWarnings("unchecked") + private void deduplicateAllOfProperties(Schema schema, Map allSchemas, Set ownProps) { + if (schema.getAllOf() == null) { + return; + } + + // Collect properties defined in any $ref'd parent within the allOf, recursively + // walking the ancestor chain (each parent may itself use allOf to extend a grandparent). + Set parentProperties = new LinkedHashSet<>(); + for (Schema allOfElement : schema.getAllOf()) { + String ref = allOfElement.get$ref(); + if (ref != null) { + String refName = ref.substring(ref.lastIndexOf('/') + 1); + collectAllProperties(refName, allSchemas, parentProperties, new LinkedHashSet<>()); + } + } + + if (parentProperties.isEmpty()) { + return; + } + + // Properties to strip: in parent schema AND not declared as own-class fields. + // This removes inherited properties (from superclasses or pure interface getters) + // while keeping properties the class declares as its own fields. + Set toStrip = new LinkedHashSet<>(parentProperties); + toStrip.removeAll(ownProps); + + if (toStrip.isEmpty()) { + return; + } + + // Strip from inline (non-$ref) allOf elements + schema.getAllOf().removeIf(allOfElement -> { + if (allOfElement.get$ref() != null) { + return false; + } + if (allOfElement.getProperties() != null) { + Map filtered = new LinkedHashMap<>(allOfElement.getProperties()); + filtered.keySet().removeAll(toStrip); + allOfElement.setProperties(filtered.isEmpty() ? null : filtered); + } + return allOfElement.getProperties() == null + && allOfElement.getRequired() == null + && allOfElement.getType() == null; + }); + + // Remove stripped properties from the schema's required list + if (schema.getRequired() != null) { + List required = new ArrayList<>(schema.getRequired()); + required.removeAll(toStrip); + schema.setRequired(required.isEmpty() ? null : required); + } + } + + /** + * Computes the schema name that swagger-core will use for the given JavaType. + * For simple types, this is just the class simple name (e.g. {@code Device}). + * For parameterized types, type parameter names are appended + * (e.g. {@code PageData} becomes {@code PageDataDevice}). + * This matches the naming convention used by swagger-core's {@code TypeNameResolver}. + */ + private static String resolveSchemaName(JavaType javaType) { + Class cls = javaType.getRawClass(); + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = + cls.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + if (schemaAnnotation != null && !schemaAnnotation.name().isEmpty()) { + return schemaAnnotation.name(); + } + StringBuilder sb = new StringBuilder(cls.getSimpleName()); + if (javaType.hasGenericTypes()) { + for (int i = 0; i < javaType.containedTypeCount(); i++) { + JavaType param = javaType.containedType(i); + if (param != null) { + sb.append(param.getRawClass().getSimpleName()); + } + } + } + return sb.toString(); + } + + /** + * Returns the JSON property names that are backed by fields declared directly in {@code cls} + * (not inherited from a superclass). Used to distinguish "own" from "inherited" properties + * when deduplicating allOf inline elements. + */ + private static Set computeOwnPropNames(Class cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) { + Map allFieldToJson = new LinkedHashMap<>(); + for (var prop : beanDesc.findProperties()) { + if (prop.getField() != null && prop.couldSerialize()) { + allFieldToJson.put(prop.getField().getName(), prop.getName()); + } + } + Set own = new LinkedHashSet<>(); + for (Field f : cls.getDeclaredFields()) { + if (Modifier.isStatic(f.getModifiers())) continue; + String jsonName = allFieldToJson.get(f.getName()); + if (jsonName != null) own.add(jsonName); + } + return own; + } + + @SuppressWarnings("unchecked") + private static void reorderSchemaProperties(Schema schema, List propOrder) { + if (schema.getProperties() != null && schema.getProperties().size() > 1) { + schema.setProperties(reorderProperties(schema.getProperties(), propOrder)); + } + if (schema.getAllOf() != null) { + for (Schema allOfElement : schema.getAllOf()) { + if (allOfElement.get$ref() != null) continue; + if (allOfElement.getProperties() != null && allOfElement.getProperties().size() > 1) { + allOfElement.setProperties(reorderProperties(allOfElement.getProperties(), propOrder)); + } + } + } + } + + private static LinkedHashMap reorderProperties(Map current, List propOrder) { + var reordered = new LinkedHashMap(); + for (String name : propOrder) { + Schema prop = current.get(name); + if (prop != null) reordered.put(name, prop); + } + // Any properties not covered by propOrder are appended + // alphabetically to guarantee a deterministic stable order. + new TreeMap<>(current).forEach((k, v) -> reordered.putIfAbsent(k, v)); + return reordered; + } + + /** + * Resolves the property ordering for a schema class. + * + *

Returns a list of JSON property names in the order they should appear in the + * OpenAPI schema. The caller uses this list to reorder the schema's property map; + * any properties not present in the returned list are appended alphabetically + * by the caller's {@code TreeMap} fallback, guaranteeing a stable, deterministic order. + * + *

Resolution strategy (first match wins): + *

    + *
  1. If {@code @JsonPropertyOrder} with an explicit {@code value()} is found on the + * class or any interface in its ancestry, that list is returned as-is. Note: if the + * annotation lists only a subset of fields, those fields are ordered first and the + * remaining properties fall through to the caller's alphabetical fallback — consistent + * with Jackson's own behaviour for partial {@code @JsonPropertyOrder}.
  2. + *
  3. Otherwise, field-backed properties are returned in declaration order (superclass + * fields first). Getter-only properties are intentionally excluded to avoid + * non-deterministic ordering across restarts.
  4. + *
+ */ + private static List resolvePropertyOrder(Class cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) { + // If an explicit @JsonPropertyOrder is present on the class or any interface in its + // ancestry, honour it directly. Walk up the class hierarchy; for each class also walk + // the full interface hierarchy (including super-interfaces) via BFS. + for (Class c = cls; c != null && c != Object.class; c = c.getSuperclass()) { + JsonPropertyOrder propOrder = c.getAnnotation(JsonPropertyOrder.class); + if (propOrder != null && !propOrder.alphabetic() && propOrder.value().length > 0) { + return Arrays.asList(propOrder.value()); + } + Deque> ifaceQueue = new ArrayDeque<>(Arrays.asList(c.getInterfaces())); + Set> visitedIfaces = new LinkedHashSet<>(); + while (!ifaceQueue.isEmpty()) { + Class iface = ifaceQueue.poll(); + if (!visitedIfaces.add(iface)) continue; + propOrder = iface.getAnnotation(JsonPropertyOrder.class); + if (propOrder != null && !propOrder.alphabetic() && propOrder.value().length > 0) { + return Arrays.asList(propOrder.value()); + } + ifaceQueue.addAll(Arrays.asList(iface.getInterfaces())); + } + } + + // Map backing field names to their JSON property names (respects @JsonProperty) + Map fieldToJsonName = new LinkedHashMap<>(); + for (var prop : beanDesc.findProperties()) { + if (prop.couldSerialize()) { + if (prop.getField() != null) { + fieldToJsonName.put(prop.getField().getName(), prop.getName()); + } else { + // For transient fields, Jackson may not associate the field with the property. + // Fall back to using the property name as the field name key. + fieldToJsonName.putIfAbsent(prop.getName(), prop.getName()); + } + } + } + + // Walk class hierarchy (superclass first) to get field declaration order + List> hierarchy = new ArrayList<>(); + for (Class c = cls; c != null && c != Object.class; c = c.getSuperclass()) { + hierarchy.add(0, c); + } + List ordered = new ArrayList<>(); + for (Class c : hierarchy) { + for (Field f : c.getDeclaredFields()) { + if (Modifier.isStatic(f.getModifiers())) continue; + String jsonName = fieldToJsonName.get(f.getName()); + if (jsonName != null) ordered.add(jsonName); + } + } + + // Return only field-backed properties in declaration order. + // Getter-only properties (no backing field) are intentionally excluded: their set can vary + // between restarts (e.g. Optional-typed getters depend on Jackson module registration order), + // so including them here would make their position non-deterministic when some are in orderedNames + // and others are only in the schema map. The converter's TreeMap fallback handles ALL + // non-field-backed properties together in one alphabetical pass, guaranteeing stable order. + return ordered; + } + private static Example errorExample(String summary, ThingsboardErrorResponse example) { + var node = (ObjectNode) JacksonUtil.valueToTree(example); + node.put("timestamp", 1609459200000L); return new Example() .summary(summary) - .value(example); + .value(node); } -} +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 0b04a6939f..e4394bace3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -417,7 +417,7 @@ public class AdminController extends BaseController { "provider sends authorization code to specified redirect uri.)") @PreAuthorize("hasAuthority('SYS_ADMIN')") @GetMapping(value = "/mail/oauth2/authorize", produces = "application/text") - public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { + public String getMailOAuth2AuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { String state = StringUtils.generateSafeToken(); if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) { CookieUtils.addCookie(response, PREV_URI_COOKIE_NAME, request.getParameter(PREV_URI_PATH_PARAMETER), 180); @@ -442,7 +442,7 @@ public class AdminController extends BaseController { } @GetMapping(value = "/mail/oauth2/code", params = {"code", "state"}) - public void codeProcessingUrl( + public void handleMailOAuth2Callback( @RequestParam(value = "code") String code, @RequestParam(value = "state") String state, HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException { Optional prevUrlOpt = CookieUtils.getCookie(request, PREV_URI_COOKIE_NAME); diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 8f7c8bbbd1..0814e94700 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -227,12 +227,12 @@ public class AlarmController extends BaseController { return tbAlarmService.unassign(alarm, System.currentTimeMillis(), getCurrentUser()); } - @ApiOperation(value = "Get Alarms (getAlarms)", + @ApiOperation(value = "Get Alarms (getAlarmsByEntity)", notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/alarm/{entityType}/{entityId}") - public PageData getAlarms( + public PageData getAlarmsByEntity( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable(ENTITY_TYPE) String strEntityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java new file mode 100644 index 0000000000..957d05d1f4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java @@ -0,0 +1,259 @@ +/** + * Copyright © 2016-2026 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.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EventInfo; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.event.EventType; +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.EntityIdFactory; +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.config.annotations.ApiOperation; +import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; + +import java.util.EnumSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +public class AlarmRuleController extends BaseController { + + private final TbCalculatedFieldService tbCalculatedFieldService; + private final EventService eventService; + + public static final String ALARM_RULE_ID = "alarmRuleId"; + + private static final String TEST_SCRIPT_EXPRESSION = + "Execute the alarm rule TBEL condition expression and return the result. " + + "Alarm rule expressions must return a boolean value. The format of request: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"expression\": \"return temperature > 50;\",\n" + + " \"arguments\": {\n" + + " \"temperature\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 55 }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Expected result JSON contains \"output\" and \"error\"."; + + @ApiOperation(value = "Create Or Update Alarm Rule (saveAlarmRule)", + notes = "Creates or Updates the Alarm Rule. When creating alarm rule, platform generates Alarm Rule Id as " + UUID_WIKI_LINK + + "The newly created Alarm Rule Id will be present in the response. " + + "Specify existing Alarm Rule Id to update the alarm rule. " + + "Referencing non-existing Alarm Rule Id will cause 'Not Found' error. " + + "Remove 'id', 'tenantId' from the request body example (below) to create new Alarm Rule entity. " + + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/alarm/rule") + public AlarmRuleDefinition saveAlarmRule(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm rule.") + @RequestBody AlarmRuleDefinition alarmRuleDefinition) throws Exception { + alarmRuleDefinition.setTenantId(getTenantId()); + checkEntityId(alarmRuleDefinition.getEntityId(), Operation.WRITE_CALCULATED_FIELD); + if (alarmRuleDefinition.getId() != null) { + checkAlarmRule(alarmRuleDefinition.getId()); + } + CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField(); + checkReferencedEntities(calculatedField.getConfiguration()); + CalculatedField saved = tbCalculatedFieldService.save(calculatedField, getCurrentUser()); + return AlarmRuleDefinition.fromCalculatedField(saved); + } + + @ApiOperation(value = "Get Alarm Rule (getAlarmRuleById)", + notes = "Fetch the Alarm Rule object based on the provided Alarm Rule Id." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rule/{alarmRuleId}") + public AlarmRuleDefinition getAlarmRuleById(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = checkAlarmRule(calculatedFieldId); + checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); + return AlarmRuleDefinition.fromCalculatedField(calculatedField); + } + + @ApiOperation(value = "Get Alarm Rules by Entity Id (getAlarmRulesByEntityId)", + notes = "Fetch the Alarm Rules based on the provided Entity Id." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/alarm/rules/{entityType}/{entityId}") + public PageData getAlarmRulesByEntityId( + @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 = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + checkParameter("entityId", entityIdStr); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); + checkEntityId(entityId, Operation.READ_CALCULATED_FIELD); + PageData result = checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, CalculatedFieldType.ALARM, pageLink)); + return result.mapData(AlarmRuleDefinition::fromCalculatedField); + } + + @ApiOperation(value = "Get alarm rules (getAlarmRules)", + notes = "Fetch tenant alarm rules based on the filter." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/alarm/rules") + public PageData getAlarmRules(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Entity type filter. If not specified, alarm rules for all supported entity types will be returned.") + @RequestParam(required = false) EntityType entityType, + @Parameter(description = "Entities filter. If not specified, alarm rules for entity type filter will be returned.") + @RequestParam(required = false) Set entities, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + SecurityUser user = getCurrentUser(); + + Set entityTypes; + if (entityType == null) { + entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream() + .filter(entry -> entry.getValue().contains(CalculatedFieldType.ALARM)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } else { + entityTypes = EnumSet.of(entityType); + } + + CalculatedFieldFilter filter = CalculatedFieldFilter.builder() + .types(EnumSet.of(CalculatedFieldType.ALARM)) + .entityTypes(entityTypes) + .entityIds(entities) + .build(); + PageData result = calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink); + return result.mapData(AlarmRuleDefinitionInfo::fromCalculatedFieldInfo); + } + + @ApiOperation(value = "Get alarm rule names (getAlarmRuleNames)", + notes = "Fetch the list of alarm rule names." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/alarm/rules/names") + public PageData getAlarmRuleNames(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, "name", sortOrder); + return calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType(getTenantId(), CalculatedFieldType.ALARM, pageLink); + } + + @ApiOperation(value = "Delete Alarm Rule (deleteAlarmRule)", + notes = "Deletes the alarm rule. Referencing non-existing Alarm Rule Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @DeleteMapping("/alarm/rule/{alarmRuleId}") + @ResponseStatus(HttpStatus.OK) + public void deleteAlarmRule(@PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws Exception { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = checkAlarmRule(calculatedFieldId); + checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); + tbCalculatedFieldService.delete(calculatedField, getCurrentUser()); + } + + @ApiOperation(value = "Get latest alarm rule debug event (getLatestAlarmRuleDebugEvent)", + notes = "Gets latest alarm rule debug event for specified alarm rule id. " + + "Referencing non-existing alarm rule id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/alarm/rule/{alarmRuleId}/debug") + public JsonNode getLatestAlarmRuleDebugEvent(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException { + checkParameter(ALARM_RULE_ID, strAlarmRuleId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId)); + CalculatedField calculatedField = checkAlarmRule(calculatedFieldId); + checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); + TenantId tenantId = getCurrentUser().getTenantId(); + return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1)) + .flatMap(events -> events.stream().map(EventInfo::getBody).findFirst()) + .orElse(null); + } + + @ApiOperation(value = "Test alarm rule TBEL expression (testAlarmRuleScript)", + notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/alarm/rule/testScript") + public JsonNode testAlarmRuleScript( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL condition expression. The expression must return a boolean value.") + @RequestBody JsonNode inputParams) throws ThingsboardException { + checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null); + return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams); + } + + private CalculatedField checkAlarmRule(CalculatedFieldId calculatedFieldId) throws ThingsboardException { + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); + checkNotNull(calculatedField); + if (calculatedField.getType() != CalculatedFieldType.ALARM) { + throw new ThingsboardException("Alarm rule not found", ThingsboardErrorCode.ITEM_NOT_FOUND); + } + return calculatedField; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java b/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java index 09972bd6c2..ae3ea1c60d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java @@ -68,7 +68,7 @@ public class ApiKeyController extends BaseController { private final ApiKeyService apiKeyService; @ApiOperation(value = "Save API key for user (saveApiKey)", - notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey '." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey {value}'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @PostMapping(value = "/apiKey") public ApiKey saveApiKey( diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index e4158d014f..4a20e2fcd2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -23,6 +24,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -217,8 +219,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/assets") public PageData getTenantAssets( @Parameter(description = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, @@ -245,8 +246,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets info objects owned by tenant. " + PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/assetInfos") public PageData getTenantAssetInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, @@ -274,25 +274,30 @@ public class AssetController extends BaseController { } } - @ApiOperation(value = "Get Tenant Asset (getTenantAsset)", + @Hidden + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/assets", params = {"assetName"}) + public Asset getTenantAsset(@RequestParam String assetName) throws ThingsboardException { + TenantId tenantId = getCurrentUser().getTenantId(); + return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName)); + } + + @ApiOperation(value = "Get Tenant Asset (getTenantAssetByName)", notes = "Requested asset must be owned by tenant that the user belongs to. " + "Asset name is an unique property of asset. So it can be used to identify the asset." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET) - @ResponseBody - public Asset getTenantAsset( + @GetMapping(value = "/tenant/asset") + public Asset getTenantAssetByName( @Parameter(description = ASSET_NAME_DESCRIPTION) @RequestParam String assetName) throws ThingsboardException { - TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName)); + return getTenantAsset(assetName); } @ApiOperation(value = "Get Customer Assets (getCustomerAssets)", notes = "Returns a page of assets objects assigned to customer. " + PAGE_DATA_PARAMETERS) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/assets") public PageData getCustomerAssets( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -324,8 +329,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets info objects assigned to customer. " + PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/assetInfos") public PageData getCustomerAssetInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -361,8 +365,7 @@ public class AssetController extends BaseController { @ApiOperation(value = "Get Assets By Ids (getAssetsByIds)", notes = "Requested assets must be owned by tenant or assigned to customer which user is performing the request. ") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/assets") public List getAssetsByIds( @Parameter(description = "A list of assets ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException, ExecutionException, InterruptedException { @@ -383,14 +386,14 @@ public class AssetController extends BaseController { return checkNotNull(assets.get()); } - @ApiOperation(value = "Find related assets (findByQuery)", + @ApiOperation(value = "Find related assets (findAssetsByQuery)", notes = "Returns all assets that are related to the specific entity. " + "The entity id, relation type, asset types, depth of the search, and other query parameters defined using complex 'AssetSearchQuery' object. " + "See 'Model' tab of the Parameters for more info.") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/assets", method = RequestMethod.POST) @ResponseBody - public List findByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { + public List findAssetsByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getAssetTypes()); @@ -469,8 +472,7 @@ public class AssetController extends BaseController { notes = "Returns a page of assets assigned to edge. " + PAGE_DATA_PARAMETERS) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/edge/{edgeId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/edge/{edgeId}/assets") public PageData getEdgeAssets( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION) @PathVariable(EDGE_ID) String strEdgeId, @@ -516,11 +518,11 @@ public class AssetController extends BaseController { return checkNotNull(filteredResult); } - @ApiOperation(value = "Import the bulk of assets (processAssetsBulkImport)", + @ApiOperation(value = "Import the bulk of assets (processAssetBulkImport)", notes = "There's an ability to import the bulk of assets using the only .csv file.") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PostMapping("/asset/bulk_import") - public BulkImportResult processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception { + public BulkImportResult processAssetBulkImport(@RequestBody BulkImportRequest request) throws Exception { SecurityUser user = getCurrentUser(); return assetBulkImportService.processBulkImport(request, user); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index 65e324a715..86483413df 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -175,8 +176,7 @@ public class AssetProfileController extends BaseController { notes = "Returns a page of asset profile objects owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/assetProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/assetProfiles") public PageData getAssetProfiles( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -196,8 +196,7 @@ public class AssetProfileController extends BaseController { notes = "Returns a page of asset profile info objects owned by tenant. " + PAGE_DATA_PARAMETERS + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/assetProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/assetProfileInfos") public PageData getAssetProfileInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -227,12 +226,10 @@ public class AssetProfileController extends BaseController { return checkNotNull(assetProfileService.findAssetProfileNamesByTenantId(tenantId, activeOnly)); } - @ApiOperation(value = "Get Asset Profiles By Ids (getAssetProfilesByIds)", - notes = "Requested asset profiles must be owned by tenant which is performing the request. " + - NEW_LINE) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/assetProfileInfos", params = {"assetProfileIds"}) - public List getAssetProfilesByIds( + public List getAssetProfilesByIdsV1( @Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("assetProfileIds") Set assetProfileUUIDs) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); @@ -243,4 +240,15 @@ public class AssetProfileController extends BaseController { return assetProfileService.findAssetProfilesByIds(tenantId, assetProfileIds); } + @ApiOperation(value = "Get Asset Profiles By Ids (getAssetProfilesByIds)", + notes = "Requested asset profiles must be owned by tenant which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/assetProfileInfos/list") + public List getAssetProfilesByIds( + @Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("assetProfileIds") Set assetProfileUUIDs) throws ThingsboardException { + return getAssetProfilesByIdsV1(assetProfileUUIDs); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index 8c0d0d2243..6a42f034ba 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -18,11 +18,10 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; @@ -74,8 +73,7 @@ public class AuditLogController extends BaseController { "and users actions (login, logout, etc.) that belong to this customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/audit/logs/customer/{customerId}") public PageData getAuditLogsByCustomerId( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -107,8 +105,7 @@ public class AuditLogController extends BaseController { "For example, RPC call to a particular device, or alarm acknowledgment for a specific device, etc. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/user/{userId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/audit/logs/user/{userId}") public PageData getAuditLogsByUserId( @Parameter(description = USER_ID_PARAM_DESCRIPTION) @PathVariable("userId") String strUserId, @@ -141,8 +138,7 @@ public class AuditLogController extends BaseController { "For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " + PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/audit/logs/entity/{entityType}/{entityId}") public PageData getAuditLogsByEntityId( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String strEntityType, @@ -176,8 +172,7 @@ public class AuditLogController extends BaseController { notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/audit/logs") public PageData getAuditLogs( @Parameter(description = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 8bda29e98b..af3a619906 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -136,7 +136,7 @@ public class AuthController extends BaseController { "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " + "If token is not valid, returns '409 Conflict'. " + "If token is expired, redirects to error page.") - @GetMapping(value = "/noauth/activate", params = {"activateToken"}) + @GetMapping(value = "/noauth/activate") public ResponseEntity checkActivateToken( @Parameter(description = "The activate token string.") @RequestParam(value = "activateToken") String activateToken) { @@ -176,7 +176,7 @@ public class AuthController extends BaseController { "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " + "If token is not valid, returns '409 Conflict'. " + "If token is expired, redirects to error page.") - @GetMapping(value = "/noauth/resetPassword", params = {"resetToken"}) + @GetMapping(value = "/noauth/resetPassword") public ResponseEntity checkResetToken( @Parameter(description = "The reset token string.") @RequestParam(value = "resetToken") String resetToken) { diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 48e0f11552..56f1cf6a4b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -71,6 +71,7 @@ import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeInfo; @@ -681,6 +682,17 @@ public abstract class BaseController { return entity; } + protected void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { + for (EntityId referencedEntityId : calculatedFieldConfig.getReferencedEntities()) { + EntityType refEntityType = referencedEntityId.getEntityType(); + switch (refEntityType) { + case TENANT -> {} + case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); + default -> throw new IllegalArgumentException("Unsupported referenced entity type: '" + refEntityType + "'."); + } + } + } + Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException { return checkEntityId(deviceId, deviceService::findDeviceById, operation); } 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 8ef957066b..84ff4b49a4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -15,15 +15,15 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.MultiValueMap; @@ -36,13 +36,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.script.api.tbel.TbelCfArg; -import org.thingsboard.script.api.tbel.TbelCfCtx; -import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; -import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; -import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; -import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.cf.CalculatedField; @@ -61,21 +54,15 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; -import java.util.ArrayList; -import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; -import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.TimeUnit; import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; @@ -94,17 +81,13 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @TbCoreComponent @RequestMapping("/api") @RequiredArgsConstructor -@Slf4j public class CalculatedFieldController extends BaseController { private final TbCalculatedFieldService tbCalculatedFieldService; private final EventService eventService; - private final TbelInvokeService tbelInvokeService; public static final String CALCULATED_FIELD_ID = "calculatedFieldId"; - public static final int TIMEOUT = 20; - private static final String TEST_SCRIPT_EXPRESSION = "Execute the Script expression and return the result. The format of request: \n\n" + MARKDOWN_CODE_BLOCK_START @@ -163,27 +146,17 @@ public class CalculatedFieldController extends BaseController { return calculatedField; } - @ApiOperation(value = "Get Calculated Fields by Entity Id (getCalculatedFieldsByEntityId)", - notes = "Fetch the Calculated Fields based on the provided Entity Id." - ) + @Hidden @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}) - public PageData getCalculatedFieldsByEntityId(@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 = PAGE_SIZE_DESCRIPTION, required = true) - @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) - @RequestParam int page, - @Parameter(description = "Calculated field type. If not specified, all types will be returned.") - @RequestParam(required = false) CalculatedFieldType type, - @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) - @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) - @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getCalculatedFieldsByEntityIdV1(@PathVariable("entityType") String entityType, + @PathVariable("entityId") String entityIdStr, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) CalculatedFieldType type, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); checkParameter("entityId", entityIdStr); EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); @@ -191,8 +164,29 @@ public class CalculatedFieldController extends BaseController { return checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, type, pageLink)); } + @ApiOperation(value = "Get Calculated Fields by Entity Id (getCalculatedFieldsByEntityId)", + notes = "Fetch the Calculated Fields based on the provided Entity Id." + ) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @GetMapping(value = "/calculatedField/{entityType}/{entityId}") + public PageData getCalculatedFieldsByEntityId( + @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 = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @Parameter(description = "Calculated field type. If not specified, all types will be returned.") + @RequestParam(required = false) CalculatedFieldType type, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + return getCalculatedFieldsByEntityIdV1(entityType, entityIdStr, pageSize, page, type, textSearch, sortProperty, sortOrder); + } + @ApiOperation(value = "Get calculated fields (getCalculatedFields)", notes = "Fetch tenant calculated fields based on the filter.") + @Parameters({ + @Parameter(name = "name", description = "Repeatable name query parameter", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @GetMapping(value = "/calculatedFields") public PageData getCalculatedFields(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -205,14 +199,13 @@ public class CalculatedFieldController extends BaseController { @RequestParam(required = false) EntityType entityType, @Parameter(description = "Entities filter. If not specified, calculated fields for entity type filter will be returned.") @RequestParam(required = false) Set entities, - @Parameter(description = "Name filter. To specify multiple names, duplicate 'name' parameter for each name, for example '?name=name1&name=name2") - @RequestParam(required = false) String name, // for Swagger only, retrieved from MultiValueMap params (due to issues when name contains comma) @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder, + @Parameter(hidden = true) @RequestParam MultiValueMap params) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); SecurityUser user = getCurrentUser(); @@ -289,88 +282,11 @@ public class CalculatedFieldController extends BaseController { notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/calculatedField/testScript") - public JsonNode testScript( + public JsonNode testCalculatedFieldScript( @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.") - @RequestBody JsonNode inputParams) { - String expression = inputParams.get("expression").asText(); - Map arguments = Objects.requireNonNullElse( - JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), - Collections.emptyMap() - ); - - ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); - ctxAndArgNames.add("ctx"); - ctxAndArgNames.addAll(arguments.keySet()); - - String output = ""; - String errorText = ""; - - CalculatedFieldTbelScriptEngine engine = null; - try { - if (tbelInvokeService == null) { - throw new IllegalArgumentException("TBEL script engine is disabled!"); - } - - engine = new CalculatedFieldTbelScriptEngine( - getTenantId(), - tbelInvokeService, - expression, - ctxAndArgNames.toArray(String[]::new) - ); - - Object[] args = new Object[ctxAndArgNames.size()]; - args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); - for (int i = 1; i < ctxAndArgNames.size(); i++) { - var arg = arguments.get(ctxAndArgNames.get(i)); - if (arg instanceof TbelCfSingleValueArg svArg) { - args[i] = svArg.getValue(); - } else { - args[i] = arg; - } - } - - JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); - output = JacksonUtil.toString(json); - } catch (Exception e) { - log.error("Error evaluating expression", e); - Throwable rootCause = ExceptionUtils.getRootCause(e); - errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); - } finally { - if (engine != null) { - engine.destroy(); - } - } - return JacksonUtil.newObjectNode() - .put("output", output) - .put("error", errorText); - } - - private long getLatestTimestamp(Map arguments) { - long lastUpdateTimestamp = -1; - for (TbelCfArg entry : arguments.values()) { - if (entry instanceof TbelCfSingleValueArg singleValueArg) { - long ts = singleValueArg.getTs(); - lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts); - } else if (entry instanceof TbelCfTsRollingArg tsRollingArg) { - long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1); - lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs); - } - } - return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; - } - - private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { - Set referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); - for (EntityId referencedEntityId : referencedEntityIds) { - EntityType entityType = referencedEntityId.getEntityType(); - switch (entityType) { - case TENANT -> { - return; - } - case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); - } - } + @RequestBody JsonNode inputParams) throws ThingsboardException { + checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null); + return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 55dbb8fe04..91569261b7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -81,8 +82,7 @@ public class ComponentDescriptorController extends BaseController { notes = "Gets the Component Descriptors using coma separated list of rule node types and optional rule chain type request parameters. " + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") - @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/components") public List getComponentDescriptorsByTypes( @Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("componentTypes") String[] strComponentTypes, diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index ab5c1c0ae4..04afb1330e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -444,106 +444,6 @@ public class ControllerConstants { " * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" + " * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n"; - protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START + - "{\n" + - " \"schedule\":{\n" + - " \"type\":\"SPECIFIC_TIME\",\n" + - " \"endsOn\":64800000,\n" + - " \"startsOn\":43200000,\n" + - " \"timezone\":\"Europe/Kiev\",\n" + - " \"daysOfWeek\":[\n" + - " 1,\n" + - " 3,\n" + - " 5\n" + - " ]\n" + - " }\n" + - "}" + - MARKDOWN_CODE_BLOCK_END; - protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START + - "{\n" + - " \"schedule\":{\n" + - " \"type\":\"CUSTOM\",\n" + - " \"items\":[\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":1\n" + - " },\n" + - " {\n" + - " \"endsOn\":64800000,\n" + - " \"enabled\":true,\n" + - " \"startsOn\":43200000,\n" + - " \"dayOfWeek\":2\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":3\n" + - " },\n" + - " {\n" + - " \"endsOn\":57600000,\n" + - " \"enabled\":true,\n" + - " \"startsOn\":36000000,\n" + - " \"dayOfWeek\":4\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":5\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":6\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":7\n" + - " }\n" + - " ],\n" + - " \"timezone\":\"Europe/Kiev\"\n" + - " }\n" + - "}" + - MARKDOWN_CODE_BLOCK_END; - protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END; - - protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START + - "{\n" + - " \"spec\":{\n" + - " \"type\":\"REPEATING\",\n" + - " \"predicate\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":5,\n" + - " \"dynamicValue\":{\n" + - " \"inherit\":true,\n" + - " \"sourceType\":\"CURRENT_DEVICE\",\n" + - " \"sourceAttribute\":\"tempAttr\"\n" + - " }\n" + - " }\n" + - " }\n" + - "}" + - MARKDOWN_CODE_BLOCK_END; - - protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START + - "{\n" + - " \"spec\":{\n" + - " \"type\":\"DURATION\",\n" + - " \"unit\":\"MINUTES\",\n" + - " \"predicate\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":30,\n" + - " \"dynamicValue\":null\n" + - " }\n" + - " }\n" + - "}" + - MARKDOWN_CODE_BLOCK_END; - protected static final String RELATION_TYPE_PARAM_DESCRIPTION = "A string value representing relation type between entities. For example, 'Contains', 'Manages'. It can be any string value."; protected static final String RELATION_TYPE_GROUP_PARAM_DESCRIPTION = "A string value representing relation type group. For example, 'COMMON'"; @@ -1328,8 +1228,6 @@ public class ControllerConstants { ALARM_FILTER_KEY + FILTER_VALUE_TYPE + NEW_LINE + DEVICE_PROFILE_FILTER_PREDICATE + NEW_LINE; protected static final String DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" + - " \"alarms\":[\n" + - " ],\n" + " \"configuration\":{\n" + " \"type\":\"DEFAULT\"\n" + " },\n" + @@ -1343,219 +1241,6 @@ public class ControllerConstants { "}" + MARKDOWN_CODE_BLOCK_END; protected static final String CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" + - " \"alarms\":[\n" + - " {\n" + - " \"id\":\"2492b935-1226-59e9-8615-17d8978a4f93\",\n" + - " \"alarmType\":\"Temperature Alarm\",\n" + - " \"clearRule\":{\n" + - " \"schedule\":null,\n" + - " \"condition\":{\n" + - " \"spec\":{\n" + - " \"type\":\"SIMPLE\"\n" + - " },\n" + - " \"condition\":[\n" + - " {\n" + - " \"key\":{\n" + - " \"key\":\"temperature\",\n" + - " \"type\":\"TIME_SERIES\"\n" + - " },\n" + - " \"value\":null,\n" + - " \"predicate\":{\n" + - " \"type\":\"NUMERIC\",\n" + - " \"value\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":30.0,\n" + - " \"dynamicValue\":null\n" + - " },\n" + - " \"operation\":\"LESS\"\n" + - " },\n" + - " \"valueType\":\"NUMERIC\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"dashboardId\":null,\n" + - " \"alarmDetails\":null\n" + - " },\n" + - " \"propagate\":false,\n" + - " \"createRules\":{\n" + - " \"MAJOR\":{\n" + - " \"schedule\":{\n" + - " \"type\":\"SPECIFIC_TIME\",\n" + - " \"endsOn\":64800000,\n" + - " \"startsOn\":43200000,\n" + - " \"timezone\":\"Europe/Kiev\",\n" + - " \"daysOfWeek\":[\n" + - " 1,\n" + - " 3,\n" + - " 5\n" + - " ]\n" + - " },\n" + - " \"condition\":{\n" + - " \"spec\":{\n" + - " \"type\":\"DURATION\",\n" + - " \"unit\":\"MINUTES\",\n" + - " \"predicate\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":30,\n" + - " \"dynamicValue\":null\n" + - " }\n" + - " },\n" + - " \"condition\":[\n" + - " {\n" + - " \"key\":{\n" + - " \"key\":\"temperature\",\n" + - " \"type\":\"TIME_SERIES\"\n" + - " },\n" + - " \"value\":null,\n" + - " \"predicate\":{\n" + - " \"type\":\"COMPLEX\",\n" + - " \"operation\":\"OR\",\n" + - " \"predicates\":[\n" + - " {\n" + - " \"type\":\"NUMERIC\",\n" + - " \"value\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":50.0,\n" + - " \"dynamicValue\":null\n" + - " },\n" + - " \"operation\":\"LESS_OR_EQUAL\"\n" + - " },\n" + - " {\n" + - " \"type\":\"NUMERIC\",\n" + - " \"value\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":30.0,\n" + - " \"dynamicValue\":null\n" + - " },\n" + - " \"operation\":\"GREATER\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"valueType\":\"NUMERIC\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"dashboardId\":null,\n" + - " \"alarmDetails\":null\n" + - " },\n" + - " \"WARNING\":{\n" + - " \"schedule\":{\n" + - " \"type\":\"CUSTOM\",\n" + - " \"items\":[\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":1\n" + - " },\n" + - " {\n" + - " \"endsOn\":64800000,\n" + - " \"enabled\":true,\n" + - " \"startsOn\":43200000,\n" + - " \"dayOfWeek\":2\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":3\n" + - " },\n" + - " {\n" + - " \"endsOn\":57600000,\n" + - " \"enabled\":true,\n" + - " \"startsOn\":36000000,\n" + - " \"dayOfWeek\":4\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":5\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":6\n" + - " },\n" + - " {\n" + - " \"endsOn\":0,\n" + - " \"enabled\":false,\n" + - " \"startsOn\":0,\n" + - " \"dayOfWeek\":7\n" + - " }\n" + - " ],\n" + - " \"timezone\":\"Europe/Kiev\"\n" + - " },\n" + - " \"condition\":{\n" + - " \"spec\":{\n" + - " \"type\":\"REPEATING\",\n" + - " \"predicate\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":5,\n" + - " \"dynamicValue\":null\n" + - " }\n" + - " },\n" + - " \"condition\":[\n" + - " {\n" + - " \"key\":{\n" + - " \"key\":\"tempConstant\",\n" + - " \"type\":\"CONSTANT\"\n" + - " },\n" + - " \"value\":30,\n" + - " \"predicate\":{\n" + - " \"type\":\"NUMERIC\",\n" + - " \"value\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":0.0,\n" + - " \"dynamicValue\":{\n" + - " \"inherit\":false,\n" + - " \"sourceType\":\"CURRENT_DEVICE\",\n" + - " \"sourceAttribute\":\"tempThreshold\"\n" + - " }\n" + - " },\n" + - " \"operation\":\"EQUAL\"\n" + - " },\n" + - " \"valueType\":\"NUMERIC\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"dashboardId\":null,\n" + - " \"alarmDetails\":null\n" + - " },\n" + - " \"CRITICAL\":{\n" + - " \"schedule\":null,\n" + - " \"condition\":{\n" + - " \"spec\":{\n" + - " \"type\":\"SIMPLE\"\n" + - " },\n" + - " \"condition\":[\n" + - " {\n" + - " \"key\":{\n" + - " \"key\":\"temperature\",\n" + - " \"type\":\"TIME_SERIES\"\n" + - " },\n" + - " \"value\":null,\n" + - " \"predicate\":{\n" + - " \"type\":\"NUMERIC\",\n" + - " \"value\":{\n" + - " \"userValue\":null,\n" + - " \"defaultValue\":50.0,\n" + - " \"dynamicValue\":null\n" + - " },\n" + - " \"operation\":\"GREATER\"\n" + - " },\n" + - " \"valueType\":\"NUMERIC\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"dashboardId\":null,\n" + - " \"alarmDetails\":null\n" + - " }\n" + - " },\n" + - " \"propagateRelationTypes\":null\n" + - " }\n" + - " ],\n" + " \"configuration\":{\n" + " \"type\":\"DEFAULT\"\n" + " },\n" + @@ -1577,40 +1262,11 @@ public class ControllerConstants { " }\n" + "}" + MARKDOWN_CODE_BLOCK_END; protected static final String DEVICE_PROFILE_DATA_DEFINITION = NEW_LINE + "# Device profile data definition" + NEW_LINE + - "Device profile data object contains alarm rules configuration, device provision strategy and transport type configuration for device connectivity. Let's review some examples. " + + "Device profile data object contains device provision strategy and transport type configuration for device connectivity. Let's review some examples. " + "First one is the default device profile data configuration and second one - the custom one. " + NEW_LINE + DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE + NEW_LINE + CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE + NEW_LINE + "Let's review some specific objects examples related to the device profile configuration:"; - protected static final String ALARM_SCHEDULE = NEW_LINE + "# Alarm Schedule" + NEW_LINE + - "Alarm Schedule JSON object represents the time interval during which the alarm rule is active. Note, " + - NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE + NEW_LINE + "means alarm rule is active all the time. " + - "**'daysOfWeek'** field represents Monday as 1, Tuesday as 2 and so on. **'startsOn'** and **'endsOn'** fields represent hours in millis (e.g. 64800000 = 18:00 or 6pm). " + - "**'enabled'** flag specifies if item in a custom rule is active for specific day of the week:" + NEW_LINE + - "## Specific Time Schedule" + NEW_LINE + - DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE + NEW_LINE + - "## Custom Schedule" + - NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE + NEW_LINE; - - protected static final String ALARM_CONDITION_TYPE = "# Alarm condition type (**'spec'**)" + NEW_LINE + - "Alarm condition type can be either simple, duration, or repeating. For example, 5 times in a row or during 5 minutes." + NEW_LINE + - "Note, **'userValue'** field is not used and reserved for future usage, **'dynamicValue'** is used for condition appliance by using the value of the **'sourceAttribute'** " + - "or else **'defaultValue'** is used (if **'sourceAttribute'** is absent).\n" + - "\n**'sourceType'** of the **'sourceAttribute'** can be: \n" + - " * 'CURRENT_DEVICE';\n" + - " * 'CURRENT_CUSTOMER';\n" + - " * 'CURRENT_TENANT'." + NEW_LINE + - "**'sourceAttribute'** can be inherited from the owner if **'inherit'** is set to true (for CURRENT_DEVICE and CURRENT_CUSTOMER)." + NEW_LINE + - "## Repeating alarm condition" + NEW_LINE + - DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE + NEW_LINE + - "## Duration alarm condition" + NEW_LINE + - DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE + NEW_LINE + - "**'unit'** can be: \n" + - " * 'SECONDS';\n" + - " * 'MINUTES';\n" + - " * 'HOURS';\n" + - " * 'DAYS'." + NEW_LINE; - protected static final String PROVISION_CONFIGURATION = "# Provision Configuration" + NEW_LINE + "There are 3 types of device provision configuration for the device profile: \n" + " * 'DISABLED';\n" + @@ -1618,8 +1274,8 @@ public class ControllerConstants { " * 'CHECK_PRE_PROVISIONED_DEVICES'." + NEW_LINE + "Please refer to the [docs](https://thingsboard.io/docs/user-guide/device-provisioning/) for more details." + NEW_LINE; - protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION + ALARM_SCHEDULE + ALARM_CONDITION_TYPE + - KEY_FILTERS_DESCRIPTION + PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION; + protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION + + PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION; protected static final String DEVICE_PROFILE_ID = "deviceProfileId"; diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 5ae949d92e..5acf09a779 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -172,8 +173,7 @@ public class CustomerController extends BaseController { notes = "Returns a page of customers owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customers", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customers") public PageData getCustomers( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -193,8 +193,7 @@ public class CustomerController extends BaseController { @ApiOperation(value = "Get Tenant Customer by Customer title (getTenantCustomer)", notes = "Get the Customer using Customer Title. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/customers", params = {"customerTitle"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/customers") public Customer getTenantCustomer( @Parameter(description = "A string value representing the Customer title.") @RequestParam String customerTitle) throws ThingsboardException { @@ -202,12 +201,10 @@ public class CustomerController extends BaseController { return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found"); } - @ApiOperation(value = "Get customers by Customer Ids (getCustomersByIds)", - notes = "Returns a list of Customer objects based on the provided ids." + - TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @GetMapping(value = "/customers", params = {"customerIds"}) - public List getCustomersByIds( + public List getCustomersByIdsV1( @Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("customerIds") Set customerUUIDs) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); @@ -218,4 +215,15 @@ public class CustomerController extends BaseController { return customerService.findCustomersByTenantIdAndIds(tenantId, customerIds); } + @ApiOperation(value = "Get customers by Customer Ids (getCustomersByIds)", + notes = "Returns a list of Customer objects based on the provided ids." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @GetMapping(value = "/customers/list") + public List getCustomersByIds( + @Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("customerIds") Set customerUUIDs) throws ThingsboardException { + return getCustomersByIdsV1(customerUUIDs); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index b16161437d..9f59044356 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -17,10 +17,10 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.servlet.http.HttpServletResponse; @@ -119,7 +119,7 @@ public class DashboardController extends BaseController { "Used to adjust view of the dashboards according to the difference between browser and server time.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/serverTime") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "1636023857137"))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "integer", format = "int64", example = "1636023857137"))) public long getServerTime() { return System.currentTimeMillis(); } @@ -131,7 +131,7 @@ public class DashboardController extends BaseController { "The actual value of the limit is configurable in the system configuration file.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/maxDatapointsLimit") - @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "5000"))) + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "integer", format = "int64", example = "5000"))) public long getMaxDatapointsLimit() { return maxDatapointsLimit; } @@ -151,6 +151,8 @@ public class DashboardController extends BaseController { @ApiOperation(value = "Get Dashboard (getDashboardById)", notes = "Get the dashboard based on 'dashboardId' parameter. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH ) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = Dashboard.class))) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/{dashboardId}") public void getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) @@ -176,6 +178,8 @@ public class DashboardController extends BaseController { "Referencing non-existing dashboard Id will cause 'Not Found' error. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Dashboard entity. " + TENANT_AUTHORITY_PARAGRAPH) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = Dashboard.class))) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping(value = "/dashboard") public void saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.") @@ -325,12 +329,12 @@ public class DashboardController extends BaseController { return tbDashboardService.unassignDashboardFromPublicCustomer(dashboard, getCurrentUser()); } - @ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboards)", + @ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboardsByTenantId)", notes = "Returns a page of dashboard info objects owned by tenant. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @GetMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"}) - public PageData getTenantDashboards( + @GetMapping(value = "/tenant/{tenantId}/dashboards") + public PageData getTenantDashboardsByTenantId( @Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TENANT_ID) String strTenantId, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -353,7 +357,7 @@ public class DashboardController extends BaseController { notes = "Returns a page of dashboard info objects owned by the tenant of a current user. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/tenant/dashboards", params = {"pageSize", "page"}) + @GetMapping(value = "/tenant/dashboards") public PageData getTenantDashboards( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -380,7 +384,7 @@ public class DashboardController extends BaseController { notes = "Returns a page of dashboard info objects owned by the specified customer. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"}) + @GetMapping(value = "/customer/{customerId}/dashboards") public PageData getCustomerDashboards( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -413,6 +417,8 @@ public class DashboardController extends BaseController { "If 'homeDashboardId' parameter is not set on the User level and the User has authority 'CUSTOMER_USER', check the same parameter for the corresponding Customer. " + "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @ApiResponse(responseCode = "200", description = "OK", + content = @Content(schema = @Schema(implementation = HomeDashboard.class))) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/home") public void getHomeDashboard(@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader, @@ -574,7 +580,7 @@ public class DashboardController extends BaseController { notes = "Returns a page of dashboard info objects assigned to the specified edge. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"}) + @GetMapping(value = "/edge/{edgeId}/dashboards") public PageData getEdgeDashboards( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, @@ -602,13 +608,10 @@ public class DashboardController extends BaseController { return checkNotNull(filteredResult); } - @ApiOperation(value = "Get dashboards by Dashboard Ids (getDashboardsByIds)", - notes = "Returns a list of DashboardInfo objects based on the provided ids. " + - TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboards", params = {"dashboardIds"}) - public List getDashboardsByIds(@Parameter(description = "A list of dashboard ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("dashboardIds") Set dashboardUUIDs) throws ThingsboardException { + public List getDashboardsByIdsV1(@RequestParam("dashboardIds") Set dashboardUUIDs) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); List dashboardIds = new ArrayList<>(); for (UUID dashboardUUID : dashboardUUIDs) { @@ -618,6 +621,16 @@ public class DashboardController extends BaseController { return filterDashboardsByReadPermission(dashboards); } + @ApiOperation(value = "Get dashboards by Dashboard Ids (getDashboardsByIds)", + notes = "Returns a list of DashboardInfo objects based on the provided ids. " + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/dashboards/list") + public List getDashboardsByIds(@Parameter(description = "A list of dashboard ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("dashboardIds") Set dashboardUUIDs) throws ThingsboardException { + return getDashboardsByIdsV1(dashboardUUIDs); + } + private Set customerIdFromStr(String[] strCustomerIds) { Set customerIds = new HashSet<>(); if (strCustomerIds != null) { diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java index 8b899ded41..b7e94d2212 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -75,6 +76,7 @@ public class DeviceConnectivityController extends BaseController { description = "OK", content = @Content( mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = JsonNode.class), examples = { @ExampleObject( name = "http", @@ -105,7 +107,18 @@ public class DeviceConnectivityController extends BaseController { return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device); } - @ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", notes = "Download server certificate.") + @ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", + notes = "Download server certificate.", + responses = { + @ApiResponse( + responseCode = "200", + description = "OK", + content = @Content( + mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, + schema = @Schema(type = "string", format = "binary") + ) + ) + }) @RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET) @ResponseBody public ResponseEntity downloadServerCertificate(@Parameter(description = PROTOCOL_PARAM_DESCRIPTION) @@ -122,7 +135,18 @@ public class DeviceConnectivityController extends BaseController { .body(pemCert); } - @ApiOperation(value = "Download generated docker-compose.yml file for gateway (downloadGatewayDockerCompose)", notes = "Download generated docker-compose.yml for gateway.") + @ApiOperation(value = "Download generated docker-compose.yml file for gateway (downloadGatewayDockerCompose)", + notes = "Download generated docker-compose.yml for gateway.", + responses = { + @ApiResponse( + responseCode = "200", + description = "OK", + content = @Content( + mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE, + schema = @Schema(type = "string", format = "binary") + ) + ) + }) @RequestMapping(value = "/device-connectivity/gateway-launch/{deviceId}/docker-compose/download", method = RequestMethod.GET) @ResponseBody public ResponseEntity downloadGatewayDockerCompose(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 78b4cc9880..2f43c39f76 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -29,6 +30,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -204,16 +206,16 @@ public class DeviceController extends BaseController { notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK + "Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" + "You may find the example of device with different type of credentials below: \n\n" + - "- Credentials type: \"Access token\" with device profile ID below: \n\n" + + "- Credentials type: **\"Access token\"** with **device profile ID** below: \n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"Access token\" with device profile default below: \n\n" + + "- Credentials type: **\"Access token\"** with **device profile default** below: \n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"X509\" with device profile ID below: \n\n" + - "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + + "- Credentials type: **\"X509\"** with **device profile ID** below: \n\n" + + "Note: **credentialsId** - format **Sha3Hash**, **certificateValue** - format **PEM** (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" + + "- Credentials type: **\"MQTT_BASIC\"** with **device profile ID** below: \n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + - "- You may find the example of LwM2M device and RPK credentials below: \n\n" + + "- You may find the example of **LwM2M** device and **RPK** credentials below: \n\n" + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " + @@ -320,14 +322,14 @@ public class DeviceController extends BaseController { "Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device.\n" + "The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'.\n" + "You may find the example of device with different type of credentials below: \n\n" + - "- Credentials type: \"Access token\" with device ID and with device ID below: \n\n" + + "- Credentials type: **\"Access token\"** with **device ID** and with **device ID** below: \n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"X509\" with device profile ID below: \n\n" + - "Note: credentialsId - format Sha3Hash, certificateValue - format PEM (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + + "- Credentials type: **\"X509\"** with **device profile ID** below: \n\n" + + "Note: **credentialsId** - format **Sha3Hash**, **certificateValue** - format **PEM** (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" + - "- Credentials type: \"MQTT_BASIC\" with device profile ID below: \n\n" + + "- Credentials type: **\"MQTT_BASIC\"** with **device profile ID** below: \n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" + - "- You may find the example of LwM2M device and RPK credentials below: \n\n" + + "- You may find the example of **LwM2M** device and **RPK** credentials below: \n\n" + "Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" + DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" + "Update to real value:\n" + @@ -350,8 +352,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/devices") public PageData getTenantDevices( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -378,8 +379,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices info objects owned by tenant. " + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/deviceInfos") public PageData getTenantDeviceInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -411,25 +411,31 @@ public class DeviceController extends BaseController { return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink)); } - @ApiOperation(value = "Get Tenant Device (getTenantDevice)", - notes = "Requested device must be owned by tenant that the user belongs to. " + - "Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/devices", params = {"deviceName"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/tenant/devices", params = {"deviceName"}) public Device getTenantDevice( - @Parameter(description = DEVICE_NAME_DESCRIPTION) @RequestParam String deviceName) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName)); } + @ApiOperation(value = "Get Tenant Device (getTenantDeviceByName)", + notes = "Requested device must be owned by tenant that the user belongs to. " + + "Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/device") + public Device getTenantDeviceByName( + @Parameter(description = DEVICE_NAME_DESCRIPTION) + @RequestParam String deviceName) throws ThingsboardException { + return getTenantDevice(deviceName); + } + @ApiOperation(value = "Get Customer Devices (getCustomerDevices)", notes = "Returns a page of devices objects assigned to customer. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/devices") public PageData getCustomerDevices( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -461,8 +467,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices info objects assigned to customer. " + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/customer/{customerId}/deviceInfos") public PageData getCustomerDeviceInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable("customerId") String strCustomerId, @@ -502,8 +507,7 @@ public class DeviceController extends BaseController { @ApiOperation(value = "Get Devices By Ids (getDevicesByIds)", notes = "Requested devices must be owned by tenant or assigned to customer which user is performing the request. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/devices") public List getDevicesByIds( @Parameter(description = "A list of devices ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException, ExecutionException, InterruptedException { @@ -524,14 +528,14 @@ public class DeviceController extends BaseController { return checkNotNull(devices.get()); } - @ApiOperation(value = "Find related devices (findByQuery)", + @ApiOperation(value = "Find related devices (findDevicesByQuery)", notes = "Returns all devices that are related to the specific entity. " + "The entity id, relation type, device types, depth of the search, and other query parameters defined using complex 'DeviceSearchQuery' object. " + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/devices", method = RequestMethod.POST) @ResponseBody - public List findByQuery( + public List findDevicesByQuery( @Parameter(description = "The device search query JSON") @RequestBody DeviceSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); @@ -730,8 +734,7 @@ public class DeviceController extends BaseController { notes = "Returns a page of devices assigned to edge. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/edge/{edgeId}/devices") public PageData getEdgeDevices( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index 343703cb7d..f70583f721 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -129,7 +130,7 @@ public class DeviceProfileController extends BaseController { return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId())); } - @ApiOperation(value = "Get time series keys (getTimeseriesKeys)", + @ApiOperation(value = "Get time series keys (getDeviceProfileTimeseriesKeys)", notes = "Get a set of unique time series keys used by devices that belong to specified profile. " + "If profile is not set returns a list of unique keys among all profiles. " + "The call is used for auto-complete in the UI forms. " + @@ -138,7 +139,7 @@ public class DeviceProfileController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/deviceProfile/devices/keys/timeseries", method = RequestMethod.GET) @ResponseBody - public List getTimeseriesKeys( + public List getDeviceProfileTimeseriesKeys( @Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException { DeviceProfileId deviceProfileId; @@ -228,8 +229,7 @@ public class DeviceProfileController extends BaseController { notes = "Returns a page of devices profile objects owned by tenant. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/deviceProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/deviceProfiles") public PageData getDeviceProfiles( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -249,8 +249,7 @@ public class DeviceProfileController extends BaseController { notes = "Returns a page of devices profile info objects owned by tenant. " + PAGE_DATA_PARAMETERS + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/deviceProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/deviceProfileInfos") public PageData getDeviceProfileInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -282,14 +281,10 @@ public class DeviceProfileController extends BaseController { return checkNotNull(deviceProfileService.findDeviceProfileNamesByTenantId(tenantId, activeOnly)); } - @ApiOperation(value = "Get Device Profile Infos By Ids (getDeviceProfilesByIds)", - notes = "Requested device profiles must be owned by tenant which is performing the request. " + - NEW_LINE) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/deviceProfileInfos", params = {"deviceProfileIds"}) - public List getDeviceProfileInfosByIds( - @Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("deviceProfileIds") Set deviceProfileUUIDs) throws ThingsboardException { + public List getDeviceProfileInfosByIdsV1(@RequestParam("deviceProfileIds") Set deviceProfileUUIDs) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); List deviceProfileIds = new ArrayList<>(); for (UUID deviceProfileUUID : deviceProfileUUIDs) { @@ -298,4 +293,15 @@ public class DeviceProfileController extends BaseController { return deviceProfileService.findDeviceProfilesByIds(tenantId, deviceProfileIds); } + @ApiOperation(value = "Get Device Profile Infos By Ids (getDeviceProfileInfosByIds)", + notes = "Requested device profiles must be owned by tenant which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/deviceProfileInfos/list") + public List getDeviceProfileInfosByIds( + @Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("deviceProfileIds") Set deviceProfileUUIDs) throws ThingsboardException { + return getDeviceProfileInfosByIdsV1(deviceProfileUUIDs); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/DomainController.java b/application/src/main/java/org/thingsboard/server/controller/DomainController.java index 0afffda159..c34bbe6ac9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DomainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DomainController.java @@ -81,31 +81,31 @@ public class DomainController extends BaseController { return tbDomainService.save(domain, getOAuth2ClientIds(ids), getCurrentUser()); } - @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", + @ApiOperation(value = "Update oauth2 clients (updateDomainOauth2Clients)", notes = "Update oauth2 clients for the specified domain. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PutMapping(value = "/domain/{id}/oauth2Clients") - public void updateOauth2Clients(@PathVariable UUID id, - @RequestBody UUID[] clientIds) throws ThingsboardException { + public void updateDomainOauth2Clients(@PathVariable UUID id, + @RequestBody UUID[] clientIds) throws ThingsboardException { DomainId domainId = new DomainId(id); Domain domain = checkDomainId(domainId, Operation.WRITE); List oAuth2ClientIds = getOAuth2ClientIds(clientIds); tbDomainService.updateOauth2Clients(domain, oAuth2ClientIds, getCurrentUser()); } - @ApiOperation(value = "Get Domain infos (getTenantDomainInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get Domain infos (getDomainInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @GetMapping(value = "/domain/infos") - public PageData getTenantDomainInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) - @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) - @RequestParam int page, - @Parameter(description = "Case-insensitive 'substring' filter based on domain's name") - @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION) - @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getDomainInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Case-insensitive 'substring' filter based on domain's name") + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.DOMAIN, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return domainService.findDomainInfosByTenantId(getTenantId(), pageLink); diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index f0d9e39d6a..361104d9a2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -277,7 +278,7 @@ public class EdgeController extends BaseController { notes = "Returns a page of edges info objects owned by tenant. " + PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"}) + @GetMapping(value = "/tenant/edgeInfos") public PageData getTenantEdgeInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -300,17 +301,24 @@ public class EdgeController extends BaseController { } } - @ApiOperation(value = "Get Tenant Edge (getTenantEdge)", - notes = "Requested edge must be owned by tenant or customer that the user belongs to. " + - "Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/tenant/edges", params = {"edgeName"}) - public Edge getTenantEdge(@Parameter(description = "Unique name of the edge", required = true) - @RequestParam String edgeName) throws ThingsboardException { + public Edge getTenantEdge(@RequestParam String edgeName) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName)); } + @ApiOperation(value = "Get Tenant Edge by name (getTenantEdgeByName)", + notes = "Requested edge must be owned by tenant or customer that the user belongs to. " + + "Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/edge") + public Edge getTenantEdgeByName(@Parameter(description = "Unique name of the edge", required = true) + @RequestParam String edgeName) throws ThingsboardException { + return getTenantEdge(edgeName); + } + @ApiOperation(value = "Set root rule chain for provided edge (setEdgeRootRuleChain)", notes = "Change root rule chain of the edge to the new provided rule chain. \n" + "This operation will send a notification to update root rule chain on remote edge service." + TENANT_AUTHORITY_PARAGRAPH) @@ -334,7 +342,7 @@ public class EdgeController extends BaseController { notes = "Returns a page of edges objects assigned to customer. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"}) + @GetMapping(value = "/customer/{customerId}/edges") public PageData getCustomerEdges( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -369,7 +377,7 @@ public class EdgeController extends BaseController { notes = "Returns a page of edges info objects assigned to customer. " + PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"}) + @GetMapping(value = "/customer/{customerId}/edgeInfos") public PageData getCustomerEdgeInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, @@ -400,12 +408,10 @@ public class EdgeController extends BaseController { return checkNotNull(result); } - @ApiOperation(value = "Get Edges By Ids (getEdgesByIds)", - notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/edges", params = {"edgeIds"}) public List getEdgesByIds( - @Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("edgeIds", strEdgeIds); SecurityUser user = getCurrentUser(); @@ -425,13 +431,23 @@ public class EdgeController extends BaseController { return checkNotNull(edges); } - @ApiOperation(value = "Find related edges (findByQuery)", + @ApiOperation(value = "Get Edges By Ids (getEdgeList)", + notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/edges/list") + public List getEdgeList( + @Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException { + return getEdgesByIds(strEdgeIds); + } + + @ApiOperation(value = "Find related edges (findEdgesByQuery)", notes = "Returns all edges that are related to the specific entity. " + "The entity id, relation type, edge types, depth of the search, and other query parameters defined using complex 'EdgeSearchQuery' object. " + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PostMapping(value = "/edges") - public List findByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { + public List findEdgesByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getEdgeTypes()); diff --git a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java index 10f4cd2f51..8dd607bb6f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -213,19 +214,19 @@ public class EntitiesVersionControlController extends BaseController { " \"timestamp\": 1655198593000,\n" + " \"id\": \"fd82625bdd7d6131cf8027b44ee967012ecaf990\",\n" + " \"name\": \"Devices and assets - v2.0\",\n" + - " \"author\": \"John Doe \"\n" + + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" + " },\n" + " {\n" + " \"timestamp\": 1655198528000,\n" + " \"id\": \"682adcffa9c8a2f863af6f00c4850323acbd4219\",\n" + " \"name\": \"Update my device\",\n" + - " \"author\": \"John Doe \"\n" + + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" + " },\n" + " {\n" + " \"timestamp\": 1655198280000,\n" + " \"id\": \"d2a6087c2b30e18cc55e7cdda345a8d0dfb959a4\",\n" + " \"name\": \"Devices and assets - v1.0\",\n" + - " \"author\": \"John Doe \"\n" + + " \"author\": \"John Doe (johndoe@gmail.com)\"\n" + " }\n" + " ],\n" + " \"totalPages\": 1,\n" + @@ -234,7 +235,7 @@ public class EntitiesVersionControlController extends BaseController { "}" + MARKDOWN_CODE_BLOCK_END + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/version/{entityType}/{externalEntityUuid}", params = {"branch", "pageSize", "page"}) + @GetMapping(value = "/version/{entityType}/{externalEntityUuid}") public DeferredResult> listEntityVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable EntityType entityType, @Parameter(description = "A string value representing external entity id. This is `externalId` property of an entity, or otherwise if not set - simply id of this entity.") @@ -263,7 +264,7 @@ public class EntitiesVersionControlController extends BaseController { "If specified branch does not exist - empty page data will be returned. " + "The response structure is the same as for `listEntityVersions` API method." + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/version/{entityType}", params = {"branch", "pageSize", "page"}) + @GetMapping(value = "/version/{entityType}") public DeferredResult> listEntityTypeVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable EntityType entityType, @Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true) @@ -288,7 +289,7 @@ public class EntitiesVersionControlController extends BaseController { "If specified branch does not exist - empty page data will be returned. " + "The response format is the same as for `listEntityVersions` API method." + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/version", params = {"branch", "pageSize", "page"}) + @GetMapping(value = "/version") public DeferredResult> listVersions(@Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true) @RequestParam String branch, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -355,7 +356,7 @@ public class EntitiesVersionControlController extends BaseController { "Returns an object with current entity data and the one at a specific version. " + "Entity data structure is the same as stored in a repository. " + TENANT_AUTHORITY_PARAGRAPH) - @GetMapping(value = "/diff/{entityType}/{internalEntityUuid}", params = {"versionId"}) + @GetMapping(value = "/diff/{entityType}/{internalEntityUuid}") public DeferredResult compareEntityDataToVersion(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable EntityType entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index 9314f584ab..a5c82f11f4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -71,25 +73,22 @@ public class EntityRelationController extends BaseController { "If the user has the authority of 'Tenant Administrator', the server checks that the entity is owned by the same tenant. " + "If the user has the authority of 'Customer User', the server checks that the entity is assigned to the same customer."; - @ApiOperation(value = "Create Relation (saveRelation)", - notes = "Creates or updates a relation between two entities in the platform. " + - "Relations unique key is a combination of from/to entity id and relation type group and relation type. " + - SECURITY_CHECKS_ENTITIES_DESCRIPTION) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @PostMapping("/relation") - public void saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true) + @PostMapping(value = "/relation") + public void saveRelationV1(@Parameter(description = "A JSON value representing the relation.", required = true) @RequestBody EntityRelation relation) throws ThingsboardException { doSave(relation); } - @ApiOperation(value = "Create Relation (saveRelationV2)", + @ApiOperation(value = "Create Relation (saveRelation)", notes = "Creates or updates a relation between two entities in the platform. " + "Relations unique key is a combination of from/to entity id and relation type group and relation type. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @PostMapping("/v2/relation") - public EntityRelation saveRelationV2(@Parameter(description = "A JSON value representing the relation.", required = true) - @RequestBody EntityRelation relation) throws ThingsboardException { + @PostMapping(value = "/v2/relation") + public EntityRelation saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true) + @RequestBody EntityRelation relation) throws ThingsboardException { return doSave(relation); } @@ -103,11 +102,10 @@ public class EntityRelationController extends BaseController { return tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); } - @ApiOperation(value = "Delete Relation (deleteRelation)", - notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @DeleteMapping(value = "/relation", params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) - public void deleteRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, + @DeleteMapping(value = "/relation") + public void deleteRelationV1(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, @@ -116,16 +114,16 @@ public class EntityRelationController extends BaseController { doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType); } - @ApiOperation(value = "Delete Relation (deleteRelationV2)", + @ApiOperation(value = "Delete Relation (deleteRelation)", notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @DeleteMapping(value = "/v2/relation", params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) - public EntityRelation deleteRelationV2(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, - @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, - @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, - @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException { + @DeleteMapping(value = "/v2/relation") + public EntityRelation deleteRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, + @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, + @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, + @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException { return doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType); } @@ -144,11 +142,11 @@ public class EntityRelationController extends BaseController { return tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); } - @ApiOperation(value = "Delete common relations (deleteCommonRelations)", + @ApiOperation(value = "Delete common relations (deleteRelations)", notes = "Deletes all the relations ('from' and 'to' direction) for the specified entity and relation type group: 'COMMON'. " + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") - @DeleteMapping(value = "/relations", params = {"entityId", "entityType"}) + @DeleteMapping(value = "/relations") public void deleteRelations(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam("entityId") String strId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam("entityType") String strType) throws ThingsboardException { checkParameter("entityId", strId); @@ -161,7 +159,7 @@ public class EntityRelationController extends BaseController { @ApiOperation(value = "Get Relation (getRelation)", notes = "Returns relation object between two specified entities if present. Otherwise throws exception. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/relation", params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) + @GetMapping(value = "/relation") public EntityRelation getRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @@ -180,14 +178,11 @@ public class EntityRelationController extends BaseController { return checkNotNull(relationService.getRelation(getTenantId(), fromId, toId, strRelationType, typeGroup)); } - @ApiOperation(value = "Get List of Relations (findByFrom)", - notes = "Returns list of relation objects for the specified entity by the 'from' direction. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE}) - public List findByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, - @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + public List findByFrom(@RequestParam(FROM_ID) String strFromId, + @RequestParam(FROM_TYPE) String strFromType, @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); @@ -197,9 +192,19 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByFrom(getTenantId(), entityId, typeGroup))); } - @ApiOperation(value = "Get List of Relation Infos (findInfoByFrom)", - notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) + @ApiOperation(value = "Get List of Relations (findEntityRelationsByFrom)", + notes = "Returns list of relation objects for the specified entity by the 'from' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/from/{fromType}/{fromId}") + public List findEntityRelationsByFrom(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByFrom(strFromId, strFromType, strRelationTypeGroup); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/relations/info", params = {FROM_ID, FROM_TYPE}) public List findInfoByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @@ -214,15 +219,24 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByFrom(getTenantId(), entityId, typeGroup).get())); } - @ApiOperation(value = "Get List of Relations (findByFrom)", - notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION) + @ApiOperation(value = "Get List of Relation Infos (findEntityRelationInfosByFrom)", + notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/info/from/{fromType}/{fromId}") + public List findEntityRelationInfosByFrom(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException { + return findInfoByFrom(strFromId, strFromType, strRelationTypeGroup); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE, RELATION_TYPE}) - public List findByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, - @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, - @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + public List findByFrom(@RequestParam(FROM_ID) String strFromId, + @RequestParam(FROM_TYPE) String strFromType, + @RequestParam(RELATION_TYPE) String strRelationType, @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); @@ -233,10 +247,21 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByFromAndType(getTenantId(), entityId, strRelationType, typeGroup))); } - @ApiOperation(value = "Get List of Relations (findByTo)", - notes = "Returns list of relation objects for the specified entity by the 'to' direction. " + + @ApiOperation(value = "Get List of Relations (findEntityRelationsByFromAndRelationType)", + notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/from/{fromType}/{fromId}/{relationType}") + public List findEntityRelationsByFromAndRelationType(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId, + @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(RELATION_TYPE) String strRelationType, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByFrom(strFromId, strFromType, strRelationType, strRelationTypeGroup); + } + + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/relations", params = {TO_ID, TO_TYPE}) public List findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, @@ -250,9 +275,19 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByTo(getTenantId(), entityId, typeGroup))); } - @ApiOperation(value = "Get List of Relation Infos (findInfoByTo)", - notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) + @ApiOperation(value = "Get List of Relations (findEntityRelationsByTo)", + notes = "Returns list of relation objects for the specified entity by the 'to' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/to/{toType}/{toId}") + public List findEntityRelationsByTo(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByTo(strToId, strToType, strRelationTypeGroup); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/relations/info", params = {TO_ID, TO_TYPE}) public List findInfoByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @@ -267,9 +302,19 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByTo(getTenantId(), entityId, typeGroup).get())); } - @ApiOperation(value = "Get List of Relations (findByTo)", - notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " + - SECURITY_CHECKS_ENTITY_DESCRIPTION) + @ApiOperation(value = "Get List of Relation Infos (findEntityRelationInfosByTo)", + notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/info/to/{toType}/{toId}") + public List findEntityRelationInfosByTo(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException { + return findInfoByTo(strToId, strToType, strRelationTypeGroup); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/relations", params = {TO_ID, TO_TYPE, RELATION_TYPE}) public List findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, @@ -286,28 +331,41 @@ public class EntityRelationController extends BaseController { return checkNotNull(filterRelationsByReadPermission(relationService.findByToAndType(getTenantId(), entityId, strRelationType, typeGroup))); } - @ApiOperation(value = "Find related entities (findByQuery)", + @ApiOperation(value = "Get List of Relations (findEntityRelationsByToAndRelationType)", + notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/relations/to/{toType}/{toId}/{relationType}") + public List findEntityRelationsByToAndRelationType(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId, + @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(RELATION_TYPE) String strRelationType, + @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) + @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { + return findByTo(strToId, strToType, strRelationType, strRelationTypeGroup); + } + + @ApiOperation(value = "Find related entities (findEntityRelationsByQuery)", notes = "Returns all entities that are related to the specific entity. " + "The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " + "See 'Model' tab of the Parameters for more info.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PostMapping("/relations") - public List findByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) - @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { + public List findEntityRelationsByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) + @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query.getParameters()); checkNotNull(query.getFilters()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); return checkNotNull(filterRelationsByReadPermission(relationService.findByQuery(getTenantId(), query).get())); } - @ApiOperation(value = "Find related entity infos (findInfoByQuery)", + @ApiOperation(value = "Find related entity infos (findEntityRelationInfosByQuery)", notes = "Returns all entity infos that are related to the specific entity. " + "The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " + "See 'Model' tab of the Parameters for more info. " + RELATION_INFO_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PostMapping("/relations/info") - public List findInfoByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) - @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { + public List findEntityRelationInfosByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true) + @RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query.getParameters()); checkNotNull(query.getFilters()); checkEntityId(query.getParameters().getEntityId(), Operation.READ); diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index e061c691ac..821a3c77a1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -23,7 +24,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -79,7 +79,6 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE; import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC; -import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -87,6 +86,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC; import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @@ -167,17 +167,25 @@ public class EntityViewController extends BaseController { tbEntityViewService.delete(entityView, getCurrentUser()); } - @ApiOperation(value = "Get Entity View by name (getTenantEntityView)", - notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/tenant/entityViews", params = {"entityViewName"}) public EntityView getTenantEntityView( - @Parameter(description = "Entity View name") @RequestParam String entityViewName) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName)); } + @ApiOperation(value = "Get Entity View by name (getTenantEntityViewByName)", + notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/tenant/entityView") + public EntityView getTenantEntityViewByName( + @Parameter(description = "Entity View name") + @RequestParam String entityViewName) throws ThingsboardException { + return getTenantEntityView(entityViewName); + } + @ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)", notes = "Creates assignment of the Entity View to customer. Customer will be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @@ -222,7 +230,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of Entity View objects assigned to customer. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"}) + @GetMapping(value = "/customer/{customerId}/entityViews") public PageData getCustomerEntityViews( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -254,7 +262,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of Entity View info objects assigned to customer. " + ENTITY_VIEW_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"}) + @GetMapping(value = "/customer/{customerId}/entityViewInfos") public PageData getCustomerEntityViewInfos( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -286,7 +294,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of entity views owned by tenant. " + ENTITY_VIEW_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/tenant/entityViews", params = {"pageSize", "page"}) + @GetMapping(value = "/tenant/entityViews") public PageData getTenantEntityViews( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -314,7 +322,7 @@ public class EntityViewController extends BaseController { notes = "Returns a page of entity views info owned by tenant. " + ENTITY_VIEW_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"}) + @GetMapping(value = "/tenant/entityViewInfos") public PageData getTenantEntityViewInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -337,13 +345,13 @@ public class EntityViewController extends BaseController { } } - @ApiOperation(value = "Find related entity views (findByQuery)", + @ApiOperation(value = "Find related entity views (findEntityViewsByQuery)", notes = "Returns all entity views that are related to the specific entity. " + "The entity id, relation type, entity view types, depth of the search, and other query parameters defined using complex 'EntityViewSearchQuery' object. " + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PostMapping(value = "/entityViews") - public List findByQuery( + public List findEntityViewsByQuery( @Parameter(description = "The entity view search query JSON") @RequestBody EntityViewSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException { checkNotNull(query); @@ -429,7 +437,7 @@ public class EntityViewController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"}) + @GetMapping(value = "/edge/{edgeId}/entityViews") public PageData getEdgeEntityViews( @PathVariable(EDGE_ID) String strEdgeId, @RequestParam int pageSize, @@ -459,11 +467,10 @@ public class EntityViewController extends BaseController { return checkNotNull(filteredResult); } - @ApiOperation(value = "Get Entity Views By Ids (getEntityViewsByIds)", - notes = "Requested entity views must be owned by tenant or assigned to customer which user is performing the request. ") + @Hidden @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/entityViews", params = {"entityViewIds"}) - public List getEntityViewsByIds(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + public List getEntityViewsByIdsV1(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("entityViewIds") Set entityViewUUIDs) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); List entityViewIds = new ArrayList<>(); @@ -474,6 +481,15 @@ public class EntityViewController extends BaseController { return filterEntityViewsByReadPermission(entityViews); } + @ApiOperation(value = "Get Entity Views By Ids (getEntityViewsByIds)", + notes = "Requested entity views must be owned by tenant or assigned to customer which user is performing the request. ") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/entityViews/list") + public List getEntityViewsByIds(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("entityViewIds") Set entityViewUUIDs) throws ThingsboardException { + return getEntityViewsByIdsV1(entityViewUUIDs); + } + private List filterEntityViewsByReadPermission(List entityViews) { return entityViews.stream().filter(entityView -> { try { diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java index 7bb2d33366..6e8c17c674 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.beans.factory.annotation.Autowired; @@ -113,13 +114,13 @@ public class EventController extends BaseController { @Autowired private EventService eventService; - @ApiOperation(value = "Get Events by type (getEvents)", + @ApiOperation(value = "Get Events by type (getEventsByType)", notes = "Returns a page of events for specified entity by specifying event type. " + PAGE_DATA_PARAMETERS) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}/{eventType}", method = RequestMethod.GET) @ResponseBody - public PageData getEvents( + public PageData getEventsByType( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(ENTITY_TYPE) String strEntityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @@ -152,16 +153,12 @@ public class EventController extends BaseController { return checkNotNull(eventService.findEvents(tenantId, entityId, resolveEventType(eventType), pageLink)); } - @ApiOperation(value = "Get Events (Deprecated)", - notes = "Returns a page of events for specified entity. Deprecated and will be removed in next minor release. " + - "The call was deprecated to improve the performance of the system. " + - "Current implementation will return 'Lifecycle' events only. " + - "Use 'Get events by type' or 'Get events by filter' instead. " + - PAGE_DATA_PARAMETERS) + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.GET) @ResponseBody - public PageData getEvents( + public PageData getEventsDeprecated( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(ENTITY_TYPE) String strEntityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @@ -194,14 +191,14 @@ public class EventController extends BaseController { return checkNotNull(eventService.findEvents(tenantId, entityId, EventType.LC_EVENT, pageLink)); } - @ApiOperation(value = "Get Events by event filter (getEvents)", + @ApiOperation(value = "Get Events by event filter (getEventsByFilter)", notes = "Returns a page of events for the chosen entity by specifying the event filter. " + PAGE_DATA_PARAMETERS + NEW_LINE + EVENT_FILTER_DEFINITION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.POST) @ResponseBody - public PageData getEvents( + public PageData getEventsByFilter( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(ENTITY_TYPE) String strEntityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) diff --git a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java index 5e3f3be442..5566c11a4a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java +++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java @@ -69,11 +69,11 @@ public class Lwm2mController extends BaseController { return lwM2MService.getServerSecurityInfo(bootstrapServer); } - @ApiOperation(hidden = true, value = "Save device with credentials (Deprecated)") + @ApiOperation(hidden = true, value = "Save LwM2M device with credentials (saveLwm2mDeviceWithCredentials) (Deprecated)") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/lwm2m/device-credentials", method = RequestMethod.POST) @ResponseBody - public Device saveDeviceWithCredentials(@RequestBody Map, Object> deviceWithDeviceCredentials) throws ThingsboardException { + public Device saveLwm2mDeviceWithCredentials(@RequestBody Map, Object> deviceWithDeviceCredentials) throws ThingsboardException { Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class)); DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator(), DEFAULT.uniquifyStrategy()); diff --git a/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java index 70ac96db67..f8a3f8ad0b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java @@ -43,12 +43,12 @@ public class MailConfigTemplateController extends BaseController { private static final String MAIL_CONFIG_TEMPLATE_DEFINITION = "Mail configuration template is set of default smtp settings for mail server that specific provider supports"; private final TbMailConfigTemplateService mailConfigTemplateService; - @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, + @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getMailConfigTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, notes = MAIL_CONFIG_TEMPLATE_DEFINITION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(method = RequestMethod.GET, produces = "application/json") @ResponseBody - public JsonNode getClientRegistrationTemplates() throws ThingsboardException, IOException { + public JsonNode getMailConfigTemplates() throws ThingsboardException, IOException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return mailConfigTemplateService.findAllMailConfigTemplates(); } diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java index 304805a820..feb4aed0e3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java @@ -81,12 +81,12 @@ public class MobileAppBundleController extends BaseController { return tbMobileAppBundleService.save(mobileAppBundle, getOAuth2ClientIds(ids), getCurrentUser()); } - @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)", + @ApiOperation(value = "Update oauth2 clients (updateMobileAppBundleOauth2Clients)", notes = "Update oauth2 clients of the specified mobile app bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PutMapping(value = "/mobile/bundle/{id}/oauth2Clients") - public void updateOauth2Clients(@PathVariable UUID id, - @RequestBody UUID[] clientIds) throws ThingsboardException { + public void updateMobileAppBundleOauth2Clients(@PathVariable UUID id, + @RequestBody UUID[] clientIds) throws ThingsboardException { MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id); MobileAppBundle mobileAppBundle = checkMobileAppBundleId(mobileAppBundleId, Operation.WRITE); List oAuth2ClientIds = getOAuth2ClientIds(clientIds); diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java index 7c77a26e42..3ca23b2ddc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java @@ -123,7 +123,7 @@ public class MobileAppController extends BaseController { return tbMobileAppService.save(mobileApp, getCurrentUser()); } - @ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get mobile app infos (getTenantMobileApps)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/mobile/app") public PageData getTenantMobileApps(@Parameter(description = "Platform type: ANDROID or IOS") @@ -142,7 +142,7 @@ public class MobileAppController extends BaseController { return mobileAppService.findMobileAppsByTenantId(getTenantId(), platformType, pageLink); } - @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get mobile info by id (getMobileAppById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/mobile/app/{id}") public MobileApp getMobileAppById(@PathVariable UUID id) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java index 72c7f15a3b..17bb87233e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -153,12 +154,10 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration(), pageLink); } - @ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)", - notes = "Returns the list of notification targets found by provided ids." + - SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @GetMapping(value = "/targets", params = {"ids"}) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public List getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + public List getNotificationTargetsByIdsV1(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("ids") UUID[] ids, @AuthenticationPrincipal SecurityUser user) { // PE: generic permission @@ -166,6 +165,17 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), targetsIds); } + @ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)", + notes = "Returns the list of notification targets found by provided ids." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping(value = "/targets/list") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public List getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("ids") UUID[] ids, + @AuthenticationPrincipal SecurityUser user) { + return getNotificationTargetsByIdsV1(ids, user); + } + @ApiOperation(value = "Get notification targets (getNotificationTargets)", notes = "Returns the page of notification targets owned by sysadmin or tenant." + NEW_LINE + PAGE_DATA_PARAMETERS + @@ -188,13 +198,10 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink); } - @ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)", - notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE + - PAGE_DATA_PARAMETERS + - SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @GetMapping(value = "/targets", params = "notificationType") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public PageData getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize, + public PageData getNotificationTargetsBySupportedNotificationTypeV1(@RequestParam int pageSize, @RequestParam int page, @RequestParam(required = false) String textSearch, @RequestParam(required = false) String sortProperty, @@ -206,6 +213,22 @@ public class NotificationTargetController extends BaseController { return notificationTargetService.findNotificationTargetsByTenantIdAndSupportedNotificationType(user.getTenantId(), notificationType, pageLink); } + @ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)", + notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE + + PAGE_DATA_PARAMETERS + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @GetMapping(value = "/targets/notificationType/{notificationType}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + public PageData getNotificationTargetsBySupportedNotificationType(@PathVariable NotificationType notificationType, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder, + @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + return getNotificationTargetsBySupportedNotificationTypeV1(pageSize, page, textSearch, sortProperty, sortOrder, notificationType, user); + } + @ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)", notes = "Deletes notification target by its id." + NEW_LINE + "This target cannot be referenced by existing scheduled notification requests or any notification rules." + diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java index 8891fcc64e..6c773c9509 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java @@ -71,12 +71,12 @@ public class OAuth2ConfigTemplateController extends BaseController { oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId); } - @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, + @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getOAuth2ClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, notes = OAUTH2_CLIENT_REGISTRATION_TEMPLATE_DEFINITION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(method = RequestMethod.GET, produces = "application/json") @ResponseBody - public List getClientRegistrationTemplates() throws ThingsboardException { + public List getOAuth2ClientRegistrationTemplates() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ); return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates(); } diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 71b8ac27a9..5e51e0bbb6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -115,32 +116,40 @@ public class OAuth2Controller extends BaseController { return tbOauth2ClientService.save(oAuth2Client, getCurrentUser()); } - @ApiOperation(value = "Get OAuth2 Client infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get OAuth2 Client infos (findOAuth2ClientInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/oauth2/client/infos") - public PageData findTenantOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) - @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) - @RequestParam int page, - @Parameter(description = "Case-insensitive 'substring' filter based on client's title") - @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION) - @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData findOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Case-insensitive 'substring' filter based on client's title") + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink); } + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/oauth2/client/infos", params = {"clientIds"}) + public List findTenantOAuth2ClientInfosByIdsV1( + @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException { + List oAuth2ClientIds = getOAuth2ClientIds(clientIds); + return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds); + } + @ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)", notes = "Fetch OAuth2 Client info objects based on the provided ids. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @GetMapping(value = "/oauth2/client/infos", params = {"clientIds"}) + @GetMapping(value = "/oauth2/client/list") public List findTenantOAuth2ClientInfosByIds( @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException { - List oAuth2ClientIds = getOAuth2ClientIds(clientIds); - return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds); + return findTenantOAuth2ClientInfosByIdsV1(clientIds); } @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 4967023ec0..ab93a29db5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -181,25 +181,25 @@ public class OtaPackageController extends BaseController { return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantId(getTenantId(), pageLink)); } - @ApiOperation(value = "Get OTA Package Infos (getOtaPackages)", + @ApiOperation(value = "Get OTA Package Infos by device profile and type (getOtaPackagesByDeviceProfileAndType)", notes = "Returns a page of OTA Package Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/otaPackages/{deviceProfileId}/{type}") - public PageData getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) - @PathVariable("deviceProfileId") String strDeviceProfileId, - @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) - @PathVariable("type") String strType, - @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) - @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) - @RequestParam int page, - @Parameter(description = OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION) - @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "type", "title", "version", "tag", "url", "fileName", "dataSize", "checksum"})) - @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getOtaPackagesByDeviceProfileAndType(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable("deviceProfileId") String strDeviceProfileId, + @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) + @PathVariable("type") String strType, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "type", "title", "version", "tag", "url", "fileName", "dataSize", "checksum"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("deviceProfileId", strDeviceProfileId); checkParameter("type", strType); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); diff --git a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java index d266b684d8..6b90fb142f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java @@ -129,7 +129,7 @@ public class QrCodeSettingsController extends BaseController { return qrCodeSettingService.saveQrCodeSettings(currentUser.getTenantId(), qrCodeSettings); } - @ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)", + @ApiOperation(value = "Get Mobile application settings (getQrCodeSettings)", notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/api/mobile/qr/settings") diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index 2d584a6e27..dd2cc19fd6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -19,7 +19,9 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -65,8 +67,7 @@ public class QueueController extends BaseController { notes = "Returns a page of queues registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/queues") public PageData getTenantQueuesByServiceType(@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam String serviceType, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @@ -122,8 +123,7 @@ public class QueueController extends BaseController { "Remove 'id', 'tenantId' from the request body example (below) to create new Queue entity. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/queues", params = {"serviceType"}, method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/queues") public Queue saveQueue(@Parameter(description = "A JSON value representing the queue.") @RequestBody Queue queue, @Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED)) diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java b/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java index 1ccc35b875..57ec9a43c9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -85,12 +86,10 @@ public class QueueStatsController extends BaseController { return checkNotNull(queueStatsService.findQueueStatsById(getTenantId(), queueStatsId)); } - @ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIds)", - notes = "Fetch the Queue stats objects based on the provided ids. ") + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/queueStats", params = {"queueStatsIds"}) - public List getQueueStatsByIds( - @Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + public List getQueueStatsByIdsV1( @RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException { checkArrayParameter("queueStatsIds", strQueueStatsIds); List queueStatsIds = new ArrayList<>(); @@ -99,4 +98,14 @@ public class QueueStatsController extends BaseController { } return queueStatsService.findQueueStatsByIds(getTenantId(), queueStatsIds); } + + @ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIds)", + notes = "Fetch the Queue stats objects based on the provided ids. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/queueStats/list") + public List getQueueStatsByIds( + @Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException { + return getQueueStatsByIdsV1(strQueueStatsIds); + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java index dc17136d63..5e555cbcbe 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java @@ -16,6 +16,8 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -43,26 +45,28 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CU @Slf4j public class RpcV1Controller extends AbstractRpcController { - @ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequest)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequestV1)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleOneWayDeviceRPCRequest( + public DeferredResult handleOneWayDeviceRPCRequestV1( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT); } - @ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequest)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequestV1)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleTwoWayDeviceRPCRequest( + public DeferredResult handleTwoWayDeviceRPCRequestV1( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT); } diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java index 412e775327..4366134927 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.FutureCallback; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -113,7 +114,7 @@ public class RpcV2Controller extends AbstractRpcController { private static final String TWO_WAY_RPC_REQUEST_DESCRIPTION = "Sends the two-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + TWO_WAY_RPC_RESULT + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; - @ApiOperation(value = "Send one-way RPC request", notes = ONE_WAY_RPC_REQUEST_DESCRIPTION) + @ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequestV2)", notes = ONE_WAY_RPC_REQUEST_DESCRIPTION) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Persistent RPC request was saved to the database or lightweight RPC request was sent to the device."), @ApiResponse(responseCode = "400", description = "Invalid structure of the request."), @@ -124,15 +125,16 @@ public class RpcV2Controller extends AbstractRpcController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleOneWayDeviceRPCRequest( + public DeferredResult handleOneWayDeviceRPCRequestV2( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); } - @ApiOperation(value = "Send two-way RPC request", notes = TWO_WAY_RPC_REQUEST_DESCRIPTION) + @ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequestV2)", notes = TWO_WAY_RPC_REQUEST_DESCRIPTION) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Persistent RPC request was saved to the database or lightweight RPC response received."), @ApiResponse(responseCode = "400", description = "Invalid structure of the request."), @@ -143,10 +145,11 @@ public class RpcV2Controller extends AbstractRpcController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleTwoWayDeviceRPCRequest( + public DeferredResult handleTwoWayDeviceRPCRequestV2( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String deviceIdStr, - @Parameter(description = "A JSON value representing the RPC request.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.", + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index accec11afe..0ac6fcdac1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -229,12 +230,12 @@ public class RuleChainController extends BaseController { return tbRuleChainService.save(ruleChain, getCurrentUser()); } - @ApiOperation(value = "Create Default Rule Chain", + @ApiOperation(value = "Create Default Rule Chain (setDeviceDefaultRuleChain)", notes = "Create rule chain from template, based on the specified name in the request. " + "Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/ruleChain/device/default") - public RuleChain saveRuleChain( + public RuleChain setDeviceDefaultRuleChain( @Parameter(description = "A JSON value representing the request.") @RequestBody DefaultRuleChainCreateRequest request) throws Exception { checkNotNull(request); @@ -281,7 +282,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chains (getRuleChains)", notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/ruleChains", params = {"pageSize", "page"}) + @GetMapping(value = "/ruleChains") public PageData getRuleChains( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -347,7 +348,7 @@ public class RuleChainController extends BaseController { notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping("/ruleChain/testScript") - public JsonNode testScript( + public JsonNode testRuleChainScript( @Parameter(description = "Script language: JS or TBEL") @RequestParam(required = false) ScriptLanguage scriptLang, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.") @@ -409,7 +410,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/ruleChains/export", params = {"limit"}) + @GetMapping(value = "/ruleChains/export") public RuleChainData exportRuleChains( @Parameter(description = "A limit of rule chains to export.", required = true) @RequestParam("limit") int limit) throws ThingsboardException { @@ -505,7 +506,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)", notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}) + @GetMapping(value = "/edge/{edgeId}/ruleChains") public PageData getEdgeRuleChains( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, @@ -582,14 +583,10 @@ public class RuleChainController extends BaseController { return checkNotNull(result); } - @ApiOperation(value = "Get Rule Chains By Ids (getRuleChainsByIds)", - notes = "Requested rule chains must be owned by tenant which is performing the request. " + - NEW_LINE) + @Hidden @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping(value = "/ruleChains", params = {"ruleChainIds"}) - public List getRuleChainsByIds( - @Parameter(description = "A list of rule chain ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) - @RequestParam("ruleChainIds") Set ruleChainUUIDs) throws Exception { + public List getRuleChainsByIdsV1(@RequestParam("ruleChainIds") Set ruleChainUUIDs) throws Exception { TenantId tenantId = getCurrentUser().getTenantId(); List ruleChainIds = new ArrayList<>(); for (UUID ruleChainUUID : ruleChainUUIDs) { @@ -598,4 +595,15 @@ public class RuleChainController extends BaseController { return ruleChainService.findRuleChainsByIds(tenantId, ruleChainIds); } + @ApiOperation(value = "Get Rule Chains By Ids (getRuleChainsByIds)", + notes = "Requested rule chains must be owned by tenant which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/ruleChains/list") + public List getRuleChainsByIds( + @Parameter(description = "A list of rule chain ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("ruleChainIds") Set ruleChainUUIDs) throws Exception { + return getRuleChainsByIdsV1(ruleChainUUIDs); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java index 2b4489e387..d63e0f5d19 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java @@ -17,6 +17,8 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.FutureCallback; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -77,7 +79,7 @@ public class RuleEngineController extends BaseController { @Autowired private AccessValidator accessValidator; - @ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequest)", + @ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequestForUser)", notes = MSG_DESCRIPTION_PREFIX + "Uses current User Id ( the one which credentials is used to perform the request) as the Rule Engine message originator. " + MSG_DESCRIPTION + @@ -86,13 +88,14 @@ public class RuleEngineController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleRuleEngineRequest( - @Parameter(description = "A JSON value representing the message.", required = true) + public DeferredResult handleRuleEngineRequestForUser( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { - return handleRuleEngineRequest(null, null, null, defaultResponseTimeout, requestBody); + return handleRuleEngineRequestForEntityWithQueueAndTimeout(null, null, null, defaultResponseTimeout, requestBody); } - @ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)", + @ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequestForEntity)", notes = MSG_DESCRIPTION_PREFIX + "Uses specified Entity Id as the Rule Engine message originator. " + MSG_DESCRIPTION + @@ -101,17 +104,18 @@ public class RuleEngineController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleRuleEngineRequest( + public DeferredResult handleRuleEngineRequestForEntity( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = "A JSON value representing the message.", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { - return handleRuleEngineRequest(entityType, entityIdStr, null, defaultResponseTimeout, requestBody); + return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, defaultResponseTimeout, requestBody); } - @ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequest)", + @ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequestForEntityWithTimeout)", notes = MSG_DESCRIPTION_PREFIX + "Uses specified Entity Id as the Rule Engine message originator. " + MSG_DESCRIPTION + @@ -120,19 +124,20 @@ public class RuleEngineController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/{timeout}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleRuleEngineRequest( + public DeferredResult handleRuleEngineRequestForEntityWithTimeout( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = "Timeout to process the request in milliseconds", required = true) @PathVariable("timeout") int timeout, - @Parameter(description = "A JSON value representing the message.", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { - return handleRuleEngineRequest(entityType, entityIdStr, null, timeout, requestBody); + return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, timeout, requestBody); } - @ApiOperation(value = "Push entity message with timeout and specified queue to the rule engine (handleRuleEngineRequest)", + @ApiOperation(value = "Push entity message with timeout and specified queue to the rule engine (handleRuleEngineRequestForEntityWithQueueAndTimeout)", notes = MSG_DESCRIPTION_PREFIX + "Uses specified Entity Id as the Rule Engine message originator. " + MSG_DESCRIPTION + @@ -142,7 +147,7 @@ public class RuleEngineController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/{queueName}/{timeout}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleRuleEngineRequest( + public DeferredResult handleRuleEngineRequestForEntityWithQueueAndTimeout( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @@ -151,7 +156,8 @@ public class RuleEngineController extends BaseController { @PathVariable("queueName") String queueName, @Parameter(description = "Timeout to process the request in milliseconds", required = true) @PathVariable("timeout") int timeout, - @Parameter(description = "A JSON value representing the message.", required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { try { SecurityUser currentUser = getCurrentUser(); @@ -244,5 +250,7 @@ public class RuleEngineController extends BaseController { response != null ? response.getData() : ""); } - private record LocalRequestMetaData(TbMsg request, SecurityUser user, DeferredResult responseWriter) {} + private record LocalRequestMetaData(TbMsg request, SecurityUser user, + DeferredResult responseWriter) { + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index a7aaf37ffd..429f9b9cf2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -118,7 +119,7 @@ public class TbResourceController extends BaseController { .body(resource); } - @ApiOperation(value = "Download resource (downloadResource)", + @ApiOperation(value = "Download resource (downloadResourceIfChanged)", notes = "Download resource with a given type and key for the given scope" + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/resource/{resourceType}/{scope}/{key}") @@ -336,11 +337,10 @@ public class TbResourceController extends BaseController { } } - @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)") + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/resource", params = {"resourceIds"}) - public List getSystemOrTenantResourcesByIds( - @Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + public List getSystemOrTenantResourcesByIdsV1( @RequestParam("resourceIds") Set resourceUuids) throws ThingsboardException { SecurityUser user = getCurrentUser(); List resourceIds = new ArrayList<>(); @@ -350,7 +350,16 @@ public class TbResourceController extends BaseController { return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds); } - @ApiOperation(value = "Get All Resource Infos (getAllResources)", + @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/resource/list") + public List getSystemOrTenantResourcesByIds( + @Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("resourceIds") Set resourceUuids) throws ThingsboardException { + return getSystemOrTenantResourcesByIdsV1(resourceUuids); + } + + @ApiOperation(value = "Get All Resource Infos (getTenantResources)", notes = "Returns a page of Resource Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") 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 856b17fc61..754c9253d9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -24,7 +24,12 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -166,7 +171,8 @@ public class TelemetryController extends BaseController { "\n\n * SERVER_SCOPE - supported for all entity types;" + "\n * CLIENT_SCOPE - supported for devices;" + "\n * SHARED_SCOPE - supported for devices. " - + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string"))))) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/{entityType}/{entityId}/keys/attributes") public DeferredResult getAttributeKeys( @@ -180,7 +186,8 @@ public class TelemetryController extends BaseController { "\n\n * SERVER_SCOPE - supported for all entity types;" + "\n * CLIENT_SCOPE - supported for devices;" + "\n * SHARED_SCOPE - supported for devices. " - + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string"))))) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}") public DeferredResult getAttributeKeysByScope( @@ -197,13 +204,18 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_START + ATTRIBUTE_DATA_EXAMPLE + MARKDOWN_CODE_BLOCK_END - + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = AttributeData.class))))) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/{entityType}/{entityId}/values/attributes") 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, + @Parameter(hidden = true) @RequestParam MultiValueMap params) throws ThingsboardException { List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); @@ -220,7 +232,11 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_START + ATTRIBUTE_DATA_EXAMPLE + MARKDOWN_CODE_BLOCK_END - + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = AttributeData.class))))) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}") public DeferredResult getAttributesByScope( @@ -228,6 +244,7 @@ public class TelemetryController extends BaseController { @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, + @Parameter(hidden = true) @RequestParam MultiValueMap params) throws ThingsboardException { List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); @@ -237,7 +254,8 @@ public class TelemetryController extends BaseController { @ApiOperation(value = "Get time series keys (getTimeseriesKeys)", notes = "Returns a set of unique time series key names for the selected entity. " + - "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string"))))) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/{entityType}/{entityId}/keys/timeseries") public DeferredResult getTimeseriesKeys( @@ -258,7 +276,11 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_START + LATEST_TS_STRICT_DATA_EXAMPLE + MARKDOWN_CODE_BLOCK_END - + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "object", additionalPropertiesSchema = TsData[].class)))) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/{entityType}/{entityId}/values/timeseries") public DeferredResult getLatestTimeseries( @@ -267,6 +289,7 @@ public class TelemetryController extends BaseController { @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, + @Parameter(hidden = true) @RequestParam MultiValueMap params) throws ThingsboardException { List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); @@ -274,7 +297,32 @@ public class TelemetryController extends BaseController { (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes)); } - @ApiOperation(value = "Get time series data (getTimeseries)", + @Hidden + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"startTs", "endTs"}) + public DeferredResult getTimeseries( + @PathVariable("entityType") String entityType, + @PathVariable("entityId") String entityIdStr, + @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam(name = "startTs") Long startTs, + @RequestParam(name = "endTs") Long endTs, + @RequestParam(name = "intervalType", required = false) IntervalType intervalType, + @RequestParam(name = "interval", defaultValue = "0") Long interval, + @RequestParam(name = "timeZone", required = false) String timeZone, + @RequestParam(name = "limit", defaultValue = "100") Integer limit, + @RequestParam(name = "agg", defaultValue = "NONE") String aggStr, + @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); + DeferredResult response = new DeferredResult<>(); + 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; + } + + @ApiOperation(value = "Get time series data (getTimeseriesHistory)", notes = "Returns a range of time series values for specified entity. " + "Returns not aggregated data by default. " + "Use aggregation function ('agg') and aggregation interval ('interval') to enable aggregation of the results on the database / server side. " + @@ -282,10 +330,14 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_START + TS_STRICT_DATA_EXAMPLE + MARKDOWN_CODE_BLOCK_END - + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "object", additionalPropertiesSchema = TsData[].class)))) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"startTs", "endTs"}) - public DeferredResult getTimeseries( + @GetMapping(value = "/{entityType}/{entityId}/values/timeseries/history") + public DeferredResult getTimeseriesHistory( @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) @RequestParam(name = "keys", required = false) String keysStr, @@ -310,13 +362,9 @@ public class TelemetryController extends BaseController { @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @Parameter(hidden = true) @RequestParam MultiValueMap params) throws ThingsboardException { - List keys = getKeys(keysStr, params); - DeferredResult response = new DeferredResult<>(); - 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; + return getTimeseries(entityType, entityIdStr, keysStr, startTs, endTs, intervalType, interval, timeZone, limit, aggStr, orderBy, useStrictDataTypes, params); } @ApiOperation(value = "Save device attributes (saveDeviceAttributes)", @@ -338,7 +386,8 @@ public class TelemetryController extends BaseController { @PathVariable("deviceId") String deviceIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -363,7 +412,8 @@ public class TelemetryController extends BaseController { @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -388,7 +438,8 @@ public class TelemetryController extends BaseController { @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); @@ -412,7 +463,8 @@ 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 = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope") String scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveTelemetry(getTenantId(), entityId, requestBody, 0L); } @@ -436,7 +488,8 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope") String scope, @Parameter(description = "A long value representing TTL (Time to Live) parameter.", required = true) @PathVariable("ttl") Long ttl, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true, + content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveTelemetry(getTenantId(), entityId, requestBody, ttl); } @@ -449,6 +502,9 @@ public class TelemetryController extends BaseController { " Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) if the value's timestamp matches the time-range and 'deleteLatest' param is true." + " The replacement value will be fetched from the 'time series' table, and its timestamp will be the most recent one before the defined time-range. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " + "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), @@ -473,6 +529,7 @@ public class TelemetryController extends BaseController { @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, + @Parameter(hidden = true) @RequestParam MultiValueMap params) throws ThingsboardException { List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); @@ -530,6 +587,9 @@ public class TelemetryController extends BaseController { @ApiOperation(value = "Delete device attributes (deleteDeviceAttributes)", notes = "Delete device attributes using provided Device Id, scope and a list of keys. " + "Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " + "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), @@ -544,6 +604,7 @@ public class TelemetryController extends BaseController { @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) @RequestParam(name = "keys", required = false) String keysStr, + @Parameter(hidden = true) @RequestParam MultiValueMap params) throws ThingsboardException { List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); @@ -552,9 +613,13 @@ public class TelemetryController extends BaseController { @ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)", notes = "Delete entity attributes using provided Entity Id, scope and a list of keys. " + + "This operation is idempotent: keys that do not exist are silently ignored and the response is still 200 OK. " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Parameters({ + @Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string"))) + }) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " + + @ApiResponse(responseCode = "200", description = "Entity attributes were removed for the selected keys in the request (keys that did not exist are silently ignored). " + "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @@ -568,6 +633,7 @@ public class TelemetryController extends BaseController { @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) @RequestParam(name = "keys", required = false) String keysStr, + @Parameter(hidden = true) @RequestParam MultiValueMap params) throws ThingsboardException { List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index efa1d36cff..dcd53f7762 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -22,7 +23,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -133,7 +133,7 @@ public class TenantController extends BaseController { @ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @GetMapping(value = "/tenants", params = {"pageSize", "page"}) + @GetMapping(value = "/tenants") public PageData getTenants( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -152,7 +152,7 @@ public class TenantController extends BaseController { @ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. " + TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @GetMapping(value = "/tenantInfos", params = {"pageSize", "page"}) + @GetMapping(value = "/tenantInfos") public PageData getTenantInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -169,9 +169,10 @@ public class TenantController extends BaseController { return checkNotNull(tenantService.findTenantInfos(pageLink)); } + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/tenants", params = {"tenantIds"}) - public List getTenantsByIds( + public List getTenantsByIdsV1( @Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam("tenantIds") Set tenantUUIDs) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); @@ -183,6 +184,15 @@ public class TenantController extends BaseController { return filterTenantsByReadPermission(tenants); } + @ApiOperation(value = "Get Tenants list (getTenantsByIds)") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/tenants/list") + public List getTenantsByIds( + @Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("tenantIds") Set tenantUUIDs) throws ThingsboardException { + return getTenantsByIdsV1(tenantUUIDs); + } + private List filterTenantsByReadPermission(List tenants) { return tenants.stream().filter(tenant -> { try { diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 18bc488394..a47768e91c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -242,7 +243,7 @@ public class TenantProfileController extends BaseController { @ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. " + TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @GetMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}) + @GetMapping(value = "/tenantProfileInfos") public PageData getTenantProfileInfos( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -258,11 +259,19 @@ public class TenantProfileController extends BaseController { return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink)); } + @Hidden @GetMapping(value = "/tenantProfiles", params = {"ids"}) @PreAuthorize("hasAuthority('SYS_ADMIN')") - public List getTenantProfilesByIds(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string"))) - @RequestParam("ids") UUID[] ids) { + public List getTenantProfilesByIds(@RequestParam("ids") UUID[] ids) { return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids); } + @ApiOperation(value = "Get Tenant Profile list (getTenantProfileList)") + @GetMapping(value = "/tenantProfiles/list") + @PreAuthorize("hasAuthority('SYS_ADMIN')") + public List getTenantProfileList(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("ids") UUID[] ids) { + return getTenantProfilesByIds(ids); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index fddeabfeaa..3b15085651 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -16,7 +16,11 @@ package org.thingsboard.server.controller; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -187,7 +191,7 @@ public class TwoFactorAuthConfigController extends BaseController { return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user, providerType); } - @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = + @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviderTypes)", notes = "Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" + "Example of response:\n" + "```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" + @@ -195,7 +199,7 @@ public class TwoFactorAuthConfigController extends BaseController { ) @GetMapping("/providers") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") - public List getAvailableTwoFaProviders() throws ThingsboardException { + public List getAvailableTwoFaProviderTypes() throws ThingsboardException { return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true) .map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream() .map(TwoFaProviderConfig::getProviderType) @@ -261,6 +265,7 @@ public class TwoFactorAuthConfigController extends BaseController { } @Data + @Schema public static class TwoFaAccountConfigUpdateRequest { private boolean useByDefault; } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index e26932ff22..8007a77cb1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; import lombok.Builder; @@ -99,7 +100,7 @@ public class TwoFactorAuthController extends BaseController { } } - @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = + @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviderInfos)", notes = "Get the list of 2FA provider infos available for user to use. Example:\n" + "```\n[\n" + " {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" + @@ -108,7 +109,7 @@ public class TwoFactorAuthController extends BaseController { "]\n```") @GetMapping("/providers") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public List getAvailableTwoFaProviders() throws ThingsboardException { + public List getAvailableTwoFaProviderInfos() throws ThingsboardException { SecurityUser user = getCurrentUser(); Optional platformTwoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(user.getTenantId(), true); return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user) @@ -166,6 +167,7 @@ public class TwoFactorAuthController extends BaseController { @Builder public static class TwoFaProviderInfo { private TwoFaProviderType type; + @JsonProperty("default") private boolean isDefault; private String contact; private Integer minVerificationCodeSendPeriod; diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index 878a3287bc..b919b664e5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -273,7 +274,7 @@ public class UserController extends BaseController { notes = "Returns a page of users owned by tenant or customer. The scope depends on authority of the user that performs the request." + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/users", params = {"pageSize", "page"}) + @GetMapping(value = "/users") public PageData getUsers( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -334,7 +335,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Get Tenant Users (getTenantAdmins)", notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @GetMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"}) + @GetMapping(value = "/tenant/{tenantId}/users") public PageData getTenantAdmins( @Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TENANT_ID) String strTenantId, @@ -357,7 +358,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Get Customer Users (getCustomerUsers)", notes = "Returns a page of users owned by customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"}) + @GetMapping(value = "/customer/{customerId}/users") public PageData getCustomerUsers( @Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable(CUSTOMER_ID) String strCustomerId, @@ -404,7 +405,7 @@ public class UserController extends BaseController { "Search is been executed by email, firstName and lastName fields. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"}) + @GetMapping(value = "/users/assign/{alarmId}") public PageData getUsersForAssign( @Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true) @PathVariable("alarmId") String strAlarmId, @@ -456,10 +457,7 @@ public class UserController extends BaseController { return userSettingsService.saveUserSettings(currentUser.getTenantId(), userSettings).getSettings(); } - @ApiOperation(value = "Update user settings (saveUserSettings)", - notes = "Update user settings for authorized user. Only specified json elements will be updated." + - "Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" + - "{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}") + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PutMapping(value = "/user/settings") public void putUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { @@ -467,8 +465,17 @@ public class UserController extends BaseController { userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, settings); } - @ApiOperation(value = "Get user settings (getUserSettings)", - notes = "Fetch the User settings based on authorized user. ") + @ApiOperation(value = "Update user settings (putGeneralUserSettings)", + notes = "Update user settings for authorized user. Only specified json elements will be updated." + + "Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" + + "{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PutMapping(value = "/user/settings/general") + public void putGeneralUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { + putUserSettings(settings); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/user/settings") public JsonNode getUserSettings() throws ThingsboardException { @@ -478,20 +485,28 @@ public class UserController extends BaseController { return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings(); } - @ApiOperation(value = "Delete user settings (deleteUserSettings)", + @ApiOperation(value = "Get user settings (getGeneralUserSettings)", + notes = "Fetch the User settings based on authorized user. ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/user/settings/general") + public JsonNode getGeneralUserSettings() throws ThingsboardException { + return getUserSettings(); + } + + @ApiOperation(value = "Delete user settings (deleteGeneralUserSettings)", notes = "Delete user settings by specifying list of json element xpaths. \n " + "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @DeleteMapping(value = "/user/settings/{paths}") - public void deleteUserSettings(@Parameter(description = PATHS) - @PathVariable(PATHS) String paths) throws ThingsboardException { + public void deleteGeneralUserSettings(@Parameter(description = PATHS) + @PathVariable(PATHS) String paths) throws ThingsboardException { checkParameter(USER_ID, paths); SecurityUser currentUser = getCurrentUser(); userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, Arrays.asList(paths.split(","))); } - @ApiOperation(value = "Update user settings (saveUserSettings)", + @ApiOperation(value = "Update user settings (putUserSettings)", notes = "Update user settings for authorized user. Only specified json elements will be updated." + "Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" + "{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}") @@ -518,15 +533,15 @@ public class UserController extends BaseController { return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings(); } - @ApiOperation(value = "Delete user settings (deleteUserSettings)", + @ApiOperation(value = "Delete user settings by type (deleteUserSettingsByType)", notes = "Delete user settings by specifying list of json element xpaths. \n " + "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @DeleteMapping(value = "/user/settings/{type}/{paths}") - public void deleteUserSettings(@Parameter(description = PATHS) - @PathVariable(PATHS) String paths, - @Parameter(description = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") - @PathVariable("type") String strType) throws ThingsboardException { + public void deleteUserSettingsByType(@Parameter(description = PATHS) + @PathVariable(PATHS) String paths, + @Parameter(description = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".") + @PathVariable("type") String strType) throws ThingsboardException { checkParameter(USER_ID, paths); UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf); checkNotReserved(strType, type); @@ -534,8 +549,7 @@ public class UserController extends BaseController { userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), type, Arrays.asList(paths.split(","))); } - @ApiOperation(value = "Get information about last visited and starred dashboards (getLastVisitedDashboards)", - notes = "Fetch the list of last visited and starred dashboards. Both lists are limited to 10 items." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/user/dashboards") public UserDashboardsInfo getUserDashboardsInfo() throws ThingsboardException { @@ -543,6 +557,14 @@ public class UserController extends BaseController { return userSettingsService.findUserDashboardsInfo(currentUser.getTenantId(), currentUser.getId()); } + @ApiOperation(value = "Get information about last visited and starred dashboards (getLastVisitedDashboards)", + notes = "Fetch the list of last visited and starred dashboards. Both lists are limited to 10 items." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/user/lastVisitedDashboards") + public UserDashboardsInfo getLastVisitedDashboards() throws ThingsboardException { + return getUserDashboardsInfo(); + } + @ApiOperation(value = "Report action of User over the dashboard (reportUserDashboardAction)", notes = "Report action of User over the dashboard. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @@ -583,12 +605,10 @@ public class UserController extends BaseController { userService.removeMobileSession(user.getTenantId(), mobileToken); } - @ApiOperation(value = "Get Users By Ids (getUsersByIds)", - notes = "Requested users must be owned by tenant or assigned to customer which user is performing the request. ") + @Hidden @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/users", params = {"userIds"}) - public List getUsersByIds( - @Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + public List getUsersByIdsV1( @RequestParam("userIds") Set userUUIDs) throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); List userIds = new ArrayList<>(); @@ -599,6 +619,16 @@ public class UserController extends BaseController { return filterUsersByReadPermission(users); } + @ApiOperation(value = "Get Users By Ids (getUsersByIds)", + notes = "Requested users must be owned by tenant or assigned to customer which user is performing the request. ") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/users/list") + public List getUsersByIds( + @Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("userIds") Set userUUIDs) throws ThingsboardException { + return getUsersByIdsV1(userUUIDs); + } + private List filterUsersByReadPermission(List users) { return users.stream().filter(user -> { try { diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index a519d92e3a..a89d870bf0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; @@ -209,8 +210,7 @@ public class WidgetTypeController extends AutoCommitController { } } - @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypesByBundleAlias) (Deprecated)", - notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"}) @Deprecated @@ -229,19 +229,27 @@ public class WidgetTypeController extends AutoCommitController { return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundle.getId())); } - @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", - notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/widgetTypes", params = {"widgetsBundleId"}) - public List getBundleWidgetTypes( + public List getBundleWidgetTypesV1( @Parameter(description = "Widget Bundle Id", required = true) @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundleId)); } - @ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetailsByBundleAlias) (Deprecated)", - notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)", + notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypes") + public List getBundleWidgetTypes( + @Parameter(description = "Widget Bundle Id", required = true) + @PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + return getBundleWidgetTypesV1(strWidgetsBundleId); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}) @Deprecated @@ -284,7 +292,7 @@ public class WidgetTypeController extends AutoCommitController { @ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)", notes = "Returns an array of Widget Type fqns that belong to specified Widget Bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @GetMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"}) + @GetMapping(value = "/widgetTypeFqns") public List getBundleWidgetTypeFqns( @Parameter(description = "Widget Bundle Id", required = true) @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { @@ -292,8 +300,7 @@ public class WidgetTypeController extends AutoCommitController { return checkNotNull(widgetTypeService.findWidgetFqnsByWidgetsBundleId(getTenantId(), widgetsBundleId)); } - @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfosByBundleAlias) (Deprecated)", - notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}) @Deprecated @@ -344,8 +351,7 @@ public class WidgetTypeController extends AutoCommitController { widgetTypeDeprecatedFilter, widgetTypes, pageLink)); } - @ApiOperation(value = "Get Widget Type (getWidgetTypeByBundleAliasAndTypeAlias) (Deprecated)", - notes = "Get the Widget Type based on the provided parameters. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}) @Deprecated diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index c2d612f014..af087c5fd2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -15,13 +15,13 @@ */ package org.thingsboard.server.controller; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -222,11 +222,10 @@ public class WidgetsBundleController extends BaseController { } } - @ApiOperation(value = "Get all Widget Bundles (getWidgetsBundles)", - notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/widgetsBundles") - public List getWidgetsBundles() throws ThingsboardException { + public List getWidgetsBundlesV1() throws ThingsboardException { if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId())); } else { @@ -235,13 +234,18 @@ public class WidgetsBundleController extends BaseController { } } - @ApiOperation(value = "Get Widgets Bundles By Ids (getWidgetsBundlesByIds)", - notes = "Requested widgets bundles must be system level or owned by tenant of the user which is performing the request. " + - NEW_LINE) + @ApiOperation(value = "Get all Widget Bundles (getAllWidgetsBundles)", + notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/widgetsBundles/all") + public List getAllWidgetsBundles() throws ThingsboardException { + return getWidgetsBundlesV1(); + } + + @Hidden @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/widgetsBundles", params = {"widgetsBundleIds"}) public List getWidgetsBundlesByIds( - @Parameter(description = "A list of widgets bundle ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) @RequestParam("widgetsBundleIds") Set widgetsBundleUUIDs) throws ThingsboardException { List widgetsBundleIds = new ArrayList<>(); for (UUID widgetsBundleUUID : widgetsBundleUUIDs) { @@ -250,4 +254,15 @@ public class WidgetsBundleController extends BaseController { return widgetsBundleService.findSystemOrTenantWidgetsBundlesByIds(getTenantId(), widgetsBundleIds); } + @ApiOperation(value = "Get Widgets Bundles By Ids (getWidgetsBundlesList)", + notes = "Requested widgets bundles must be system level or owned by tenant of the user which is performing the request. " + + NEW_LINE) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @GetMapping(value = "/widgetsBundles/list", params = {"widgetsBundleIds"}) + public List getWidgetsBundlesList( + @Parameter(description = "A list of widgets bundle ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true) + @RequestParam("widgetsBundleIds") Set widgetsBundleUUIDs) throws ThingsboardException { + return getWidgetsBundlesByIds(widgetsBundleUUIDs); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java index f175c46706..4adb97f380 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java @@ -15,10 +15,22 @@ */ package org.thingsboard.server.service.entitiy.cf; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal; +import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; +import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; @@ -31,10 +43,16 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.TimeUnit; @TbCoreComponent @Service @@ -42,8 +60,13 @@ import java.util.Set; @RequiredArgsConstructor public class DefaultTbCalculatedFieldService extends AbstractTbEntityService implements TbCalculatedFieldService { + private static final int TIMEOUT = 20; + private final CalculatedFieldService calculatedFieldService; + @Autowired(required = false) + private TbelInvokeService tbelInvokeService; + @Override public CalculatedField save(CalculatedField calculatedField, SecurityUser user) throws ThingsboardException { ActionType actionType = calculatedField.getId() == null ? ActionType.ADDED : ActionType.UPDATED; @@ -89,6 +112,75 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp } } + @Override + public JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams) { + String expression = inputParams.get("expression").asText(); + Map arguments = Objects.requireNonNullElse( + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), + Collections.emptyMap() + ); + + ArrayList ctxAndArgNames = new ArrayList<>(arguments.size() + 1); + ctxAndArgNames.add("ctx"); + ctxAndArgNames.addAll(arguments.keySet()); + + String output = ""; + String errorText = ""; + + CalculatedFieldTbelScriptEngine engine = null; + try { + if (tbelInvokeService == null) { + throw new IllegalArgumentException("TBEL script engine is disabled!"); + } + + engine = new CalculatedFieldTbelScriptEngine( + tenantId, + tbelInvokeService, + expression, + ctxAndArgNames.toArray(String[]::new) + ); + + Object[] args = new Object[ctxAndArgNames.size()]; + args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments)); + for (int i = 1; i < ctxAndArgNames.size(); i++) { + var arg = arguments.get(ctxAndArgNames.get(i)); + if (arg instanceof TbelCfSingleValueArg svArg) { + args[i] = svArg.getValue(); + } else { + args[i] = arg; + } + } + + JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); + output = JacksonUtil.toString(json); + } catch (Exception e) { + log.error("Error evaluating expression", e); + Throwable rootCause = ObjectUtils.firstNonNull(ExceptionUtils.getRootCause(e), e); + errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getClass().getSimpleName()); + } finally { + if (engine != null) { + engine.destroy(); + } + } + return JacksonUtil.newObjectNode() + .put("output", output) + .put("error", errorText); + } + + private static long getLatestTimestamp(Map arguments) { + long lastUpdateTimestamp = -1; + for (TbelCfArg entry : arguments.values()) { + if (entry instanceof TbelCfSingleValueArg singleValueArg) { + long ts = singleValueArg.getTs(); + lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts); + } else if (entry instanceof TbelCfTsRollingArg tsRollingArg) { + long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1); + lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs); + } + } + return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; + } + private void checkForEntityChange(CalculatedField oldCalculatedField, CalculatedField newCalculatedField) { if (!oldCalculatedField.getEntityId().equals(newCalculatedField.getEntityId())) { throw new IllegalArgumentException("Changing the calculated field target entity after initialization is prohibited."); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java index 5fbb96e636..9dbb20dc2f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy.cf; +import com.fasterxml.jackson.databind.JsonNode; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -35,4 +36,6 @@ public interface TbCalculatedFieldService { void delete(CalculatedField calculatedField, SecurityUser user); + JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams); + } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index cf8d9b1139..27bd88fc1a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -64,7 +64,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS private final RateLimitService rateLimitService; private final TbLogEntityActionService logEntityActionService; - protected static final List SUPPORTED_ENTITY_TYPES = List.of( + public static final List SUPPORTED_ENTITY_TYPES = List.of( EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE, EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE, EntityType.DEVICE, @@ -131,7 +131,10 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS @Override public Comparator getEntityTypeComparatorForImport() { - return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf); + return Comparator.comparingInt(type -> { + int index = SUPPORTED_ENTITY_TYPES.indexOf(type); + return index >= 0 ? index : Integer.MAX_VALUE; + }); } @SuppressWarnings("unchecked") diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java index d6ed0754e3..586febd686 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java @@ -42,10 +42,6 @@ public abstract class BaseEntityExportService ctx, E mainEntity, D exportData) { } - protected D newExportData() { - return (D) new EntityExportData(); - } - public abstract Set getSupportedEntityTypes(); protected void replaceUuidsRecursively(EntitiesExportCtx ctx, JsonNode node, Set skippedRootFields, Pattern includedFieldsPattern) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index a488ad165e..a38e2e058e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java @@ -70,7 +70,8 @@ public class DefaultEntityExportService ctx, I entityId) throws ThingsboardException { - D exportData = newExportData(); + @SuppressWarnings("unchecked") + D exportData = (D) EntityExportData.newInstance(entityId.getEntityType()); E entity = exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entityId); if (entity == null) { @@ -78,7 +79,6 @@ public class DefaultEntityExportService(); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java index 57beddef59..4a18a23b80 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java @@ -48,11 +48,6 @@ public class DeviceExportService extends BaseEntityExportService getSupportedEntityTypes() { return Set.of(EntityType.DEVICE); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java index af29e3c5df..775ab87fcb 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java @@ -36,11 +36,6 @@ public class OtaPackageExportService extends BaseEntityExportService getSupportedEntityTypes() { return Set.of(EntityType.OTA_PACKAGE); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java index 52452bcca4..ce93574103 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java @@ -61,11 +61,6 @@ public class RuleChainExportService extends BaseEntityExportService getSupportedEntityTypes() { return Set.of(EntityType.RULE_CHAIN); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java index cb2df97a57..7e381b39bb 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java @@ -38,11 +38,6 @@ public class WidgetTypeExportService extends BaseEntityExportService getSupportedEntityTypes() { return Set.of(EntityType.WIDGET_TYPE); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java index 3f579bbc43..3d4b80ce1c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java @@ -45,11 +45,6 @@ public class WidgetsBundleExportService extends BaseEntityExportService getSupportedEntityTypes() { return Set.of(EntityType.WIDGETS_BUNDLE); diff --git a/application/src/main/resources/thingsboard-openapi.properties b/application/src/main/resources/thingsboard-openapi.properties new file mode 100644 index 0000000000..410c4072a5 --- /dev/null +++ b/application/src/main/resources/thingsboard-openapi.properties @@ -0,0 +1,51 @@ +# +# Copyright © 2016-2026 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. +# + +# Lightweight startup for OpenAPI spec generation. +# Activated only via: --spring.profiles.active=openapi + +spring.main.banner-mode=off + +# Testcontainers PostgreSQL +database.ts.type=sql +database.ts_latest.type=sql +spring.datasource.url=jdbc:tc:postgresql:16.6:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.hikari.maximumPoolSize=5 +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +spring.jpa.properties.hibernate.order_by.default_null_ordering=last +spring.jpa.show-sql=false +spring.jpa.hibernate.ddl-auto=none + +# Disable transports +transport.http.enabled=false +transport.mqtt.enabled=false +transport.coap.enabled=false +transport.lwm2m.enabled=false +transport.snmp.enabled=false +coap.server.enabled=false + +# Disable edges, integrations, EDQS +edges.enabled=false +integrations.rpc.enabled=false +service.integrations.supported=NONE +transport.gateway.dashboard.sync.enabled=false +queue.edqs.sync.enabled=false +queue.edqs.api.supported=false +usage.stats.report.enabled=false +service.type=monolith diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index cf94940e07..7041a71086 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -48,8 +48,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.predic import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate.StringOperation; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; @@ -128,32 +127,32 @@ public class AlarmRulesTest extends AbstractControllerTest { ); Condition clearRule = new Condition("return temperature <= 25;", null, null); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, clearRule); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); postTelemetry(deviceId, "{\"temperature\":100}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isSeverityUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); postTelemetry(deviceId, "{\"temperature\":101}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); postTelemetry(deviceId, "{\"temperature\":20}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCleared()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); @@ -185,11 +184,11 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition(simpleExpression, null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":100}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -209,11 +208,11 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"values\": {\"temperature\": 50}, \"ts\": " + (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30) + "}")); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -236,15 +235,15 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); for (int i = 0; i < 4; i++) { postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(10); } - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -255,12 +254,12 @@ public class AlarmRulesTest extends AbstractControllerTest { postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(10); } - checkAlarmResult(calculatedField, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> { assertThat(alarmResult.isUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); }); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isSeverityUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -288,14 +287,14 @@ public class AlarmRulesTest extends AbstractControllerTest { new AlarmConditionValue<>(null, "eventsCount"), null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"eventsCount\":" + eventsCount + "}"); for (int i = 0; i < eventsCount; i++) { postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(10); } - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -319,13 +318,13 @@ public class AlarmRulesTest extends AbstractControllerTest { ); Condition clearRule = new Condition("return powerConsumption < 3000;", null, clearDurationMs); - CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 5 seconds", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 5 seconds", arguments, createRules, clearRule); postTelemetry(deviceId, "{\"powerConsumption\":3500}"); Thread.sleep(createDurationMs - 2000); - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -334,9 +333,9 @@ public class AlarmRulesTest extends AbstractControllerTest { postTelemetry(deviceId, "{\"powerConsumption\":2000}"); Thread.sleep(clearDurationMs - 2000); - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCleared()).isTrue(); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); assertThat(alarmResult.getConditionDuration()).isBetween(clearDurationMs, clearDurationMs + 2000); @@ -363,12 +362,12 @@ public class AlarmRulesTest extends AbstractControllerTest { new AlarmConditionValue(null, "duration"), null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 2 seconds", arguments, createRules, null); postTelemetry(deviceId, "{\"powerConsumption\":3500}"); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"duration\":" + createDurationMs + "}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -391,10 +390,10 @@ public class AlarmRulesTest extends AbstractControllerTest { new AlarmConditionValue(2000L, null), null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 2 seconds", arguments, createRules, null); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -424,12 +423,12 @@ public class AlarmRulesTest extends AbstractControllerTest { device.setCustomerId(customerId); device = doPost("/api/device", device, Device.class); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":50}"); postTelemetry(deviceId, "{\"temperature\":51}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -462,21 +461,21 @@ public class AlarmRulesTest extends AbstractControllerTest { "location", StringOperation.NOT_CONTAINS, new AlarmConditionValue<>(null, "locationFilter") ), null, null); - CalculatedField calculatedField = createAlarmCf(customerId, "New resident", + AlarmRuleDefinition alarmRule = createAlarmRule(customerId, "New resident", arguments, createRules, clearRule); loginSysAdmin(); postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"locationFilter\":\"Kyiv\"}"); loginTenantAdmin(); postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Kyiv\"}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Lviv\"}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCleared()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); @@ -501,7 +500,7 @@ public class AlarmRulesTest extends AbstractControllerTest { new AlarmConditionValue<>(null, "schedule")) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); String schedule = """ {"timezone":"Europe/Kiev","items":[{"enabled":false,"dayOfWeek":1,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":2,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":3,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":4,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":5,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":6,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":7,"startsOn":0,"endsOn":0}]} @@ -510,14 +509,14 @@ public class AlarmRulesTest extends AbstractControllerTest { postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(1000); - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); schedule = schedule.replace("\"enabled\":false", "\"enabled\":true"); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}"); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { // checking multiple debug events due to scheduled reevaluation (which also produces debug events) - CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream() + CalculatedFieldDebugEvent debugEvent = getDebugEvents(alarmRule.getId(), 5).stream() .filter(event -> event.getResult() != null) .findFirst().orElse(null); assertThat(debugEvent).isNotNull(); @@ -565,18 +564,18 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition(criticalExpression, null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "No Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "No Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isSeverityUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -596,19 +595,19 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); }); - calculatedField.setName("New alarm type"); - calculatedField = saveCalculatedField(calculatedField); - checkAlarmResult(calculatedField, alarmResult -> { + alarmRule.setName("New alarm type"); + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -628,18 +627,18 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(1000); - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); - AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration(); ((TbelAlarmConditionExpression) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition().getExpression()) .setExpression("return temperature >= 50;"); - calculatedField = saveCalculatedField(calculatedField); - checkAlarmResult(calculatedField, alarmResult -> { + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -662,13 +661,13 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); for (int i = 0; i < eventsCountMajor; i++) { postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(10); } - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -676,18 +675,18 @@ public class AlarmRulesTest extends AbstractControllerTest { }); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); assertThat(alarmResult.getConditionRepeats()).isEqualTo(6); }); // decreasing required events count for critical rule - AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration(); ((RepeatingAlarmCondition) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition()) .setCount(new AlarmConditionValue<>(6, null)); - calculatedField = saveCalculatedField(calculatedField); - checkAlarmResult(calculatedField, alarmResult -> { + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isSeverityUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -719,20 +718,20 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= temperatureThreshold;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":50}"); Thread.sleep(1000); // not created because tenant's threshold 100 is used - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); - ((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getArguments().get("temperatureThreshold") + ((AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration()).getArguments().get("temperatureThreshold") .setRefDynamicSourceConfiguration(null); // using threshold 50 on device level - calculatedField = saveCalculatedField(calculatedField); + alarmRule = saveAlarmRule(alarmRule); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -755,7 +754,7 @@ public class AlarmRulesTest extends AbstractControllerTest { Map createRules = Map.of( AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature and Humidity Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature and Humidity Alarm", arguments, createRules, null, configuration -> { configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( "temperature is ${temperature}, humidity is ${humidity}" @@ -765,18 +764,18 @@ public class AlarmRulesTest extends AbstractControllerTest { postTelemetry(deviceId, "{\"temperature\":50}"); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getDetails().get("data").asText()) .isEqualTo("temperature is 50, humidity is 50"); }); - ((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( + ((AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( "UPDATED temperature is ${temperature}, humidity is ${humidity}" ); - calculatedField = saveCalculatedField(calculatedField); - checkAlarmResult(calculatedField, alarmResult -> { + alarmRule = saveAlarmRule(alarmRule); + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isFalse(); assertThat(alarmResult.isUpdated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); @@ -805,16 +804,16 @@ public class AlarmRulesTest extends AbstractControllerTest { new AlarmConditionValue<>(schedule, null)) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "Illegal parking alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "Illegal parking alarm", arguments, createRules, null); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"parkingSpotOccupied\":true}"); Thread.sleep(10000); - assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + assertThat(getLatestAlarmResult(alarmRule.getId())).isNull(); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { - CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream() + CalculatedFieldDebugEvent debugEvent = getDebugEvents(alarmRule.getId(), 5).stream() .filter(event -> event.getResult() != null) .findFirst().orElse(null); assertThat(debugEvent).isNotNull(); @@ -838,11 +837,11 @@ public class AlarmRulesTest extends AbstractControllerTest { AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) ); - CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, null); postTelemetry(deviceId, "{\"temperature\":50}"); - Alarm alarm = checkAlarmResult(calculatedField, alarmResult -> { + Alarm alarm = checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); @@ -851,7 +850,7 @@ public class AlarmRulesTest extends AbstractControllerTest { doPost("/api/alarm/" + alarm.getId() + "/clear", AlarmInfo.class); Thread.sleep(1000); postTelemetry(deviceId, "{\"temperature\":50}"); - checkAlarmResult(calculatedField, alarmResult -> { + checkAlarmResult(alarmRule, alarmResult -> { assertThat(alarmResult.getAlarm().getId()).isNotEqualTo(alarm.getId()); assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); @@ -861,21 +860,21 @@ public class AlarmRulesTest extends AbstractControllerTest { // TODO: MSA tests - private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, Consumer assertion) { - return checkAlarmResult(calculatedField, null, assertion); + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer assertion) { + return checkAlarmResult(alarmRule, null, assertion); } - private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Predicate waitFor, Consumer assertion) { TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> getLatestAlarmResult(calculatedField.getId()), result -> + .until(() -> getLatestAlarmResult(alarmRule.getId()), result -> result != null && (waitFor == null || waitFor.test(result))); assertion.accept(alarmResult); Alarm alarm = alarmResult.getAlarm(); assertThat(alarm.getOriginator()).isEqualTo(originatorId); - assertThat(alarm.getType()).isEqualTo(calculatedField.getName()); + assertThat(alarm.getType()).isEqualTo(alarmRule.getName()); return alarmResult; } @@ -895,38 +894,37 @@ public class AlarmRulesTest extends AbstractControllerTest { return JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); } - private CalculatedField createAlarmCf(EntityId entityId, - String alarmType, - Map arguments, - Map createConditions, - Condition clearCondition, - Consumer... modifier) { + private AlarmRuleDefinition createAlarmRule(EntityId entityId, + String alarmType, + Map arguments, + Map createConditions, + Condition clearCondition, + Consumer... modifier) { Map createRules = new HashMap<>(); createConditions.forEach((severity, condition) -> { createRules.put(severity, toAlarmRule(condition)); }); AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null; - CalculatedField calculatedField = new CalculatedField(); - calculatedField.setEntityId(entityId); - calculatedField.setName(alarmType); - calculatedField.setType(CalculatedFieldType.ALARM); + AlarmRuleDefinition alarmRule = new AlarmRuleDefinition(); + alarmRule.setEntityId(entityId); + alarmRule.setName(alarmType); AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); configuration.setArguments(arguments); configuration.setCreateRules(createRules); configuration.setClearRule(clearRule); - calculatedField.setConfiguration(configuration); - calculatedField.setDebugSettings(DebugSettings.all()); + alarmRule.setConfiguration(configuration); + alarmRule.setDebugSettings(DebugSettings.all()); if (modifier.length > 0) { modifier[0].accept(configuration); } - CalculatedField savedCalculatedField = saveCalculatedField(calculatedField); + AlarmRuleDefinition savedAlarmRule = saveAlarmRule(alarmRule); CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> getDebugEvents(savedCalculatedField.getId(), 1), + .until(() -> getDebugEvents(savedAlarmRule.getId(), 1), events -> !events.isEmpty()).get(0); latestEventId = debugEvent.getId(); - return savedCalculatedField; + return savedAlarmRule; } private AlarmRule toAlarmRule(Condition condition) { diff --git a/application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java new file mode 100644 index 0000000000..59314378aa --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AIModelApiClientTest.java @@ -0,0 +1,168 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.AiModel; +import org.thingsboard.client.model.OpenAiChatModelConfig; +import org.thingsboard.client.model.OpenAiProviderConfig; +import org.thingsboard.client.model.PageDataAiModel; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AIModelApiClientTest extends AbstractApiClientTest { + + private static final String AI_PREFIX = "AiTest_"; + + @Test + public void testSaveAndGetAiModel() throws Exception { + long ts = System.currentTimeMillis(); + String name = AI_PREFIX + "save_" + ts; + + AiModel model = buildAiModel(name, "gpt-4o", 0.7); + AiModel saved = client.saveAiModel(model); + assertNotNull(saved); + assertNotNull(saved.getId()); + assertEquals(name, saved.getName()); + assertNotNull(saved.getConfiguration()); + + // get by id + AiModel fetched = client.getAiModelById(saved.getId().getId()); + assertNotNull(fetched); + assertEquals(name, fetched.getName()); + assertEquals(saved.getId().getId(), fetched.getId().getId()); + } + + @Test + public void testGetAiModelById() throws Exception { + long ts = System.currentTimeMillis(); + AiModel saved = createAiModel("getbyid_" + ts); + + AiModel fetched = client.getAiModelById(saved.getId().getId()); + assertNotNull(fetched); + assertEquals(saved.getName(), fetched.getName()); + assertEquals(saved.getId().getId(), fetched.getId().getId()); + } + + @Test + public void testUpdateAiModel() throws Exception { + long ts = System.currentTimeMillis(); + AiModel saved = createAiModel("update_" + ts); + + saved.setName(AI_PREFIX + "updated_" + ts); + OpenAiChatModelConfig updatedConfig = new OpenAiChatModelConfig(); + updatedConfig.setModelId("gpt-4o-mini"); + updatedConfig.setTemperature(0.3); + updatedConfig.setMaxOutputTokens(2048); + updatedConfig.setMaxRetries(50); + OpenAiProviderConfig providerConfig = new OpenAiProviderConfig(); + providerConfig.setApiKey("test-api-key"); + providerConfig.setBaseUrl("https://api.openai.com/v1"); + updatedConfig.setProviderConfig(providerConfig); + updatedConfig.setProvider("OPENAI"); + saved.setConfiguration(updatedConfig); + + AiModel updated = client.saveAiModel(saved); + assertNotNull(updated); + assertEquals(saved.getId().getId(), updated.getId().getId()); + assertEquals(AI_PREFIX + "updated_" + ts, updated.getName()); + } + + @Test + public void testDeleteAiModel() throws Exception { + long ts = System.currentTimeMillis(); + AiModel saved = createAiModel("delete_" + ts); + + UUID modelId = saved.getId().getId(); + client.getAiModelById(modelId); + + Boolean deleted = client.deleteAiModelById(modelId); + assertTrue(deleted); + + assertReturns404(() -> client.getAiModelById(modelId)); + } + + @Test + public void testGetAiModels() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 3; i++) { + createAiModel("list_" + ts + "_" + i); + } + + PageDataAiModel page = client.getAiModels(100, 0, AI_PREFIX + "list_" + ts, null, null); + assertNotNull(page); + assertEquals(3, page.getTotalElements().intValue()); + for (AiModel m : page.getData()) { + assertTrue(m.getName().startsWith(AI_PREFIX + "list_" + ts)); + } + } + + @Test + public void testGetAiModelById_notFound() { + UUID nonExistentId = UUID.randomUUID(); + assertReturns404(() -> client.getAiModelById(nonExistentId)); + } + + @Test + public void testGetAiModelsPagination() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 5; i++) { + createAiModel("paged_" + ts + "_" + i); + } + + PageDataAiModel page1 = client.getAiModels(2, 0, AI_PREFIX + "paged_" + ts, null, null); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + PageDataAiModel lastPage = client.getAiModels(2, 2, AI_PREFIX + "paged_" + ts, null, null); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + + private AiModel buildAiModel(String name, String modelId, double temperature) { + OpenAiChatModelConfig config = new OpenAiChatModelConfig(); + config.setModelId(modelId); + config.setTemperature(temperature); + config.setMaxRetries(50); + OpenAiProviderConfig openAiProviderConfig = new OpenAiProviderConfig(); + openAiProviderConfig.setApiKey("test-api-key"); + openAiProviderConfig.setBaseUrl("https://api.openai.com/v1"); + config.setProviderConfig(openAiProviderConfig); + config.setProvider("OPENAI"); + + AiModel model = new AiModel(); + model.setName(name); + model.setConfiguration(config); + return model; + } + + private AiModel createAiModel(String suffix) throws Exception { + return client.saveAiModel(buildAiModel(AI_PREFIX + suffix, "gpt-4o", 0.7)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java new file mode 100644 index 0000000000..6bbda65a59 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AbstractApiClientTest.java @@ -0,0 +1,127 @@ +/** + * Copyright © 2016-2026 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.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.ThingsboardClient; +import org.thingsboard.client.model.ActivateUserRequest; +import org.thingsboard.client.model.Authority; +import org.thingsboard.client.model.JwtPair; +import org.thingsboard.client.model.User; +import org.thingsboard.client.model.UserId; +import org.thingsboard.server.common.data.util.ThrowingRunnable; +import org.thingsboard.server.controller.AbstractControllerTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@Slf4j +public abstract class AbstractApiClientTest extends AbstractControllerTest { + + protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + protected static final ObjectMapper MAPPER = new ObjectMapper(); + + protected static final String TEST_PREFIX = "ApiClientTestDevice_"; + protected static final String TEST_PREFIX_2 = "ApiClientTestDevice2_"; + protected static final String CUSTOMER_USERNAME = "javaClientCustomer@thingsboard.org"; + protected static final String TENANT_ADMIN_USERNAME = "javaClientTenant@thingsboard.org"; + protected static final String TEST_PASSWORD = "password123"; + + protected ThingsboardClient client; + + // FQN for Tenant/Customer to avoid collision with AbstractWebTest fields + protected org.thingsboard.client.model.Tenant savedClientTenant; + protected User clientTenantAdmin; + protected org.thingsboard.client.model.Customer savedClientCustomer; + protected User savedClientCustomerUser; + + @Before + public void setUpJavaClient() throws Exception { + client = ThingsboardClient.builder() + .url("http://localhost:" + wsPort) + .build(); + client.login("sysadmin@thingsboard.org", "sysadmin"); + + org.thingsboard.client.model.Tenant tenant = new org.thingsboard.client.model.Tenant(); + tenant.setTitle("Java client test tenant"); + savedClientTenant = client.saveTenant(tenant); + + clientTenantAdmin = new User(); + clientTenantAdmin.setAuthority(Authority.TENANT_ADMIN); + clientTenantAdmin.setTenantId(savedClientTenant.getId()); + clientTenantAdmin.setEmail(TENANT_ADMIN_USERNAME); + clientTenantAdmin = client.saveUser(clientTenantAdmin, "false"); + activateUserAndAuthorize(clientTenantAdmin); + + org.thingsboard.client.model.Customer customer = new org.thingsboard.client.model.Customer(); + customer.setTitle("Java client test customer"); + customer.setTenantId(savedClientTenant.getId()); + savedClientCustomer = client.saveCustomer(customer, null, null, null); + + User customerUser = new User(); + customerUser.setAuthority(Authority.CUSTOMER_USER); + customerUser.setTenantId(savedClientTenant.getId()); + customerUser.setCustomerId(savedClientCustomer.getId()); + customerUser.setEmail(CUSTOMER_USERNAME); + savedClientCustomerUser = client.saveUser(customerUser, "false"); + activateUser(savedClientCustomerUser.getId(), "password123", false); + } + + @After + public void tearDownJavaClient() { + client.login("sysadmin@thingsboard.org", "sysadmin"); + client.deleteTenant(savedClientTenant.getId().getId().toString()); + } + + protected String getBaseUrl() { + return "http://localhost:" + wsPort; + } + + protected void activateUserAndAuthorize(User user) throws ApiException { + JwtPair jwtPair = activateUser(user.getId(), TEST_PASSWORD, false); + client.setToken(jwtPair.getToken()); + } + + protected JwtPair activateUser(UserId userId, String password, boolean sendActivationMail) throws ApiException { + ActivateUserRequest activateRequest = new ActivateUserRequest(); + activateRequest.setActivateToken(getActivateToken(userId)); + activateRequest.setPassword(password); + return client.activateUser(activateRequest, sendActivationMail); + } + + protected String getActivateToken(UserId userId) throws ApiException { + String activateTokenRegex = "/api/noauth/activate?activateToken="; + String activationLink = client.getActivationLink(userId.getId().toString()); + return activationLink.substring(activationLink.lastIndexOf(activateTokenRegex) + activateTokenRegex.length()); + } + + protected void assertReturns404(ThrowingRunnable operation) { + try { + operation.run(); + fail("Expected ApiException with 404 status code"); + } catch (ApiException exception) { + assertEquals("Expected 404 status code but got " + exception.getCode(), + 404, exception.getCode()); + } catch (Exception e) { + fail("Expected ApiException but got " + e.getClass().getName() + ": " + e.getMessage()); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java new file mode 100644 index 0000000000..4d11bf3fbe --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AdminApiClientTest.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2026 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.client; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.thingsboard.client.model.AdminSettings; +import org.thingsboard.client.model.FeaturesInfo; +import org.thingsboard.client.model.JwtSettings; +import org.thingsboard.client.model.SecuritySettings; +import org.thingsboard.client.model.SystemInfo; +import org.thingsboard.client.model.UpdateMessage; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AdminApiClientTest extends AbstractApiClientTest { + + @Test + public void testAdminSettingsLifecycle() throws Exception { + // authenticate as sysadmin for admin settings management + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // get mail settings + AdminSettings mailSettings = client.getAdminSettings("mail"); + assertNotNull(mailSettings); + assertNotNull(mailSettings.getKey()); + assertEquals("mail", mailSettings.getKey()); + assertNotNull(mailSettings.getJsonValue()); + + // get general settings + AdminSettings generalSettings = client.getAdminSettings("general"); + assertNotNull(generalSettings); + assertEquals("general", generalSettings.getKey()); + assertNotNull(generalSettings.getJsonValue()); + assertNotNull(generalSettings.getJsonValue().get("baseUrl").asText()); + + // update general settings and restore + ((ObjectNode) generalSettings.getJsonValue()).put("prohibitDifferentUrl", true); + AdminSettings updatedGeneralSettings = client.saveAdminSettings(generalSettings); + assertTrue(updatedGeneralSettings.getJsonValue().get("prohibitDifferentUrl").asBoolean()); + + // get security settings + SecuritySettings securitySettings = client.getSecuritySettings(); + assertNotNull(securitySettings); + assertNotNull(securitySettings.getPasswordPolicy()); + Integer originalMaxAttempts = securitySettings.getMaxFailedLoginAttempts(); + + // update security settings + securitySettings.setMaxFailedLoginAttempts(10); + SecuritySettings updatedSecurity = client.saveSecuritySettings(securitySettings); + assertNotNull(updatedSecurity); + assertEquals(10, updatedSecurity.getMaxFailedLoginAttempts().intValue()); + + // restore original security settings + updatedSecurity.setMaxFailedLoginAttempts(originalMaxAttempts); + client.saveSecuritySettings(updatedSecurity); + + // get JWT settings + JwtSettings jwtSettings = client.getJwtSettings(); + assertNotNull(jwtSettings); + assertNotNull(jwtSettings.getTokenExpirationTime()); + assertNotNull(jwtSettings.getRefreshTokenExpTime()); + assertEquals("thingsboard.io", jwtSettings.getTokenIssuer()); + assertNotNull(jwtSettings.getTokenSigningKey()); + + // get system info + SystemInfo systemInfo = client.getSystemInfo(); + assertNotNull(systemInfo); + + // get features info + FeaturesInfo featuresInfo = client.getFeaturesInfo(); + assertNotNull(featuresInfo); + assertFalse(featuresInfo.getSmsEnabled()); + assertFalse(featuresInfo.getOauthEnabled()); + + // check updates + UpdateMessage updateMessage = client.checkUpdates(); + assertNotNull(updateMessage); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java new file mode 100644 index 0000000000..0ebc247018 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AlarmApiClientTest.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.Alarm; +import org.thingsboard.client.model.AlarmInfo; +import org.thingsboard.client.model.AlarmSeverity; +import org.thingsboard.client.model.AlarmStatus; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntitySubtype; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.PageDataAlarmInfo; +import org.thingsboard.client.model.PageDataEntitySubtype; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AlarmApiClientTest extends AbstractApiClientTest { + + @Test + public void testAlarmLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdAlarms = new ArrayList<>(); + + // First, create devices to attach alarms to + Device device1 = new Device(); + device1.setName("Device_For_Alarm_" + timestamp + "_1"); + device1.setType("default"); + Device createdDevice1 = client.saveDevice(device1, null, null, null, null); + + Device device2 = new Device(); + device2.setName("Device_For_Alarm_" + timestamp + "_2"); + device2.setType("thermostat"); + Device createdDevice2 = client.saveDevice(device2, null, null, null, null); + + // Create 2 alarms (1 for each device) + for (int i = 0; i < 2; i++) { + Alarm alarm = new Alarm(); + alarm.setType(((i % 2 == 0) ? "Temperature Alarm" : "Connection Alarm")); + alarm.setSeverity(((i % 2 == 0) ? AlarmSeverity.CRITICAL : AlarmSeverity.WARNING)); + alarm.setOriginator((i % 2 == 0) ? createdDevice1.getId() : createdDevice2.getId()); + + Alarm createdAlarm = client.saveAlarm(alarm); + assertNotNull(createdAlarm); + assertNotNull(createdAlarm.getId()); + assertEquals(alarm.getType(), createdAlarm.getType()); + assertEquals(alarm.getSeverity(), createdAlarm.getSeverity()); + + createdAlarms.add(createdAlarm); + } + + // Get all alarms + PageDataAlarmInfo allAlarms = client.getAllAlarms(100, 0, null, null, null, null, null, null, null, null, null); + + assertNotNull(allAlarms); + assertNotNull(allAlarms.getData()); + int initialSize = allAlarms.getData().size(); + assertEquals("Expected at least 2 alarms, but got " + initialSize, 2, initialSize); + + // Get alarms by entity (device1) + PageDataAlarmInfo device1Alarms = client.getAlarmsV2(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), 100, 0, null, null, null, null, null, null, null, null, null); + assertNotNull(device1Alarms); + assertEquals("Expected 1 alarms for device1", 1, device1Alarms.getData().size()); + + // Get alarm by id + Alarm searchAlarm = createdAlarms.get(0); + Alarm fetchedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString()); + assertEquals(searchAlarm.getType(), fetchedAlarm.getType()); + assertEquals(searchAlarm.getSeverity(), fetchedAlarm.getSeverity()); + + // Get alarm info + AlarmInfo alarmInfo = client.getAlarmInfoById(searchAlarm.getId().getId().toString()); + assertNotNull(alarmInfo); + assertEquals(searchAlarm.getId().getId(), alarmInfo.getId().getId()); + + // Acknowledge alarm + client.ackAlarm(searchAlarm.getId().getId().toString()); + + // Verify alarm is acknowledged + Alarm ackedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString()); + assertEquals(AlarmStatus.ACTIVE_ACK, ackedAlarm.getStatus()); + + // Clear alarm + client.clearAlarm(searchAlarm.getId().getId().toString()); + + // Verify alarm is cleared + Alarm clearedAlarm = client.getAlarmById(searchAlarm.getId().getId().toString()); + assertEquals(AlarmStatus.CLEARED_ACK, clearedAlarm.getStatus()); + + // Get highest severity alarm for device + AlarmSeverity highestSeverity = client.getHighestAlarmSeverity(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), null, null, null); + assertNotNull(highestSeverity); + assertEquals(AlarmSeverity.CRITICAL, highestSeverity); + + // Assign alarm to customer + client.assignAlarm(createdAlarms.get(0).getId().getId().toString(), clientTenantAdmin.getId().getId().toString()); + + // Verify assignment + Alarm assignedAlarm = client.getAlarmById(createdAlarms.get(0).getId().getId().toString()); + assertEquals(clientTenantAdmin.getId().getId(), assignedAlarm.getAssigneeId().getId()); + + // Unassign alarm + client.unassignAlarm(createdAlarms.get(0).getId().getId().toString()); + + // Verify unassignment + Alarm unassignedAlarm = client.getAlarmById(createdAlarms.get(0).getId().getId().toString()); + assertNull(unassignedAlarm.getAssigneeId()); + + // Get alarm types + PageDataEntitySubtype pageDataEntitySubtype = client.getAlarmTypes(100, 0, null, null); + assertEquals(2, pageDataEntitySubtype.getData().size()); + List alarmTypes = pageDataEntitySubtype.getData().stream() + .map(EntitySubtype::getType) + .collect(Collectors.toList()); + assertTrue(alarmTypes.containsAll(List.of("Temperature Alarm", "Connection Alarm"))); + + // Get alarms V2 (alternative endpoint) + PageDataAlarmInfo alarmsV2 = client.getAlarmsV2(EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), 100, 0, null, null, null, null, null, null, null, null, null); + assertNotNull(alarmsV2); + assertEquals(1, alarmsV2.getData().size()); + + // Get all alarms V2 + PageDataAlarmInfo allAlarmsV2 = client.getAllAlarmsV2(100, 0, null, null, null, null, null, null, null, null, null); + assertEquals(2, allAlarmsV2.getData().size()); + + // Delete alarm + UUID alarmToDeleteId = createdAlarms.get(0).getId().getId(); + client.deleteAlarm(alarmToDeleteId.toString()); + + // Verify the alarm is deleted (should return 404) + assertReturns404(() -> + client.getAlarmById(alarmToDeleteId.toString()) + ); + + // Verify count after deletion + PageDataAlarmInfo alarmsAfterDelete = client.getAllAlarms(100, 0, null, null, null, null, null, null, null, null, null); + assertEquals(initialSize - 1, alarmsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java new file mode 100644 index 0000000000..31da96b7b4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AlarmCommentApiClientTest.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2026 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.client; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.thingsboard.client.model.Alarm; +import org.thingsboard.client.model.AlarmComment; +import org.thingsboard.client.model.AlarmCommentInfo; +import org.thingsboard.client.model.AlarmSeverity; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.PageDataAlarmCommentInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class AlarmCommentApiClientTest extends AbstractApiClientTest { + + @Test + public void testAlarmComments() throws Exception { + long timestamp = System.currentTimeMillis(); + + // Create device for alarm + Device device = new Device(); + device.setName("Device_For_Comments_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + + // Create alarm + Alarm alarm = new Alarm(); + alarm.setType("Temperature Alarm"); + alarm.setSeverity(AlarmSeverity.CRITICAL); + alarm.setOriginator(createdDevice.getId()); + + Alarm createdAlarm = client.saveAlarm(alarm); + String alarmId = createdAlarm.getId().getId().toString(); + + List createdComments = new ArrayList<>(); + + // Create multiple comments + for (int i = 0; i < 5; i++) { + AlarmComment alarmComment = new AlarmComment(); + String message = "Test comment #" + i + " at " + timestamp; + ObjectNode comment = OBJECT_MAPPER.createObjectNode().put("message", message); + alarmComment.setComment(comment); + + AlarmComment commentInfo = client.saveAlarmComment(alarmId, alarmComment); + + assertNotNull(commentInfo); + assertNotNull(commentInfo.getId()); + JsonNode commentValue = commentInfo.getComment(); + assertEquals(message, commentValue.get("message").asText()); + assertNotNull(commentInfo.getCreatedTime()); + + createdComments.add(commentInfo); + } + + // Get all comments for the alarm + PageDataAlarmCommentInfo allComments = client.getAlarmComments(alarmId, 100, 0, null, null); + assertEquals("Expected 5 comments", 5, allComments.getData().size()); + + // Update a comment + AlarmComment commentToUpdate = createdComments.get(2); + JsonNode comment = commentToUpdate.getComment(); + ((ObjectNode) comment).put("message", "New comment"); + commentToUpdate.setComment(comment); + + AlarmComment updatedComment = client.saveAlarmComment(alarmId, commentToUpdate); + assertEquals("New comment", updatedComment.getComment().get("message").asText()); + + // Delete a comment + UUID commentToDeleteId = createdComments.get(0).getId().getId(); + + client.deleteAlarmComment(alarmId, commentToDeleteId.toString()); + + // Verify comment was updated to "deleted" + PageDataAlarmCommentInfo commentsAfterDelete = client.getAlarmComments(alarmId, 100, 0, null, null); + List data = commentsAfterDelete.getData(); + AlarmCommentInfo deletedComment = data.stream() + .filter(alarmCommentInfo -> alarmCommentInfo.getId().getId().equals(commentToDeleteId)) + .findFirst() + .get(); + assertEquals("User " + clientTenantAdmin.getEmail() + " deleted his comment", deletedComment.getComment().get("text").asText()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java new file mode 100644 index 0000000000..4b774a442a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/ApiKeyApiClientTest.java @@ -0,0 +1,117 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.ApiKey; +import org.thingsboard.client.model.ApiKeyInfo; +import org.thingsboard.client.model.PageDataApiKeyInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class ApiKeyApiClientTest extends AbstractApiClientTest { + + @Test + public void testApiKeyLifecycle() throws Exception { + String userId = clientTenantAdmin.getId().getId().toString(); + + ApiKeyInfo request = new ApiKeyInfo(); + request.setDescription("Test API key"); + request.setUserId(clientTenantAdmin.getId()); + request.setEnabled(true); + ApiKey created = client.saveApiKey(request); + + assertNotNull(created); + assertNotNull(created.getId()); + assertNotNull(created.getValue()); + assertFalse(created.getValue().isBlank()); + assertEquals("Test API key", created.getDescription()); + + UUID keyId = created.getId().getId(); + + PageDataApiKeyInfo keysPage = client.getUserApiKeys(userId, 100, 0, null, null, null); + assertNotNull(keysPage); + assertNotNull(keysPage.getData()); + assertTrue("Newly created API key should appear in user's key list", + keysPage.getData().stream() + .anyMatch(k -> k.getId().getId().equals(keyId))); + + client.deleteApiKey(keyId); + + PageDataApiKeyInfo keysAfterDelete = client.getUserApiKeys(userId, 100, 0, null, null, null); + assertTrue("Deleted API key should not appear in user's key list", + keysAfterDelete.getData().stream() + .noneMatch(k -> k.getId().getId().equals(keyId))); + } + + @Test + public void testEnableDisableApiKey() throws Exception { + ApiKeyInfo request = new ApiKeyInfo(); + request.setDescription("Enable/disable test key"); + request.setUserId(clientTenantAdmin.getId()); + request.setEnabled(true); + ApiKey created = client.saveApiKey(request); + assertNotNull(created); + + UUID keyId = created.getId().getId(); + + ApiKeyInfo disabled = client.enableApiKey(keyId, false); + assertNotNull(disabled); + assertEquals(Boolean.FALSE, disabled.getEnabled()); + + ApiKeyInfo enabled = client.enableApiKey(keyId, true); + assertNotNull(enabled); + assertEquals(Boolean.TRUE, enabled.getEnabled()); + + client.deleteApiKey(keyId); + } + + @Test + public void testGetUserApiKeys() throws Exception { + String userId = clientTenantAdmin.getId().getId().toString(); + + int initialCount = client.getUserApiKeys(userId, 100, 0, null, null, null) + .getData().size(); + + UUID[] createdIds = new UUID[3]; + for (int i = 0; i < 3; i++) { + ApiKeyInfo request = new ApiKeyInfo(); + request.setDescription("Paging test key " + i); + request.setUserId(clientTenantAdmin.getId()); + createdIds[i] = client.saveApiKey(request).getId().getId(); + } + + PageDataApiKeyInfo afterCreate = client.getUserApiKeys(userId, 100, 0, null, null, null); + assertEquals(initialCount + 3, afterCreate.getData().size()); + assertEquals(Long.valueOf(initialCount + 3), afterCreate.getTotalElements()); + + PageDataApiKeyInfo page1 = client.getUserApiKeys(userId, 2, 0, null, null, null); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + for (UUID id : createdIds) { + client.deleteApiKey(id); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java new file mode 100644 index 0000000000..070470ac2a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AssetApiClientTest.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.Asset; +import org.thingsboard.client.model.PageDataAsset; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class AssetApiClientTest extends AbstractApiClientTest { + + @Test + public void testAssetLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdAssets = new ArrayList<>(); + + // create 20 assets + for (int i = 0; i < 20; i++) { + Asset asset = new Asset(); + String assetName = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + asset.setName(assetName); + asset.setLabel("Test Asset " + i); + asset.setType(((i % 2 == 0) ? "default" : "building")); + + Asset createdAsset = client.saveAsset(asset, null, null, null); + assertNotNull(createdAsset); + assertNotNull(createdAsset.getId()); + assertEquals(assetName, createdAsset.getName()); + + createdAssets.add(createdAsset); + } + + // find all, check count + PageDataAsset allAssets = client.getTenantAssets(100, 0, null, null, null, null); + + assertNotNull(allAssets); + assertNotNull(allAssets.getData()); + int initialSize = allAssets.getData().size(); + assertEquals("Expected at least 20 assets, but got " + allAssets.getData().size(), 20, initialSize); + + //find all with search text, check count + PageDataAsset allAssetsBySearchText = client.getTenantAssets(100, 0, null, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 test assets", 10, allAssetsBySearchText.getData().size()); + + // find by id + Asset searchAsset = createdAssets.get(10); + Asset asset = client.getAssetById(searchAsset.getId().getId().toString()); + assertEquals(searchAsset.getName(), asset.getName()); + + // delete asset + UUID assetToDeleteId = createdAssets.get(0).getId().getId(); + client.deleteAsset(assetToDeleteId.toString()); + + // Verify the asset is deleted + PageDataAsset assetsAfterDelete = client.getTenantAssets(100, 0, null, null, null, null); + assertEquals(initialSize - 1, assetsAfterDelete.getData().size()); + + assertReturns404(() -> + client.getAssetById(assetToDeleteId.toString()) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java new file mode 100644 index 0000000000..7f6a2f667b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/AssetProfileApiClientTest.java @@ -0,0 +1,135 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.AssetProfile; +import org.thingsboard.client.model.AssetProfileInfo; +import org.thingsboard.client.model.EntityInfo; +import org.thingsboard.client.model.PageDataAssetProfile; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class AssetProfileApiClientTest extends AbstractApiClientTest { + + @Test + public void testAssetProfileLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdProfiles = new ArrayList<>(); + + // Get initial count (there should be a default profile) + PageDataAssetProfile initialProfiles = client.getAssetProfiles(100, 0, null, null, null); + assertNotNull(initialProfiles); + int initialSize = initialProfiles.getData().size(); + assertTrue("Expected at least 1 default asset profile", initialSize == 1); + + // Get default asset profile info + AssetProfileInfo defaultProfileInfo = client.getDefaultAssetProfileInfo(); + assertNotNull(defaultProfileInfo); + assertEquals(defaultProfileInfo.getName(), "default"); + + // Create multiple asset profiles + for (int i = 0; i < 5; i++) { + AssetProfile profile = new AssetProfile(); + profile.setName("Test Asset Profile " + timestamp + "_" + i); + profile.setDescription("Test description " + i); + + AssetProfile created = client.saveAssetProfile(profile); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(profile.getName(), created.getName()); + assertEquals(profile.getDescription(), created.getDescription()); + assertFalse(created.getDefault()); + + createdProfiles.add(created); + } + + // Find all, check count + PageDataAssetProfile allProfiles = client.getAssetProfiles(100, 0, null, null, null); + assertNotNull(allProfiles); + assertEquals(initialSize + 5, allProfiles.getData().size()); + + // Find all with text search + PageDataAssetProfile filteredProfiles = client.getAssetProfiles(100, 0, "Test Asset Profile " + timestamp, null, null); + assertEquals(5, filteredProfiles.getData().size()); + + // Get by id + AssetProfile searchProfile = createdProfiles.get(2); + AssetProfile fetchedProfile = client.getAssetProfileById(searchProfile.getId().getId().toString(), false); + assertEquals(searchProfile.getName(), fetchedProfile.getName()); + assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription()); + + // Update asset profile + fetchedProfile.setDescription("Updated description"); + AssetProfile updatedProfile = client.saveAssetProfile(fetchedProfile); + assertEquals("Updated description", updatedProfile.getDescription()); + assertEquals(fetchedProfile.getName(), updatedProfile.getName()); + + // Get asset profile info by id + AssetProfileInfo profileInfo = client.getAssetProfileInfoById(searchProfile.getId().getId().toString()); + assertNotNull(profileInfo); + assertEquals(searchProfile.getName(), profileInfo.getName()); + + // Get asset profile infos (paginated) + PageDataAssetProfile profileInfos = client.getAssetProfiles(100, 0, null, null, null); + assertNotNull(profileInfos); + assertEquals(initialSize + 5, profileInfos.getData().size()); + + // Set a profile as default + AssetProfile profileToSetDefault = createdProfiles.get(1); + AssetProfile newDefault = client.setDefaultAssetProfile(profileToSetDefault.getId().getId().toString()); + assertNotNull(newDefault); + assertTrue(newDefault.getDefault()); + + // Verify default profile info now points to the new default + AssetProfileInfo newDefaultInfo = client.getDefaultAssetProfileInfo(); + assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName()); + + // Get asset profile names + List profileNames = client.getAssetProfileNames(false); + assertNotNull(profileNames); + assertEquals(createdProfiles.size() + 1, profileNames.size()); + + // Delete asset profile (cannot delete the default one, so delete a non-default one) + UUID profileToDeleteId = createdProfiles.get(0).getId().getId(); + client.deleteAssetProfile(profileToDeleteId.toString()); + + // Verify the profile is deleted + assertReturns404(() -> + client.getAssetProfileById(profileToDeleteId.toString(), false)); + + // Verify count after deletion + PageDataAssetProfile profilesAfterDelete = client.getAssetProfiles(100, 0, null, null, null); + assertEquals(initialSize + 4, profilesAfterDelete.getData().size()); + + // Restore original default profile + AssetProfile originalDefault = initialProfiles.getData().stream() + .filter(AssetProfile::getDefault) + .findFirst() + .orElseThrow(); + client.setDefaultAssetProfile(originalDefault.getId().getId().toString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java new file mode 100644 index 0000000000..7594485c14 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/CalculatedFieldApiClientTest.java @@ -0,0 +1,286 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.AlarmCalculatedFieldConfiguration; +import org.thingsboard.client.model.AlarmConditionValueAlarmSchedule; +import org.thingsboard.client.model.AlarmRule; +import org.thingsboard.client.model.AlarmSeverity; +import org.thingsboard.client.model.Argument; +import org.thingsboard.client.model.ArgumentType; +import org.thingsboard.client.model.CalculatedField; +import org.thingsboard.client.model.CalculatedFieldType; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.PageDataCalculatedField; +import org.thingsboard.client.model.ReferencedEntityKey; +import org.thingsboard.client.model.SimpleAlarmCondition; +import org.thingsboard.client.model.SimpleCalculatedFieldConfiguration; +import org.thingsboard.client.model.SpecificTimeSchedule; +import org.thingsboard.client.model.TbelAlarmConditionExpression; +import org.thingsboard.client.model.TimeSeriesOutput; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class CalculatedFieldApiClientTest extends AbstractApiClientTest { + + @Test + public void testCalculatedFieldLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdFields = new ArrayList<>(); + + // create devices to attach calculated fields to + Device device1 = new Device(); + device1.setName("CalcFieldDevice1_" + timestamp); + device1.setType("default"); + Device createdDevice1 = client.saveDevice(device1, null, null, null, null); + + Device device2 = new Device(); + device2.setName("CalcFieldDevice2_" + timestamp); + device2.setType("default"); + Device createdDevice2 = client.saveDevice(device2, null, null, null, null); + + // create calculated fields on device1 + for (int i = 0; i < 5; i++) { + CalculatedField cf = new CalculatedField(); + cf.setName(TEST_PREFIX + "CalcField_" + timestamp + "_" + i); + cf.setType(CalculatedFieldType.SIMPLE); + + cf.setEntityId(createdDevice1.getId()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument arg = new Argument(); + ReferencedEntityKey refKey = new ReferencedEntityKey(); + refKey.setKey("temperature"); + refKey.setType(ArgumentType.TS_LATEST); + arg.setRefEntityKey(refKey); + config.putArgumentsItem("temp", arg); + + config.setExpression("temp * " + (i + 1)); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("scaledTemp_" + i); + config.setOutput(output); + + cf.setConfiguration(config); + + CalculatedField created = client.saveCalculatedField(cf); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(cf.getName(), created.getName()); + assertEquals(CalculatedFieldType.SIMPLE, created.getType()); + + createdFields.add(created); + } + + // create calculated fields on device2 + for (int i = 0; i < 3; i++) { + CalculatedField cf = new CalculatedField(); + cf.setName(TEST_PREFIX + "CalcField2_" + timestamp + "_" + i); + cf.setType(CalculatedFieldType.SIMPLE); + cf.setEntityId(createdDevice2.getId()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument arg = new Argument(); + ReferencedEntityKey refKey = new ReferencedEntityKey(); + refKey.setKey("humidity"); + refKey.setType(ArgumentType.TS_LATEST); + arg.setRefEntityKey(refKey); + config.putArgumentsItem("hum", arg); + + config.setExpression("hum + " + i); + + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("adjustedHumidity_" + i); + config.setOutput(output); + + cf.setConfiguration(config); + + CalculatedField created = client.saveCalculatedField(cf); + assertNotNull(created); + createdFields.add(created); + } + + // get calculated fields by entity id for device1 + PageDataCalculatedField device1Fields = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), + 100, 0, CalculatedFieldType.SIMPLE, null, null, null); + assertNotNull(device1Fields); + assertEquals(5, device1Fields.getData().size()); + + // get calculated fields by entity id for device2 + PageDataCalculatedField device2Fields = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice2.getId().getId().toString(), + 100, 0, CalculatedFieldType.SIMPLE, null, null, null); + assertEquals(3, device2Fields.getData().size()); + + // get by id + CalculatedField searchField = createdFields.get(2); + CalculatedField fetchedField = client.getCalculatedFieldById(searchField.getId().getId().toString()); + assertEquals(searchField.getName(), fetchedField.getName()); + assertEquals(searchField.getType(), fetchedField.getType()); + assertNotNull(fetchedField.getConfiguration()); + SimpleCalculatedFieldConfiguration fetchedConfig = + (SimpleCalculatedFieldConfiguration) fetchedField.getConfiguration(); + assertEquals("temp * 3", fetchedConfig.getExpression()); + + // update calculated field + fetchedField.setName(fetchedField.getName() + "_updated"); + fetchedConfig.setExpression("temp * 100"); + CalculatedField updatedField = client.saveCalculatedField(fetchedField); + assertEquals(fetchedField.getName(), updatedField.getName()); + SimpleCalculatedFieldConfiguration updatedConfig = + (SimpleCalculatedFieldConfiguration) updatedField.getConfiguration(); + assertEquals("temp * 100", updatedConfig.getExpression()); + + // delete calculated field + UUID fieldToDeleteId = createdFields.get(0).getId().getId(); + client.deleteCalculatedField(fieldToDeleteId.toString()); + + // verify deletion + assertReturns404(() -> + client.getCalculatedFieldById(fieldToDeleteId.toString()) + ); + + PageDataCalculatedField device1FieldsAfterDelete = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice1.getId().getId().toString(), + 100, 0, null, null, null, null); + assertEquals(4, device1FieldsAfterDelete.getData().size()); + } + + @Test + public void testAlarmCalculatedFieldLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create a device to attach the alarm calculated field to + Device device = new Device(); + device.setName("AlarmCalcFieldDevice_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + + // build the alarm calculated field configuration + AlarmCalculatedFieldConfiguration config = new AlarmCalculatedFieldConfiguration(); + + // argument: temperature time-series + Argument tempArg = new Argument(); + ReferencedEntityKey refKey = new ReferencedEntityKey(); + refKey.setKey("temperature"); + refKey.setType(ArgumentType.TS_LATEST); + tempArg.setRefEntityKey(refKey); + config.putArgumentsItem("temp", tempArg); + + // create rule: HIGH_TEMPERATURE when temp > 50 (TBEL expression) + TbelAlarmConditionExpression createExpression = new TbelAlarmConditionExpression(); + createExpression.setExpression("return temp > 50;"); + SimpleAlarmCondition createCondition = new SimpleAlarmCondition(); + createCondition.setExpression(createExpression); + SpecificTimeSchedule specificTimeSchedule = new SpecificTimeSchedule().addDaysOfWeekItem(3); + AlarmConditionValueAlarmSchedule schedule = new AlarmConditionValueAlarmSchedule().staticValue(specificTimeSchedule); + createCondition.setSchedule(schedule); + AlarmRule createRule = new AlarmRule(); + createRule.setCondition(createCondition); + createRule.setAlarmDetails("Temperature is too high: ${temp}"); + config.setCreateRules(Map.of( + AlarmSeverity.CRITICAL.name(), createRule + )); + + // clear rule: when temp drops below 30 + TbelAlarmConditionExpression clearExpression = new TbelAlarmConditionExpression(); + clearExpression.setExpression("return temp < 30;"); + SimpleAlarmCondition clearCondition = new SimpleAlarmCondition(); + clearCondition.setExpression(clearExpression); + AlarmRule clearRule = new AlarmRule(); + clearRule.setCondition(clearCondition); + config.setClearRule(clearRule); + + config.setPropagate(true); + config.setPropagateToOwner(false); + + // create calculated field + CalculatedField cf = new CalculatedField(); + cf.setName(TEST_PREFIX + "AlarmCalcField_" + timestamp); + cf.setType(CalculatedFieldType.ALARM); + + cf.setEntityId(createdDevice.getId()); + cf.setConfiguration(config); + + CalculatedField created = client.saveCalculatedField(cf); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(cf.getName(), created.getName()); + assertEquals(CalculatedFieldType.ALARM, created.getType()); + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) created.getConfiguration(); + AlarmConditionValueAlarmSchedule createdSchedule = configuration.getCreateRules().get(AlarmSeverity.CRITICAL.name()).getCondition().getSchedule(); + SpecificTimeSchedule staticSchedule = (SpecificTimeSchedule) createdSchedule.getStaticValue(); + assertEquals(Set.of(3), staticSchedule.getDaysOfWeek()); + + // get by id and verify configuration + CalculatedField fetched = client.getCalculatedFieldById(created.getId().getId().toString()); + assertNotNull(fetched); + assertEquals(created.getName(), fetched.getName()); + assertEquals(CalculatedFieldType.ALARM, fetched.getType()); + assertNotNull(fetched.getConfiguration()); + AlarmCalculatedFieldConfiguration fetchedConfig = + (AlarmCalculatedFieldConfiguration) fetched.getConfiguration(); + assertNotNull(fetchedConfig.getCreateRules()); + assertEquals(1, fetchedConfig.getCreateRules().size()); + assertTrue(fetchedConfig.getCreateRules().containsKey("CRITICAL")); + assertNotNull(fetchedConfig.getClearRule()); + assertEquals(Boolean.TRUE, fetchedConfig.getPropagate()); + + // update: add a second create rule for CRITICAL_TEMPERATURE + TbelAlarmConditionExpression criticalExpression = new TbelAlarmConditionExpression(); + criticalExpression.setExpression("return temp > 80;"); + SimpleAlarmCondition criticalCondition = new SimpleAlarmCondition(); + criticalCondition.setExpression(criticalExpression); + AlarmRule criticalRule = new AlarmRule(); + criticalRule.setCondition(criticalCondition); + fetchedConfig.putCreateRulesItem(AlarmSeverity.INDETERMINATE.name(), criticalRule); + fetched.setConfiguration(fetchedConfig); + + CalculatedField updated = client.saveCalculatedField(fetched); + AlarmCalculatedFieldConfiguration updatedConfig = + (AlarmCalculatedFieldConfiguration) updated.getConfiguration(); + assertEquals(2, updatedConfig.getCreateRules().size()); + assertTrue(updatedConfig.getCreateRules().containsKey("INDETERMINATE")); + + // filter by entity and ALARM type + PageDataCalculatedField deviceFields = client.getCalculatedFieldsByEntityId( + EntityType.DEVICE.toString(), createdDevice.getId().getId().toString(), + 100, 0, CalculatedFieldType.ALARM, null, null, null); + assertNotNull(deviceFields); + assertEquals(1, deviceFields.getData().size()); + + // delete and verify + UUID fieldId = created.getId().getId(); + client.deleteCalculatedField(fieldId.toString()); + assertReturns404(() -> client.getCalculatedFieldById(fieldId.toString())); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java new file mode 100644 index 0000000000..fff3670c85 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/CustomerApiClientTest.java @@ -0,0 +1,113 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.Customer; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.PageDataCustomer; +import org.thingsboard.client.model.PageDataDevice; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class CustomerApiClientTest extends AbstractApiClientTest { + + @Test + public void testCustomerLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdCustomers = new ArrayList<>(); + + // create 20 customers + for (int i = 0; i < 20; i++) { + Customer customer = new Customer(); + String customerTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + customer.setTitle(customerTitle); + customer.setEmail("customer_" + timestamp + "_" + i + "@test.com"); + + Customer createdCustomer = client.saveCustomer(customer, null, null, null); + assertNotNull(createdCustomer); + assertNotNull(createdCustomer.getId()); + assertEquals(customerTitle, createdCustomer.getTitle()); + + createdCustomers.add(createdCustomer); + } + + // find all, check count (includes savedClientCustomer from AbstractApiClientTest setup) + PageDataCustomer allCustomers = client.getCustomers(100, 0, null, null, null); + assertNotNull(allCustomers); + assertNotNull(allCustomers.getData()); + int initialSize = allCustomers.getData().size(); + assertEquals("Expected 21 customers (20 created + 1 from setup), but got " + initialSize, 21, initialSize); + + // find all with search text, check count + PageDataCustomer filteredCustomers = client.getCustomers(100, 0, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 customers matching prefix", 10, filteredCustomers.getData().size()); + + // find by id + Customer searchCustomer = createdCustomers.get(10); + Customer fetchedCustomer = client.getCustomerById(searchCustomer.getId().getId().toString()); + assertEquals(searchCustomer.getTitle(), fetchedCustomer.getTitle()); + + // find by title + Customer fetchedByTitle = client.getTenantCustomer(searchCustomer.getTitle()); + assertEquals(searchCustomer.getId().getId(), fetchedByTitle.getId().getId()); + + // update customer + fetchedCustomer.setCity("New York"); + fetchedCustomer.setCountry("US"); + Customer updatedCustomer = client.saveCustomer(fetchedCustomer, null, null, null); + assertEquals("New York", updatedCustomer.getCity()); + assertEquals("US", updatedCustomer.getCountry()); + + // assign device to customer and verify + Device device = new Device(); + device.setName("CustomerTestDevice_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + + String customerId = createdCustomers.get(0).getId().getId().toString(); + client.assignDeviceToCustomer(customerId, createdDevice.getId().getId().toString()); + + PageDataDevice customerDevices = client.getCustomerDevices(customerId, 100, 0, null, null, null, null); + assertEquals(1, customerDevices.getData().size()); + assertEquals(createdDevice.getName(), customerDevices.getData().get(0).getName()); + + // unassign device from customer + client.unassignDeviceFromCustomer(createdDevice.getId().getId().toString()); + PageDataDevice devicesAfterUnassign = client.getCustomerDevices(customerId, 100, 0, null, null, null, null); + assertEquals(0, devicesAfterUnassign.getData().size()); + + // delete customer + UUID customerToDeleteId = createdCustomers.get(0).getId().getId(); + client.deleteCustomer(customerToDeleteId.toString()); + + // verify deletion + PageDataCustomer customersAfterDelete = client.getCustomers(100, 0, null, null, null); + assertEquals(initialSize - 1, customersAfterDelete.getData().size()); + + assertReturns404(() -> + client.getCustomerById(customerToDeleteId.toString()) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java new file mode 100644 index 0000000000..386e2db84f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DashboardApiClientTest.java @@ -0,0 +1,123 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.Dashboard; +import org.thingsboard.client.model.DashboardInfo; +import org.thingsboard.client.model.PageDataDashboardInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class DashboardApiClientTest extends AbstractApiClientTest { + + @Test + public void testDashboardLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create 20 dashboards + for (int i = 0; i < 20; i++) { + Dashboard dashboard = new Dashboard(); + String dashboardTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + dashboard.setTitle(dashboardTitle); + + client.saveDashboard(dashboard, null); + } + + // find all, check count + PageDataDashboardInfo allDashboards = client.getTenantDashboards(100, 0, null, null, null, null); + assertNotNull(allDashboards); + assertNotNull(allDashboards.getData()); + int initialSize = allDashboards.getData().size(); + assertEquals("Expected 20 dashboards, but got " + initialSize, 20, initialSize); + + List createdDashboards = allDashboards.getData(); + + // find all with search text, check count + PageDataDashboardInfo filteredDashboards = client.getTenantDashboards(100, 0, null, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 dashboards matching prefix", 10, filteredDashboards.getData().size()); + + // find by id + DashboardInfo searchDashboard = createdDashboards.get(10); + DashboardInfo fetchedDashboard = client.getDashboardInfoById(searchDashboard.getId().getId().toString()); + assertEquals(searchDashboard.getTitle(), fetchedDashboard.getTitle()); + + // update dashboard + Dashboard dashboardToUpdate = new Dashboard(); + dashboardToUpdate.setId(fetchedDashboard.getId()); + dashboardToUpdate.setTitle(fetchedDashboard.getTitle() + "_updated"); + dashboardToUpdate.setVersion(fetchedDashboard.getVersion()); + client.saveDashboard(dashboardToUpdate, null); + + DashboardInfo updatedDashboard = client.getDashboardInfoById(fetchedDashboard.getId().getId().toString()); + assertEquals(fetchedDashboard.getTitle() + "_updated", updatedDashboard.getTitle()); + + // assign dashboard to customer and verify + String customerId = savedClientCustomer.getId().getId().toString(); + String dashboardId = createdDashboards.get(0).getId().getId().toString(); + client.assignDashboardToCustomer(customerId, dashboardId); + + PageDataDashboardInfo customerDashboards = client.getCustomerDashboards(customerId, 100, 0, null, null, null, null); + assertEquals(1, customerDashboards.getData().size()); + assertEquals(createdDashboards.get(0).getTitle(), customerDashboards.getData().get(0).getTitle()); + + // unassign dashboard from customer + client.unassignDashboardFromCustomer(customerId, dashboardId); + PageDataDashboardInfo dashboardsAfterUnassign = client.getCustomerDashboards(customerId, 100, 0, null, null, null, null); + assertEquals(0, dashboardsAfterUnassign.getData().size()); + + // make dashboard public and verify + client.assignDashboardToPublicCustomer(dashboardId); + DashboardInfo publicDashboard = client.getDashboardInfoById(dashboardId); + assertNotNull(publicDashboard.getAssignedCustomers()); + assertTrue(publicDashboard.getAssignedCustomers().size() > 0); + + // remove public access + client.unassignDashboardFromPublicCustomer(dashboardId); + + // delete dashboard + UUID dashboardToDeleteId = createdDashboards.get(0).getId().getId(); + client.deleteDashboard(dashboardToDeleteId.toString()); + + // verify deletion + PageDataDashboardInfo dashboardsAfterDelete = client.getTenantDashboards(100, 0, null, null, null, null); + assertEquals(initialSize - 1, dashboardsAfterDelete.getData().size()); + + assertReturns404(() -> + client.getDashboardInfoById(dashboardToDeleteId.toString()) + ); + } + + @Test + public void testGetServerTime() throws Exception { + Long serverTime = client.getServerTime(); + assertNotNull(serverTime); + } + + @Test + public void testGetMaxDatapointsLimit() throws Exception { + Long maxDatapointsLimit = client.getMaxDatapointsLimit(); + assertNotNull(maxDatapointsLimit); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java new file mode 100644 index 0000000000..f3695b9fc1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DeviceApiClientTest.java @@ -0,0 +1,114 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.DeviceCredentials; +import org.thingsboard.client.model.DeviceCredentialsType; +import org.thingsboard.client.model.PageDataDevice; +import org.thingsboard.client.model.SaveDeviceWithCredentialsRequest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class DeviceApiClientTest extends AbstractApiClientTest { + + @Test + public void testDeviceLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdDevices = new ArrayList<>(); + + // create 20 devices + for (int i = 0; i < 20; i++) { + Device device = new Device(); + String deviceName = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + device.setName(deviceName); + device.setLabel("Test Device " + i); + device.setType(((i % 2 == 0) ? "default" : "thermostat")); + + Device createdDevice = client.saveDevice(device, null, null, null, null); + assertNotNull(createdDevice); + assertNotNull(createdDevice.getId()); + assertEquals(deviceName, createdDevice.getName()); + + createdDevices.add(createdDevice); + } + + // find all, check count + PageDataDevice allDevices = client.getTenantDevices(100, 0, null, null, null, null); + + assertNotNull(allDevices); + assertNotNull(allDevices.getData()); + int initialSize = allDevices.getData().size(); + assertEquals("Expected at least 20 devices, but got " + allDevices.getData().size(), 20, initialSize); + + //find all with search text, check count + PageDataDevice allDevicesBySearchText = client.getTenantDevices(10, 0, null, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 test devices", 10, allDevicesBySearchText.getData().size()); + + // find by id + Device searchDevice = createdDevices.get(10); + Device device = client.getDeviceById(searchDevice.getId().getId().toString()); + assertEquals(searchDevice.getName(), device.getName()); + + // create device with credentials + Device deviceWithCreds = new Device(); + deviceWithCreds.setName("device-with-creds"); + + DeviceCredentials creds = new DeviceCredentials(); + creds.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + creds.setCredentialsId("TEST_ACCESS_TOKEN"); + + SaveDeviceWithCredentialsRequest request = new SaveDeviceWithCredentialsRequest(); + request.setDevice(deviceWithCreds); + request.setCredentials(creds); + + Device savedDeviceWithCreds = client.saveDeviceWithCredentials(request, null, null, null); + assertEquals("device-with-creds", savedDeviceWithCreds.getName()); + + // find credentials by device id + DeviceCredentials fetchedCreds = client.getDeviceCredentialsByDeviceId(savedDeviceWithCreds.getId().getId().toString()); + assertEquals(creds.getCredentialsId(), fetchedCreds.getCredentialsId()); + + // delete device + UUID deviceToDeleteId = createdDevices.get(0).getId().getId(); + client.deleteDevice(deviceToDeleteId.toString()); + + // Verify the device is deleted + PageDataDevice devicesAfterDelete = client.getTenantDevices(100, 0, null, null, null, null); + assertEquals(initialSize, devicesAfterDelete.getData().size()); + + assertReturns404(() -> + client.getDeviceById(deviceToDeleteId.toString())); + + // assign device to customer + client.assignDeviceToCustomer(savedClientCustomer.getId().getId().toString(), savedDeviceWithCreds.getId().getId().toString()); + + // check customer devices + PageDataDevice pageDataDevice = client.getCustomerDevices(savedClientCustomer.getId().getId().toString(), 100, 0, null, null, null, null); + List data = pageDataDevice.getData(); + assertEquals(1, data.size()); + assertEquals(savedDeviceWithCreds.getName(), data.get(0).getName()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java new file mode 100644 index 0000000000..d8204cab41 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DeviceConnectivityApiClientTest.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2026 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.client; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Test; +import org.thingsboard.client.model.Device; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; + +@DaoSqlTest +public class DeviceConnectivityApiClientTest extends AbstractApiClientTest { + + @Test + public void testGetDevicePublishTelemetryCommands() throws Exception { + Device device = new Device(); + device.setName(TEST_PREFIX + System.currentTimeMillis()); + device.setType("default"); + + Device savedDevice = client.saveDevice(device, null, null, null, null); + String token = client.getDeviceCredentialsByDeviceId(savedDevice.getId().getId().toString()).getCredentialsId(); + + String deviceId = savedDevice.getId().getId().toString(); + + JsonNode commands = client.getDevicePublishTelemetryCommands(deviceId); + assertEquals("curl -v -X POST http://localhost:8080/api/v1/" + token + "/telemetry --header Content-Type:application/json --data \"{temperature:25}\"", commands.get("http").get("http").asText()); + assertEquals("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry -u \"" + token + "\" -m \"{temperature:25}\"", commands.get("mqtt").get("mqtt").asText()); + assertEquals("coap-client -v 6 -m POST -t \"application/json\" -e \"{temperature:25}\" coap://localhost:5683/api/v1/" + token + "/telemetry", commands.get("coap").get("coap").asText()); + } + + @Test + public void testGetDevicePublishTelemetryCommands_nonExistentDevice() { + String nonExistentId = UUID.randomUUID().toString(); + assertReturns404(() -> client.getDevicePublishTelemetryCommands(nonExistentId)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java new file mode 100644 index 0000000000..e8fcb4ffd1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DeviceProfileApiClientTest.java @@ -0,0 +1,157 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.DefaultDeviceProfileConfiguration; +import org.thingsboard.client.model.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.client.model.DeviceProfile; +import org.thingsboard.client.model.DeviceProfileData; +import org.thingsboard.client.model.DeviceProfileInfo; +import org.thingsboard.client.model.DeviceProfileType; +import org.thingsboard.client.model.DeviceTransportType; +import org.thingsboard.client.model.EntityInfo; +import org.thingsboard.client.model.PageDataDeviceProfile; +import org.thingsboard.client.model.PageDataDeviceProfileInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class DeviceProfileApiClientTest extends AbstractApiClientTest { + + @Test + public void testDeviceProfileLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdProfiles = new ArrayList<>(); + + // Get initial count (there should be a default profile) + PageDataDeviceProfile initialProfiles = client.getDeviceProfiles(100, 0, null, null, null); + assertNotNull(initialProfiles); + int initialSize = initialProfiles.getData().size(); + assertTrue("Expected at least 1 default device profile", initialSize >= 1); + + // Get default device profile info + DeviceProfileInfo defaultProfileInfo = client.getDefaultDeviceProfileInfo(); + assertNotNull(defaultProfileInfo); + assertNotNull(defaultProfileInfo.getName()); + + // Create multiple device profiles + for (int i = 0; i < 5; i++) { + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setName("Test Device Profile " + timestamp + "_" + i); + deviceProfile.setDescription("Test description " + i); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + configuration.setType(DeviceProfileType.DEFAULT.getValue()); + deviceProfileData.setConfiguration(configuration); + DefaultDeviceProfileTransportConfiguration transportConf = new DefaultDeviceProfileTransportConfiguration(); + transportConf.setType(DeviceTransportType.DEFAULT.getValue()); + deviceProfileData.setTransportConfiguration(transportConf); + deviceProfile.setProfileData(deviceProfileData); + deviceProfile.setDefault(false); + deviceProfile.setDefaultRuleChainId(null); + + DeviceProfile created = client.saveDeviceProfile(deviceProfile); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(deviceProfile.getName(), created.getName()); + assertEquals(deviceProfile.getDescription(), created.getDescription()); + assertEquals(DeviceProfileType.DEFAULT, created.getType()); + assertEquals(DeviceTransportType.DEFAULT, created.getTransportType()); + assertFalse(created.getDefault()); + + createdProfiles.add(created); + } + + // Find all, check count + PageDataDeviceProfile allProfiles = client.getDeviceProfiles(100, 0, null, null, null); + assertNotNull(allProfiles); + assertEquals(initialSize + 5, allProfiles.getData().size()); + + // Find all with text search + PageDataDeviceProfile filteredProfiles = client.getDeviceProfiles(100, 0, "Test Device Profile " + timestamp, null, null); + assertEquals(5, filteredProfiles.getData().size()); + + // Get by id + DeviceProfile searchProfile = createdProfiles.get(2); + DeviceProfile fetchedProfile = client.getDeviceProfileById(searchProfile.getId().getId().toString(), false); + assertEquals(searchProfile.getName(), fetchedProfile.getName()); + assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription()); + + // Update device profile + fetchedProfile.setDescription("Updated description"); + DeviceProfile updatedProfile = client.saveDeviceProfile(fetchedProfile); + assertEquals("Updated description", updatedProfile.getDescription()); + assertEquals(fetchedProfile.getName(), updatedProfile.getName()); + + // Get device profile info by id + DeviceProfileInfo profileInfo = client.getDefaultDeviceProfileInfo(); + assertNotNull(profileInfo); + assertEquals(searchProfile.getType().getValue().toLowerCase(), profileInfo.getName()); + assertEquals(DeviceTransportType.DEFAULT, profileInfo.getTransportType()); + + // Get device profile infos (paginated) + PageDataDeviceProfileInfo profileInfos = client.getDeviceProfileInfos(100, 0, null, null, null, null); + assertNotNull(profileInfos); + assertEquals(initialSize + 5, profileInfos.getData().size()); + + // Set a profile as default + DeviceProfile profileToSetDefault = createdProfiles.get(1); + DeviceProfile newDefault = client.setDefaultDeviceProfile(profileToSetDefault.getId().getId().toString()); + assertNotNull(newDefault); + assertTrue(newDefault.getDefault()); + + // Verify default profile info now points to the new default + DeviceProfileInfo newDefaultInfo = client.getDefaultDeviceProfileInfo(); + assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName()); + + // Get device profile names + List profileNames = client.getDeviceProfileNames(false); + assertNotNull(profileNames); + assertEquals(createdProfiles.size() + 1, profileNames.size()); + + // Delete device profile (cannot delete the default one, so delete a non-default one) + UUID profileToDeleteId = createdProfiles.get(0).getId().getId(); + client.deleteDeviceProfile(profileToDeleteId.toString()); + + // Verify the profile is deleted + assertReturns404(() -> + client.getDeviceProfileById(profileToDeleteId.toString(), false)); + + // Verify count after deletion + PageDataDeviceProfile profilesAfterDelete = client.getDeviceProfiles(100, 0, null, null, null); + assertEquals(initialSize + 4, profilesAfterDelete.getData().size()); + + // Restore original default profile + DeviceProfile originalDefault = initialProfiles.getData().stream() + .filter(DeviceProfile::getDefault) + .findFirst() + .orElseThrow(); + client.setDefaultDeviceProfile(originalDefault.getId().getId().toString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.java new file mode 100644 index 0000000000..bd10a3deca --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/DomainApiClientTest.java @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.After; +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Domain; +import org.thingsboard.client.model.DomainInfo; +import org.thingsboard.client.model.PageDataDomainInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class DomainApiClientTest extends AbstractApiClientTest { + + List createdDomains = new ArrayList<>(); + + @After + public void afterDomainTest() { + createdDomains.forEach(domain -> { + try { + client.deleteDomain(domain.getId().getId()); + } catch (ApiException e) { + // ignore + } + }); + } + + @Test + public void testDomainLifecycle() throws Exception { + client.login("sysadmin@thingsboard.org", "sysadmin"); + + long timestamp = System.currentTimeMillis(); + + // create 5 domains + for (int i = 0; i < 5; i++) { + Domain domain = new Domain(); + domain.setName("domain." + i + ".com"); + domain.setOauth2Enabled(false); + domain.setPropagateToEdge(false); + + Domain created = client.saveDomain(domain, null); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(domain.getName(), created.getName()); + assertEquals(false, created.getOauth2Enabled()); + + createdDomains.add(created); + } + + // list tenant domains with text search + PageDataDomainInfo filteredDomains = client.getTenantDomainInfos(100, 0, + "domain.", null, null); + assertNotNull(filteredDomains); + assertEquals(5, filteredDomains.getData().size()); + + // get domain info by id + Domain searchDomain = createdDomains.get(2); + DomainInfo fetchedInfo = client.getDomainInfoById(searchDomain.getId().getId()); + assertEquals(searchDomain.getName(), fetchedInfo.getName()); + assertEquals(searchDomain.getOauth2Enabled(), fetchedInfo.getOauth2Enabled()); + assertNotNull(fetchedInfo.getOauth2ClientInfos()); + + // update domain + Domain domainToUpdate = createdDomains.get(3); + domainToUpdate.setPropagateToEdge(true); + Domain updatedDomain = client.saveDomain(domainToUpdate, null); + assertEquals(true, updatedDomain.getPropagateToEdge()); + + // delete domain + UUID domainToDeleteId = createdDomains.get(0).getId().getId(); + createdDomains.remove(0); + client.deleteDomain(domainToDeleteId); + + // verify deletion + assertReturns404(() -> + client.getDomainInfoById(domainToDeleteId) + ); + + PageDataDomainInfo domainsAfterDelete = client.getTenantDomainInfos(100, 0, + "domain.", null, null); + assertEquals(4, domainsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java new file mode 100644 index 0000000000..6aeb7f0ef6 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EdgeApiClientTest.java @@ -0,0 +1,141 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.Edge; +import org.thingsboard.client.model.EdgeInfo; +import org.thingsboard.client.model.PageDataEdge; +import org.thingsboard.client.model.PageDataEdgeInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class EdgeApiClientTest extends AbstractApiClientTest { + + @Test + public void testEdgeLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdEdges = new ArrayList<>(); + + // create 5 edges + for (int i = 0; i < 5; i++) { + Edge edge = new Edge(); + edge.setName(TEST_PREFIX + "Edge_" + timestamp + "_" + i); + edge.setType("gateway"); + edge.setLabel("Test Edge " + i); + edge.setRoutingKey("routing_key_" + timestamp + "_" + i); + edge.setSecret("secret_key_" + timestamp + "_" + i); + + Edge created = client.saveEdge(edge); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(edge.getName(), created.getName()); + assertEquals("gateway", created.getType()); + assertNotNull(created.getRoutingKey()); + assertNotNull(created.getSecret()); + + createdEdges.add(created); + } + + // list tenant edges with text search + PageDataEdge filteredEdges = client.getTenantEdges(100, 0, null, + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertNotNull(filteredEdges); + assertEquals(5, filteredEdges.getData().size()); + + // list tenant edges with type filter + PageDataEdge typedEdges = client.getTenantEdges(100, 0, "gateway", + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(5, typedEdges.getData().size()); + + // get tenant edge infos + PageDataEdgeInfo edgeInfos = client.getTenantEdgeInfos(100, 0, null, + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(5, edgeInfos.getData().size()); + + // get edge by id + Edge searchEdge = createdEdges.get(2); + Edge fetchedEdge = client.getEdgeById(searchEdge.getId().getId().toString()); + assertEquals(searchEdge.getName(), fetchedEdge.getName()); + assertEquals(searchEdge.getType(), fetchedEdge.getType()); + assertEquals(searchEdge.getRoutingKey(), fetchedEdge.getRoutingKey()); + + // get edge by name + Edge fetchedByName = client.getTenantEdgeByName(searchEdge.getName()); + assertEquals(searchEdge.getId().getId(), fetchedByName.getId().getId()); + + // get edges by list of ids + List idsToFetch = List.of( + createdEdges.get(0).getId().getId().toString(), + createdEdges.get(1).getId().getId().toString() + ); + List edgeList = client.getEdgeList(idsToFetch); + assertEquals(2, edgeList.size()); + + // update edge + Edge edgeToUpdate = createdEdges.get(3); + edgeToUpdate.setLabel("Updated Label"); + Edge updatedEdge = client.saveEdge(edgeToUpdate); + assertEquals("Updated Label", updatedEdge.getLabel()); + + // assign edge to customer + String customerId = savedClientCustomer.getId().getId().toString(); + String edgeId = createdEdges.get(1).getId().getId().toString(); + Edge assignedEdge = client.assignEdgeToCustomer(customerId, edgeId); + assertNotNull(assignedEdge.getCustomerId()); + + // get customer edges + PageDataEdge customerEdges = client.getCustomerEdges(customerId, 100, 0, + null, TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(1, customerEdges.getData().size()); + + // get customer edge infos + PageDataEdgeInfo customerEdgeInfos = client.getCustomerEdgeInfos(customerId, 100, 0, + null, TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(1, customerEdgeInfos.getData().size()); + EdgeInfo edgeInfo = customerEdgeInfos.getData().get(0); + assertNotNull(edgeInfo.getCustomerTitle()); + + // unassign edge from customer + Edge unassignedEdge = client.unassignEdgeFromCustomer(edgeId); + assertNotNull(unassignedEdge); + + PageDataEdge customerEdgesAfter = client.getCustomerEdges(customerId, 100, 0, + null, TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(0, customerEdgesAfter.getData().size()); + + // delete edge + UUID edgeToDeleteId = createdEdges.get(0).getId().getId(); + client.deleteEdge(edgeToDeleteId.toString()); + + // verify deletion + assertReturns404(() -> + client.getEdgeById(edgeToDeleteId.toString()) + ); + + PageDataEdge edgesAfterDelete = client.getTenantEdges(100, 0, null, + TEST_PREFIX + "Edge_" + timestamp, null, null); + assertEquals(4, edgesAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java new file mode 100644 index 0000000000..2571306eb6 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EntityQueryApiClientTest.java @@ -0,0 +1,289 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.AliasEntityId; +import org.thingsboard.client.model.Asset; +import org.thingsboard.client.model.AssetTypeFilter; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.DeviceTypeFilter; +import org.thingsboard.client.model.Direction; +import org.thingsboard.client.model.EntityData; +import org.thingsboard.client.model.EntityDataPageLink; +import org.thingsboard.client.model.EntityDataQuery; +import org.thingsboard.client.model.EntityDataSortOrder; +import org.thingsboard.client.model.EntityKey; +import org.thingsboard.client.model.EntityKeyType; +import org.thingsboard.client.model.EntityKeyValueType; +import org.thingsboard.client.model.EntityListFilter; +import org.thingsboard.client.model.EntityNameFilter; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.FilterPredicateValueString; +import org.thingsboard.client.model.KeyFilter; +import org.thingsboard.client.model.PageDataEntityData; +import org.thingsboard.client.model.SingleEntityFilter; +import org.thingsboard.client.model.StringFilterPredicate; +import org.thingsboard.client.model.StringOperation; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class EntityQueryApiClientTest extends AbstractApiClientTest { + + private static final String QUERY_TEST_PREFIX = "QueryTest_"; + + private EntityDataPageLink pageLink(int pageSize) { + return new EntityDataPageLink() + .pageSize(pageSize) + .page(0) + .sortOrder(new EntityDataSortOrder() + .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")) + .direction(Direction.ASC)); + } + + @Test + public void testFindByDeviceTypeFilter() throws Exception { + long ts = System.currentTimeMillis(); + String type1 = "temperatureSensor"; + String type2 = "humiditySensor"; + + for (int i = 0; i < 3; i++) { + Device d = new Device(); + d.setName(QUERY_TEST_PREFIX + "temp_" + ts + "_" + i); + d.setType(type1); + client.saveDevice(d, null, null, null, null); + } + for (int i = 0; i < 2; i++) { + Device d = new Device(); + d.setName(QUERY_TEST_PREFIX + "hum_" + ts + "_" + i); + d.setType(type2); + client.saveDevice(d, null, null, null, null); + } + + // filter by single device type + EntityDataQuery singleTypeQuery = new EntityDataQuery() + .entityFilter(new DeviceTypeFilter() + .deviceTypes(List.of(type1))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(singleTypeQuery); + assertNotNull(result); + assertEquals(3, result.getTotalElements().intValue()); + for (EntityData entity : result.getData()) { + assertNotNull(entity.getEntityId()); + } + + // filter by multiple device types + EntityDataQuery multiTypeQuery = new EntityDataQuery() + .entityFilter(new DeviceTypeFilter() + .deviceTypes(List.of(type1, type2))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData multiResult = client.findEntityDataByQuery(multiTypeQuery); + assertNotNull(multiResult); + assertEquals(5, multiResult.getTotalElements().intValue()); + + // filter by device type + name filter + EntityDataQuery nameFilterQuery = new EntityDataQuery() + .entityFilter(new DeviceTypeFilter() + .deviceTypes(List.of(type1, type2)) + .deviceNameFilter(QUERY_TEST_PREFIX + "temp_" + ts)) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData nameResult = client.findEntityDataByQuery(nameFilterQuery); + assertNotNull(nameResult); + assertEquals(3, nameResult.getTotalElements().intValue()); + } + + @Test + public void testFindByEntityNameFilter() throws Exception { + long ts = System.currentTimeMillis(); + String prefix = QUERY_TEST_PREFIX + "named_" + ts; + + for (int i = 0; i < 4; i++) { + Device d = new Device(); + d.setName(prefix + "_" + i); + d.setType("default"); + client.saveDevice(d, null, null, null, null); + } + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityNameFilter() + .entityType(EntityType.DEVICE) + .entityNameFilter(prefix)) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(4, result.getTotalElements().intValue()); + assertFalse(result.getHasNext()); + } + + @Test + public void testFindByEntityListFilter() throws Exception { + long ts = System.currentTimeMillis(); + + Device d1 = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_1").type("default"), null, null, null, null); + Device d2 = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_2").type("default"), null, null, null, null); + client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "list_" + ts + "_3").type("default"), null, null, null, null); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityListFilter() + .entityType(EntityType.DEVICE) + .entityList(List.of( + d1.getId().getId().toString(), + d2.getId().getId().toString()))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(2, result.getTotalElements().intValue()); + + List returnedIds = result.getData().stream() + .map(e -> e.getEntityId().getId().toString()) + .collect(Collectors.toList()); + assertTrue(returnedIds.contains(d1.getId().getId().toString())); + assertTrue(returnedIds.contains(d2.getId().getId().toString())); + } + + @Test + public void testFindBySingleEntityFilter() throws Exception { + long ts = System.currentTimeMillis(); + Device device = client.saveDevice(new Device().name(QUERY_TEST_PREFIX + "single_" + ts).type("default"), null, null, null, null); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new SingleEntityFilter() + .singleEntity(new AliasEntityId() + .id(device.getId().getId()) + .entityType(EntityType.DEVICE))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(1, result.getTotalElements().intValue()); + assertEquals(device.getId().getId().toString(), + result.getData().get(0).getEntityId().getId().toString()); + } + + @Test + public void testFindByAssetTypeFilter() throws Exception { + long ts = System.currentTimeMillis(); + String assetType = "building"; + + for (int i = 0; i < 3; i++) { + Asset a = new Asset(); + a.setName(QUERY_TEST_PREFIX + "asset_" + ts + "_" + i); + a.setType(assetType); + client.saveAsset(a, null, null, null); + } + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new AssetTypeFilter() + .assetTypes(List.of(assetType))) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(3, result.getTotalElements().intValue()); + } + + @Test + public void testFindWithKeyFilter() throws Exception { + long ts = System.currentTimeMillis(); + String matchName = QUERY_TEST_PREFIX + "kf_match_" + ts; + String noMatchName = QUERY_TEST_PREFIX + "kf_other_" + ts; + + client.saveDevice(new Device().name(matchName).type("default"), null, null, null, null); + client.saveDevice(new Device().name(noMatchName).type("default"), null, null, null, null); + + KeyFilter nameKeyFilter = new KeyFilter() + .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")) + .valueType(EntityKeyValueType.STRING) + .predicate(new StringFilterPredicate() + .operation(StringOperation.CONTAINS) + .value(new FilterPredicateValueString().defaultValue("kf_match")) + .ignoreCase(true)); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityNameFilter() + .entityType(EntityType.DEVICE) + .entityNameFilter(QUERY_TEST_PREFIX + "kf_")) + .addKeyFiltersItem(nameKeyFilter) + .pageLink(pageLink(10)) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + PageDataEntityData result = client.findEntityDataByQuery(query); + assertNotNull(result); + assertEquals(1, result.getTotalElements().intValue()); + } + + @Test + public void testFindWithPagination() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 5; i++) { + Device d = new Device(); + d.setName(QUERY_TEST_PREFIX + "page_" + ts + "_" + i); + d.setType("default"); + client.saveDevice(d, null, null, null, null); + } + + EntityDataPageLink smallPage = new EntityDataPageLink() + .pageSize(2) + .page(0) + .sortOrder(new EntityDataSortOrder() + .key(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")) + .direction(Direction.ASC)); + + EntityDataQuery query = new EntityDataQuery() + .entityFilter(new EntityNameFilter() + .entityType(EntityType.DEVICE) + .entityNameFilter(QUERY_TEST_PREFIX + "page_" + ts)) + .pageLink(smallPage) + .addEntityFieldsItem(new EntityKey().type(EntityKeyType.ENTITY_FIELD).key("name")); + + // first page + PageDataEntityData page1 = client.findEntityDataByQuery(query); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + // last page + smallPage.setPage(2); + PageDataEntityData lastPage = client.findEntityDataByQuery(query); + assertNotNull(lastPage); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java new file mode 100644 index 0000000000..1792ba9ff1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EntityRelationApiClientTest.java @@ -0,0 +1,182 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.Asset; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntityRelation; +import org.thingsboard.client.model.EntityRelationInfo; +import org.thingsboard.client.model.EntityRelationsQuery; +import org.thingsboard.client.model.EntitySearchDirection; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.RelationEntityTypeFilter; +import org.thingsboard.client.model.RelationTypeGroup; +import org.thingsboard.client.model.RelationsSearchParameters; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class EntityRelationApiClientTest extends AbstractApiClientTest { + + @Test + public void testEntityRelationLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create assets and devices to relate + Asset building = new Asset(); + building.setName(TEST_PREFIX + "Building_" + timestamp); + building.setType("building"); + building = client.saveAsset(building, null, null, null); + + Asset floor = new Asset(); + floor.setName(TEST_PREFIX + "Floor_" + timestamp); + floor.setType("floor"); + floor = client.saveAsset(floor, null, null, null); + + Device device1 = new Device(); + device1.setName(TEST_PREFIX + "Sensor_" + timestamp + "_1"); + device1.setType("sensor"); + device1 = client.saveDevice(device1, null, null, null, null); + + Device device2 = new Device(); + device2.setName(TEST_PREFIX + "Sensor_" + timestamp + "_2"); + device2.setType("sensor"); + device2 = client.saveDevice(device2, null, null, null, null); + + Device device3 = new Device(); + device3.setName(TEST_PREFIX + "Sensor_" + timestamp + "_3"); + device3.setType("sensor"); + device3 = client.saveDevice(device3, null, null, null, null); + + // create relations: building -> Contains -> floor, floor -> Contains -> device1/device2/device3 + EntityRelation buildingToFloor = new EntityRelation(); + buildingToFloor.setFrom(building.getId()); + buildingToFloor.setTo(floor.getId()); + buildingToFloor.setType("Contains"); + buildingToFloor.setTypeGroup(RelationTypeGroup.COMMON); + EntityRelation savedRelation = client.saveRelation(buildingToFloor); + assertNotNull(savedRelation); + assertEquals("Contains", savedRelation.getType()); + + client.saveRelation(new EntityRelation() + .from(floor.getId()) + .to(device1.getId()) + .type("Contains") + .typeGroup(RelationTypeGroup.COMMON)); + client.saveRelation(new EntityRelation() + .from(floor.getId()) + .to(device2.getId()) + .type("Contains").typeGroup(RelationTypeGroup.COMMON)); + client.saveRelation(new EntityRelation() + .from(floor.getId()) + .to(device3.getId()) + .type("Manages") + .typeGroup(RelationTypeGroup.COMMON)); + + // get specific relation + EntityRelation fetched = client.getRelation( + building.getId().getId().toString(), "ASSET", + "Contains", + floor.getId().getId().toString(), "ASSET", + RelationTypeGroup.COMMON.getValue()); + assertNotNull(fetched); + assertEquals("Contains", fetched.getType()); + + // find all relations from floor + List fromFloor = client.findEntityRelationsByFrom("ASSET", + floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(3, fromFloor.size()); + + // find relations from floor with type filter "Contains" + List containsFromFloor = client.findEntityRelationsByFromAndRelationType("ASSET", + floor.getId().getId().toString(), "Contains", RelationTypeGroup.COMMON.getValue()); + assertEquals(2, containsFromFloor.size()); + + // find relations to device1 + List toDevice1 = client.findEntityRelationsByTo("DEVICE", + device1.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(1, toDevice1.size()); + assertEquals("Contains", toDevice1.get(0).getType()); + + // find relations to device3 with type filter "Manages" + List managesToDevice3 = client.findEntityRelationsByToAndRelationType("DEVICE", + device3.getId().getId().toString(), "Manages", RelationTypeGroup.COMMON.getValue()); + assertEquals(1, managesToDevice3.size()); + + // find info by from (includes entity names) + List infoFromFloor = client.findEntityRelationInfosByFrom("ASSET", + floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(3, infoFromFloor.size()); + Device finalDevice = device1; + assertTrue(infoFromFloor.stream().anyMatch(info -> + finalDevice.getName().equals(info.getToName()))); + + // find info by to + List infoToDevice2 = client.findEntityRelationInfosByTo("DEVICE", + device2.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(1, infoToDevice2.size()); + assertEquals(floor.getName(), infoToDevice2.get(0).getFromName()); + + // find by query - search from building, direction FROM, max 2 levels + RelationsSearchParameters params = new RelationsSearchParameters(); + params.setRootId(building.getId().getId()); + params.setRootType(EntityType.ASSET); + params.setDirection(EntitySearchDirection.FROM); + params.setRelationTypeGroup(RelationTypeGroup.COMMON); + params.setMaxLevel(2); + + RelationEntityTypeFilter filter = new RelationEntityTypeFilter(); + filter.setRelationType("Contains"); + filter.setEntityTypes(List.of(EntityType.ASSET, EntityType.DEVICE)); + + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(params); + query.setFilters(List.of(filter)); + + List queryResult = client.findEntityRelationsByQuery(query); + assertTrue(queryResult.size() >= 3); + + // find info by query + List infoQueryResult = client.findEntityRelationInfosByQuery(query); + assertTrue(infoQueryResult.size() >= 3); + + // delete single relation + client.deleteRelation( + floor.getId().getId().toString(), "ASSET", + "Manages", + device3.getId().getId().toString(), "DEVICE", + RelationTypeGroup.COMMON.getValue()); + + // verify deletion + List afterDelete = client.findEntityRelationsByFrom("ASSET", + floor.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(2, afterDelete.size()); + + // delete all relations for building + client.deleteRelations(building.getId().getId().toString(), "ASSET"); + + List afterDeleteAll = client.findEntityRelationsByFrom("ASSET", + building.getId().getId().toString(), RelationTypeGroup.COMMON.getValue()); + assertEquals(0, afterDeleteAll.size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java new file mode 100644 index 0000000000..60c0e86662 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/EntityViewApiClientTest.java @@ -0,0 +1,267 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.AttributesEntityView; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.EntitySubtype; +import org.thingsboard.client.model.EntityView; +import org.thingsboard.client.model.EntityViewInfo; +import org.thingsboard.client.model.PageDataEntityView; +import org.thingsboard.client.model.PageDataEntityViewInfo; +import org.thingsboard.client.model.TelemetryEntityView; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class EntityViewApiClientTest extends AbstractApiClientTest { + + private static final String EV_PREFIX = "EvTest_"; + + @Test + public void testSaveAndGetEntityView() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + + EntityView ev = new EntityView(); + ev.setName(EV_PREFIX + "save_" + ts); + ev.setType("testType"); + ev.setEntityId(device.getId()); + ev.setKeys(new TelemetryEntityView() + .timeseries(List.of("temperature", "humidity")) + .attributes(new AttributesEntityView() + .cs(List.of("firmware")) + .ss(List.of("active")) + .sh(List.of()))); + ev.setStartTimeMs(1000L); + ev.setEndTimeMs(2000L); + + EntityView saved = client.saveEntityView(ev, null, null, null); + assertNotNull(saved); + assertNotNull(saved.getId()); + assertEquals(ev.getName(), saved.getName()); + assertEquals("testType", saved.getType()); + assertEquals(device.getId().getId(), saved.getEntityId().getId()); + assertEquals(List.of("temperature", "humidity"), saved.getKeys().getTimeseries()); + assertEquals(1000L, saved.getStartTimeMs().longValue()); + assertEquals(2000L, saved.getEndTimeMs().longValue()); + + // get by id + String evId = saved.getId().getId().toString(); + EntityView fetched = client.getEntityViewById(evId); + assertNotNull(fetched); + assertEquals(saved.getName(), fetched.getName()); + assertEquals(saved.getType(), fetched.getType()); + assertEquals(saved.getEntityId().getId(), fetched.getEntityId().getId()); + } + + @Test + public void testGetEntityViewInfoById() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "info_" + ts, "infoType", device); + + EntityViewInfo info = client.getEntityViewInfoById(saved.getId().getId().toString()); + assertNotNull(info); + assertEquals(saved.getName(), info.getName()); + assertEquals("infoType", info.getType()); + assertNotNull(info.getEntityId()); + } + + @Test + public void testUpdateEntityView() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "update_" + ts, "default", device); + + saved.setName(EV_PREFIX + "updated_" + ts); + saved.setKeys(new TelemetryEntityView() + .timeseries(List.of("temperature", "pressure")) + .attributes(new AttributesEntityView() + .cs(List.of()) + .ss(List.of()) + .sh(List.of()))); + + EntityView updated = client.saveEntityView(saved, null, null, null); + assertEquals(EV_PREFIX + "updated_" + ts, updated.getName()); + assertEquals(List.of("temperature", "pressure"), updated.getKeys().getTimeseries()); + assertEquals(saved.getId().getId(), updated.getId().getId()); + } + + @Test + public void testDeleteEntityView() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "delete_" + ts, "default", device); + + String evId = saved.getId().getId().toString(); + client.getEntityViewById(evId); + + client.deleteEntityView(evId); + + assertReturns404(() -> client.getEntityViewById(evId)); + } + + @Test + public void testGetTenantEntityViews() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + + for (int i = 0; i < 3; i++) { + createEntityView(EV_PREFIX + "tenant_" + ts + "_" + i, "tenantViewType", device); + } + + PageDataEntityView page = client.getTenantEntityViews(100, 0, null, EV_PREFIX + "tenant_" + ts, null, null); + assertNotNull(page); + assertEquals(3, page.getTotalElements().intValue()); + for (EntityView ev : page.getData()) { + assertTrue(ev.getName().startsWith(EV_PREFIX + "tenant_" + ts)); + } + } + + @Test + public void testGetTenantEntityViewInfos() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + createEntityView(EV_PREFIX + "tinfo_" + ts, "default", device); + + PageDataEntityViewInfo page = client.getTenantEntityViewInfos(100, 0, null, EV_PREFIX + "tinfo_" + ts, null, null); + assertNotNull(page); + assertEquals(1, page.getTotalElements().intValue()); + assertEquals(EV_PREFIX + "tinfo_" + ts, page.getData().get(0).getName()); + } + + @Test + public void testAssignAndUnassignEntityViewToCustomer() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "assign_" + ts, "default", device); + + String evId = saved.getId().getId().toString(); + String customerId = savedClientCustomer.getId().getId().toString(); + + // assign to customer + EntityView assigned = client.assignEntityViewToCustomer(customerId, evId); + assertNotNull(assigned); + assertEquals(savedClientCustomer.getId().getId(), assigned.getCustomerId().getId()); + + // verify in customer entity views + PageDataEntityView customerViews = client.getCustomerEntityViews( + customerId, 100, 0, null, EV_PREFIX + "assign_" + ts, null, null); + assertEquals(1, customerViews.getTotalElements().intValue()); + assertEquals(saved.getName(), customerViews.getData().get(0).getName()); + + // unassign from customer + EntityView unassigned = client.unassignEntityViewFromCustomer(evId); + assertNotNull(unassigned); + + PageDataEntityView afterUnassign = client.getCustomerEntityViews( + customerId, 100, 0, null, EV_PREFIX + "assign_" + ts, null, null); + assertEquals(0, afterUnassign.getTotalElements().intValue()); + } + + @Test + public void testGetCustomerEntityViewInfos() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + EntityView saved = createEntityView(EV_PREFIX + "cinfo_" + ts, "default", device); + + String evId = saved.getId().getId().toString(); + String customerId = savedClientCustomer.getId().getId().toString(); + + client.assignEntityViewToCustomer(customerId, evId); + + PageDataEntityViewInfo infos = client.getCustomerEntityViewInfos( + customerId, 100, 0, null, EV_PREFIX + "cinfo_" + ts, null, null); + assertNotNull(infos); + assertEquals(1, infos.getTotalElements().intValue()); + assertEquals(saved.getName(), infos.getData().get(0).getName()); + } + + @Test + public void testGetEntityViewTypes() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + createEntityView(EV_PREFIX + "types_" + ts, "uniqueEvType_" + ts, device); + + List types = client.getEntityViewTypes(); + assertNotNull(types); + assertFalse(types.isEmpty()); + + List typeNames = types.stream() + .map(EntitySubtype::getType) + .collect(Collectors.toList()); + assertTrue(typeNames.contains("uniqueEvType_" + ts)); + } + + @Test + public void testGetEntityViewById_notFound() { + String nonExistentId = UUID.randomUUID().toString(); + assertReturns404(() -> client.getEntityViewById(nonExistentId)); + } + + @Test + public void testGetTenantEntityViewsPagination() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createTestDevice(String.valueOf(ts)); + + for (int i = 0; i < 5; i++) { + createEntityView(EV_PREFIX + "paged_" + ts + "_" + i, "default", device); + } + + PageDataEntityView page1 = client.getTenantEntityViews(2, 0, null, EV_PREFIX + "paged_" + ts, null, null); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + PageDataEntityView lastPage = client.getTenantEntityViews(2, 2, null, EV_PREFIX + "paged_" + ts, null, null); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + + private Device createTestDevice(String suffix) throws Exception { + Device device = new Device(); + device.setName(EV_PREFIX + "device_" + suffix); + device.setType("default"); + return client.saveDevice(device, null, null, null, null); + } + + private EntityView createEntityView(String name, String type, Device device) throws Exception { + EntityView ev = new EntityView(); + ev.setName(name); + ev.setType(type); + ev.setEntityId(device.getId()); + ev.setKeys(new TelemetryEntityView() + .timeseries(List.of("temperature")) + .attributes(new AttributesEntityView() + .cs(List.of()) + .ss(List.of()) + .sh(List.of()))); + return client.saveEntityView(ev, null, null, null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java new file mode 100644 index 0000000000..fd448eaee1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/MobileAppApiClientTest.java @@ -0,0 +1,156 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.MobileApp; +import org.thingsboard.client.model.MobileAppBundle; +import org.thingsboard.client.model.MobileAppBundleInfo; +import org.thingsboard.client.model.MobileAppStatus; +import org.thingsboard.client.model.PageDataMobileApp; +import org.thingsboard.client.model.PageDataMobileAppBundleInfo; +import org.thingsboard.client.model.PlatformType; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class MobileAppApiClientTest extends AbstractApiClientTest { + + @Test + public void testMobileAppLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdApps = new ArrayList<>(); + + // create 3 Android apps + for (int i = 0; i < 3; i++) { + MobileApp app = new MobileApp(); + app.setPkgName("com.test.android." + timestamp + "." + i); + app.setTitle(TEST_PREFIX + "AndroidApp_" + timestamp + "_" + i); + app.setAppSecret("secret_android_" + timestamp + "_" + i); + app.setPlatformType(PlatformType.ANDROID); + app.setStatus(MobileAppStatus.DRAFT); + + MobileApp created = client.saveMobileApp(app); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(app.getPkgName(), created.getPkgName()); + assertEquals(PlatformType.ANDROID, created.getPlatformType()); + assertEquals(MobileAppStatus.DRAFT, created.getStatus()); + + createdApps.add(created); + } + + // create 2 iOS apps + for (int i = 0; i < 2; i++) { + MobileApp app = new MobileApp(); + app.setPkgName("com.test.ios." + timestamp + "." + i); + app.setTitle(TEST_PREFIX + "IosApp_" + timestamp + "_" + i); + app.setAppSecret("secret_ios_" + timestamp + "_" + i); + app.setPlatformType(PlatformType.IOS); + app.setStatus(MobileAppStatus.DRAFT); + + MobileApp created = client.saveMobileApp(app); + assertNotNull(created); + createdApps.add(created); + } + + // list all tenant mobile apps + PageDataMobileApp allApps = client.getTenantMobileApps(100, 0, null, + null, null, null); + assertNotNull(allApps); + assertEquals(5, allApps.getData().size()); + + // list with platform type filter + PageDataMobileApp androidApps = client.getTenantMobileApps(100, 0, PlatformType.ANDROID, + null, null, null); + assertEquals(3, androidApps.getData().size()); + + PageDataMobileApp iosApps = client.getTenantMobileApps(100, 0, PlatformType.IOS, + null, null, null); + assertEquals(2, iosApps.getData().size()); + + // get mobile app by id + MobileApp searchApp = createdApps.get(1); + MobileApp fetchedApp = client.getMobileAppById(searchApp.getId().getId()); + assertEquals(searchApp.getPkgName(), fetchedApp.getPkgName()); + assertEquals(searchApp.getTitle(), fetchedApp.getTitle()); + assertEquals(searchApp.getPlatformType(), fetchedApp.getPlatformType()); + + // update mobile app + MobileApp appToUpdate = createdApps.get(2); + appToUpdate.setTitle(appToUpdate.getTitle() + "_updated"); + MobileApp updatedApp = client.saveMobileApp(appToUpdate); + assertEquals(appToUpdate.getTitle(), updatedApp.getTitle()); + + // create mobile app bundle with android and ios apps + MobileAppBundle bundle = new MobileAppBundle(); + bundle.setTitle(TEST_PREFIX + "Bundle_" + timestamp); + bundle.setDescription("Test bundle"); + bundle.setAndroidAppId(createdApps.get(0).getId()); + bundle.setIosAppId(createdApps.get(3).getId()); + bundle.setOauth2Enabled(false); + + MobileAppBundle savedBundle = client.saveMobileAppBundle(bundle, null); + assertNotNull(savedBundle); + assertNotNull(savedBundle.getId()); + assertEquals(bundle.getTitle(), savedBundle.getTitle()); + + // get bundle info by id + MobileAppBundleInfo bundleInfo = client.getMobileAppBundleInfoById(savedBundle.getId().getId()); + assertEquals(savedBundle.getTitle(), bundleInfo.getTitle()); + assertEquals("Test bundle", bundleInfo.getDescription()); + assertNotNull(bundleInfo.getAndroidPkgName()); + assertNotNull(bundleInfo.getIosPkgName()); + + // list tenant bundles + PageDataMobileAppBundleInfo bundles = client.getTenantMobileAppBundleInfos(100, 0, + TEST_PREFIX + "Bundle_" + timestamp, null, null); + assertEquals(1, bundles.getData().size()); + + // update bundle + savedBundle.setDescription("Updated description"); + MobileAppBundle updatedBundle = client.saveMobileAppBundle(savedBundle, null); + assertEquals("Updated description", updatedBundle.getDescription()); + + // delete bundle + client.deleteMobileAppBundle(savedBundle.getId().getId()); + + // verify bundle deletion + assertReturns404(() -> + client.getMobileAppBundleInfoById(savedBundle.getId().getId()) + ); + + // delete mobile app + UUID appToDeleteId = createdApps.get(0).getId().getId(); + client.deleteMobileApp(appToDeleteId); + + // verify app deletion + assertReturns404(() -> + client.getMobileAppById(appToDeleteId) + ); + + PageDataMobileApp appsAfterDelete = client.getTenantMobileApps(100, 0, null, + null, null, null); + assertEquals(4, appsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java new file mode 100644 index 0000000000..026b8784ef --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/NotificationApiClientTest.java @@ -0,0 +1,278 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.EntityActionNotificationRuleTriggerConfig; +import org.thingsboard.client.model.EntityActionRecipientsConfig; +import org.thingsboard.client.model.EntityType; +import org.thingsboard.client.model.NotificationDeliveryMethod; +import org.thingsboard.client.model.NotificationRequest; +import org.thingsboard.client.model.NotificationRequestInfo; +import org.thingsboard.client.model.NotificationRule; +import org.thingsboard.client.model.NotificationRuleInfo; +import org.thingsboard.client.model.NotificationRuleTriggerType; +import org.thingsboard.client.model.NotificationSettings; +import org.thingsboard.client.model.NotificationTarget; +import org.thingsboard.client.model.NotificationTemplate; +import org.thingsboard.client.model.NotificationTemplateConfig; +import org.thingsboard.client.model.NotificationType; +import org.thingsboard.client.model.PageDataNotification; +import org.thingsboard.client.model.PageDataNotificationRequestInfo; +import org.thingsboard.client.model.PageDataNotificationRuleInfo; +import org.thingsboard.client.model.PageDataNotificationTarget; +import org.thingsboard.client.model.PageDataNotificationTemplate; +import org.thingsboard.client.model.PlatformUsersNotificationTargetConfig; +import org.thingsboard.client.model.TenantAdministratorsFilter; +import org.thingsboard.client.model.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class NotificationApiClientTest extends AbstractApiClientTest { + + @Test + public void testNotificationLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // === 1. Notification Target CRUD === + + // Create target + TenantAdministratorsFilter usersFilter = new TenantAdministratorsFilter(); + PlatformUsersNotificationTargetConfig targetConfig = + new PlatformUsersNotificationTargetConfig().usersFilter(usersFilter); + NotificationTarget target = + new NotificationTarget() + .name("Test Target " + timestamp) + ._configuration(targetConfig); + + NotificationTarget savedTarget = client.saveNotificationTarget(target); + assertNotNull(savedTarget); + assertNotNull(savedTarget.getId()); + assertEquals("Test Target " + timestamp, savedTarget.getName()); + + // Get target by ID + NotificationTarget fetchedTarget = + client.getNotificationTargetById(savedTarget.getId().getId()); + assertEquals(savedTarget.getName(), fetchedTarget.getName()); + + // List targets + PageDataNotificationTarget targetsPage = + client.getNotificationTargets(100, 0, null, null, null); + assertNotNull(targetsPage); + assertNotNull(targetsPage.getData()); + assertTrue( + targetsPage.getData().stream() + .anyMatch(t -> t.getName().equals(savedTarget.getName()))); + + // Update target + savedTarget.setName("Updated Target " + timestamp); + NotificationTarget updatedTarget = client.saveNotificationTarget(savedTarget); + assertEquals("Updated Target " + timestamp, updatedTarget.getName()); + + // === 2. Notification Template CRUD === + + // Create template + WebDeliveryMethodNotificationTemplate webTemplate = + new WebDeliveryMethodNotificationTemplate() + .subject("Test Subject") + .body("Test notification body") + .enabled(true); + NotificationTemplateConfig templateConfig = + new NotificationTemplateConfig() + .putDeliveryMethodsTemplatesItem("WEB", webTemplate); + NotificationTemplate template = + new NotificationTemplate() + .name("Test Template " + timestamp) + .notificationType(NotificationType.GENERAL) + ._configuration(templateConfig); + + NotificationTemplate savedTemplate = client.saveNotificationTemplate(template); + assertNotNull(savedTemplate); + assertNotNull(savedTemplate.getId()); + assertEquals("Test Template " + timestamp, savedTemplate.getName()); + + // Get template by ID + NotificationTemplate fetchedTemplate = + client.getNotificationTemplateById(savedTemplate.getId().getId()); + assertEquals(savedTemplate.getName(), fetchedTemplate.getName()); + assertEquals(NotificationType.GENERAL, fetchedTemplate.getNotificationType()); + + // List templates + PageDataNotificationTemplate templatesPage = + client.getNotificationTemplates(100, 0, null, null, null, null); + assertNotNull(templatesPage); + assertTrue( + templatesPage.getData().stream() + .anyMatch(t -> t.getName().equals(savedTemplate.getName()))); + + // Update template + savedTemplate.setName("Updated Template " + timestamp); + NotificationTemplate updatedTemplate = client.saveNotificationTemplate(savedTemplate); + assertEquals("Updated Template " + timestamp, updatedTemplate.getName()); + + // === 3. Send notification & read notifications === + + // Send notification request + NotificationRequest request = + new NotificationRequest() + .targets(List.of(savedTarget.getId().getId())) + .templateId(savedTemplate.getId()); + NotificationRequest sentRequest = client.createNotificationRequest(request); + assertNotNull(sentRequest); + assertNotNull(sentRequest.getId()); + + // Get request by ID + NotificationRequestInfo fetchedRequest = + client.getNotificationRequestById(sentRequest.getId().getId()); + assertNotNull(fetchedRequest); + + // List requests + PageDataNotificationRequestInfo requestsPage = + client.getNotificationRequests(100, 0, null, null, null); + assertNotNull(requestsPage); + assertFalse(requestsPage.getData().isEmpty()); + + // Get notifications for current user + PageDataNotification notificationsPage = + client.getNotifications(100, 0, null, null, null, null, null); + assertNotNull(notificationsPage); + assertFalse(notificationsPage.getData().isEmpty()); + + // Get unread count + Integer unreadCount = client.getUnreadNotificationsCount("WEB"); + assertNotNull(unreadCount); + assertTrue("Expected at least one unread notification", unreadCount > 0); + + // Mark single notification as read + client.markNotificationAsRead( + notificationsPage.getData().get(0).getId().getId()); + + // Mark all as read + client.markAllNotificationsAsRead(null); + Integer unreadAfterMarkAll = client.getUnreadNotificationsCount(null); + assertEquals("Expected no unread notifications after marking all as read", 0, unreadAfterMarkAll.intValue()); + + // === 4. Notification Settings === + + NotificationSettings settings = client.getNotificationSettings(); + assertNotNull(settings); + + List deliveryMethods = client.getAvailableDeliveryMethods(); + assertNotNull(deliveryMethods); + assertTrue(deliveryMethods.contains(NotificationDeliveryMethod.WEB)); + + // === 5. Cleanup === + + // Delete notification request + client.deleteNotificationRequest(sentRequest.getId().getId()); + assertReturns404(() -> client.getNotificationRequestById(sentRequest.getId().getId())); + + // Delete template + client.deleteNotificationTemplateById(savedTemplate.getId().getId()); + assertReturns404(() -> client.getNotificationTemplateById(savedTemplate.getId().getId())); + + // Delete target + client.deleteNotificationTargetById(savedTarget.getId().getId()); + assertReturns404(() -> client.getNotificationTargetById(savedTarget.getId().getId())); + } + + @Test + public void testNotificationRuleLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // Create a target for the rule recipients + TenantAdministratorsFilter usersFilter = new TenantAdministratorsFilter(); + PlatformUsersNotificationTargetConfig targetConfig = + new PlatformUsersNotificationTargetConfig().usersFilter(usersFilter); + NotificationTarget target = + new NotificationTarget() + .name("Rule Test Target " + timestamp) + ._configuration(targetConfig); + NotificationTarget savedTarget = client.saveNotificationTarget(target); + + // Create a template of type ENTITY_ACTION + WebDeliveryMethodNotificationTemplate webTemplate = + new WebDeliveryMethodNotificationTemplate() + .subject("Entity action: ${entityType}") + .body("Entity ${entityName} was ${actionType}") + .enabled(true); + NotificationTemplateConfig templateConfig = + new NotificationTemplateConfig() + .putDeliveryMethodsTemplatesItem("WEB", webTemplate); + NotificationTemplate template = + new NotificationTemplate() + .name("Rule Test Template " + timestamp) + .notificationType(NotificationType.ENTITY_ACTION) + ._configuration(templateConfig); + NotificationTemplate savedTemplate = client.saveNotificationTemplate(template); + + // Build trigger config: fire on DEVICE create/update + EntityActionNotificationRuleTriggerConfig triggerConfig = + new EntityActionNotificationRuleTriggerConfig() + .addEntityTypesItem(EntityType.DEVICE) + .created(true) + .updated(true) + .deleted(false); + + // Build recipients config + EntityActionRecipientsConfig recipientsConfig = new EntityActionRecipientsConfig() + .addTargetsItem(savedTarget.getId().getId()); + + // saveNotificationRule - create + NotificationRule rule = new NotificationRule() + .name("Test Rule " + timestamp) + .enabled(true) + .templateId(savedTemplate.getId()) + .triggerType(NotificationRuleTriggerType.ENTITY_ACTION) + .triggerConfig(triggerConfig) + .recipientsConfig(recipientsConfig); + + NotificationRule savedRule = client.saveNotificationRule(rule); + assertNotNull(savedRule); + assertNotNull(savedRule.getId()); + assertEquals("Test Rule " + timestamp, savedRule.getName()); + assertEquals(NotificationRuleTriggerType.ENTITY_ACTION, savedRule.getTriggerType()); + assertEquals(Boolean.TRUE, savedRule.getEnabled()); + + // getNotificationRuleById + NotificationRuleInfo fetchedRule = client.getNotificationRuleById(savedRule.getId().getId()); + assertNotNull(fetchedRule); + assertEquals(savedRule.getName(), fetchedRule.getName()); + assertEquals(NotificationRuleTriggerType.ENTITY_ACTION, fetchedRule.getTriggerType()); + + // getNotificationRules - verify it appears in the list + PageDataNotificationRuleInfo rulesPage = client.getNotificationRules(100, 0, null, null, null); + assertNotNull(rulesPage); + assertTrue(rulesPage.getData().stream() + .anyMatch(r -> r.getId().getId().equals(savedRule.getId().getId()))); + + // deleteNotificationRule + client.deleteNotificationRule(savedRule.getId().getId()); + assertReturns404(() -> client.getNotificationRuleById(savedRule.getId().getId())); + + // Cleanup + client.deleteNotificationTemplateById(savedTemplate.getId().getId()); + client.deleteNotificationTargetById(savedTarget.getId().getId()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java new file mode 100644 index 0000000000..63f05710ae --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/Oauth2ApiClientTest.java @@ -0,0 +1,138 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.MapperType; +import org.thingsboard.client.model.OAuth2BasicMapperConfig; +import org.thingsboard.client.model.OAuth2Client; +import org.thingsboard.client.model.OAuth2ClientInfo; +import org.thingsboard.client.model.OAuth2MapperConfig; +import org.thingsboard.client.model.PageDataOAuth2ClientInfo; +import org.thingsboard.client.model.PlatformType; +import org.thingsboard.client.model.TenantNameStrategyType; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class Oauth2ApiClientTest extends AbstractApiClientTest { + + private OAuth2Client createOAuth2Client(String title, String clientId, String clientSecret) { + OAuth2BasicMapperConfig basicConfig = new OAuth2BasicMapperConfig(); + basicConfig.setEmailAttributeKey("email"); + basicConfig.setFirstNameAttributeKey("given_name"); + basicConfig.setLastNameAttributeKey("family_name"); + basicConfig.setTenantNameStrategy(TenantNameStrategyType.DOMAIN); + + OAuth2MapperConfig mapperConfig = new OAuth2MapperConfig(); + mapperConfig.setType(MapperType.BASIC); + mapperConfig.setAllowUserCreation(true); + mapperConfig.setActivateUser(false); + mapperConfig.setBasic(basicConfig); + + OAuth2Client oAuth2Client = new OAuth2Client(); + oAuth2Client.setTitle(title); + oAuth2Client.setClientId(clientId); + oAuth2Client.setClientSecret(clientSecret); + oAuth2Client.setAuthorizationUri("https://accounts.google.com/o/oauth2/v2/auth"); + oAuth2Client.setAccessTokenUri("https://oauth2.googleapis.com/token"); + oAuth2Client.setScope(List.of("openid", "email", "profile")); + oAuth2Client.setUserInfoUri("https://openidconnect.googleapis.com/v1/userinfo"); + oAuth2Client.setUserNameAttributeName("email"); + oAuth2Client.setClientAuthenticationMethod("POST"); + oAuth2Client.setLoginButtonLabel(title); + oAuth2Client.setMapperConfig(mapperConfig); + oAuth2Client.setPlatforms(List.of(PlatformType.WEB)); + + return oAuth2Client; + } + + @Test + public void testOAuth2ClientLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdClients = new ArrayList<>(); + + // create 5 OAuth2 clients + for (int i = 0; i < 5; i++) { + String title = TEST_PREFIX + "OAuth2_" + timestamp + "_" + i; + OAuth2Client oAuth2Client = createOAuth2Client(title, + "client_id_" + timestamp + "_" + i, + "client_secret_" + timestamp + "_" + i); + + OAuth2Client created = client.saveOAuth2Client(oAuth2Client); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(title, created.getTitle()); + assertEquals("POST", created.getClientAuthenticationMethod()); + assertNotNull(created.getMapperConfig()); + assertEquals(MapperType.BASIC, created.getMapperConfig().getType()); + + createdClients.add(created); + } + + // list tenant OAuth2 client infos + PageDataOAuth2ClientInfo clientInfos = client.findTenantOAuth2ClientInfos(100, 0, + TEST_PREFIX + "OAuth2_" + timestamp, null, null); + assertNotNull(clientInfos); + assertEquals(5, clientInfos.getData().size()); + + // get OAuth2 client by id + OAuth2Client searchClient = createdClients.get(2); + OAuth2Client fetchedClient = client.getOAuth2ClientById(searchClient.getId().getId()); + assertEquals(searchClient.getTitle(), fetchedClient.getTitle()); + assertEquals(searchClient.getClientId(), fetchedClient.getClientId()); + assertEquals(searchClient.getAuthorizationUri(), fetchedClient.getAuthorizationUri()); + assertEquals(3, fetchedClient.getScope().size()); + + // fetch client infos by ids + List idsToFetch = List.of( + createdClients.get(0).getId().getId().toString(), + createdClients.get(1).getId().getId().toString() + ); + List fetchedInfos = client.findTenantOAuth2ClientInfosByIds(idsToFetch); + assertEquals(2, fetchedInfos.size()); + + // update OAuth2 client + OAuth2Client clientToUpdate = client.getOAuth2ClientById(createdClients.get(3).getId().getId()); + clientToUpdate.setTitle(clientToUpdate.getTitle() + "_updated"); + clientToUpdate.setLoginButtonLabel("Updated Login"); + clientToUpdate.setPlatforms(List.of(PlatformType.WEB, PlatformType.ANDROID)); + OAuth2Client updatedClient = client.saveOAuth2Client(clientToUpdate); + assertEquals(clientToUpdate.getTitle(), updatedClient.getTitle()); + assertEquals("Updated Login", updatedClient.getLoginButtonLabel()); + assertEquals(2, updatedClient.getPlatforms().size()); + + // delete OAuth2 client + UUID clientToDeleteId = createdClients.get(0).getId().getId(); + client.deleteOauth2Client(clientToDeleteId); + + // verify deletion + assertReturns404(() -> + client.getOAuth2ClientById(clientToDeleteId) + ); + + PageDataOAuth2ClientInfo clientsAfterDelete = client.findTenantOAuth2ClientInfos(100, 0, + TEST_PREFIX + "OAuth2_" + timestamp, null, null); + assertEquals(4, clientsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java new file mode 100644 index 0000000000..c71f2cb2e3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/OtaPackageApiClientTest.java @@ -0,0 +1,261 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.ChecksumAlgorithm; +import org.thingsboard.client.model.DeviceProfileId; +import org.thingsboard.client.model.DeviceProfileInfo; +import org.thingsboard.client.model.OtaPackage; +import org.thingsboard.client.model.OtaPackageInfo; +import org.thingsboard.client.model.OtaPackageType; +import org.thingsboard.client.model.PageDataOtaPackageInfo; +import org.thingsboard.client.model.SaveOtaPackageInfoRequest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.io.File; +import java.io.FileWriter; +import java.nio.file.Files; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class OtaPackageApiClientTest extends AbstractApiClientTest { + + private static final String OTA_PREFIX = "OtaTest_"; + + private DeviceProfileId getDefaultDeviceProfileId() throws Exception { + DeviceProfileInfo profileInfo = client.getDefaultDeviceProfileInfo(); + return (DeviceProfileId) profileInfo.getId(); + } + + private SaveOtaPackageInfoRequest buildOtaPackageInfoRequest( + String title, String version, OtaPackageType type, + DeviceProfileId deviceProfileId, boolean usesUrl, String url) { + SaveOtaPackageInfoRequest request = new SaveOtaPackageInfoRequest(); + request.setTitle(title); + request.setType(type); + request.setUrl(url); + request.setVersion(version); + request.setDeviceProfileId(deviceProfileId); + return request; + } + + private OtaPackageInfo createFirmwareInfo(String suffix) throws Exception { + DeviceProfileId profileId = getDefaultDeviceProfileId(); + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + OTA_PREFIX + suffix, "1.0." + System.currentTimeMillis(), + OtaPackageType.FIRMWARE, profileId, false, null); + return client.saveOtaPackageInfo(request); + } + + private OtaPackageInfo createFirmwareWithUrl(String suffix) throws Exception { + DeviceProfileId profileId = getDefaultDeviceProfileId(); + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + OTA_PREFIX + suffix, "1.0." + System.currentTimeMillis(), + OtaPackageType.FIRMWARE, profileId, true, "https://example.com/firmware.bin"); + return client.saveOtaPackageInfo(request); + } + + @Test + public void testSaveAndGetOtaPackageInfo() throws Exception { + long ts = System.currentTimeMillis(); + DeviceProfileId profileId = getDefaultDeviceProfileId(); + String title = OTA_PREFIX + "save_" + ts; + String version = "1.0." + ts; + + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + title, version, OtaPackageType.FIRMWARE, profileId, true, "https://example.com/fw.bin"); + + OtaPackageInfo saved = client.saveOtaPackageInfo(request); + assertNotNull(saved); + assertNotNull(saved.getId()); + assertEquals(title, saved.getTitle()); + assertEquals(version, saved.getVersion()); + assertEquals(OtaPackageType.FIRMWARE, saved.getType()); + assertTrue(saved.getUrl().contains("example.com")); + + // get info by id + String pkgId = saved.getId().getId().toString(); + OtaPackageInfo fetched = client.getOtaPackageInfoById(pkgId); + assertNotNull(fetched); + assertEquals(title, fetched.getTitle()); + assertEquals(version, fetched.getVersion()); + } + + @Test + public void testGetOtaPackageById() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo saved = createFirmwareWithUrl("getbyid_" + ts); + + OtaPackage fullPkg = client.getOtaPackageById(saved.getId().getId().toString()); + assertNotNull(fullPkg); + assertEquals(saved.getTitle(), fullPkg.getTitle()); + assertEquals(saved.getVersion(), fullPkg.getVersion()); + } + + @Test + public void testSaveOtaPackageInfoForSoftware() throws Exception { + long ts = System.currentTimeMillis(); + DeviceProfileId profileId = getDefaultDeviceProfileId(); + String title = OTA_PREFIX + "sw_" + ts; + + SaveOtaPackageInfoRequest request = buildOtaPackageInfoRequest( + title, "2.0." + ts, OtaPackageType.SOFTWARE, profileId, true, "https://example.com/sw.bin"); + + OtaPackageInfo saved = client.saveOtaPackageInfo(request); + assertNotNull(saved); + assertEquals(OtaPackageType.SOFTWARE, saved.getType()); + assertEquals(title, saved.getTitle()); + } + + @Test + public void testSaveOtaPackageData() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo info = createFirmwareInfo("data_" + ts); + + File tempFile = Files.createTempFile("ota_test_", ".bin").toFile(); + tempFile.deleteOnExit(); + try (FileWriter writer = new FileWriter(tempFile)) { + writer.write("test firmware content " + ts); + } + + OtaPackageInfo updated = client.saveOtaPackageData( + info.getId().getId().toString(), "MD5", tempFile, null); + assertNotNull(updated); + assertTrue(updated.getHasData()); + assertNotNull(updated.getFileName()); + assertNotNull(updated.getDataSize()); + assertTrue(updated.getDataSize() > 0); + assertEquals(ChecksumAlgorithm.MD5, updated.getChecksumAlgorithm()); + } + + @Test + public void testDownloadOtaPackage() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo info = createFirmwareInfo("download_" + ts); + + String content = "downloadable firmware " + ts; + File tempFile = Files.createTempFile("ota_dl_", ".bin").toFile(); + tempFile.deleteOnExit(); + try (FileWriter writer = new FileWriter(tempFile)) { + writer.write(content); + } + + client.saveOtaPackageData(info.getId().getId().toString(), "MD5", tempFile, null); + + File downloaded = client.downloadOtaPackage(info.getId().getId().toString()); + assertNotNull(downloaded); + assertTrue(downloaded.length() > 0); + String downloadedContent = Files.readString(downloaded.toPath()); + assertEquals(content, downloadedContent); + } + + @Test + public void testDeleteOtaPackage() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo saved = createFirmwareWithUrl("delete_" + ts); + + String pkgId = saved.getId().getId().toString(); + client.getOtaPackageInfoById(pkgId); + + client.deleteOtaPackage(pkgId); + + assertReturns404(() -> client.getOtaPackageInfoById(pkgId)); + } + + @Test + public void testGetOtaPackages() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 3; i++) { + createFirmwareWithUrl("list_" + ts + "_" + i); + } + + PageDataOtaPackageInfo page = client.getOtaPackages(100, 0, OTA_PREFIX + "list_" + ts, null, null); + assertNotNull(page); + assertEquals(3, page.getTotalElements().intValue()); + for (OtaPackageInfo pkg : page.getData()) { + assertTrue(pkg.getTitle().startsWith(OTA_PREFIX + "list_" + ts)); + } + } + + @Test + public void testGetOtaPackagesByDeviceProfileAndType() throws Exception { + long ts = System.currentTimeMillis(); + DeviceProfileId profileId = getDefaultDeviceProfileId(); + + createFirmwareWithUrl("byprofile_" + ts + "_0"); + createFirmwareWithUrl("byprofile_" + ts + "_1"); + + PageDataOtaPackageInfo page = client.getOtaPackagesByDeviceProfileAndType( + profileId.getId().toString(), "FIRMWARE", 100, 0, + OTA_PREFIX + "byprofile_" + ts, null, null); + assertNotNull(page); + assertEquals(2, page.getTotalElements().intValue()); + } + + @Test + public void testGetOtaPackageInfoById_notFound() { + String nonExistentId = UUID.randomUUID().toString(); + assertReturns404(() -> client.getOtaPackageInfoById(nonExistentId)); + } + + @Test + public void testGetOtaPackagesPagination() throws Exception { + long ts = System.currentTimeMillis(); + + for (int i = 0; i < 5; i++) { + createFirmwareWithUrl("paged_" + ts + "_" + i); + } + + PageDataOtaPackageInfo page1 = client.getOtaPackages(2, 0, OTA_PREFIX + "paged_" + ts, null, null); + assertNotNull(page1); + assertEquals(5, page1.getTotalElements().intValue()); + assertEquals(3, page1.getTotalPages().intValue()); + assertEquals(2, page1.getData().size()); + assertTrue(page1.getHasNext()); + + PageDataOtaPackageInfo lastPage = client.getOtaPackages(2, 2, OTA_PREFIX + "paged_" + ts, null, null); + assertEquals(1, lastPage.getData().size()); + assertFalse(lastPage.getHasNext()); + } + + @Test + public void testUpdateOtaPackageInfo() throws Exception { + long ts = System.currentTimeMillis(); + OtaPackageInfo saved = createFirmwareWithUrl("update_" + ts); + + SaveOtaPackageInfoRequest updateReq = new SaveOtaPackageInfoRequest(); + updateReq.setId(saved.getId()); + updateReq.setTitle(saved.getTitle()); + updateReq.setType(saved.getType()); + updateReq.setVersion(saved.getVersion()); + updateReq.setDeviceProfileId(saved.getDeviceProfileId()); + updateReq.setUrl(saved.getUrl()); + updateReq.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("infoKey", "infoValue")); + + OtaPackageInfo updated = client.saveOtaPackageInfo(updateReq); + assertNotNull(updated); + assertEquals(saved.getId().getId(), updated.getId().getId()); + assertEquals("infoValue", updated.getAdditionalInfo().get("infoKey").asText()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java new file mode 100644 index 0000000000..80719052d1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/RpcV1ApiClientTest.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Device; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.junit.Assert.assertEquals; + +@DaoSqlTest +public class RpcV1ApiClientTest extends AbstractApiClientTest { + + private static final String ONE_WAY_BODY = + "{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}"; + private static final String TWO_WAY_BODY = + "{\"method\":\"getGpio\",\"params\":{\"pin\":7},\"persistent\":true}"; + + @Test + public void testHandleOneWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleOneWayDeviceRPCRequestV1(deviceId, ONE_WAY_BODY); + } catch (ApiException e) { + assertEquals("handleOneWayDeviceRPCRequest got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + @Test + public void testHandleTwoWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleTwoWayDeviceRPCRequestV1(deviceId, TWO_WAY_BODY); + } catch (ApiException e) { + assertEquals("handleTwoWayDeviceRPCRequest got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + private Device createNewDevice(String name) throws ApiException { + Device device = new Device(); + device.setName(name); + device.setType("default"); + return client.saveDevice(device, null, null, null, null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java new file mode 100644 index 0000000000..02b7e0f0ba --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/RpcV2ApiClientTest.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.Rpc; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class RpcV2ApiClientTest extends AbstractApiClientTest { + + private static final String PERSISTENT_BODY = + "{\"method\":\"setGpio\",\"params\":{\"pin\":7,\"value\":1},\"persistent\":true}"; + + @Test + public void testHandleOneWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleOneWayDeviceRPCRequestV2(deviceId, PERSISTENT_BODY); + } catch (ApiException e) { + assertEquals("handleOneWayDeviceRPCRequest1 got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + @Test + public void testHandleTwoWayDeviceRPCRequest() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + try { + client.handleTwoWayDeviceRPCRequestV2(deviceId, PERSISTENT_BODY); + } catch (ApiException e) { + assertEquals("handleTwoWayDeviceRPCRequest1 got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + @Test + public void testGetPersistedRpcAndDeleteRpc() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + String rpcId = postPersistentRpcAndGetId(deviceId); + assertNotNull(rpcId); + + Rpc rpc = client.getPersistedRpc(rpcId); + assertNotNull(rpc); + assertNotNull(rpc.getId()); + + client.deleteRpc(rpcId); + + assertReturns404(() -> client.getPersistedRpc(rpcId)); + + client.deleteDevice(deviceId); + } + + @Test + public void testGetPersistedRpcNotFound() { + assertReturns404(() -> client.getPersistedRpc(UUID.randomUUID().toString())); + } + + @Test + public void testGetPersistedRpcByDevice() throws Exception { + long ts = System.currentTimeMillis(); + Device device = createNewDevice(TEST_PREFIX + ts); + String deviceId = device.getId().getId().toString(); + + postPersistentRpcAndGetId(deviceId); + + try { + client.getPersistedRpcByDevice(deviceId, 100, 0, null, null, null, null); + } catch (ApiException e) { + assertEquals("getPersistedRpcByDevice got an unexpected HTTP error: " + e.getCode(), + 0, e.getCode()); + } + + client.deleteDevice(deviceId); + } + + private Device createNewDevice(String name) throws ApiException { + Device device = new Device(); + device.setName(name); + device.setType("default"); + return client.saveDevice(device, null, null, null, null); + } + + private String postPersistentRpcAndGetId(String deviceId) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(getBaseUrl() + "/api/plugins/rpc/oneway/" + deviceId)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + client.getToken()) + .POST(HttpRequest.BodyPublishers.ofString(PERSISTENT_BODY)) + .build(); + HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + return OBJECT_MAPPER.readTree(response.body()).get("rpcId").asText(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java new file mode 100644 index 0000000000..8adcecaf24 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/RuleChainApiClientTest.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.NodeConnectionInfo; +import org.thingsboard.client.model.PageDataRuleChain; +import org.thingsboard.client.model.RuleChain; +import org.thingsboard.client.model.RuleChainMetaData; +import org.thingsboard.client.model.RuleChainType; +import org.thingsboard.client.model.RuleNode; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class RuleChainApiClientTest extends AbstractApiClientTest { + + @Test + public void testRuleChainAndNodeLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdChains = new ArrayList<>(); + + // create 5 rule chains + for (int i = 0; i < 5; i++) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(TEST_PREFIX + "RuleChain_" + timestamp + "_" + i); + ruleChain.setType(RuleChainType.CORE); + ruleChain.setDebugMode(false); + + RuleChain created = client.saveRuleChain(ruleChain); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(ruleChain.getName(), created.getName()); + assertEquals(RuleChainType.CORE, created.getType()); + + createdChains.add(created); + } + + // list rule chains with text search + PageDataRuleChain filteredChains = client.getRuleChains(100, 0, null, + TEST_PREFIX + "RuleChain_" + timestamp, null, null); + assertNotNull(filteredChains); + assertEquals(5, filteredChains.getData().size()); + + // get rule chain by id + RuleChain searchChain = createdChains.get(2); + RuleChain fetchedChain = client.getRuleChainById(searchChain.getId().getId().toString()); + assertEquals(searchChain.getName(), fetchedChain.getName()); + assertEquals(searchChain.getType(), fetchedChain.getType()); + + // get metadata (initially has default node) + RuleChainMetaData metadata = client.getRuleChainMetaData(searchChain.getId().getId().toString()); + assertNotNull(metadata); + assertEquals(searchChain.getId().getId(), metadata.getRuleChainId().getId()); + + // save metadata with rule nodes and connections + RuleChainMetaData newMetadata = new RuleChainMetaData(metadata.getRuleChainId()); + newMetadata.setVersion(metadata.getVersion()); + newMetadata.setFirstNodeIndex(0); + + // node 0: message type switch + RuleNode switchNode = new RuleNode(); + switchNode.setName("Message Type Switch"); + switchNode.setType("org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode"); + switchNode.setConfiguration(OBJECT_MAPPER.createObjectNode().put("version", 0)); + switchNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 200).put("layoutY", 150)); + + // node 1: log node for telemetry + RuleNode logNode = new RuleNode(); + logNode.setName("Log Telemetry"); + logNode.setType("org.thingsboard.rule.engine.action.TbLogNode"); + logNode.setConfiguration(OBJECT_MAPPER.createObjectNode() + .put("scriptLang", "TBEL") + .put("jsScript", "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);") + .put("tbelScript", "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);")); + logNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 500).put("layoutY", 100)); + + // node 2: save timeseries + RuleNode saveNode = new RuleNode(); + saveNode.setName("Save Timeseries"); + saveNode.setType("org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode"); + saveNode.setConfiguration(OBJECT_MAPPER.createObjectNode() + .put("defaultTTL", 0) + .put("skipLatestPersistence", false) + .put("useServerTs", false)); + saveNode.setAdditionalInfo(OBJECT_MAPPER.createObjectNode().put("layoutX", 500).put("layoutY", 250)); + + newMetadata.setNodes(List.of(switchNode, logNode, saveNode)); + + // connection: switch -> log (on "Post telemetry") + NodeConnectionInfo conn1 = new NodeConnectionInfo(); + conn1.setFromIndex(0); + conn1.setToIndex(1); + conn1.setType("Post telemetry"); + + // connection: switch -> save timeseries (on "Post telemetry") + NodeConnectionInfo conn2 = new NodeConnectionInfo(); + conn2.setFromIndex(0); + conn2.setToIndex(2); + conn2.setType("Post telemetry"); + + newMetadata.setConnections(List.of(conn1, conn2)); + newMetadata.setRuleChainConnections(List.of()); + + RuleChainMetaData savedMetadata = client.saveRuleChainMetaData(newMetadata, false); + assertNotNull(savedMetadata); + assertEquals(3, savedMetadata.getNodes().size()); + assertEquals(2, savedMetadata.getConnections().size()); + + // verify saved nodes + RuleChainMetaData fetchedMetadata = client.getRuleChainMetaData(searchChain.getId().getId().toString()); + assertEquals(3, fetchedMetadata.getNodes().size()); + assertTrue(fetchedMetadata.getNodes().stream() + .anyMatch(node -> "Log Telemetry".equals(node.getName()))); + assertTrue(fetchedMetadata.getNodes().stream() + .anyMatch(node -> "Save Timeseries".equals(node.getName()))); + + // get output labels + client.getRuleChainOutputLabels(searchChain.getId().getId().toString()); + + // update rule chain + RuleChain chainToUpdate = createdChains.get(3); + chainToUpdate.setName(chainToUpdate.getName() + "_updated"); + chainToUpdate.setDebugMode(true); + RuleChain updatedChain = client.saveRuleChain(chainToUpdate); + assertEquals(chainToUpdate.getName(), updatedChain.getName()); + assertEquals(true, updatedChain.getDebugMode()); + + // delete rule chain + UUID chainToDeleteId = createdChains.get(0).getId().getId(); + client.deleteRuleChain(chainToDeleteId.toString()); + + // verify deletion + assertReturns404(() -> + client.getRuleChainById(chainToDeleteId.toString()) + ); + + PageDataRuleChain chainsAfterDelete = client.getRuleChains(100, 0, null, + TEST_PREFIX + "RuleChain_" + timestamp, null, null); + assertEquals(4, chainsAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java new file mode 100644 index 0000000000..2a06549554 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TbImageApiClientTest.java @@ -0,0 +1,151 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.PageDataTbResourceInfo; +import org.thingsboard.client.model.ResourceExportData; +import org.thingsboard.client.model.TbImageDeleteResult; +import org.thingsboard.client.model.TbResourceInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TbImageApiClientTest extends AbstractApiClientTest { + + private File createTempImage(String name, Color color) throws IOException { + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + Graphics2D g = img.createGraphics(); + g.setColor(color); + g.fillRect(0, 0, 100, 100); + g.dispose(); + + File tempFile = File.createTempFile(name, ".png"); + tempFile.deleteOnExit(); + ImageIO.write(img, "png", tempFile); + return tempFile; + } + + @Test + public void testImageLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdImages = new ArrayList<>(); + Color[] colors = {Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.CYAN}; + + // upload 5 images + for (int i = 0; i < 5; i++) { + String title = TEST_PREFIX + "Image_" + timestamp + "_" + i; + File imageFile = createTempImage("test_image_" + i, colors[i]); + + TbResourceInfo uploaded = client.uploadImage(imageFile, title, null); + assertNotNull(uploaded); + assertNotNull(uploaded.getResourceKey()); + assertEquals(title, uploaded.getTitle()); + assertNotNull(uploaded.getLink()); + + createdImages.add(uploaded); + } + + // list images with text search + PageDataTbResourceInfo filteredImages = client.getImages(100, 0, null, false, + TEST_PREFIX + "Image_" + timestamp, null, null); + assertNotNull(filteredImages); + assertEquals(5, filteredImages.getData().size()); + + // get image info by type and key + TbResourceInfo searchImage = createdImages.get(2); + TbResourceInfo fetchedInfo = client.getImageInfo("tenant", searchImage.getResourceKey()); + assertEquals(searchImage.getTitle(), fetchedInfo.getTitle()); + assertEquals(searchImage.getResourceKey(), fetchedInfo.getResourceKey()); + + // download image + File downloadedImage = client.downloadImage("tenant", searchImage.getResourceKey(), null, null); + assertNotNull(downloadedImage); + assertTrue(downloadedImage.exists()); + assertTrue(downloadedImage.length() > 0); + + // download image preview + File preview = client.downloadImagePreview("tenant", searchImage.getResourceKey(), null, null); + assertNotNull(preview); + assertTrue(preview.exists()); + assertTrue(preview.length() > 0); + + // update image file + File updatedImageFile = createTempImage("updated_image", Color.MAGENTA); + TbResourceInfo updatedImage = client.updateImage("tenant", searchImage.getResourceKey(), updatedImageFile); + assertNotNull(updatedImage); + assertEquals(searchImage.getResourceKey(), updatedImage.getResourceKey()); + + // update image info (title) + TbResourceInfo infoToUpdate = client.getImageInfo("tenant", createdImages.get(3).getResourceKey()); + infoToUpdate.setTitle(infoToUpdate.getTitle() + "_updated"); + TbResourceInfo updatedInfo = client.updateImageInfo("tenant", infoToUpdate.getResourceKey(), infoToUpdate); + assertEquals(infoToUpdate.getTitle(), updatedInfo.getTitle()); + + // make image public + TbResourceInfo publicImage = client.updateImagePublicStatus("tenant", + createdImages.get(1).getResourceKey(), true); + assertTrue(publicImage.getPublic()); + assertNotNull(publicImage.getPublicResourceKey()); + assertNotNull(publicImage.getPublicLink()); + + // download public image + File publicDownload = client.downloadPublicImage(publicImage.getPublicResourceKey(), null, null); + assertNotNull(publicDownload); + assertTrue(publicDownload.exists()); + assertTrue(publicDownload.length() > 0); + + // make image private again + TbResourceInfo privateImage = client.updateImagePublicStatus("tenant", + createdImages.get(1).getResourceKey(), false); + assertEquals(false, privateImage.getPublic()); + + // export image + ResourceExportData exportData = client.exportImage("tenant", createdImages.get(4).getResourceKey()); + assertNotNull(exportData); + assertNotNull(exportData.getData()); + assertEquals(createdImages.get(4).getTitle(), exportData.getTitle()); + assertEquals(createdImages.get(4).getResourceKey(), exportData.getResourceKey()); + + // delete image + String keyToDelete = createdImages.get(0).getResourceKey(); + TbImageDeleteResult deleteResult = client.deleteImage("tenant", keyToDelete, false); + assertNotNull(deleteResult); + assertTrue(deleteResult.getSuccess()); + + // verify deletion + assertReturns404(() -> + client.getImageInfo("tenant", keyToDelete) + ); + + PageDataTbResourceInfo imagesAfterDelete = client.getImages(100, 0, null, false, + TEST_PREFIX + "Image_" + timestamp, null, null); + assertEquals(4, imagesAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java new file mode 100644 index 0000000000..0651ce6cc3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TbResourceApiClientTest.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.PageDataTbResourceInfo; +import org.thingsboard.client.model.ResourceType; +import org.thingsboard.client.model.TbResource; +import org.thingsboard.client.model.TbResourceInfo; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.io.File; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TbResourceApiClientTest extends AbstractApiClientTest { + + @Test + public void testResourceLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdResources = new ArrayList<>(); + + // create 5 JS_MODULE resources + for (int i = 0; i < 5; i++) { + TbResource resource = new TbResource(); + resource.setTitle(TEST_PREFIX + "Resource_" + timestamp + "_" + i); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setResourceKey("test_module_" + timestamp + "_" + i + ".js"); + resource.setFileName("test_module_" + timestamp + "_" + i + ".js"); + + String jsContent = "export default function test" + i + "() { return " + i + "; }"; + resource.setData(Base64.getEncoder().encodeToString(jsContent.getBytes())); + + TbResourceInfo created = client.saveResource(resource); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(resource.getTitle(), created.getTitle()); + assertEquals(ResourceType.JS_MODULE, created.getResourceType()); + + createdResources.add(created); + } + + // get tenant resources, check count + PageDataTbResourceInfo tenantResources = client.getTenantResources(100, 0, null, null, null); + assertNotNull(tenantResources); + assertNotNull(tenantResources.getData()); + int initialSize = tenantResources.getData().size(); + assertTrue("Expected at least 5 resources, but got " + initialSize, initialSize >= 5); + + // find with text search + PageDataTbResourceInfo filteredResources = client.getTenantResources(100, 0, + TEST_PREFIX + "Resource_" + timestamp, null, null); + assertEquals(5, filteredResources.getData().size()); + + // get resources with type filter + PageDataTbResourceInfo jsResources = client.getResources(100, 0, + ResourceType.JS_MODULE.getValue(), null, TEST_PREFIX + "Resource_" + timestamp, null, null); + assertEquals(5, jsResources.getData().size()); + + // get resource info by id + TbResourceInfo searchResource = createdResources.get(2); + TbResourceInfo fetchedInfo = client.getResourceInfoById(searchResource.getId().getId().toString()); + assertEquals(searchResource.getTitle(), fetchedInfo.getTitle()); + assertEquals(searchResource.getResourceKey(), fetchedInfo.getResourceKey()); + + // get full resource by id (includes data) + TbResource fullResource = client.getResourceById(searchResource.getId().getId().toString()); + assertNotNull(fullResource); + assertEquals(searchResource.getTitle(), fullResource.getTitle()); + assertNotNull(fullResource.getData()); + + // download resource + File downloadedFile = client.downloadResource(searchResource.getId().getId().toString()); + assertNotNull(downloadedFile); + assertTrue(downloadedFile.exists()); + assertTrue(downloadedFile.length() > 0); + + // get resources by list of ids + List idsToFetch = List.of( + createdResources.get(0).getId().getId().toString(), + createdResources.get(1).getId().getId().toString() + ); + List resourceList = client.getSystemOrTenantResourcesByIds(idsToFetch); + assertEquals(2, resourceList.size()); + + // update resource + TbResource resourceToUpdate = client.getResourceById(createdResources.get(3).getId().getId().toString()); + resourceToUpdate.setTitle(resourceToUpdate.getTitle() + "_updated"); + String updatedContent = "export default function updated() { return 42; }"; + resourceToUpdate.setData(Base64.getEncoder().encodeToString(updatedContent.getBytes())); + TbResourceInfo updatedResource = client.saveResource(resourceToUpdate); + assertEquals(resourceToUpdate.getTitle(), updatedResource.getTitle()); + + // delete resource + UUID resourceToDeleteId = createdResources.get(0).getId().getId(); + client.deleteResource(resourceToDeleteId.toString(), false); + + // verify deletion + assertReturns404(() -> + client.getResourceInfoById(resourceToDeleteId.toString()) + ); + + PageDataTbResourceInfo resourcesAfterDelete = client.getTenantResources(100, 0, + TEST_PREFIX + "Resource_" + timestamp, null, null); + assertEquals(4, resourcesAfterDelete.getData().size()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java new file mode 100644 index 0000000000..a2dbe0eda4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TelemetryApiClientTest.java @@ -0,0 +1,150 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.AttributeData; +import org.thingsboard.client.model.Device; +import org.thingsboard.client.model.TsData; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TelemetryApiClientTest extends AbstractApiClientTest { + + @Test + public void testTelemetryLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + + // create a device for telemetry operations + Device device = new Device(); + device.setName("TelemetryTestDevice_" + timestamp); + device.setType("default"); + Device createdDevice = client.saveDevice(device, null, null, null, null); + assertNotNull(createdDevice); + + String entityType = "DEVICE"; + String entityId = createdDevice.getId().getId().toString(); + + // save server-side attributes + String serverAttributes = "{\"serverAttr1\": \"value1\", \"serverAttr2\": 42}"; + client.saveEntityAttributesV2(entityType, entityId, "SERVER_SCOPE", serverAttributes); + + // save shared attributes + String sharedAttributes = "{\"sharedAttr1\": \"sharedValue1\", \"sharedAttr2\": true}"; + client.saveEntityAttributesV2(entityType, entityId, "SHARED_SCOPE", sharedAttributes); + + // get attribute keys + List allKeys = client.getAttributeKeys(entityType, entityId); + assertNotNull(allKeys); + assertTrue(allKeys.containsAll(List.of("serverAttr1", "serverAttr2", "sharedAttr1", "sharedAttr2"))); + + // get attribute keys by scope + List serverKeys = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertEquals(2 + 1, serverKeys.size()); //active attribute is automatically added to server scope + assertTrue(serverKeys.containsAll(List.of("serverAttr1", "serverAttr2", "active"))); + + // get attributes by scope + List serverAttrs = client.getAttributesByScope(entityType, entityId, "SERVER_SCOPE", "serverAttr1,serverAttr2", null); + assertNotNull(serverAttrs); + assertEquals(2, serverAttrs.size()); + + // get all attributes + List allAttrs = client.getAttributes(entityType, entityId, "serverAttr1,sharedAttr1", null); + assertEquals(2, allAttrs.size()); + assertEquals("value1", allAttrs.stream().filter(attr -> attr.getKey().equals("serverAttr1")).findFirst().orElseThrow().getValue().toString()); + assertEquals("sharedValue1", allAttrs.stream().filter(attr -> attr.getKey().equals("sharedAttr1")).findFirst().orElseThrow().getValue().toString()); + + // save timeseries data + long ts1 = timestamp - 60000; + long ts2 = timestamp - 30000; + long ts3 = timestamp; + String telemetryBody = "{\"ts\":" + ts1 + ",\"values\":{\"temperature\":25.5,\"humidity\":60}}"; + client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody); + + String telemetryBody2 = "{\"ts\":" + ts2 + ",\"values\":{\"temperature\":26.0,\"humidity\":58}}"; + client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody2); + + String telemetryBody3 = "{\"ts\":" + ts3 + ",\"values\":{\"temperature\":27.1,\"humidity\":55}}"; + client.saveEntityTelemetry(entityType, entityId, "ANY", telemetryBody3); + + // get timeseries keys + List tsKeys = client.getTimeseriesKeys(entityType, entityId); + assertNotNull(tsKeys); + assertEquals(2, tsKeys.size()); + assertTrue(tsKeys.containsAll(List.of("humidity", "temperature"))); + + // get latest timeseries + Map> latestData = client.getLatestTimeseries(entityType, entityId, "temperature,humidity", false, null); + assertNotNull(latestData); + assertNotNull(latestData.get("temperature")); + assertFalse(latestData.get("temperature").isEmpty()); + assertEquals("27.1", latestData.get("temperature").get(0).getValue().toString()); + + // get timeseries history + Map> historyData = client.getTimeseriesHistory( + entityType, entityId, + ts1 - 1000, ts3 + 1000, "temperature", + null, null, null, null, "NONE", "ASC", false, null); + assertNotNull(historyData); + List tempHistory = historyData.get("temperature"); + assertNotNull(tempHistory); + assertEquals(3, tempHistory.size()); + assertEquals("25.5", tempHistory.get(0).getValue().toString()); + assertEquals("27.1", tempHistory.get(2).getValue().toString()); + + // delete timeseries + client.deleteEntityTimeseries(entityType, entityId, "humidity", true, null, null, true, false, null); + + List keysAfterDelete = client.getTimeseriesKeys(entityType, entityId); + assertFalse(keysAfterDelete.contains("humidity")); + + // delete attributes + client.deleteEntityAttributes(entityType, entityId, "SERVER_SCOPE", "serverAttr1", null); + + List serverKeysAfterDelete = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertFalse(serverKeysAfterDelete.contains("serverAttr1")); + assertTrue(serverKeysAfterDelete.contains("serverAttr2")); + + // save device attributes using device-specific endpoint + client.saveDeviceAttributes(entityId, "SERVER_SCOPE", "{\"deviceSpecificAttr\": \"test\"}"); + + List deviceKeys = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertTrue(deviceKeys.contains("deviceSpecificAttr")); + + // delete device attributes + client.deleteDeviceAttributes(entityId, "SERVER_SCOPE", "deviceSpecificAttr", null); + + List deviceKeysAfterDelete = client.getAttributeKeysByScope(entityType, entityId, "SERVER_SCOPE"); + assertFalse(deviceKeysAfterDelete.contains("deviceSpecificAttr")); + + // save telemetry with TTL + String ttlTelemetry = "{\"ts\":" + timestamp + ",\"values\":{\"shortLived\":99}}"; + client.saveEntityTelemetryWithTTL(entityType, entityId, "ANY", 86400L, ttlTelemetry); + + Map> latestWithTtl = client.getLatestTimeseries(entityType, entityId, "shortLived", false, null); + assertNotNull(latestWithTtl.get("shortLived")); + assertEquals("99", latestWithTtl.get("shortLived").get(0).getValue().toString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java new file mode 100644 index 0000000000..150486eb8c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TenantApiClientTest.java @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.Authority; +import org.thingsboard.client.model.PageDataTenant; +import org.thingsboard.client.model.PageDataUser; +import org.thingsboard.client.model.Tenant; +import org.thingsboard.client.model.User; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class TenantApiClientTest extends AbstractApiClientTest { + + @Test + public void testTenantLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdTenants = new ArrayList<>(); + + // authenticate as sysadmin for tenant management + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // create 20 tenants + for (int i = 0; i < 20; i++) { + Tenant tenant = new Tenant(); + String tenantTitle = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i; + tenant.setTitle(tenantTitle); + tenant.setEmail("tenant_" + timestamp + "_" + i + "@test.com"); + tenant.setCountry("US"); + tenant.setCity("City" + i); + + Tenant createdTenant = client.saveTenant(tenant); + assertNotNull(createdTenant); + assertNotNull(createdTenant.getId()); + assertEquals(tenantTitle, createdTenant.getTitle()); + + createdTenants.add(createdTenant); + } + + try { + // find all with search text, check count + PageDataTenant filteredTenants = client.getTenants(100, 0, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 tenants matching prefix", 10, filteredTenants.getData().size()); + + // find by id + Tenant searchTenant = createdTenants.get(10); + Tenant fetchedTenant = client.getTenantById(searchTenant.getId().getId().toString()); + assertEquals(searchTenant.getTitle(), fetchedTenant.getTitle()); + assertEquals(searchTenant.getEmail(), fetchedTenant.getEmail()); + + // update tenant + fetchedTenant.setCity("Updated City"); + fetchedTenant.setCountry("DE"); + Tenant updatedTenant = client.saveTenant(fetchedTenant); + assertEquals("Updated City", updatedTenant.getCity()); + assertEquals("DE", updatedTenant.getCountry()); + + // create a tenant admin for one of the tenants and verify listing + Tenant tenantForAdmin = createdTenants.get(0); + User adminUser = new User(); + adminUser.setEmail("tenanttest_admin_" + timestamp + "@test.com"); + adminUser.setAuthority(Authority.TENANT_ADMIN); + adminUser.setTenantId(tenantForAdmin.getId()); + adminUser.setFirstName("TestAdmin"); + User savedAdmin = client.saveUser(adminUser, "false"); + assertNotNull(savedAdmin); + + PageDataUser tenantAdmins = client.getTenantAdmins( + tenantForAdmin.getId().getId().toString(), 100, 0, null, null, null); + assertEquals(1, tenantAdmins.getData().size()); + assertEquals(savedAdmin.getEmail(), tenantAdmins.getData().get(0).getEmail()); + + // delete tenant + UUID tenantToDeleteId = createdTenants.get(0).getId().getId(); + client.deleteTenant(tenantToDeleteId.toString()); + createdTenants.remove(0); + + // verify deletion + PageDataTenant tenantsAfterDelete = client.getTenants(100, 0, TEST_PREFIX_2, null, null); + assertEquals(10, tenantsAfterDelete.getData().size()); + + assertReturns404(() -> + client.getTenantById(tenantToDeleteId.toString()) + ); + } finally { + // clean up all created tenants (deleting tenant cascades to users) + client.login("sysadmin@thingsboard.org", "sysadmin"); + for (Tenant tenant : createdTenants) { + try { + client.deleteTenant(tenant.getId().getId().toString()); + } catch (ApiException ignored) { + } + } + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java new file mode 100644 index 0000000000..68dd5f7c0e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TenantProfileApiClientTest.java @@ -0,0 +1,179 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.ApiException; +import org.thingsboard.client.model.DefaultTenantProfileConfiguration; +import org.thingsboard.client.model.EntityInfo; +import org.thingsboard.client.model.PageDataEntityInfo; +import org.thingsboard.client.model.PageDataTenantProfile; +import org.thingsboard.client.model.TenantProfile; +import org.thingsboard.client.model.TenantProfileData; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TenantProfileApiClientTest extends AbstractApiClientTest { + + @Test + public void testTenantProfileLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdProfiles = new ArrayList<>(); + + // authenticate as sysadmin for tenant profile management + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // get initial count (there should be a default profile) + PageDataTenantProfile initialProfiles = client.getTenantProfiles(100, 0, null, null, null); + assertNotNull(initialProfiles); + int initialSize = initialProfiles.getData().size(); + assertTrue("Expected at least 1 default tenant profile", initialSize >= 1); + + // get default tenant profile info + EntityInfo defaultProfileInfo = client.getDefaultTenantProfileInfo(); + assertNotNull(defaultProfileInfo); + assertNotNull(defaultProfileInfo.getName()); + + try { + // create 5 tenant profiles + for (int i = 0; i < 5; i++) { + TenantProfile profile = new TenantProfile(); + profile.setName(TEST_PREFIX + "TenantProfile_" + timestamp + "_" + i); + profile.setDescription("Test tenant profile " + i); + profile.setIsolatedTbRuleEngine(false); + + TenantProfileData profileData = new TenantProfileData(); + DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration(); + config.setMaxDevices(100L); + config.setMaxAssets(100L); + config.setMaxCustomers(50L); + config.setMaxUsers(50L); + config.setMaxDashboards(50L); + config.setMaxRuleChains(20L); + config.setMaxDataPointsPerRollingArg(20L); + config.setMaxRelatedEntitiesToReturnPerCfArgument(20); + config.setMaxRelationLevelPerCfArgument(20); + profileData.setConfiguration(config); + profile.setProfileData(profileData); + profile.setDefault(false); + + TenantProfile created = client.saveTenantProfile(profile); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(profile.getName(), created.getName()); + assertEquals(profile.getDescription(), created.getDescription()); + assertFalse(created.getDefault()); + + createdProfiles.add(created); + } + + // find all, check count + PageDataTenantProfile allProfiles = client.getTenantProfiles(100, 0, null, null, null); + assertNotNull(allProfiles); + assertEquals(initialSize + 5, allProfiles.getData().size()); + + // find with text search + PageDataTenantProfile filteredProfiles = client.getTenantProfiles(100, 0, + TEST_PREFIX + "TenantProfile_" + timestamp, null, null); + assertEquals(5, filteredProfiles.getData().size()); + + // get by id + TenantProfile searchProfile = createdProfiles.get(2); + TenantProfile fetchedProfile = client.getTenantProfileById(searchProfile.getId().getId().toString()); + assertEquals(searchProfile.getName(), fetchedProfile.getName()); + assertEquals(searchProfile.getDescription(), fetchedProfile.getDescription()); + + // update tenant profile + fetchedProfile.setDescription("Updated description"); + TenantProfile updatedProfile = client.saveTenantProfile(fetchedProfile); + assertEquals("Updated description", updatedProfile.getDescription()); + assertEquals(fetchedProfile.getName(), updatedProfile.getName()); + + // get tenant profile infos (paginated) + PageDataEntityInfo profileInfos = client.getTenantProfileInfos(100, 0, null, null, null); + assertNotNull(profileInfos); + assertEquals(initialSize + 5, profileInfos.getData().size()); + + // get profiles by list of ids + List idsToFetch = List.of( + createdProfiles.get(0).getId().getId().toString(), + createdProfiles.get(1).getId().getId().toString() + ); + List profileList = client.getTenantProfileList(idsToFetch); + assertEquals(2, profileList.size()); + + // set a profile as default + TenantProfile profileToSetDefault = createdProfiles.get(1); + client.setDefaultTenantProfile(profileToSetDefault.getId().getId().toString()); + EntityInfo defaultTenantProfileInfo = client.getDefaultTenantProfileInfo(); + assertEquals(profileToSetDefault.getName(), defaultTenantProfileInfo.getName()); + + // verify default profile info now points to the new default + EntityInfo newDefaultInfo = client.getDefaultTenantProfileInfo(); + assertEquals(profileToSetDefault.getName(), newDefaultInfo.getName()); + + // restore original default profile + TenantProfile originalDefault = initialProfiles.getData().stream() + .filter(TenantProfile::getDefault) + .findFirst() + .orElseThrow(); + client.setDefaultTenantProfile(originalDefault.getId().getId().toString()); + + // delete tenant profile (cannot delete the default one) + UUID profileToDeleteId = createdProfiles.get(0).getId().getId(); + client.deleteTenantProfile(profileToDeleteId.toString()); + createdProfiles.remove(0); + + // verify deletion + assertReturns404(() -> + client.getTenantProfileById(profileToDeleteId.toString()) + ); + + PageDataTenantProfile profilesAfterDelete = client.getTenantProfiles(100, 0, null, null, null); + assertEquals(initialSize + 4, profilesAfterDelete.getData().size()); + } finally { + // clean up created profiles + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // ensure original default is restored before deleting test profiles + TenantProfile originalDefault = initialProfiles.getData().stream() + .filter(TenantProfile::getDefault) + .findFirst() + .orElseThrow(); + try { + client.setDefaultTenantProfile(originalDefault.getId().getId().toString()); + } catch (ApiException ignored) { + } + + for (TenantProfile profile : createdProfiles) { + try { + client.deleteTenantProfile(profile.getId().getId().toString()); + } catch (ApiException ignored) { + } + } + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java new file mode 100644 index 0000000000..5f371b5d82 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/TwoFactorAuthApiClientTest.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.AccountTwoFaSettings; +import org.thingsboard.client.model.PlatformTwoFaSettings; +import org.thingsboard.client.model.TotpTwoFaAccountConfig; +import org.thingsboard.client.model.TotpTwoFaProviderConfig; +import org.thingsboard.client.model.TwoFaAccountConfig; +import org.thingsboard.client.model.TwoFaProviderType; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@DaoSqlTest +public class TwoFactorAuthApiClientTest extends AbstractApiClientTest { + + @Test + public void testTwoFactorAuthLifecycle() throws Exception { + // save original platform 2FA settings as sysadmin + client.login("sysadmin@thingsboard.org", "sysadmin"); + + // configure platform 2FA settings with TOTP provider + TotpTwoFaProviderConfig totpProviderConfig = new TotpTwoFaProviderConfig(); + totpProviderConfig.setIssuerName("TestThingsBoard"); + + PlatformTwoFaSettings newSettings = new PlatformTwoFaSettings(); + newSettings.setProviders(List.of(totpProviderConfig)); + newSettings.setMinVerificationCodeSendPeriod(30); + newSettings.setTotalAllowedTimeForVerification(300); + newSettings.setMaxVerificationFailuresBeforeUserLockout(5); + + PlatformTwoFaSettings savedSettings = client.savePlatformTwoFaSettings(newSettings); + assertNotNull(savedSettings); + assertNotNull(savedSettings.getProviders()); + assertFalse(savedSettings.getProviders().isEmpty()); + assertEquals(30, savedSettings.getMinVerificationCodeSendPeriod().intValue()); + assertEquals(300, savedSettings.getTotalAllowedTimeForVerification().intValue()); + + // get available 2FA providers (should include TOTP) + List providerTypes = client.getAvailableTwoFaProviderTypes(); + assertNotNull(providerTypes); + assertTrue(providerTypes.contains(TwoFaProviderType.TOTP)); + + // get account 2FA settings (should be empty initially) + AccountTwoFaSettings accountSettings = client.getAccountTwoFaSettings(); + assertNull(accountSettings); + + // generate TOTP account config + TwoFaAccountConfig generatedConfig = client.generateTwoFaAccountConfig(TwoFaProviderType.TOTP.getValue()); + assertNotNull(generatedConfig); + TotpTwoFaAccountConfig totpConfig = (TotpTwoFaAccountConfig) generatedConfig; + assertNotNull(totpConfig); + assertNotNull(totpConfig.getAuthUrl()); + assertTrue(totpConfig.getAuthUrl().startsWith("otpauth://totp/")); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java new file mode 100644 index 0000000000..154858405f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/UserApiClientTest.java @@ -0,0 +1,135 @@ +/** + * Copyright © 2016-2026 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.client; + +import org.junit.Test; +import org.thingsboard.client.model.Authority; +import org.thingsboard.client.model.Customer; +import org.thingsboard.client.model.JwtPair; +import org.thingsboard.client.model.PageDataUser; +import org.thingsboard.client.model.User; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class UserApiClientTest extends AbstractApiClientTest { + + @Test + public void testUserLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdUsers = new ArrayList<>(); + + // create 20 tenant admin users + for (int i = 0; i < 20; i++) { + User user = new User(); + String email = ((i % 2 == 0) ? TEST_PREFIX : TEST_PREFIX_2) + timestamp + "_" + i + "@test.com"; + user.setEmail(email); + user.setAuthority(Authority.TENANT_ADMIN); + user.setTenantId(savedClientTenant.getId()); + user.setFirstName("First" + i); + user.setLastName("Last" + i); + + User createdUser = client.saveUser(user, "false"); + assertNotNull(createdUser); + assertNotNull(createdUser.getId()); + assertEquals(email, createdUser.getEmail()); + assertEquals(Authority.TENANT_ADMIN, createdUser.getAuthority()); + + createdUsers.add(createdUser); + } + + // find all tenant admins, check count (20 created + 1 from setup) + PageDataUser allUsers = client.getUsers(100, 0, null, null, null); + assertNotNull(allUsers); + assertNotNull(allUsers.getData()); + int initialSize = allUsers.getData().size(); + assertEquals("Expected 21 users (20 created + 2 from setup), but got " + initialSize, 22, initialSize); + + // find with search text, check count + PageDataUser filteredUsers = client.getUsers(100, 0, TEST_PREFIX_2, null, null); + assertEquals("Expected exactly 10 users matching prefix", 10, filteredUsers.getData().size()); + + // find by id + User searchUser = createdUsers.get(10); + User fetchedUser = client.getUserById(searchUser.getId().getId().toString()); + assertEquals(searchUser.getEmail(), fetchedUser.getEmail()); + assertEquals(searchUser.getFirstName(), fetchedUser.getFirstName()); + + // update user + fetchedUser.setFirstName("UpdatedFirst"); + fetchedUser.setLastName("UpdatedLast"); + User updatedUser = client.saveUser(fetchedUser, "false"); + assertEquals("UpdatedFirst", updatedUser.getFirstName()); + assertEquals("UpdatedLast", updatedUser.getLastName()); + + // activate user and get token + activateUser(createdUsers.get(0).getId(), "password123", false); + JwtPair userToken = client.getUserToken(createdUsers.get(0).getId().getId().toString()); + assertNotNull(userToken); + assertNotNull(userToken.getToken()); + + // disable user credentials + client.setUserCredentialsEnabled(createdUsers.get(0).getId().getId().toString(), "false"); + + // re-enable user credentials + client.setUserCredentialsEnabled(createdUsers.get(0).getId().getId().toString(), "true"); + + // create customer users and verify listing + Customer customer2 = new Customer(); + customer2.setTitle("User test customer " + timestamp); + customer2.setEmail("usertest_" + timestamp + "@test.com"); + Customer savedCustomer2 = client.saveCustomer(customer2, null, null, null); + + List customerUsers = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + User customerUser = new User(); + customerUser.setEmail("custuser_" + timestamp + "_" + i + "@test.com"); + customerUser.setAuthority(Authority.CUSTOMER_USER); + customerUser.setTenantId(savedClientTenant.getId()); + customerUser.setCustomerId(savedCustomer2.getId()); + customerUser.setFirstName("CustFirst" + i); + customerUser.setLastName("CustLast" + i); + + User created = client.saveUser(customerUser, "false"); + assertNotNull(created); + customerUsers.add(created); + } + + // list customer users + PageDataUser customerUserPage = client.getCustomerUsers( + savedCustomer2.getId().getId().toString(), 100, 0, null, null, null); + assertEquals("Expected 5 customer users", 5, customerUserPage.getData().size()); + + // delete user + UUID userToDeleteId = createdUsers.get(0).getId().getId(); + client.deleteUser(userToDeleteId.toString()); + + // verify deletion + PageDataUser usersAfterDelete = client.getUsers(100, 0, null, null, null); + assertEquals(initialSize + 5 - 1, usersAfterDelete.getData().size()); + + assertReturns404(() -> + client.getUserById(userToDeleteId.toString()) + ); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java b/application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java new file mode 100644 index 0000000000..ee3984bb4b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/client/WidgetTypeApiClientTest.java @@ -0,0 +1,155 @@ +/** + * Copyright © 2016-2026 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.client; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Test; +import org.thingsboard.client.model.PageDataWidgetTypeInfo; +import org.thingsboard.client.model.WidgetTypeDetails; +import org.thingsboard.client.model.WidgetTypeInfo; +import org.thingsboard.client.model.WidgetsBundle; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@DaoSqlTest +public class WidgetTypeApiClientTest extends AbstractApiClientTest { + + private JsonNode createDescriptor(String type) { + return OBJECT_MAPPER.createObjectNode() + .put("type", type) + .put("sizeX", 7.5) + .put("sizeY", 5) + .put("resources", "[]") + .put("templateHtml", "
Test
") + .put("templateCss", ".test-widget { font-size: 14px; }") + .put("controllerScript", "self.onInit = function() {};") + .put("settingsSchema", "{}") + .put("dataKeySettingsSchema", "{}"); + } + + @Test + public void testWidgetTypeLifecycle() throws Exception { + long timestamp = System.currentTimeMillis(); + List createdWidgetTypes = new ArrayList<>(); + + // create a widgets bundle + WidgetsBundle bundle = new WidgetsBundle(null, null, null, + TEST_PREFIX + "Bundle_" + timestamp, null, false, + "Test bundle description", null, null); + WidgetsBundle savedBundle = client.saveWidgetsBundle(bundle); + assertNotNull(savedBundle); + assertNotNull(savedBundle.getId()); + assertEquals(bundle.getTitle(), savedBundle.getTitle()); + + // create 5 widget types + for (int i = 0; i < 5; i++) { + String name = TEST_PREFIX + "Widget_" + timestamp + "_" + i; + JsonNode descriptor = createDescriptor("latest"); + + WidgetTypeDetails widgetType = new WidgetTypeDetails(null, null, null, name, descriptor); + widgetType.setDescription("Test widget " + i); + widgetType.setDeprecated(false); + widgetType.setTags(List.of("test", "automated")); + + WidgetTypeDetails created = client.saveWidgetType(widgetType, false); + assertNotNull(created); + assertNotNull(created.getId()); + assertEquals(name, created.getName()); + assertNotNull(created.getFqn()); + + createdWidgetTypes.add(created); + } + + // list widget types with text search (tenant only) + PageDataWidgetTypeInfo filteredTypes = client.getWidgetTypes(100, 0, + TEST_PREFIX + "Widget_" + timestamp, null, null, + true, false, null, null, null); + assertNotNull(filteredTypes); + assertEquals(5, filteredTypes.getData().size()); + + // get widget type details by id + WidgetTypeDetails searchWidget = createdWidgetTypes.get(2); + WidgetTypeDetails fetchedDetails = client.getWidgetTypeById( + searchWidget.getId().getId().toString(), true); + assertEquals(searchWidget.getName(), fetchedDetails.getName()); + assertEquals(searchWidget.getFqn(), fetchedDetails.getFqn()); + assertEquals("Test widget 2", fetchedDetails.getDescription()); + + // get widget type info by id + WidgetTypeInfo fetchedInfo = client.getWidgetTypeInfoById( + searchWidget.getId().getId().toString()); + assertEquals(searchWidget.getName(), fetchedInfo.getName()); + + // add widget types to bundle + List widgetTypeIds = createdWidgetTypes.stream() + .map(wt -> wt.getId().getId().toString()) + .collect(Collectors.toList()); + client.updateWidgetsBundleWidgetTypes(savedBundle.getId().getId().toString(), widgetTypeIds); + + // get bundle widget type fqns + List bundleFqns = client.getBundleWidgetTypeFqns(savedBundle.getId().getId().toString()); + assertEquals(5, bundleFqns.size()); + + // get bundle widget types details + List bundleDetails = client.getBundleWidgetTypesDetails( + savedBundle.getId().getId().toString(), false); + assertEquals(5, bundleDetails.size()); + + // get bundle widget types infos (paginated) + PageDataWidgetTypeInfo bundleInfos = client.getBundleWidgetTypesInfos( + savedBundle.getId().getId().toString(), 100, 0, + null, null, null, null, null, null); + assertEquals(5, bundleInfos.getData().size()); + + // update widget type + WidgetTypeDetails widgetToUpdate = client.getWidgetTypeById( + createdWidgetTypes.get(3).getId().getId().toString(), true); + widgetToUpdate.setDescription("Updated description"); + widgetToUpdate.setDeprecated(true); + widgetToUpdate.setTags(List.of("test", "updated")); + WidgetTypeDetails updatedWidget = client.saveWidgetType(widgetToUpdate, false); + assertEquals("Updated description", updatedWidget.getDescription()); + assertEquals(true, updatedWidget.getDeprecated()); + + // delete widget type + String widgetToDeleteId = createdWidgetTypes.get(0).getId().getId().toString(); + client.deleteWidgetType(widgetToDeleteId); + + // verify deletion + assertReturns404(() -> + client.getWidgetTypeById(widgetToDeleteId, false) + ); + + PageDataWidgetTypeInfo typesAfterDelete = client.getWidgetTypes(100, 0, + TEST_PREFIX + "Widget_" + timestamp, null, null, + true, false, null, null, null); + assertEquals(4, typesAfterDelete.getData().size()); + + // delete widgets bundle + client.deleteWidgetsBundle(savedBundle.getId().getId().toString()); + + assertReturns404(() -> + client.getWidgetsBundleById(savedBundle.getId().getId().toString(), false) + ); + } + +} 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 6a658ffd66..055e1e19a0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -94,6 +94,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -1482,6 +1483,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doPost("/api/calculatedField", calculatedField, CalculatedField.class); } + protected AlarmRuleDefinition saveAlarmRule(AlarmRuleDefinition alarmRule) { + return doPost("/api/alarm/rule", alarmRule, AlarmRuleDefinition.class); + } + protected PageData getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java new file mode 100644 index 0000000000..7c64fd6525 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java @@ -0,0 +1,420 @@ +/** + * Copyright © 2016-2026 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.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinition; +import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class AlarmRuleControllerTest extends AbstractControllerTest { + + private Tenant savedTenant; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = saveTenant(tenant); + assertThat(savedTenant).isNotNull(); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + deleteTenant(savedTenant.getId()); + } + + @Test + public void testSaveAlarmRule() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "High Temperature"); + + AlarmRuleDefinition saved = saveAlarmRule(alarmRule); + + assertThat(saved).isNotNull(); + assertThat(saved.getId()).isNotNull(); + assertThat(saved.getCreatedTime()).isGreaterThan(0); + assertThat(saved.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(saved.getEntityId()).isEqualTo(testDevice.getId()); + assertThat(saved.getName()).isEqualTo("High Temperature"); + assertThat(saved.getConfiguration()).isNotNull(); + assertThat(saved.getConfiguration().getCreateRules()).containsKey(AlarmSeverity.CRITICAL); + + saved.setName("Updated Alarm Rule"); + AlarmRuleDefinition updated = saveAlarmRule(saved); + + assertThat(updated.getName()).isEqualTo("Updated Alarm Rule"); + assertThat(updated.getVersion()).isEqualTo(saved.getVersion() + 1); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetAlarmRuleById() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "Test Alarm"); + + AlarmRuleDefinition saved = saveAlarmRule(alarmRule); + AlarmRuleDefinition fetched = doGet("/api/alarm/rule/" + saved.getId().getId(), AlarmRuleDefinition.class); + + assertThat(fetched).isNotNull(); + assertThat(fetched).isEqualTo(saved); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetAlarmRuleById_notFound() throws Exception { + doGet("/api/alarm/rule/" + UUID.randomUUID()) + .andExpect(status().isNotFound()); + } + + @Test + public void testGetAlarmRuleById_calculatedFieldNotAlarm() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField cf = createSimpleCalculatedField(testDevice.getId()); + CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class); + + doGet("/api/alarm/rule/" + savedCf.getId().getId()) + .andExpect(status().isNotFound()); + + doDelete("/api/calculatedField/" + savedCf.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetAlarmRulesByEntityId() throws Exception { + Device device1 = createDevice("Device 1", "1234567890"); + Device device2 = createDevice("Device 2", "0987654321"); + AlarmRuleDefinition rule1 = saveAlarmRule(createTestAlarmRule(device1.getId(), "Rule 1")); + saveAlarmRule(createTestAlarmRule(device2.getId(), "Rule 2")); + + PageData result = doGetTypedWithPageLink( + "/api/alarm/rules/" + EntityType.DEVICE + "/" + device1.getUuidId() + "?", + new TypeReference<>() {}, new PageLink(10)); + + assertThat(result.getData()).hasSize(1); + assertThat(result.getData().get(0).getId()).isEqualTo(rule1.getId()); + assertThat(result.getData().get(0).getName()).isEqualTo("Rule 1"); + } + + @Test + public void testGetAlarmRules() throws Exception { + Device device = createDevice("Device A", "1234567890"); + AlarmRuleDefinition deviceRule = saveAlarmRule(createTestAlarmRule(device.getId(), "Device Alarm")); + + DeviceProfile profile = doPost("/api/deviceProfile", createDeviceProfile("Profile A"), DeviceProfile.class); + AlarmRuleDefinition profileRule = saveAlarmRule(createTestAlarmRule(profile.getId(), "Profile Alarm")); + + // All alarm rules + List all = getAlarmRules(null, null); + assertThat(all).extracting(AlarmRuleDefinition::getName) + .contains("Device Alarm", "Profile Alarm"); + + // Filter by entity type: DEVICE + List deviceRules = getAlarmRules(EntityType.DEVICE, null); + assertThat(deviceRules).extracting(AlarmRuleDefinition::getName) + .containsOnly("Device Alarm"); + + // Filter by entity type: DEVICE_PROFILE + List profileRules = getAlarmRules(EntityType.DEVICE_PROFILE, null); + assertThat(profileRules).extracting(AlarmRuleDefinition::getName) + .containsOnly("Profile Alarm"); + + // Filter by specific entity IDs + List specificRules = getAlarmRules(EntityType.DEVICE, List.of(device.getUuidId())); + assertThat(specificRules).extracting(AlarmRuleDefinition::getName) + .containsOnly("Device Alarm"); + + // Verify entity names are populated + AlarmRuleDefinitionInfo deviceInfo = all.stream() + .filter(r -> r.getName().equals("Device Alarm")).findFirst().orElseThrow(); + assertThat(deviceInfo.getEntityName()).isEqualTo("Device A"); + + AlarmRuleDefinitionInfo profileInfo = all.stream() + .filter(r -> r.getName().equals("Profile Alarm")).findFirst().orElseThrow(); + assertThat(profileInfo.getEntityName()).isEqualTo("Profile A"); + } + + @Test + public void testGetAlarmRules_textSearch() throws Exception { + Device device = createDevice("Device A", "1234567890"); + saveAlarmRule(createTestAlarmRule(device.getId(), "Temperature Alarm")); + saveAlarmRule(createTestAlarmRule(device.getId(), "Humidity Alarm")); + + PageData result = doGetTypedWithPageLink( + "/api/alarm/rules?textSearch=Temp&", + new TypeReference<>() {}, new PageLink(10)); + + assertThat(result.getData()).hasSize(1); + assertThat(result.getData().get(0).getName()).isEqualTo("Temperature Alarm"); + } + + @Test + public void testGetAlarmRuleNames() throws Exception { + Device device = createDevice("Device A", "1234567890"); + saveAlarmRule(createTestAlarmRule(device.getId(), "Alpha Alarm")); + saveAlarmRule(createTestAlarmRule(device.getId(), "Beta Alarm")); + + PageData names = getAlarmRuleNames(new PageLink(10, 0, + null, new SortOrder("", SortOrder.Direction.ASC))); + assertThat(names.getTotalElements()).isEqualTo(2); + assertThat(names.getData()).isSortedAccordingTo(Comparator.naturalOrder()); + assertThat(names.getData()).contains("Alpha Alarm", "Beta Alarm"); + + names = getAlarmRuleNames(new PageLink(10, 0, + null, new SortOrder("", SortOrder.Direction.DESC))); + assertThat(names.getData()).isSortedAccordingTo(Comparator.reverseOrder()); + + names = getAlarmRuleNames(new PageLink(10, 0, + "Alpha", new SortOrder("", SortOrder.Direction.ASC))); + assertThat(names.getTotalElements()).isEqualTo(1); + assertThat(names.getData()).containsOnly("Alpha Alarm"); + } + + @Test + public void testDeleteAlarmRule() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "To Delete")); + + assertThat(saved).isNotNull(); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + doGet("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isNotFound()); + } + + @Test + public void testDeleteAlarmRule_notFound() throws Exception { + doDelete("/api/alarm/rule/" + UUID.randomUUID()) + .andExpect(status().isNotFound()); + } + + @Test + public void testDeleteAlarmRule_calculatedFieldNotAlarm() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField cf = createSimpleCalculatedField(testDevice.getId()); + CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class); + + doDelete("/api/alarm/rule/" + savedCf.getId().getId()) + .andExpect(status().isNotFound()); + + doDelete("/api/calculatedField/" + savedCf.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetLatestAlarmRuleDebugEvent() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "Debug Test")); + + doGet("/api/alarm/rule/" + saved.getId().getId() + "/debug") + .andExpect(status().isOk()); + + doDelete("/api/alarm/rule/" + saved.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testGetLatestAlarmRuleDebugEvent_notFound() throws Exception { + doGet("/api/alarm/rule/" + UUID.randomUUID() + "/debug") + .andExpect(status().isNotFound()); + } + + @Test + public void testTestAlarmRuleScript() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "expression": "return temperature > 50;", + "arguments": { + "temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 55 } + } + } + """); + + JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); + + assertThat(result).isNotNull(); + assertThat(result.has("output")).isTrue(); + assertThat(result.has("error")).isTrue(); + assertThat(result.get("error").asText()).isEmpty(); + assertThat(result.get("output").asText()).isEqualTo("true"); + } + + @Test + public void testTestAlarmRuleScript_returnsFalse() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "expression": "return temperature > 50;", + "arguments": { + "temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 30 } + } + } + """); + + JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); + + assertThat(result).isNotNull(); + assertThat(result.get("error").asText()).isEmpty(); + assertThat(result.get("output").asText()).isEqualTo("false"); + } + + @Test + public void testTestAlarmRuleScript_missingExpression() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "arguments": {} + } + """); + + doPost("/api/alarm/rule/testScript", request) + .andExpect(status().isBadRequest()); + } + + @Test + public void testTestAlarmRuleScript_invalidExpression() throws Exception { + JsonNode request = JacksonUtil.toJsonNode(""" + { + "expression": "invalid syntax {{{{", + "arguments": {} + } + """); + + JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class); + + assertThat(result).isNotNull(); + assertThat(result.get("error").asText()).isNotEmpty(); + } + + // --- Helper methods --- + + private AlarmRuleDefinition createTestAlarmRule(EntityId entityId, String name) { + AlarmRuleDefinition alarmRule = new AlarmRuleDefinition(); + alarmRule.setEntityId(entityId); + alarmRule.setName(name); + alarmRule.setConfigurationVersion(1); + alarmRule.setAdditionalInfo(JacksonUtil.newObjectNode()); + + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("0"); + configuration.setArguments(Map.of("temperature", argument)); + + AlarmRule rule = new AlarmRule(); + TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression(); + expression.setExpression("return temperature >= 50;"); + SimpleAlarmCondition condition = new SimpleAlarmCondition(); + condition.setExpression(expression); + rule.setCondition(condition); + configuration.setCreateRules(Map.of(AlarmSeverity.CRITICAL, rule)); + + alarmRule.setConfiguration(configuration); + return alarmRule; + } + + private CalculatedField createSimpleCalculatedField(EntityId entityId) { + CalculatedField cf = new CalculatedField(); + cf.setEntityId(entityId); + cf.setType(CalculatedFieldType.SIMPLE); + cf.setName("Simple CF"); + cf.setConfigurationVersion(1); + cf.setAdditionalInfo(JacksonUtil.newObjectNode()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + config.setArguments(Map.of("T", arg)); + config.setExpression("T * 2"); + TimeSeriesOutput output = new TimeSeriesOutput(); + output.setName("result"); + config.setOutput(output); + cf.setConfiguration(config); + + return cf; + } + + private List getAlarmRules(EntityType entityType, List entities) throws Exception { + StringBuilder url = new StringBuilder("/api/alarm/rules?"); + if (entityType != null) { + url.append("entityType=").append(entityType).append("&"); + } + if (entities != null) { + url.append("entities=").append(String.join(",", + entities.stream().map(UUID::toString).toList())).append("&"); + } + return doGetTypedWithPageLink(url.toString(), + new TypeReference>() {}, new PageLink(10)).getData(); + } + + private PageData getAlarmRuleNames(PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/alarm/rules/names?", + new TypeReference>() {}, pageLink); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java index d5458ce793..193dc57c36 100644 --- a/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java @@ -266,7 +266,6 @@ public class NotificationEdgeTest extends AbstractEdgeTest { notificationRule.setTriggerConfig(triggerConfig); EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM); Map> escalationTable = new HashMap<>(); escalationTable.put(Integer.valueOf("1"), new ArrayList<>()); recipientsConfig.setEscalationTable(escalationTable); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java index cbde708ff8..02b063a499 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java @@ -234,6 +234,8 @@ public class RelatedEntitiesAggregationCalculatedFieldStateTest { config.setUseLatestTs(true); + config.setScheduledUpdateInterval(10); + calculatedField.setConfiguration(config); calculatedField.setVersion(1L); return calculatedField; diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java index f5607415fa..cf99669777 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -188,8 +188,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest rule.setTriggerType(triggerConfig.getTriggerType()); rule.setTriggerConfig(triggerConfig); - DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(triggerConfig.getTriggerType()); + DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(triggerConfig.getTriggerType()); recipientsConfig.setTargets(DaoUtil.toUUIDs(targets)); rule.setRecipientsConfig(recipientsConfig); diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index e3342dbf43..94af26d3d7 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -211,7 +211,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { notificationRule.setTriggerConfig(triggerConfig); EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM); Map> escalationTable = new HashMap<>(); recipientsConfig.setEscalationTable(escalationTable); Map clients = new HashMap<>(); @@ -329,7 +328,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { notificationRule.setTriggerConfig(triggerConfig); EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM); Map> escalationTable = new HashMap<>(); recipientsConfig.setEscalationTable(escalationTable); @@ -640,8 +638,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); rule.setTriggerConfig(triggerConfig); - DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); + DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId())); rule.setRecipientsConfig(recipientsConfig); rule = saveNotificationRule(rule); @@ -671,8 +668,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { triggerConfig.setCreated(true); rule.setTriggerConfig(triggerConfig); NotificationTarget target = createNotificationTarget(tenantAdminUserId); - DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); + DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); recipientsConfig.setTargets(List.of(target.getUuidId())); rule.setRecipientsConfig(recipientsConfig); rule = saveNotificationRule(rule); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java index da72fb67d7..d854fa3eaf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.validation.NoXss; @@ -35,9 +36,11 @@ import java.util.function.Supplier; * Created by ashvayka on 19.02.18. */ @Slf4j +@Schema public abstract class BaseDataWithAdditionalInfo extends BaseData implements HasAdditionalInfo { @NoXss + @Schema private transient JsonNode additionalInfo; @JsonIgnore private byte[] additionalInfoBytes; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index 150dbf2a5c..956cf054dd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -134,13 +134,18 @@ public class Customer extends ContactBased implements HasTenantId, E return super.getPhone(); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Email", example = "example@company.com") + @Schema(description = "Email", example = "example@company.com") @Override public String getEmail() { return super.getEmail(); } - @Schema(description = "Additional parameters of the customer",implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the customer. " + + "May include: 'description' (string), 'homeDashboardId' (string, UUID of the home dashboard), " + + "'homeDashboardHideToolbar' (boolean, whether to hide the dashboard toolbar), " + + "'isPublic' (boolean, whether this is a public customer).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"Regional customer\",\"homeDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"homeDashboardHideToolbar\":false,\"isPublic\":false}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java index 168158d427..2c65a3dfc8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java @@ -33,7 +33,7 @@ import java.util.Optional; import java.util.stream.Collectors; @EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"title", "image", "mobileHide", "mobileOrder", "configuration", "name", "resources"}) +@JsonPropertyOrder({"id", "createdTime", "tenantId", "title", "name", "image", "mobileHide", "mobileOrder", "assignedCustomers", "configuration", "resources", "version"}) public class Dashboard extends DashboardInfo implements ExportableEntity { private static final long serialVersionUID = 872682138346187503L; @@ -70,7 +70,7 @@ public class Dashboard extends DashboardInfo implements ExportableEntity implements HasL this.label = label; } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Device Profile Id.") + @Schema(description = "JSON object with Device Profile Id. If not provided, the type will be used to determine the profile. If neither deviceProfileId nor type is specified, the default device profile will be used.") public DeviceProfileId getDeviceProfileId() { return deviceProfileId; } @@ -231,7 +231,12 @@ public class Device extends BaseDataWithAdditionalInfo implements HasL this.softwareId = softwareId; } - @Schema(description = "Additional parameters of the device",implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the device. " + + "May include: 'gateway' (boolean, whether the device is a gateway), " + + "'description' (string), " + + "'lastConnectedGateway' (string, UUID of the last gateway that connected this device).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"gateway\":false,\"description\":\"Temperature sensor\",\"lastConnectedGateway\":\"784f394c-42b6-435a-983c-b7beff2784f9\"}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java index 8bf65873a3..e21b8d3cd0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java @@ -22,6 +22,7 @@ import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.Value; import org.thingsboard.server.common.data.id.DashboardId; +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.TenantId; @@ -75,4 +76,10 @@ public class DeviceProfileInfo extends EntityInfo { profile.getType(), profile.getTransportType()); } + @Override + @Schema(implementation = DeviceProfileId.class, description = "JSON object with the Device Profile Id.") + public EntityId getId() { + return super.getId(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java index d8d73cd37e..5da4f2cd91 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java @@ -116,7 +116,10 @@ public class EntityView extends BaseDataWithAdditionalInfo return super.getCreatedTime(); } - @Schema(description = "Additional parameters of the entity view", implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the entity view. " + + "May include: 'description' (string).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"Temperature readings view\"}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java index 2a3b95ba0c..92a446fcb6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/FeaturesInfo.java @@ -15,13 +15,29 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@JsonPropertyOrder({ + "emailEnabled", + "smsEnabled", + "notificationEnabled", + "oauthEnabled", + "twoFaEnabled" +}) +@Schema @Data public class FeaturesInfo { + @JsonProperty("emailEnabled") boolean isEmailEnabled; + @JsonProperty("smsEnabled") boolean isSmsEnabled; + @JsonProperty("notificationEnabled") boolean isNotificationEnabled; + @JsonProperty("oauthEnabled") boolean isOauthEnabled; + @JsonProperty("twoFaEnabled") boolean isTwoFaEnabled; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index 246fba56c5..647b584666 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -43,17 +43,17 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp @Schema(description = "JSON object with Tenant Id. Tenant Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY) private TenantId tenantId; - @Schema(description = "JSON object with Device Profile Id. Device Profile Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "JSON object with Device Profile Id. Device Profile Id of the ota package can't be changed.") private DeviceProfileId deviceProfileId; - @Schema(description = "OTA Package type.", example = "FIRMWARE", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "OTA Package type.", example = "FIRMWARE") private OtaPackageType type; @Length(fieldName = "title") @NoXss - @Schema(description = "OTA Package title.", example = "fw", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "OTA Package title.", example = "fw") private String title; @Length(fieldName = "version") @NoXss - @Schema(description = "OTA Package version.", example = "1.0", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "OTA Package version.", example = "1.0") private String version; @Length(fieldName = "tag") @NoXss @@ -61,7 +61,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp private String tag; @Length(fieldName = "url") @NoXss - @Schema(description = "OTA Package url.", example = "http://thingsboard.org/fw/1", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "OTA Package url.", example = "http://thingsboard.org/fw/1") private String url; @Schema(description = "Indicates OTA Package 'has data'. Field is returned from DB ('true' if data exists or url is set). If OTA Package 'has data' is 'false' we can not assign the OTA Package to the Device or Device Profile.", example = "true", accessMode = Schema.AccessMode.READ_ONLY) private boolean hasData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java index 930e4dddca..f1de65f1f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @@ -39,9 +40,15 @@ public class ShortCustomerInfo { @NoXss private String title; + @JsonProperty("public") @Schema(description = "Indicates special 'Public' customer used to embed dashboards on public websites.") - @Getter @Setter - private boolean isPublic; + private boolean publicCustomer; + + @JsonProperty("public") + public boolean isPublic() { return publicCustomer; } + + @JsonProperty("public") + public void setPublic(boolean publicCustomer) { this.publicCustomer = publicCustomer; } @Override public boolean equals(Object o) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java index 73bd3cddc4..3fd86d1163 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemInfo.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -23,6 +24,7 @@ import java.util.List; @Data public class SystemInfo { @Schema(description = "Is monolith.") + @JsonProperty("monolith") private boolean isMonolith; @Schema(description = "System data.") private List systemData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index 4365357d2d..4343205b74 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -28,6 +28,7 @@ import java.io.Serial; import java.util.Base64; import java.util.Optional; +@Schema @Slf4j @Data @EqualsAndHashCode(callSuper = true) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index f1cca4e80c..16e95d79ea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -46,15 +46,17 @@ public class TbResourceInfo extends BaseData implements HasName, H @Length(fieldName = "title") @Schema(description = "Resource title.", example = "BinaryAppDataContainer id=19 v1.0") private String title; - @Schema(description = "Resource type.", example = "LWM2M_MODEL", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Resource type.", example = "LWM2M_MODEL") private ResourceType resourceType; - @Schema(description = "Resource sub type.", example = "IOT_SVG", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Resource sub type.", example = "IOT_SVG") private ResourceSubType resourceSubType; @NoXss @Length(fieldName = "resourceKey") - @Schema(description = "Resource key.", example = "19_1.0", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Resource key.", example = "19_1.0") private String resourceKey; + @Schema(description = "Whether the resource is public.", example = "false") private boolean isPublic; + @Schema(description = "Public resource key.") private String publicResourceKey; @Schema(description = "Resource search text.", example = "19_1.0:binaryappdatacontainer", accessMode = Schema.AccessMode.READ_ONLY) private String searchText; @@ -63,10 +65,12 @@ public class TbResourceInfo extends BaseData implements HasName, H private String etag; @NoXss @Length(fieldName = "file name") - @Schema(description = "Resource file name.", example = "19.xml", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Resource file name.", example = "19.xml") private String fileName; + @Schema(description = "Resource descriptor.") private JsonNode descriptor; + @Schema(description = "External resource Id used for import/export.") private TbResourceId externalId; public TbResourceInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index cdbcef8331..374e6b526a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -164,7 +164,11 @@ public class Tenant extends ContactBased implements HasTenantId, HasTi return super.getEmail(); } - @Schema(description = "Additional parameters of the tenant", implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the tenant. " + + "May include: 'description' (string), 'homeDashboardId' (string, UUID of the home dashboard), " + + "'homeDashboardHideToolbar' (boolean, whether to hide the dashboard toolbar).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"Main tenant\",\"homeDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"homeDashboardHideToolbar\":true}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java index 69ba115af1..e1b40b48f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java @@ -91,7 +91,7 @@ public class User extends BaseDataWithAdditionalInfo implements HasName, return super.getCreatedTime(); } - @Schema(description = "JSON object with the Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "JSON object with the Tenant Id.") public TenantId getTenantId() { return tenantId; } @@ -100,7 +100,7 @@ public class User extends BaseDataWithAdditionalInfo implements HasName, this.tenantId = tenantId; } - @Schema(description = "JSON object with the Customer Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "JSON object with the Customer Id.") public CustomerId getCustomerId() { return customerId; } @@ -161,7 +161,16 @@ public class User extends BaseDataWithAdditionalInfo implements HasName, this.phone = phone; } - @Schema(description = "Additional parameters of the user", implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the user. " + + "May include: 'defaultDashboardId' (string, UUID of the default dashboard), " + + "'defaultDashboardFullscreen' (boolean), " + + "'homeDashboardId' (string, UUID of the home dashboard), " + + "'homeDashboardHideToolbar' (boolean), " + + "'lang' (string, user locale, e.g. 'en_US'), " + + "'authProviderName' (string, name of the authentication provider).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"defaultDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"defaultDashboardFullscreen\":false," + + "\"homeDashboardId\":\"784f394c-42b6-435a-983c-b7beff2784f9\",\"homeDashboardHideToolbar\":true,\"lang\":\"en_US\"}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java index ffca023b23..4d4fcfa58e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java @@ -46,7 +46,6 @@ public final class AiModel extends BaseData implements HasTenantId, H private static final long serialVersionUID = 9017108678716011604L; @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "JSON object representing the ID of the tenant associated with this AI model", example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" @@ -54,7 +53,6 @@ public final class AiModel extends BaseData implements HasTenantId, H private TenantId tenantId; @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "Version of the AI model record; increments automatically whenever the record is changed", example = "7", diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java index a553d66bf0..f1004790d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java @@ -17,8 +17,17 @@ package org.thingsboard.server.common.data.ai.dto; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + description = "Response from chat API", + discriminatorProperty = "status", + discriminatorMapping = { + @DiscriminatorMapping(value = "SUCCESS", schema = TbChatResponse.Success.class), + @DiscriminatorMapping(value = "FAILURE", schema = TbChatResponse.Failure.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = "status", @@ -33,7 +42,8 @@ public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatRes @Schema( description = "Indicates whether the request was successful or not", - example = "SUCCESS" + example = "SUCCESS", + requiredMode = Schema.RequiredMode.REQUIRED ) String getStatus(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.java index 55f35a3960..4edf49045b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.java @@ -19,11 +19,18 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import dev.langchain4j.data.message.Content; import dev.langchain4j.data.message.TextContent; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import static org.thingsboard.server.common.data.ai.dto.TbContent.TbTextContent; +@Schema( + discriminatorProperty = "contentType", + discriminatorMapping = { + @DiscriminatorMapping(value = "TEXT", schema = TbTextContent.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -46,7 +53,8 @@ public sealed interface TbContent permits TbTextContent { } @Schema( - description = "Text-based content part of a user's prompt" + description = "Text-based content part of a user's prompt", + allOf = TbContent.class ) record TbTextContent( @NotBlank diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.java index 2d060dbf4a..e1f0b84a4d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.ai.dto; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; @@ -24,9 +25,12 @@ import java.util.List; public record TbUserMessage( @NotEmpty @Valid - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - description = "A list of content parts that make up the complete user prompt" + @ArraySchema( + arraySchema = @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + description = "A list of content parts that make up the complete user prompt" + ), + schema = @Schema(ref = "#/components/schemas/TbContent") ) List contents ) {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index adcc176edc..d036969254 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.ai.model; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig; @@ -55,10 +57,31 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS"), @JsonSubTypes.Type(value = OllamaChatModelConfig.class, name = "OLLAMA") }) +@Schema( + name = "AiModelConfig", + description = "Root configuration for AI models", + discriminatorProperty = "provider", + discriminatorMapping = { + @DiscriminatorMapping(value = "OPENAI", schema = OpenAiChatModelConfig.class), + @DiscriminatorMapping(value = "AZURE_OPENAI", schema = AzureOpenAiChatModelConfig.class), + @DiscriminatorMapping(value = "GOOGLE_AI_GEMINI", schema = GoogleAiGeminiChatModelConfig.class), + @DiscriminatorMapping(value = "GOOGLE_VERTEX_AI_GEMINI", schema = GoogleVertexAiGeminiChatModelConfig.class), + @DiscriminatorMapping(value = "MISTRAL_AI", schema = MistralAiChatModelConfig.class), + @DiscriminatorMapping(value = "ANTHROPIC", schema = AnthropicChatModelConfig.class), + @DiscriminatorMapping(value = "AMAZON_BEDROCK", schema = AmazonBedrockChatModelConfig.class), + @DiscriminatorMapping(value = "GITHUB_MODELS", schema = GitHubModelsChatModelConfig.class), + @DiscriminatorMapping(value = "OLLAMA", schema = OllamaChatModelConfig.class) + } +) public interface AiModelConfig { + @Schema( + description = "AI Provider", + requiredMode = Schema.RequiredMode.REQUIRED + ) AiProvider provider(); + @Schema(hidden = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java index ee80230fee..490ae67eb2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; +@Schema @Builder public record AmazonBedrockChatModelConfig( + @Schema(ref = "#/components/schemas/AmazonBedrockProviderConfig") @NotNull @Valid AmazonBedrockProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java index 44b55c4c22..77352b06c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; +@Schema @Builder public record AnthropicChatModelConfig( + @Schema(ref = "#/components/schemas/AnthropicProviderConfig") @NotNull @Valid AnthropicProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java index 1e0d6d0c18..d4df82d237 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; +@Schema @Builder public record AzureOpenAiChatModelConfig( + @Schema(ref = "#/components/schemas/AzureOpenAiProviderConfig") @NotNull @Valid AzureOpenAiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java index 606f30599b..264683f7b4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig; +@Schema @Builder public record GitHubModelsChatModelConfig( + @Schema(ref = "#/components/schemas/GitHubModelsProviderConfig") @NotNull @Valid GitHubModelsProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java index 422c92ba97..82ba3a0295 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; +@Schema @Builder public record GoogleAiGeminiChatModelConfig( + @Schema(ref = "#/components/schemas/GoogleAiGeminiProviderConfig") @NotNull @Valid GoogleAiGeminiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java index 3e298e52e2..4d20a240e6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; +@Schema @Builder public record GoogleVertexAiGeminiChatModelConfig( + @Schema(ref = "#/components/schemas/GoogleVertexAiGeminiProviderConfig") @NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java index 5713217fe9..60c208c181 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; +@Schema @Builder public record MistralAiChatModelConfig( + @Schema(ref = "#/components/schemas/MistralAiProviderConfig") @NotNull @Valid MistralAiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java index 3f1856f630..5f7776a0a6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; +@Schema @Builder public record OllamaChatModelConfig( + @Schema(ref = "#/components/schemas/OllamaProviderConfig") @NotNull @Valid OllamaProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java index f74292cd76..3845aaa543 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.ai.model.chat; import dev.langchain4j.model.chat.ChatModel; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -27,8 +28,10 @@ import lombok.With; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +@Schema @Builder public record OpenAiChatModelConfig( + @Schema(ref = "#/components/schemas/OpenAiProviderConfig") @NotNull @Valid OpenAiProviderConfig providerConfig, @NotBlank String modelId, @PositiveOrZero Double temperature, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java index dc38440a66..17e886e946 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record AmazonBedrockProviderConfig( @NotNull String region, @NotNull String accessKeyId, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java index 6e7abeee84..4108a72439 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record AnthropicProviderConfig( @NotNull String apiKey ) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java index 05d9e99569..6687f8a08e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record AzureOpenAiProviderConfig( @NotNull String endpoint, String serviceVersion, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.java index 56409c81ad..529eb2b989 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record GitHubModelsProviderConfig( @NotNull String personalAccessToken ) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java index 3492a1096d..5b20ed3d96 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record GoogleAiGeminiProviderConfig( @NotNull String apiKey ) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java index 0e0928e038..72c4b5e940 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +@Schema public record GoogleVertexAiGeminiProviderConfig( @NotBlank String fileName, // not used on BE, but needed for UI @NotNull String projectId, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java index 46165f9843..b0ca42de16 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.ai.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +@Schema public record MistralAiProviderConfig( @NotNull String apiKey ) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java index 0a4da6c66c..606cc75b9d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java @@ -17,14 +17,26 @@ package org.thingsboard.server.common.data.ai.provider; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +@Schema public record OllamaProviderConfig( @NotNull String baseUrl, @NotNull @Valid OllamaAuth auth ) implements AiProviderConfig { + @Schema( + description = "Ollama authentication schemes", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "NONE", schema = OllamaAuth.None.class), + @DiscriminatorMapping(value = "BASIC", schema = OllamaAuth.Basic.class), + @DiscriminatorMapping(value = "TOKEN", schema = OllamaAuth.Token.class) + } + ) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java index 59eabc58a6..900eb6734c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java @@ -16,12 +16,14 @@ package org.thingsboard.server.common.data.ai.provider; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; import lombok.Builder; import org.apache.commons.lang3.StringUtils; import java.util.Objects; +@Schema @Builder public record OpenAiProviderConfig( String baseUrl, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index abd1515ff2..9f9bdec49d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -84,6 +84,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha @Schema(description = "Timestamp of the alarm assignment, in milliseconds", example = "1634115928465") private long assignTs; @Schema(description = "JSON object with alarm details") + @JsonProperty private transient JsonNode details; @Schema(description = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") private boolean propagate; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java index 0a011c287f..ef7499fa33 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java @@ -45,7 +45,7 @@ public class AlarmComment extends BaseData implements HasName { private AlarmId alarmId; @Schema(description = "JSON object with User id.", accessMode = Schema.AccessMode.READ_ONLY) private UserId userId; - @Schema(description = "Defines origination of comment. System type means comment was created by TB. OTHER type means comment was created by user.", example = "SYSTEM/OTHER", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Defines origination of comment. System type means comment was created by TB. OTHER type means comment was created by user.", example = "SYSTEM/OTHER") private AlarmCommentType type; @Schema(description = "JSON object with text of comment.") @NoXss @@ -56,7 +56,7 @@ public class AlarmComment extends BaseData implements HasName { @Schema(description = "JSON object with the alarm comment Id. " + "Specify this field to update the alarm comment. " + "Referencing non-existing alarm Id will cause error. " + - "Omit this field to create new alarm.", accessMode = Schema.AccessMode.READ_ONLY) + "Omit this field to create new alarm.") @Override public AlarmCommentId getId() { return super.getId(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java index 93370c0c30..3ab3f0a526 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java @@ -22,8 +22,10 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.id.DashboardId; +@Schema @Data @AllArgsConstructor @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java index 1d50f3343c..87bd44ce1f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; @@ -29,6 +31,14 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmC import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SIMPLE", schema = SimpleAlarmCondition.class), + @DiscriminatorMapping(value = "DURATION", schema = DurationAlarmCondition.class), + @DiscriminatorMapping(value = "REPEATING", schema = RepeatingAlarmCondition.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java index 0a4a995ab0..3599ee5c3c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java @@ -21,8 +21,11 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.concurrent.TimeUnit; +@Schema @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java index 28472d0556..42cba04ec0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; +@Schema @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java index 73dfac9e60..f91ff1f3f0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema public class SimpleAlarmCondition extends AlarmCondition { @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java index e02d438c0d..5251f385df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java @@ -20,7 +20,16 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SIMPLE", schema = SimpleAlarmConditionExpression.class), + @DiscriminatorMapping(value = "TBEL", schema = TbelAlarmConditionExpression.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java index 448d4586ff..f14e97a704 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition.expression; +import io.swagger.v3.oas.annotations.media.ArraySchema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; @@ -25,9 +26,12 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.predic import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; import org.thingsboard.server.common.data.query.EntityKeyValueType; +import io.swagger.v3.oas.annotations.media.Schema; + import java.io.Serializable; import java.util.List; +@Schema @Data public class AlarmConditionFilter implements Serializable { @@ -36,6 +40,7 @@ public class AlarmConditionFilter implements Serializable { @NotNull private EntityKeyValueType valueType; private ComplexOperation operation; + @ArraySchema(schema = @Schema(ref = "#/components/schemas/AlarmRuleKeyFilterPredicate")) @Valid @NotEmpty private List predicates; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java index 492fc683dd..9ed265f3b8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition.expression; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "AlarmRuleComplexOperation") public enum ComplexOperation { AND, OR diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java index ec5407b19d..390bbb3230 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition.expression; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java index 15731ae1fb..64a73d2f6d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java @@ -18,8 +18,10 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predi import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +@Schema(name = "AlarmRuleBooleanFilterPredicate") @Data public class BooleanFilterPredicate implements SimpleKeyFilterPredicate { @@ -29,11 +31,13 @@ public class BooleanFilterPredicate implements SimpleKeyFilterPredicate @NotNull private AlarmConditionValue value; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, ref = "#/components/schemas/AlarmRuleFilterPredicateType") @Override public FilterPredicateType getType() { return FilterPredicateType.BOOLEAN; } + @Schema(name = "AlarmRuleBooleanOperation") public enum BooleanOperation { EQUAL, NOT_EQUAL diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java index 5fa7c17107..17c5279b3e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java @@ -15,17 +15,22 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; import java.util.List; +@Schema(name = "AlarmRuleComplexFilterPredicate") @Data public class ComplexFilterPredicate implements KeyFilterPredicate { private ComplexOperation operation; + @ArraySchema(schema = @Schema(ref = "#/components/schemas/AlarmRuleKeyFilterPredicate")) private List predicates; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, ref = "#/components/schemas/AlarmRuleFilterPredicateType") @Override public FilterPredicateType getType() { return FilterPredicateType.COMPLEX; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java index 3b5fa638d8..de05017130 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(name = "AlarmRuleFilterPredicateType") public enum FilterPredicateType { STRING, NUMERIC, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java index 24f7a6acf6..d6409d5d67 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java @@ -19,9 +19,23 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import java.io.Serializable; +@Schema( + name = "AlarmRuleKeyFilterPredicate", + description = "Filter predicate for alarm rule key-based filtering", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "STRING", schema = StringFilterPredicate.class), + @DiscriminatorMapping(value = "NUMERIC", schema = NumericFilterPredicate.class), + @DiscriminatorMapping(value = "BOOLEAN", schema = BooleanFilterPredicate.class), + @DiscriminatorMapping(value = "NO_DATA", schema = NoDataFilterPredicate.class), + @DiscriminatorMapping(value = "COMPLEX", schema = ComplexFilterPredicate.class) + } +) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @Type(value = StringFilterPredicate.class, name = "STRING"), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java index d6f733d2c1..1f719cb80c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java @@ -20,10 +20,12 @@ import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import java.util.concurrent.TimeUnit; +@Schema @Data @AllArgsConstructor @NoArgsConstructor @@ -35,6 +37,7 @@ public class NoDataFilterPredicate implements KeyFilterPredicate { @NotNull private AlarmConditionValue duration; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, ref = "#/components/schemas/AlarmRuleFilterPredicateType") @Override public FilterPredicateType getType() { return FilterPredicateType.NO_DATA; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java index 5622836c93..5bddbc4387 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java @@ -20,8 +20,10 @@ import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +@Schema(name = "AlarmRuleNumericFilterPredicate") @Data @AllArgsConstructor @NoArgsConstructor @@ -33,11 +35,13 @@ public class NumericFilterPredicate implements SimpleKeyFilterPredicate @NotNull private AlarmConditionValue value; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, ref = "#/components/schemas/AlarmRuleFilterPredicateType") @Override public FilterPredicateType getType() { return FilterPredicateType.NUMERIC; } + @Schema(name = "AlarmRuleNumericOperation") public enum NumericOperation { EQUAL, NOT_EQUAL, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java index 952a5c2d46..1d2a3785db 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +@Schema(name = "AlarmRuleSimpleKeyFilterPredicate") public interface SimpleKeyFilterPredicate extends KeyFilterPredicate { AlarmConditionValue getValue(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java index 1053f21271..602730b7e0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java @@ -18,8 +18,10 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predi import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +@Schema(name = "AlarmRuleStringFilterPredicate") @Data public class StringFilterPredicate implements SimpleKeyFilterPredicate { @@ -30,11 +32,13 @@ public class StringFilterPredicate implements SimpleKeyFilterPredicate { private AlarmConditionValue value; private boolean ignoreCase; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, ref = "#/components/schemas/AlarmRuleFilterPredicateType") @Override public FilterPredicateType getType() { return FilterPredicateType.STRING; } + @Schema(name= "AlarmRuleStringOperation") public enum StringOperation { EQUAL, NOT_EQUAL, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java index e764a9fb46..127487b471 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java @@ -21,8 +21,19 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; + import java.io.Serializable; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "ANY_TIME", schema = AnyTimeSchedule.class), + @DiscriminatorMapping(value = "SPECIFIC_TIME", schema = SpecificTimeSchedule.class), + @DiscriminatorMapping(value = "CUSTOM", schema = CustomTimeSchedule.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java index 73e9e80159..28093fd845 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition.schedule; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema public enum AlarmScheduleType { ANY_TIME, SPECIFIC_TIME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java index 2d0454c16e..873e1f882d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition.schedule; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema public class AnyTimeSchedule implements AlarmSchedule { @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java index bbb1fd0390..aea9db5be4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition.schedule; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; +@Schema @Data public class CustomTimeSchedule implements AlarmSchedule { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java index 47d60e732b..d0756a3195 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java @@ -17,8 +17,11 @@ package org.thingsboard.server.common.data.alarm.rule.condition.schedule; import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + import java.io.Serializable; +@Schema @Data public class CustomTimeScheduleItem implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java index 733081b81f..a23dbe48a7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.alarm.rule.condition.schedule; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.Set; +@Schema @Data public class SpecificTimeSchedule implements AlarmSchedule { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index ad53f070f6..000f99f303 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -169,7 +169,10 @@ public class Asset extends BaseDataWithAdditionalInfo implements HasLab this.assetProfileId = assetProfileId; } - @Schema(description = "Additional parameters of the asset",implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the asset. " + + "May include: 'description' (string).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"Building A asset\"}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java index e5d0855989..a2d879ff0e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java @@ -23,6 +23,7 @@ import lombok.ToString; import lombok.Value; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -66,4 +67,10 @@ public class AssetProfileInfo extends EntityInfo { this(profile.getId(), profile.getTenantId(), profile.getName(), profile.getImage(), profile.getDefaultDashboardId()); } + @Override + @Schema(implementation = AssetProfileId.class, description = "JSON object with the Asset Profile Id.") + public EntityId getId() { + return super.getId(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index fbb5e9cac7..27502eb70e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -22,42 +22,203 @@ import java.util.Optional; public enum ActionType { - ADDED(TbMsgType.ENTITY_CREATED), // log entity - DELETED(TbMsgType.ENTITY_DELETED), // log string id - UPDATED(TbMsgType.ENTITY_UPDATED), // log entity - ATTRIBUTES_UPDATED(TbMsgType.ATTRIBUTES_UPDATED), // log attributes/values - ATTRIBUTES_DELETED(TbMsgType.ATTRIBUTES_DELETED), // log attributes - TIMESERIES_UPDATED(TbMsgType.TIMESERIES_UPDATED), // log timeseries update - TIMESERIES_DELETED(TbMsgType.TIMESERIES_DELETED), // log timeseries - RPC_CALL, // log method and params - CREDENTIALS_UPDATED, // log new credentials - ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED), // log customer name - UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED), // log customer name - ACTIVATED, // log string id - SUSPENDED, // log string id - CREDENTIALS_READ(true), // log device id - ATTRIBUTES_READ(true), // log attributes + /** + * Entity created. Pushes {@link TbMsgType#ENTITY_CREATED} to rule engine. + * Audit log payload: full entity JSON. + */ + ADDED(TbMsgType.ENTITY_CREATED), + /** + * Entity deleted. Pushes {@link TbMsgType#ENTITY_DELETED} to rule engine. + * Audit log payload: entity string id. + */ + DELETED(TbMsgType.ENTITY_DELETED), + /** + * Entity updated. Pushes {@link TbMsgType#ENTITY_UPDATED} to rule engine. + * Audit log payload: full entity JSON. + */ + UPDATED(TbMsgType.ENTITY_UPDATED), + /** + * Server-side or shared attributes updated via API. + * Pushes {@link TbMsgType#ATTRIBUTES_UPDATED} to rule engine. + * Rule engine msg metadata includes {@code scope} ({@code SERVER_SCOPE} or {@code SHARED_SCOPE}). + * Rule engine msg data: key-value pairs of the updated attributes. + * Audit log payload: updated attributes and their values. + */ + ATTRIBUTES_UPDATED(TbMsgType.ATTRIBUTES_UPDATED), + /** + * Attributes deleted via API. + * Pushes {@link TbMsgType#ATTRIBUTES_DELETED} to rule engine. + * Rule engine msg metadata includes {@code scope} ({@code SERVER_SCOPE} or {@code SHARED_SCOPE}). + * Rule engine msg data: {@code {"attributes": ["key1", "key2"]}}. + * Audit log payload: list of deleted attribute keys. + */ + ATTRIBUTES_DELETED(TbMsgType.ATTRIBUTES_DELETED), + /** + * Timeseries data saved via API (not from device transport). + * Pushes {@link TbMsgType#TIMESERIES_UPDATED} to rule engine. + * Rule engine msg data: {@code {"timeseries": [{"ts": ..., "values": {...}}, ...]}}. + * Audit log payload: timeseries entries. + */ + TIMESERIES_UPDATED(TbMsgType.TIMESERIES_UPDATED), + /** + * Timeseries data deleted via API. + * Pushes {@link TbMsgType#TIMESERIES_DELETED} to rule engine. + * Rule engine msg data: {@code {"timeseries": ["key1", ...], "startTs": ..., "endTs": ...}}. + * Audit log payload: deleted timeseries keys. + */ + TIMESERIES_DELETED(TbMsgType.TIMESERIES_DELETED), + /** + * RPC call to device. Does not push to rule engine (RPC has its own lifecycle messages). + * Audit log payload: RPC method and params. + */ + RPC_CALL, + /** + * Device credentials updated. Does not push to rule engine. + * Audit log payload: new credentials value. + */ + CREDENTIALS_UPDATED, + /** + * Entity assigned to a customer. Pushes {@link TbMsgType#ENTITY_ASSIGNED} to rule engine. + * Rule engine msg metadata includes {@code assignedCustomerId} and {@code assignedCustomerName}. + * Audit log payload: customer name. + */ + ASSIGNED_TO_CUSTOMER(TbMsgType.ENTITY_ASSIGNED), + /** + * Entity unassigned from a customer. Pushes {@link TbMsgType#ENTITY_UNASSIGNED} to rule engine. + * Rule engine msg metadata includes {@code unassignedCustomerId} and {@code unassignedCustomerName}. + * Audit log payload: customer name. + */ + UNASSIGNED_FROM_CUSTOMER(TbMsgType.ENTITY_UNASSIGNED), + /** + * User account or integration activated. Does not push to rule engine. + * Audit log payload: entity string id. + */ + ACTIVATED, + /** + * User account or integration suspended. Does not push to rule engine. + * Audit log payload: entity string id. + */ + SUSPENDED, + /** + * Device credentials read. Read-only action. Does not push to rule engine. + * Audit log payload: device id. + */ + CREDENTIALS_READ(true), + /** + * Attributes read. Read-only action. Does not push to rule engine. + * Audit log payload: attribute keys read. + */ + ATTRIBUTES_READ(true), + /** + * Relation created or updated. Pushes {@link TbMsgType#RELATION_ADD_OR_UPDATE} to rule engine. + * Rule engine msg data: relation JSON ({@code from}, {@code to}, {@code type}, {@code typeGroup}). + */ RELATION_ADD_OR_UPDATE(TbMsgType.RELATION_ADD_OR_UPDATE), + /** + * Relation deleted. Pushes {@link TbMsgType#RELATION_DELETED} to rule engine. + * Rule engine msg data: relation JSON ({@code from}, {@code to}, {@code type}, {@code typeGroup}). + */ RELATION_DELETED(TbMsgType.RELATION_DELETED), + /** + * All relations for an entity deleted. Pushes {@link TbMsgType#RELATIONS_DELETED} to rule engine. + * Rule engine msg data: empty JSON object. + */ RELATIONS_DELETED(TbMsgType.RELATIONS_DELETED), - REST_API_RULE_ENGINE_CALL, // log call to rule engine from REST API + /** + * REST API call to rule engine. Does not push to rule engine directly + * (the REST controller creates a {@link TbMsgType#REST_API_REQUEST} message itself). + * Audit log payload: call details. + */ + REST_API_RULE_ENGINE_CALL, + /** + * Alarm acknowledged by a user. Pushes {@link TbMsgType#ALARM_ACK} to rule engine. + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ALARM_ACK(TbMsgType.ALARM_ACK, true), + /** + * Alarm cleared by a user. Pushes {@link TbMsgType#ALARM_CLEAR} to rule engine. + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ALARM_CLEAR(TbMsgType.ALARM_CLEAR, true), + /** + * Alarm deleted by a user. Pushes {@link TbMsgType#ALARM_DELETE} to rule engine. + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ALARM_DELETE(TbMsgType.ALARM_DELETE, true), + /** + * Alarm assigned to a user. Pushes {@link TbMsgType#ALARM_ASSIGNED} to rule engine. + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ALARM_ASSIGNED(TbMsgType.ALARM_ASSIGNED, true), + /** + * Alarm unassigned from a user. Pushes {@link TbMsgType#ALARM_UNASSIGNED} to rule engine. + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ALARM_UNASSIGNED(TbMsgType.ALARM_UNASSIGNED, true), + /** + * User logged in. Does not push to rule engine. + */ LOGIN, + /** + * User logged out. Does not push to rule engine. + */ LOGOUT, + /** + * User account locked out due to too many failed login attempts. Does not push to rule engine. + */ LOCKOUT, + /** + * Entity assigned from another tenant (incoming side of cross-tenant transfer). + * Pushes {@link TbMsgType#ENTITY_ASSIGNED_FROM_TENANT} to rule engine. + * Rule engine msg metadata includes {@code assignedFromTenantId} and {@code assignedFromTenantName}. + */ ASSIGNED_FROM_TENANT(TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), + /** + * Entity assigned to another tenant (outgoing side of cross-tenant transfer). + * Pushes {@link TbMsgType#ENTITY_ASSIGNED_TO_TENANT} to rule engine. + * Rule engine msg metadata includes {@code assignedToTenantId} and {@code assignedToTenantName}. + */ ASSIGNED_TO_TENANT(TbMsgType.ENTITY_ASSIGNED_TO_TENANT), + /** + * Device provisioned successfully. Pushes {@link TbMsgType#PROVISION_SUCCESS} to rule engine. + * Rule engine msg data: full device JSON. + */ PROVISION_SUCCESS(TbMsgType.PROVISION_SUCCESS), + /** + * Device provisioning failed. Pushes {@link TbMsgType#PROVISION_FAILURE} to rule engine. + * Rule engine msg data: full device JSON. + */ PROVISION_FAILURE(TbMsgType.PROVISION_FAILURE), - ASSIGNED_TO_EDGE(TbMsgType.ENTITY_ASSIGNED_TO_EDGE), // log edge name + /** + * Entity assigned to an Edge instance. Pushes {@link TbMsgType#ENTITY_ASSIGNED_TO_EDGE} to rule engine. + * Rule engine msg metadata includes {@code assignedEdgeId} and {@code assignedEdgeName}. + * Audit log payload: edge name. + */ + ASSIGNED_TO_EDGE(TbMsgType.ENTITY_ASSIGNED_TO_EDGE), + /** + * Entity unassigned from an Edge instance. Pushes {@link TbMsgType#ENTITY_UNASSIGNED_FROM_EDGE} to rule engine. + * Rule engine msg metadata includes {@code unassignedEdgeId} and {@code unassignedEdgeName}. + */ UNASSIGNED_FROM_EDGE(TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), + /** + * Comment added to an alarm. Pushes {@link TbMsgType#COMMENT_CREATED} to rule engine. + * Rule engine msg metadata includes {@code comment} (JSON string of the AlarmComment object). + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ ADDED_COMMENT(TbMsgType.COMMENT_CREATED), + /** + * Alarm comment updated. Pushes {@link TbMsgType#COMMENT_UPDATED} to rule engine. + * Rule engine msg metadata includes {@code comment} (JSON string of the AlarmComment object). + * Rule engine msg data: full alarm JSON. Originator: alarm id. + */ UPDATED_COMMENT(TbMsgType.COMMENT_UPDATED), + /** + * Alarm comment deleted. Does not push to rule engine. + */ DELETED_COMMENT, + /** + * SMS sent. Does not push to rule engine. + */ SMS_SENT; @Getter diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java new file mode 100644 index 0000000000..bd0c76bc05 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java @@ -0,0 +1,166 @@ +/** + * Copyright © 2016-2026 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.cf; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasAdditionalInfo; +import org.thingsboard.server.common.data.HasDebugSettings; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +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.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +@Schema +@Data +@EqualsAndHashCode(callSuper = true) +public class AlarmRuleDefinition extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo { + + private TenantId tenantId; + private EntityId entityId; + + @NoXss + @Length(fieldName = "name") + @Schema(description = "User defined name of the alarm rule.") + private String name; + @Deprecated + @Schema(description = "Enable/disable debug. ", example = "false", deprecated = true) + private boolean debugMode; + @Schema(description = "Debug settings object.") + private DebugSettings debugSettings; + @Schema(description = "Version of alarm rule configuration.", example = "0") + private int configurationVersion; + @Schema(implementation = AlarmCalculatedFieldConfiguration.class) + @Valid + @NotNull + private AlarmCalculatedFieldConfiguration configuration; + private Long version; + @NoXss + @Schema(description = "Additional parameters of the alarm rule. " + + "May include: 'description' (string).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"High temperature alarm rule\"}") + private JsonNode additionalInfo; + + public AlarmRuleDefinition() {} + + public AlarmRuleDefinition(CalculatedFieldId id) { + super(id); + } + + public AlarmRuleDefinition(AlarmRuleDefinition alarmRuleDefinition) { + super(alarmRuleDefinition); + this.tenantId = alarmRuleDefinition.tenantId; + this.entityId = alarmRuleDefinition.entityId; + this.name = alarmRuleDefinition.name; + this.debugMode = alarmRuleDefinition.debugMode; + this.debugSettings = alarmRuleDefinition.debugSettings; + this.configurationVersion = alarmRuleDefinition.configurationVersion; + this.configuration = alarmRuleDefinition.configuration; + this.version = alarmRuleDefinition.version; + this.additionalInfo = alarmRuleDefinition.additionalInfo; + } + + @Schema(description = "JSON object with the Alarm Rule Id. Referencing non-existing Alarm Rule Id will cause error.") + @Override + public CalculatedFieldId getId() { + return super.getId(); + } + + @Schema(description = "Timestamp of the alarm rule creation, in milliseconds", example = "1609459200000", accessMode = Schema.AccessMode.READ_ONLY) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + // Getter is ignored for serialization + @JsonIgnore + public boolean isDebugMode() { + return debugMode; + } + + // Setter is annotated for deserialization + @JsonSetter + public void setDebugMode(boolean debugMode) { + this.debugMode = debugMode; + } + + public CalculatedField toCalculatedField() { + CalculatedField cf = new CalculatedField(); + cf.setId(this.id); + cf.setCreatedTime(this.createdTime); + cf.setTenantId(this.tenantId); + cf.setEntityId(this.entityId); + cf.setType(CalculatedFieldType.ALARM); + cf.setName(this.name); + cf.setDebugMode(this.debugMode); + cf.setDebugSettings(this.debugSettings); + cf.setConfigurationVersion(this.configurationVersion); + cf.setConfiguration(this.configuration); + cf.setVersion(this.version); + cf.setAdditionalInfo(this.additionalInfo); + return cf; + } + + public static AlarmRuleDefinition fromCalculatedField(CalculatedField cf) { + AlarmRuleDefinition def = new AlarmRuleDefinition(); + def.setId(cf.getId()); + def.setCreatedTime(cf.getCreatedTime()); + def.setTenantId(cf.getTenantId()); + def.setEntityId(cf.getEntityId()); + def.setName(cf.getName()); + def.setDebugMode(cf.isDebugMode()); + def.setDebugSettings(cf.getDebugSettings()); + def.setConfigurationVersion(cf.getConfigurationVersion()); + if (!(cf.getConfiguration() instanceof AlarmCalculatedFieldConfiguration config)) { + throw new IllegalArgumentException("Expected ALARM calculated field, got " + cf.getType()); + } + def.setConfiguration(config); + def.setVersion(cf.getVersion()); + def.setAdditionalInfo(cf.getAdditionalInfo()); + return def; + } + + @Override + public String toString() { + return new StringBuilder() + .append("AlarmRuleDefinition[") + .append("tenantId=").append(tenantId) + .append(", entityId=").append(entityId) + .append(", name='").append(name) + .append(", configurationVersion=").append(configurationVersion) + .append(", configuration=").append(configuration) + .append(", additionalInfo=").append(additionalInfo) + .append(", version=").append(version) + .append(", createdTime=").append(createdTime) + .append(", id=").append(id).append(']') + .toString(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java new file mode 100644 index 0000000000..145dfc4081 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 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.cf; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class AlarmRuleDefinitionInfo extends AlarmRuleDefinition { + + private String entityName; + + public AlarmRuleDefinitionInfo(AlarmRuleDefinition alarmRuleDefinition, String entityName) { + super(alarmRuleDefinition); + this.entityName = entityName; + } + + public static AlarmRuleDefinitionInfo fromCalculatedFieldInfo(CalculatedFieldInfo cfi) { + AlarmRuleDefinition def = AlarmRuleDefinition.fromCalculatedField(cfi); + return new AlarmRuleDefinitionInfo(def, cfi.getEntityName()); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java index 2dfa82f9ab..2d3928ee88 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java @@ -82,7 +82,7 @@ public class CalculatedField extends BaseData implements HasN private DebugSettings debugSettings; @Schema(description = "Version of calculated field configuration.", example = "0") private int configurationVersion; - @Schema(implementation = SimpleCalculatedFieldConfiguration.class) + @Schema(implementation = CalculatedFieldConfiguration.class) @Valid @NotNull private CalculatedFieldConfiguration configuration; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java index ef3869fccb..d22666723b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import lombok.Data; @@ -34,6 +35,7 @@ import java.util.stream.Stream; import static java.util.Map.Entry.comparingByKey; +@Schema @Data public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java index 9c5f84f75b..b41e296cba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java @@ -16,10 +16,12 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.lang.Nullable; import org.thingsboard.server.common.data.id.EntityId; +@Schema @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class Argument { 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 1dd87a3974..852eefc4a4 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import org.thingsboard.server.common.data.id.EntityId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java index 2a17232f0f..4fbac188bd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +@Schema @Data @AllArgsConstructor @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java index 02b453b20a..22e566e39d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java @@ -18,7 +18,16 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "IMMEDIATE", schema = AttributesImmediateOutputStrategy.class), + @DiscriminatorMapping(value = "RULE_CHAIN", schema = AttributesRuleChainOutputStrategy.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java index 5d0b4a3abe..935d97fabc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; +@Schema @Data @NoArgsConstructor public class AttributesRuleChainOutputStrategy implements AttributesOutputStrategy { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java index d2c6ebb78c..0ba9db43cb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.Map; +@Schema @Data public abstract class BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration { 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 18213b0b7b..73d130c16f 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 @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; @@ -34,6 +36,18 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SIMPLE", schema = SimpleCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "SCRIPT", schema = ScriptCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "GEOFENCING", schema = GeofencingCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "ALARM", schema = AlarmCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "PROPAGATION", schema = PropagationCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "RELATED_ENTITIES_AGGREGATION", schema = RelatedEntitiesAggregationCalculatedFieldConfiguration.class), + @DiscriminatorMapping(value = "ENTITY_AGGREGATION", schema = EntityAggregationCalculatedFieldConfiguration.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java index 90c4b3fa39..72b7f88bf8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -19,7 +19,16 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "RELATION_PATH_QUERY", schema = RelationPathQueryDynamicSourceConfiguration.class), + @DiscriminatorMapping(value = "CURRENT_OWNER", schema = CurrentOwnerDynamicSourceConfiguration.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java index 46df6cbad7..2ee6901e9c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java @@ -20,10 +20,19 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.AttributeScope; import java.util.Objects; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "TIME_SERIES", schema = TimeSeriesOutput.class), + @DiscriminatorMapping(value = "ATTRIBUTES", schema = AttributesOutput.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java index d0ae8789b9..7390424b3d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.relation.RelationPathLevel; import java.util.List; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class PropagationCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements HasRelationPathLevel, HasUseLatestTsConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ReferencedEntityKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ReferencedEntityKey.java index 59627751d0..fd36d1bd61 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ReferencedEntityKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ReferencedEntityKey.java @@ -16,10 +16,12 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.AttributeScope; +@Schema @Data @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java index 208bb3cc80..b59f69afc1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -28,6 +30,7 @@ import java.util.List; @Data public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { + @ArraySchema(schema = @Schema(implementation = RelationPathLevel.class)) private List levels; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java index f5b45c6d5c..f78ff72af0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -22,14 +22,14 @@ public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends Ca boolean isScheduledUpdateEnabled(); @PositiveOrZero - int getScheduledUpdateInterval(); + Integer getScheduledUpdateInterval(); - void setScheduledUpdateInterval(int interval); + void setScheduledUpdateInterval(Integer interval); default void validate(long minAllowedScheduledUpdateInterval) { if (getScheduledUpdateInterval() < minAllowedScheduledUpdateInterval) { - throw new IllegalArgumentException("Scheduled update interval is less than configured " + - "minimum allowed interval in tenant profile: " + minAllowedScheduledUpdateInterval); + throw new IllegalArgumentException("Scheduled update interval (" + getScheduledUpdateInterval() + + " seconds) is less than minimum allowed interval in tenant profile: " + minAllowedScheduledUpdateInterval + " seconds"); } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java index 464f7f54e4..40c28967b3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data +@Schema @EqualsAndHashCode(callSuper = true) public class ScriptCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java index 53210d1cf8..e3ca07b5be 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data +@Schema @EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration, HasUseLatestTsConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java index a16b9cab89..942a734908 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class TimeSeriesOutput implements Output { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java index 327cad3d5f..aca968f422 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java @@ -17,7 +17,16 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "IMMEDIATE", schema = TimeSeriesImmediateOutputStrategy.class), + @DiscriminatorMapping(value = "RULE_CHAIN", schema = TimeSeriesRuleChainOutputStrategy.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java index d4ebb231a2..1d68d3e8ee 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; +@Schema @Data @NoArgsConstructor public class TimeSeriesRuleChainOutputStrategy implements TimeSeriesOutputStrategy { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java index 32db324db9..4c9c0915f3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java @@ -19,7 +19,16 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "key", schema = AggKeyInput.class), + @DiscriminatorMapping(value = "function", schema = AggFunctionInput.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java index d905248aa5..7cb833ab3a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.aggregation; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -22,6 +23,7 @@ import lombok.Data; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.HasRelationPathLevel; import org.thingsboard.server.common.data.cf.configuration.HasUseLatestTsConfig; import org.thingsboard.server.common.data.cf.configuration.Output; @@ -30,6 +32,7 @@ import org.thingsboard.server.common.data.relation.RelationPathLevel; import java.util.Map; +@Schema @Data public class RelatedEntitiesAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration, HasRelationPathLevel, HasUseLatestTsConfig { @@ -45,7 +48,7 @@ public class RelatedEntitiesAggregationCalculatedFieldConfiguration implements A private Output output; private boolean useLatestTs; - private int scheduledUpdateInterval; + private Integer scheduledUpdateInterval; @Override public CalculatedFieldType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java index 53235136bb..70be733928 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.aggregation.single; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -23,6 +24,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; @@ -31,6 +33,7 @@ import org.thingsboard.server.common.data.cf.configuration.aggregation.single.in import java.util.Map; +@Schema @Data public class EntityAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java index 2eada7f165..55ffb0243b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java @@ -19,10 +19,25 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZoneId; import java.time.ZonedDateTime; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "HOUR", schema = HourInterval.class), + @DiscriminatorMapping(value = "DAY", schema = DayInterval.class), + @DiscriminatorMapping(value = "WEEK", schema = WeekInterval.class), + @DiscriminatorMapping(value = "WEEK_SUN_SAT", schema = WeekSunSatInterval.class), + @DiscriminatorMapping(value = "MONTH", schema = MonthInterval.class), + @DiscriminatorMapping(value = "QUARTER", schema = QuarterInterval.class), + @DiscriminatorMapping(value = "YEAR", schema = YearInterval.class), + @DiscriminatorMapping(value = "CUSTOM", schema = CustomInterval.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/CustomInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/CustomInterval.java index 3b72f309d8..933f2569fb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/CustomInterval.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/CustomInterval.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -24,6 +25,7 @@ import lombok.NoArgsConstructor; import java.time.Duration; import java.time.ZonedDateTime; +@Schema @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/DayInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/DayInterval.java index 1ff5ed25c2..23cb648de2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/DayInterval.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/DayInterval.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +@Schema @Data @NoArgsConstructor public class DayInterval extends BaseAggInterval { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/HourInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/HourInterval.java index f20387db82..7a95f2d387 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/HourInterval.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/HourInterval.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -22,6 +23,7 @@ import lombok.NoArgsConstructor; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +@Schema @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/MonthInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/MonthInterval.java index 3e2dee2eaf..d49ff9b335 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/MonthInterval.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/MonthInterval.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +@Schema @Data @NoArgsConstructor public class MonthInterval extends BaseAggInterval { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/QuarterInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/QuarterInterval.java index 6a9123f76f..a6f29f53f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/QuarterInterval.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/QuarterInterval.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; @@ -22,6 +23,7 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.ZonedDateTime; +@Schema @Data @NoArgsConstructor public class QuarterInterval extends BaseAggInterval { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekInterval.java index 311a5254ef..fad2f072d3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekInterval.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekInterval.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; @@ -23,6 +24,7 @@ import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; +@Schema @Data @NoArgsConstructor public class WeekInterval extends BaseAggInterval { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekSunSatInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekSunSatInterval.java index 732b82446a..a3d5876b0e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekSunSatInterval.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekSunSatInterval.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; @@ -23,6 +24,7 @@ import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; +@Schema @Data @NoArgsConstructor public class WeekSunSatInterval extends BaseAggInterval { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/YearInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/YearInterval.java index 9f1015f01e..882aaa1e56 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/YearInterval.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/YearInterval.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; @@ -22,6 +23,7 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.ZonedDateTime; +@Schema @Data @NoArgsConstructor public class YearInterval extends BaseAggInterval { 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 cb408c5dbf..62ece3d2dc 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 @@ -16,12 +16,14 @@ package org.thingsboard.server.common.data.cf.configuration.geofencing; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.HasUseLatestTsConfig; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; @@ -36,6 +38,7 @@ import java.util.Set; import static java.util.stream.Collectors.toSet; +@Schema @Data public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration, HasUseLatestTsConfig { @@ -48,7 +51,7 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal private Map zoneGroups; private boolean scheduledUpdateEnabled; - private int scheduledUpdateInterval; + private Integer scheduledUpdateInterval; @NotNull private Output output; @@ -88,6 +91,9 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal @Override public void validate() { + if (scheduledUpdateEnabled && scheduledUpdateInterval == null) { + throw new IllegalArgumentException("Refresh interval is required when periodic zone group refresh is enabled."); + } zoneGroups.forEach((key, value) -> value.validate(key)); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java index 3f23e4fe14..86a61e31b6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.device.data; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; @@ -25,6 +26,7 @@ import java.util.HashMap; import java.util.Map; @Data +@Schema public class CoapDeviceTransportConfiguration extends PowerSavingConfiguration implements DeviceTransportConfiguration { private static final long serialVersionUID = 6061442236008925609L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java index 85c1d83744..a0040adb4b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java @@ -19,7 +19,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileType; -@Schema +@Schema(description = "Default device configuration") @Data public class DefaultDeviceConfiguration implements DeviceConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceTransportConfiguration.java index ae7b2e10ab..022a0ca528 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceTransportConfiguration.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.device.data; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; +@Schema @Data public class DefaultDeviceTransportConfiguration implements DeviceTransportConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java index 9fa449fdfa..6fe689d17c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java @@ -19,12 +19,19 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.DeviceProfileType; import java.io.Serializable; -@Schema +@Schema( + description = "Device configuration", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultDeviceConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -34,7 +41,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = DefaultDeviceConfiguration.class, name = "DEFAULT")}) public interface DeviceConfiguration extends Serializable { - @JsonIgnore + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Device profile type") DeviceProfileType getType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java index 84c8e3b463..fa7df3392d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java @@ -19,12 +19,23 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.DeviceTransportType; import java.io.Serializable; -@Schema +@Schema( + description = "Configuration for device transport", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultDeviceTransportConfiguration.class), + @DiscriminatorMapping(value = "MQTT", schema = MqttDeviceTransportConfiguration.class), + @DiscriminatorMapping(value = "COAP", schema = CoapDeviceTransportConfiguration.class), + @DiscriminatorMapping(value = "LWM2M", schema = Lwm2mDeviceTransportConfiguration.class), + @DiscriminatorMapping(value = "SNMP", schema = SnmpDeviceTransportConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -37,6 +48,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = Lwm2mDeviceTransportConfiguration.class, name = "LWM2M"), @JsonSubTypes.Type(value = SnmpDeviceTransportConfiguration.class, name = "SNMP")}) public interface DeviceTransportConfiguration extends Serializable { + @JsonIgnore DeviceTransportType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java index 079ceffada..656262ab45 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java @@ -18,12 +18,14 @@ package org.thingsboard.server.common.data.device.data; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import java.util.HashMap; import java.util.Map; +@Schema @Data public class Lwm2mDeviceTransportConfiguration extends PowerSavingConfiguration implements DeviceTransportConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java index 0adb842ec6..bf83fd31fc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java @@ -18,12 +18,14 @@ package org.thingsboard.server.common.data.device.data; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import java.util.HashMap; import java.util.Map; +@Schema @Data public class MqttDeviceTransportConfiguration implements DeviceTransportConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java index 5509845855..21f2b619d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.device.data; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; @Data +@Schema public class PowerSavingConfiguration implements Serializable { private static final long serialVersionUID = 2905389805488525362L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java index 8357b65f21..5c732b9e62 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.device.data; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.ToString; import org.thingsboard.server.common.data.DeviceTransportType; @@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.transport.snmp.AuthenticationProtocol; import org.thingsboard.server.common.data.transport.snmp.PrivacyProtocol; import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion; +@Schema @Data @ToString(of = {"host", "port", "protocolVersion"}) public class SnmpDeviceTransportConfiguration implements DeviceTransportConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java index 838239b091..120df6f3b2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.Data; @@ -23,14 +24,14 @@ import lombok.Data; import java.io.Serializable; import java.util.List; -@Schema +@Schema(hidden = true) @Data @JsonIgnoreProperties(ignoreUnknown = true) @Deprecated public class AlarmCondition implements Serializable { @Valid - @Schema(description = "JSON array of alarm condition filters") + @ArraySchema(schema = @Schema(ref = "#/components/schemas/AlarmConditionFilter")) private List condition; @Schema(description = "JSON object representing alarm condition type") private AlarmConditionSpec spec; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java index b2a3083f6f..96dfee9703 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; -@Schema +@Schema(hidden = true) @Data @Deprecated public class AlarmConditionFilter implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java index d3066ec8cc..fd6a12d223 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java @@ -19,9 +19,20 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import java.io.Serializable; +@Schema( + description = "Specification for alarm conditions", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SIMPLE", schema = SimpleAlarmConditionSpec.class), + @DiscriminatorMapping(value = "DURATION", schema = DurationAlarmConditionSpec.class), + @DiscriminatorMapping(value = "REPEATING", schema = RepeatingAlarmConditionSpec.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index 9ceea43677..7a9be0f302 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serial; import java.io.Serializable; -@Schema +@Schema(hidden = true) @Data @Deprecated public class AlarmRule implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java index d46fedf87b..884d9f13c5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java @@ -18,10 +18,14 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.query.DynamicValue; import java.io.Serializable; +@Schema( + hidden = true +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java index 7f78dbc46d..1183f9ae63 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(hidden = true) @Deprecated public enum AlarmScheduleType { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java index b33b56ba82..d1672af07e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AllowCreateNewDevicesDeviceProfileProvisionConfiguration.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @Data +@Schema public class AllowCreateNewDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { private final String provisionDeviceSecret; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java index e7acb04cd3..70463dfbe7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.query.DynamicValue; +@Schema(hidden = true) @Deprecated public class AnyTimeSchedule implements AlarmSchedule { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java index 7a1d6d1a95..935da4cbbe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @Data +@Schema public class CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { private final String provisionDeviceSecret; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceProfileTransportConfiguration.java index b12d0bdf40..66d7f15fc4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceProfileTransportConfiguration.java @@ -15,14 +15,18 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; +@Schema @Data public class CoapDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + @Schema(implementation = CoapDeviceTypeConfiguration.class) private CoapDeviceTypeConfiguration coapDeviceTypeConfiguration; + @Schema private PowerSavingConfiguration clientSettings; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java index 66907288e9..12efa4a14c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java @@ -19,10 +19,20 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.CoapDeviceType; import java.io.Serializable; +@Schema( + description = "CoAP device type configuration", + discriminatorProperty = "coapDeviceType", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultCoapDeviceTypeConfiguration.class), + @DiscriminatorMapping(value = "EFENTO", schema = EfentoCoapDeviceTypeConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java index 878a85360b..cea1d76d36 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.query.DynamicValue; import java.util.List; +@Schema(hidden = true) @Data @Deprecated public class CustomTimeSchedule implements AlarmSchedule { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java index 0d4f5cf018..d91c978ac5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serializable; +@Schema(hidden = true) @Data @Deprecated public class CustomTimeScheduleItem implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java index 716f733a0a..0455f9f370 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java @@ -15,14 +15,17 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.CoapDeviceType; @Data +@Schema public class DefaultCoapDeviceTypeConfiguration implements CoapDeviceTypeConfiguration { private static final long serialVersionUID = -4287100699186773773L; + @Schema private TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileConfiguration.java index 5058024680..13cd6b4a58 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileConfiguration.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileType; +@Schema(description = "Default device profile configuration") @Data public class DefaultDeviceProfileConfiguration implements DeviceProfileConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java index 012b09c8e1..f00922119a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; +@Schema @Data public class DefaultDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java index 98546180e2..69c788539b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java @@ -19,10 +19,19 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.DeviceProfileType; import java.io.Serializable; +@Schema( + description = "Device profile configuration", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultDeviceProfileConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java index 636f546386..da7282f48a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java @@ -36,7 +36,7 @@ public class DeviceProfileData implements Serializable { @Schema(description = "JSON object of provisioning strategy type per device profile") private DeviceProfileProvisionConfiguration provisionConfiguration; @Valid - @Schema(description = "JSON array of alarm rules configuration per device profile") + @Schema(hidden = true) private List alarms; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java index 45ac5207f5..b94c2c9a28 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java @@ -19,10 +19,22 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import java.io.Serializable; +@Schema( + description = "Device profile provision configuration", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DISABLED", schema = DisabledDeviceProfileProvisionConfiguration.class), + @DiscriminatorMapping(value = "ALLOW_CREATE_NEW_DEVICES", schema = AllowCreateNewDevicesDeviceProfileProvisionConfiguration.class), + @DiscriminatorMapping(value = "CHECK_PRE_PROVISIONED_DEVICES", schema = CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration.class), + @DiscriminatorMapping(value = "X509_CERTIFICATE_CHAIN", schema = X509CertificateChainProvisionConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -35,6 +47,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = X509CertificateChainProvisionConfiguration.class, name = "X509_CERTIFICATE_CHAIN")}) public interface DeviceProfileProvisionConfiguration extends Serializable { + @Schema(description = "Provision device secret", example = "secret123") String getProvisionDeviceSecret(); @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java index 17627ecc6b..ea035248ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java @@ -19,10 +19,23 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.DeviceTransportType; import java.io.Serializable; +@Schema( + description = "Configuration for device profile transport", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultDeviceProfileTransportConfiguration.class), + @DiscriminatorMapping(value = "MQTT", schema = MqttDeviceProfileTransportConfiguration.class), + @DiscriminatorMapping(value = "LWM2M", schema = Lwm2mDeviceProfileTransportConfiguration.class), + @DiscriminatorMapping(value = "COAP", schema = CoapDeviceProfileTransportConfiguration.class), + @DiscriminatorMapping(value = "SNMP", schema = SnmpDeviceProfileTransportConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java index 7492c077c8..df5883230a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DisabledDeviceProfileProvisionConfiguration.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @Data +@Schema public class DisabledDeviceProfileProvisionConfiguration implements DeviceProfileProvisionConfiguration { private final String provisionDeviceSecret; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java index 202c11019c..b40b8f2c82 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java @@ -16,17 +16,21 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.query.FilterPredicateValue; import java.util.concurrent.TimeUnit; +@Schema(description = "Duration Alarm Condition Specification") @Data @JsonIgnoreProperties(ignoreUnknown = true) @Deprecated public class DurationAlarmConditionSpec implements AlarmConditionSpec { + @Schema(description = "Duration time unit") private TimeUnit unit; + @Schema(description = "Duration predicate") private FilterPredicateValue predicate; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java index b5d6bc36f4..8d6831ebd9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.CoapDeviceType; @Data +@Schema public class EfentoCoapDeviceTypeConfiguration implements CoapDeviceTypeConfiguration { private static final long serialVersionUID = -8523081152598707064L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/JsonTransportPayloadConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/JsonTransportPayloadConfiguration.java index 16d5ca7686..3103aff8c4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/JsonTransportPayloadConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/JsonTransportPayloadConfiguration.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.TransportPayloadType; @Data +@Schema public class JsonTransportPayloadConfiguration implements TransportPayloadTypeConfiguration { @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java index d73e4bf1fe..300ede57c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.device.data.PowerMode; @@ -22,20 +24,25 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguratio import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; -import static org.eclipse.leshan.core.LwM2m.Version.V1_0; -import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; - import java.util.Collections; import java.util.List; +import static org.eclipse.leshan.core.LwM2m.Version.V1_0; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; + @Data +@Schema public class Lwm2mDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { private static final long serialVersionUID = 6257277825459600068L; + @Schema(description = "Configuration for mapping LwM2M resources to telemetry and attributes") private TelemetryMappingConfiguration observeAttr; + @Schema(description = "Flag indicating whether LwM2M bootstrap server update is enabled") private boolean bootstrapServerUpdateEnable; + @ArraySchema(schema = @Schema(implementation = LwM2MBootstrapServerCredential.class)) private List bootstrap; + @Schema(description = "Other LwM2M client settings") private OtherConfiguration clientLwM2mSettings; public Lwm2mDeviceProfileTransportConfiguration() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java index 4e165e7745..a5df705800 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.StringUtils; @@ -23,19 +25,27 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.util.Objects; import java.util.Set; +@Schema @Data public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + @Schema @NoXss private String deviceTelemetryTopic = MqttTopics.DEVICE_TELEMETRY_TOPIC; + @Schema @NoXss private String deviceAttributesTopic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + @Schema @NoXss private String deviceAttributesSubscribeTopic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + @Schema private TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; + @Schema private boolean sparkplug; + @ArraySchema(schema = @Schema(implementation = String.class)) private Set sparkplugAttributesMetricNames; + @Schema private boolean sendAckOnValidationException; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java index c13f0b2d09..a1e55f3452 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java @@ -18,12 +18,14 @@ package org.thingsboard.server.common.data.device.profile; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; import com.squareup.wire.schema.Location; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.DynamicProtoUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; +@Schema @Slf4j @Data public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeConfiguration { @@ -43,6 +45,9 @@ public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeC private boolean enableCompatibilityWithJsonPayloadFormat; private boolean useJsonPayloadFormatForDefaultDownlinkTopics; + @Schema( + description = "Transport payload type", requiredMode = Schema.RequiredMode.REQUIRED + ) @Override public TransportPayloadType getTransportPayloadType() { return TransportPayloadType.PROTOBUF; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java index 323b29a327..1685b5da8e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java @@ -16,17 +16,21 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.query.FilterPredicateValue; +@Schema @Data @JsonIgnoreProperties(ignoreUnknown = true) @Deprecated public class RepeatingAlarmConditionSpec implements AlarmConditionSpec { + @Schema(description = "Repeating predicate") private FilterPredicateValue predicate; @Override + @Schema(description = "Type of the Alarm Condition Specification", requiredMode = Schema.RequiredMode.REQUIRED) public AlarmConditionSpecType getType() { return AlarmConditionSpecType.REPEATING; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java index 53a87a6b99..caa65777e9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java @@ -16,8 +16,10 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data @JsonIgnoreProperties(ignoreUnknown = true) @Deprecated diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java index ee1656c04a..a83920b777 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java @@ -16,16 +16,20 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; import java.util.List; +@Schema @Data public class SnmpDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { private Integer timeoutMs; private Integer retries; + @ArraySchema(schema = @Schema(implementation = SnmpCommunicationConfig.class)) private List communicationConfigs; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java index dfaf6c763c..8bd56d07be 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.query.DynamicValue; import java.util.Set; +@Schema(hidden = true) @Data @Deprecated public class SpecificTimeSchedule implements AlarmSchedule { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/TransportPayloadTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/TransportPayloadTypeConfiguration.java index c7111f8ffd..38176bd740 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/TransportPayloadTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/TransportPayloadTypeConfiguration.java @@ -19,10 +19,20 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.TransportPayloadType; import java.io.Serializable; +@Schema( + description = "Configuration for transport payload type", + discriminatorProperty = "transportPayloadType", + discriminatorMapping = { + @DiscriminatorMapping(value = "JSON", schema = JsonTransportPayloadConfiguration.class), + @DiscriminatorMapping(value = "PROTOBUF", schema = ProtoTransportPayloadConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java index ad0cdaa6bf..ab95dae195 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/X509CertificateChainProvisionConfiguration.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.DeviceProfileProvisionType; +@Schema @Data @NoArgsConstructor public class X509CertificateChainProvisionConfiguration implements DeviceProfileProvisionConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java index 393e08efee..6e83e82692 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java @@ -16,11 +16,13 @@ package org.thingsboard.server.common.data.device.profile.lwm2m; import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.eclipse.leshan.core.LwM2m; import java.io.Serializable; +@Schema @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class ObjectAttributes implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java index 0b0d4095b1..cdf9ed89df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.device.profile.lwm2m; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -23,6 +24,7 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.device.data.PowerMode; import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; +@Schema @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java index 7362ac3ac3..6513e2fe55 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.device.profile.lwm2m; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; @@ -27,16 +28,23 @@ import java.util.Set; @Data @NoArgsConstructor +@Schema public class TelemetryMappingConfiguration implements Serializable { private static final long serialVersionUID = -7594999741305410419L; + @Schema(description = "Map of LwM2M resource paths to telemetry key names") private Map keyName; + @Schema(description = "Set of resources to observe") private Set observe; + @Schema(description = "Set of attribute keys") private Set attribute; + @Schema(description = "Set of telemetry keys") private Set telemetry; + @Schema(description = "Map of resource paths to specific LwM2M object attributes") private Map attributeLwm2m; private Boolean initAttrTelAsObsStrategy; + @Schema(description = "Observation strategy for telemetry") private TelemetryObserveStrategy observeStrategy; @JsonCreator diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index d50882f699..6866271aec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.edge; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -153,4 +154,13 @@ public class Edge extends BaseDataWithAdditionalInfo implements HasLabel return this.secret; } + @Schema(description = "Additional parameters of the edge. " + + "May include: 'description' (string).", + implementation = com.fasterxml.jackson.databind.JsonNode.class, + example = "{\"description\":\"Edge at location A\"}") + @Override + public JsonNode getAdditionalInfo() { + return super.getAdditionalInfo(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java index 8779416106..01463dade8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.event; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.StringUtils; @@ -26,10 +27,17 @@ public abstract class DebugEventFilter implements EventFilter { @Schema(description = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152") protected String server; @Schema(description = "Boolean value to filter the errors", allowableValues = {"false", "true"}) + @JsonProperty("isError") protected boolean isError; @Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") protected String errorStr; + @JsonProperty("isError") + public boolean isError() { + return isError; + } + + @JsonProperty("isError") public void setIsError(boolean isError) { this.isError = isError; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java index 07789b9208..4d955f1817 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java @@ -17,9 +17,21 @@ package org.thingsboard.server.common.data.event; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; -@Schema +@Schema( + description = "Filter for various event types", + discriminatorProperty = "eventType", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEBUG_RULE_NODE", schema = RuleNodeDebugEventFilter.class), + @DiscriminatorMapping(value = "DEBUG_RULE_CHAIN", schema = RuleChainDebugEventFilter.class), + @DiscriminatorMapping(value = "ERROR", schema = ErrorEventFilter.class), + @DiscriminatorMapping(value = "LC_EVENT", schema = LifeCycleEventFilter.class), + @DiscriminatorMapping(value = "STATS", schema = StatisticsEventFilter.class), + @DiscriminatorMapping(value = "DEBUG_CALCULATED_FIELD", schema = CalculatedFieldDebugEventFilter.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java index da7ade45e1..5923b8c02c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public class AdminSettingsId extends UUIDBased implements EntityId { @Serial @@ -33,7 +34,7 @@ public class AdminSettingsId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS") @Override public EntityType getEntityType() { return EntityType.ADMIN_SETTINGS; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java index dbb8b25635..70476564d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public final class AiModelId extends UUIDBased implements EntityId { @Serial @@ -36,6 +37,7 @@ public final class AiModelId extends UUIDBased implements EntityId { @Override @Schema( requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_ONLY, description = "Entity type of the AI model", example = "AI_MODEL", allowableValues = "AI_MODEL" diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java index 444492c328..db2c63d614 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class AlarmId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -36,7 +36,7 @@ public class AlarmId extends UUIDBased implements EntityId { return new AlarmId(UUID.fromString(alarmId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ALARM", allowableValues = "ALARM") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ALARM", allowableValues = "ALARM") @Override public EntityType getEntityType() { return EntityType.ALARM; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java index 514c29665c..186dcaca40 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiKeyId.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public class ApiKeyId extends UUIDBased implements EntityId { @Serial @@ -38,7 +39,7 @@ public class ApiKeyId extends UUIDBased implements EntityId { } @Override - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_KEY", allowableValues = "API_KEY") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "API_KEY", allowableValues = "API_KEY") public EntityType getEntityType() { return EntityType.API_KEY; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java index 78e5c90bee..02d6d387a2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class ApiUsageStateId extends UUIDBased implements EntityId { @JsonCreator @@ -34,7 +34,7 @@ public class ApiUsageStateId extends UUIDBased implements EntityId { return new ApiUsageStateId(UUID.fromString(userId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE") @Override public EntityType getEntityType() { return EntityType.API_USAGE_STATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java index dc42fffeb5..2d57cc365d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class AssetId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -36,7 +36,7 @@ public class AssetId extends UUIDBased implements EntityId { return new AssetId(UUID.fromString(assetId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET", allowableValues = "ASSET") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ASSET", allowableValues = "ASSET") @Override public EntityType getEntityType() { return EntityType.ASSET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java index f4f7a3de70..981f381a4a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class AssetProfileId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +36,7 @@ public class AssetProfileId extends UUIDBased implements EntityId { return new AssetProfileId(UUID.fromString(assetProfileId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE") @Override public EntityType getEntityType() { return EntityType.ASSET_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java index 5168515cd8..094df0e935 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldId.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class CalculatedFieldId extends UUIDBased implements EntityId { @Serial @@ -38,7 +38,7 @@ public class CalculatedFieldId extends UUIDBased implements EntityId { return new CalculatedFieldId(UUID.fromString(calculatedFieldId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD", allowableValues = "CALCULATED_FIELD") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "CALCULATED_FIELD", allowableValues = "CALCULATED_FIELD") @Override public EntityType getEntityType() { return EntityType.CALCULATED_FIELD; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java index a3b1e28147..4de326f26c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public final class CustomerId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -32,7 +32,7 @@ public final class CustomerId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "CUSTOMER", allowableValues = "CUSTOMER") @Override public EntityType getEntityType() { return EntityType.CUSTOMER; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java index bd326257a6..c98e70177e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class DashboardId extends UUIDBased implements EntityId { @JsonCreator @@ -34,7 +34,7 @@ public class DashboardId extends UUIDBased implements EntityId { return new DashboardId(UUID.fromString(dashboardId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DASHBOARD", allowableValues = "DASHBOARD") @Override public EntityType getEntityType() { return EntityType.DASHBOARD; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java index 7dd840815b..a7b9c12e19 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class DeviceId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -37,7 +37,7 @@ public class DeviceId extends UUIDBased implements EntityId { } @Override - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE", allowableValues = "DEVICE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DEVICE", allowableValues = "DEVICE") public EntityType getEntityType() { return EntityType.DEVICE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java index 2adba3640c..dea0dd2a65 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class DeviceProfileId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +36,7 @@ public class DeviceProfileId extends UUIDBased implements EntityId { return new DeviceProfileId(UUID.fromString(deviceProfileId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE") @Override public EntityType getEntityType() { return EntityType.DEVICE_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java index 0768eec45e..223ea8d8b5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DomainId.java @@ -17,10 +17,12 @@ 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(allOf = EntityId.class) public class DomainId extends UUIDBased implements EntityId { @JsonCreator @@ -32,6 +34,7 @@ public class DomainId extends UUIDBased implements EntityId { return new DomainId(UUID.fromString(oauth2DomainId)); } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "DOMAIN", allowableValues = "DOMAIN") @Override public EntityType getEntityType() { return EntityType.DOMAIN; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java index 05019a608f..45a95d6632 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public class EdgeId extends UUIDBased implements EntityId { @Serial @@ -43,7 +44,7 @@ public class EdgeId extends UUIDBased implements EntityId { return new EdgeId(UUID.fromString(edgeId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "EDGE", allowableValues = "EDGE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "EDGE", allowableValues = "EDGE") @Override public EntityType getEntityType() { return EntityType.EDGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java index c57dc79514..c96b4c38d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; @@ -26,7 +27,47 @@ import java.util.UUID; @JsonDeserialize(using = EntityIdDeserializer.class) @JsonSerialize(using = EntityIdSerializer.class) -@Schema +@Schema( + discriminatorProperty = "entityType", + discriminatorMapping = { + @DiscriminatorMapping(value = "ADMIN_SETTINGS", schema = AdminSettingsId.class), + @DiscriminatorMapping(value = "AI_MODEL", schema = AiModelId.class), + @DiscriminatorMapping(value = "ALARM", schema = AlarmId.class), + @DiscriminatorMapping(value = "API_KEY", schema = ApiKeyId.class), + @DiscriminatorMapping(value = "API_USAGE_STATE", schema = ApiUsageStateId.class), + @DiscriminatorMapping(value = "ASSET", schema = AssetId.class), + @DiscriminatorMapping(value = "ASSET_PROFILE", schema = AssetProfileId.class), + @DiscriminatorMapping(value = "CALCULATED_FIELD", schema = CalculatedFieldId.class), + @DiscriminatorMapping(value = "CUSTOMER", schema = CustomerId.class), + @DiscriminatorMapping(value = "DASHBOARD", schema = DashboardId.class), + @DiscriminatorMapping(value = "DEVICE", schema = DeviceId.class), + @DiscriminatorMapping(value = "DEVICE_PROFILE", schema = DeviceProfileId.class), + @DiscriminatorMapping(value = "DOMAIN", schema = DomainId.class), + @DiscriminatorMapping(value = "EDGE", schema = EdgeId.class), + @DiscriminatorMapping(value = "ENTITY_VIEW", schema = EntityViewId.class), + @DiscriminatorMapping(value = "JOB", schema = JobId.class), + @DiscriminatorMapping(value = "MOBILE_APP", schema = MobileAppId.class), + @DiscriminatorMapping(value = "MOBILE_APP_BUNDLE", schema = MobileAppBundleId.class), + @DiscriminatorMapping(value = "NOTIFICATION", schema = NotificationId.class), + @DiscriminatorMapping(value = "NOTIFICATION_REQUEST", schema = NotificationRequestId.class), + @DiscriminatorMapping(value = "NOTIFICATION_RULE", schema = NotificationRuleId.class), + @DiscriminatorMapping(value = "NOTIFICATION_TARGET", schema = NotificationTargetId.class), + @DiscriminatorMapping(value = "NOTIFICATION_TEMPLATE", schema = NotificationTemplateId.class), + @DiscriminatorMapping(value = "OAUTH2_CLIENT", schema = OAuth2ClientId.class), + @DiscriminatorMapping(value = "OTA_PACKAGE", schema = OtaPackageId.class), + @DiscriminatorMapping(value = "QUEUE", schema = QueueId.class), + @DiscriminatorMapping(value = "QUEUE_STATS", schema = QueueStatsId.class), + @DiscriminatorMapping(value = "RPC", schema = RpcId.class), + @DiscriminatorMapping(value = "RULE_CHAIN", schema = RuleChainId.class), + @DiscriminatorMapping(value = "RULE_NODE", schema = RuleNodeId.class), + @DiscriminatorMapping(value = "TB_RESOURCE", schema = TbResourceId.class), + @DiscriminatorMapping(value = "TENANT", schema = TenantId.class), + @DiscriminatorMapping(value = "TENANT_PROFILE", schema = TenantProfileId.class), + @DiscriminatorMapping(value = "USER", schema = UserId.class), + @DiscriminatorMapping(value = "WIDGETS_BUNDLE", schema = WidgetsBundleId.class), + @DiscriminatorMapping(value = "WIDGET_TYPE", schema = WidgetTypeId.class) + } +) public interface EntityId extends HasUUID, Serializable { //NOSONAR, the constant is closely related to EntityId UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java index cd5b0164db..8bd357a4fc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java @@ -25,6 +25,7 @@ import java.util.UUID; /** * Created by Victor Basanets on 8/27/2017. */ +@Schema(allOf = EntityId.class) public class EntityViewId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -38,7 +39,7 @@ public class EntityViewId extends UUIDBased implements EntityId { return new EntityViewId(UUID.fromString(entityViewID)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW") @Override public EntityType getEntityType() { return EntityType.ENTITY_VIEW; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java index 51e6b8e539..2d369e8b71 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public class JobId extends UUIDBased implements EntityId { @Serial @@ -33,7 +34,7 @@ public class JobId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "JOB", allowableValues = "JOB") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "JOB", allowableValues = "JOB") @Override public EntityType getEntityType() { return EntityType.JOB; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java index 6a9c11207a..cfd0fd109f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppBundleId.java @@ -17,10 +17,12 @@ 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(allOf = EntityId.class) public class MobileAppBundleId extends UUIDBased implements EntityId{ @JsonCreator @@ -32,6 +34,7 @@ public class MobileAppBundleId extends UUIDBased implements EntityId{ return new MobileAppBundleId(UUID.fromString(mobileAppId)); } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "MOBILE_APP_BUNDLE", allowableValues = "MOBILE_APP_BUNDLE") @Override public EntityType getEntityType() { return EntityType.MOBILE_APP_BUNDLE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java index f19e52cc46..bf3571bf7e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppId.java @@ -17,10 +17,12 @@ 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(allOf = EntityId.class) public class MobileAppId extends UUIDBased implements EntityId{ @JsonCreator @@ -32,6 +34,7 @@ public class MobileAppId extends UUIDBased implements EntityId{ return new MobileAppId(UUID.fromString(mobileAppId)); } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "MOBILE_APP", allowableValues = "MOBILE_APP") @Override public EntityType getEntityType() { return EntityType.MOBILE_APP; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java index 3ba6bf4ca4..a702648a52 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class NotificationId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class NotificationId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java index 5ff42bc0e2..3caa5f1f8c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class NotificationRequestId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_REQUEST; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java index ff57e4d252..39e2ccf54c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class NotificationRuleId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_RULE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java index d1a1cfd00b..593ce1c1aa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class NotificationTargetId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TARGET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java index ae2ea582fd..5a1635044a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class NotificationTemplateId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TEMPLATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java index 9ef5871135..491643b921 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientId.java @@ -17,10 +17,12 @@ 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(allOf = EntityId.class) public class OAuth2ClientId extends UUIDBased implements EntityId { @JsonCreator @@ -32,6 +34,7 @@ public class OAuth2ClientId extends UUIDBased implements EntityId { return new OAuth2ClientId(UUID.fromString(oauth2ClientId)); } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "OAUTH2_CLIENT", allowableValues = "OAUTH2_CLIENT") @Override public EntityType getEntityType() { return EntityType.OAUTH2_CLIENT; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java index 0261b2973e..f9f508f13f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public class OtaPackageId extends UUIDBased implements EntityId { @Serial @@ -37,7 +38,7 @@ public class OtaPackageId extends UUIDBased implements EntityId { return new OtaPackageId(UUID.fromString(firmwareId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE") @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java index c7ee46da2a..3c00c08bc1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class QueueId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +36,7 @@ public class QueueId extends UUIDBased implements EntityId { return new QueueId(UUID.fromString(queueId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE", allowableValues = "QUEUE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "QUEUE", allowableValues = "QUEUE") @Override public EntityType getEntityType() { return EntityType.QUEUE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java index 7a921e8755..7869b7bbfe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueStatsId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class QueueStatsId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +36,7 @@ public class QueueStatsId extends UUIDBased implements EntityId { return new QueueStatsId(UUID.fromString(queueId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "QUEUE_STATS", allowableValues = "QUEUE_STATS") @Override public EntityType getEntityType() { return EntityType.QUEUE_STATS; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java index 559855e194..a03b0138d6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public final class RpcId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -31,7 +32,7 @@ public final class RpcId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RPC", allowableValues = "RPC") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RPC", allowableValues = "RPC") @Override public EntityType getEntityType() { return EntityType.RPC; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java index 0512f518af..c77cec808e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class RuleChainId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class RuleChainId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN") @Override public EntityType getEntityType() { return EntityType.RULE_CHAIN; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java index 645acfe5e2..6be3371969 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class RuleNodeId extends UUIDBased implements EntityId { @JsonCreator @@ -29,7 +30,7 @@ public class RuleNodeId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "RULE_NODE", allowableValues = "RULE_NODE") @Override public EntityType getEntityType() { return EntityType.RULE_NODE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java index cf171855b2..b08dcb56b4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class TbResourceId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -31,7 +32,7 @@ public class TbResourceId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE") @Override public EntityType getEntityType() { return EntityType.TB_RESOURCE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index f219452c07..47be35fb84 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; +@Schema(allOf = EntityId.class) public final class TenantId extends UUIDBased implements EntityId { @JsonIgnore @@ -54,7 +55,7 @@ public final class TenantId extends UUIDBased implements EntityId { return this.equals(SYS_TENANT_ID); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT", allowableValues = "TENANT") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TENANT", allowableValues = "TENANT") @Override public EntityType getEntityType() { return EntityType.TENANT; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java index 5b46b772ba..7ced9faa43 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public class TenantProfileId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +36,7 @@ public class TenantProfileId extends UUIDBased implements EntityId { return new TenantProfileId(UUID.fromString(tenantProfileId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE") @Override public EntityType getEntityType() { return EntityType.TENANT_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java index d4f113bef5..0bb939d958 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; -@Schema +@Schema(allOf = EntityId.class) public class UserId extends UUIDBased implements EntityId { @JsonCreator @@ -34,7 +34,7 @@ public class UserId extends UUIDBased implements EntityId { return new UserId(UUID.fromString(userId)); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "USER", allowableValues = "USER") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "USER", allowableValues = "USER") @Override public EntityType getEntityType() { return EntityType.USER; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java index fe8b40a7b9..cd9dcec8b3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public final class WidgetTypeId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -31,7 +32,7 @@ public final class WidgetTypeId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE") @Override public EntityType getEntityType() { return EntityType.WIDGET_TYPE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java index f88d81bf74..295ac8c59d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@Schema(allOf = EntityId.class) public final class WidgetsBundleId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -31,7 +32,7 @@ public final class WidgetsBundleId extends UUIDBased implements EntityId { super(id); } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE") + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, description = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE") @Override public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java index 28ebcd2095..4de12e3eeb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/DummyJobConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -24,6 +25,7 @@ import lombok.ToString; import java.util.List; +@Schema(description = "Dummy job configuration") @Data @EqualsAndHashCode(callSuper = true) @AllArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java index 9480ba11c3..6a10dc7ea1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobConfiguration.java @@ -20,6 +20,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; import org.thingsboard.server.common.data.job.task.TaskResult; @@ -27,6 +30,12 @@ import org.thingsboard.server.common.data.job.task.TaskResult; import java.io.Serializable; import java.util.List; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DUMMY", schema = DummyJobConfiguration.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @@ -37,6 +46,7 @@ public abstract class JobConfiguration implements Serializable { @NotBlank private String tasksKey; // internal + @ArraySchema(schema = @Schema(ref = "#/components/schemas/TaskResult")) private List toReprocess; // internal @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index 2009f10f9f..05c2d51879 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -20,6 +20,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.job.task.TaskResult; @@ -28,6 +31,13 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +@Schema( + description = "Job execution result", + discriminatorProperty = "jobType", + discriminatorMapping = { + @DiscriminatorMapping(value = "DUMMY", schema = DummyJobResult.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonSubTypes({ @@ -37,15 +47,24 @@ import java.util.List; @NoArgsConstructor public abstract class JobResult implements Serializable { + @Schema(description = "Count of successfully completed tasks") private int successfulCount; + @Schema(description = "Count of failed tasks") private int failedCount; + @Schema(description = "Count of discarded tasks") private int discardedCount; - private Integer totalCount = null; // set when all tasks are submitted + @Schema(description = "Total number of tasks, set when all tasks are submitted", nullable = true) + private Integer totalCount = null; + @ArraySchema(schema = @Schema(ref = "#/components/schemas/TaskResult")) private List results = new ArrayList<>(); + @Schema(description = "General error message if the job failed") private String generalError; + @Schema(description = "Timestamp of the job start, in milliseconds") private long startTs; + @Schema(description = "Timestamp of the job finish, in milliseconds") private long finishTs; + @Schema(description = "Timestamp of the job cancellation, in milliseconds") private long cancellationTs; @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java index 97f28ba853..925cc73898 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.kv; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; +@Schema public enum DataType { BOOLEAN(0), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java index f29863d731..877c727381 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/AbstractMobilePage.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.Views; +@Schema @Data public abstract class AbstractMobilePage implements MobilePage { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java index 83f82a571b..156436225f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/CustomMobilePage.java @@ -24,6 +24,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.Views; +@Schema @Data @Builder @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java index 9f6114e4a4..232049adbb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DashboardPage.java @@ -24,6 +24,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.Views; +@Schema @Data @Builder @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java index 710d951edc..20f25afe19 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/DefaultMobilePage.java @@ -24,6 +24,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.Views; +@Schema @Data @Builder @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java index a45fd90838..783d3f68a9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobileLayoutConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.mobile.layout; import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.AllArgsConstructor; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.Views; import java.util.ArrayList; import java.util.List; +@Schema @Data @Builder @NoArgsConstructor @@ -35,7 +37,7 @@ import java.util.List; @EqualsAndHashCode public class MobileLayoutConfig { - @Schema(description = "List of pages") + @ArraySchema(schema = @Schema(implementation = MobilePage.class)) @JsonView(Views.Public.class) @Valid private List pages = new ArrayList<>(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java index 61fa5a0c46..4ab8388f8b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/MobilePage.java @@ -19,10 +19,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.Views; import java.io.Serializable; +@Schema( + description = "Configuration for a mobile page", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "DEFAULT", schema = DefaultMobilePage.class), + @DiscriminatorMapping(value = "DASHBOARD", schema = DashboardPage.class), + @DiscriminatorMapping(value = "WEB_VIEW", schema = WebViewPage.class), + @DiscriminatorMapping(value = "CUSTOM", schema = CustomMobilePage.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -36,6 +48,7 @@ import java.io.Serializable; }) public interface MobilePage extends Serializable { + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) @JsonView(Views.Private.class) MobilePageType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java index c8eafbf58c..89e82ab90e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/layout/WebViewPage.java @@ -24,6 +24,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.Views; +@Schema @Data @Builder @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java index 72e185327b..20420e3971 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/qrCodeSettings/BadgePosition.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.mobile.qrCodeSettings; + public enum BadgePosition { RIGHT, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java index b6a4fec9f8..26b0d10efb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.notification; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.notification.template.NotificationTemp import java.util.List; import java.util.UUID; +@Schema @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestInfo.java index 06b02c6183..c54e845a0d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestInfo.java @@ -15,18 +15,23 @@ */ package org.thingsboard.server.common.data.notification; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.util.List; +@Schema @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class NotificationRequestInfo extends NotificationRequest { + @Schema private String templateName; + @ArraySchema(schema = @Schema(implementation = NotificationDeliveryMethod.class)) private List deliveryMethods; public NotificationRequestInfo(NotificationRequest request, String templateName, List deliveryMethods) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java index c102cfcae4..0e742b6fb1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.notification; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; @@ -27,14 +28,19 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +@Schema(description = "Notification request processing statistics") @Data public class NotificationRequestStats { + @Schema(description = "Number of successfully sent notifications per delivery method", example = "{\"WEB\": 10, \"EMAIL\": 5}") private final Map sent; @JsonIgnore private final AtomicInteger totalSent; + @Schema(description = "Errors per delivery method. Each entry maps recipient name to error message") private final Map> errors; + @Schema(description = "Total number of errors across all delivery methods") private final AtomicInteger totalErrors; + @Schema(description = "General error message if the entire request failed") private String error; @JsonIgnore private final Map> processedRecipients; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java index 9223f53509..bff983f2f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/DefaultNotificationRuleRecipientsConfig.java @@ -15,17 +15,20 @@ */ package org.thingsboard.server.common.data.notification.rule; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import java.util.List; import java.util.Map; import java.util.UUID; +@Schema(description = "Default notification rule recipients configuration") @Data -@EqualsAndHashCode(callSuper = true) -public class DefaultNotificationRuleRecipientsConfig extends NotificationRuleRecipientsConfig { +@EqualsAndHashCode +public abstract class DefaultNotificationRuleRecipientsConfig implements NotificationRuleRecipientsConfig { @NotEmpty private List targets; @@ -35,4 +38,114 @@ public class DefaultNotificationRuleRecipientsConfig extends NotificationRuleRec return Map.of(0, targets); } + public static DefaultNotificationRuleRecipientsConfig forTriggerType(NotificationRuleTriggerType triggerType) { + return switch (triggerType) { + case ENTITY_ACTION -> new EntityActionRecipientsConfig(); + case ALARM_COMMENT -> new AlarmCommentRecipientsConfig(); + case ALARM_ASSIGNMENT -> new AlarmAssignmentRecipientsConfig(); + case DEVICE_ACTIVITY -> new DeviceActivityRecipientsConfig(); + case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT -> new RuleEngineComponentLifecycleEventRecipientsConfig(); + case EDGE_CONNECTION -> new EdgeConnectionRecipientsConfig(); + case EDGE_COMMUNICATION_FAILURE -> new EdgeCommunicationFailureRecipientsConfig(); + case NEW_PLATFORM_VERSION -> new NewPlatformVersionRecipientsConfig(); + case ENTITIES_LIMIT -> new EntitiesLimitRecipientsConfig(); + case API_USAGE_LIMIT -> new ApiUsageLimitRecipientsConfig(); + case RATE_LIMITS -> new RateLimitsRecipientsConfig(); + case TASK_PROCESSING_FAILURE -> new TaskProcessingFailureRecipientsConfig(); + case RESOURCES_SHORTAGE -> new ResourceShortageRecipientsConfig(); + default -> throw new IllegalArgumentException("Unsupported trigger type for default recipients config: " + triggerType); + }; + } + + public static class EntityActionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ENTITY_ACTION; + } + } + + public static class AlarmCommentRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM_COMMENT; + } + } + + public static class AlarmAssignmentRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM_ASSIGNMENT; + } + } + + public static class DeviceActivityRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.DEVICE_ACTIVITY; + } + } + + public static class RuleEngineComponentLifecycleEventRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT; + } + } + + public static class EdgeConnectionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.EDGE_CONNECTION; + } + } + + public static class EdgeCommunicationFailureRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE; + } + } + + public static class NewPlatformVersionRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.NEW_PLATFORM_VERSION; + } + } + + public static class EntitiesLimitRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ENTITIES_LIMIT; + } + } + + public static class ApiUsageLimitRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.API_USAGE_LIMIT; + } + } + + public static class RateLimitsRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.RATE_LIMITS; + } + } + + public static class TaskProcessingFailureRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.TASK_PROCESSING_FAILURE; + } + } + + public static class ResourceShortageRecipientsConfig extends DefaultNotificationRuleRecipientsConfig { + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.RESOURCES_SHORTAGE; + } + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java index 459a15c0fd..fa5a10f983 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/EscalatedNotificationRuleRecipientsConfig.java @@ -15,21 +15,29 @@ */ package org.thingsboard.server.common.data.notification.rule; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import java.util.List; import java.util.Map; import java.util.UUID; +@Schema(description = "Escalated notification rule recipients configuration") @Data -@EqualsAndHashCode(callSuper = true) -public class EscalatedNotificationRuleRecipientsConfig extends NotificationRuleRecipientsConfig { +@EqualsAndHashCode +public class EscalatedNotificationRuleRecipientsConfig implements NotificationRuleRecipientsConfig { @NotEmpty private Map> escalationTable; + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM; + } + @Override public Map> getTargetsTable() { return escalationTable; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java index de4e05a2cb..0475a9a02d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleRecipientsConfig.java @@ -17,12 +17,11 @@ package org.thingsboard.server.common.data.notification.rule; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import jakarta.validation.constraints.NotNull; -import lombok.Data; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import java.io.Serializable; @@ -30,19 +29,47 @@ import java.util.List; import java.util.Map; import java.util.UUID; +@Schema( + discriminatorProperty = "triggerType", + discriminatorMapping = { + @DiscriminatorMapping(value = "ENTITY_ACTION", schema = DefaultNotificationRuleRecipientsConfig.EntityActionRecipientsConfig.class), + @DiscriminatorMapping(value = "ALARM", schema = EscalatedNotificationRuleRecipientsConfig.class), + @DiscriminatorMapping(value = "ALARM_COMMENT", schema = DefaultNotificationRuleRecipientsConfig.AlarmCommentRecipientsConfig.class), + @DiscriminatorMapping(value = "ALARM_ASSIGNMENT", schema = DefaultNotificationRuleRecipientsConfig.AlarmAssignmentRecipientsConfig.class), + @DiscriminatorMapping(value = "DEVICE_ACTIVITY", schema = DefaultNotificationRuleRecipientsConfig.DeviceActivityRecipientsConfig.class), + @DiscriminatorMapping(value = "RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT", schema = DefaultNotificationRuleRecipientsConfig.RuleEngineComponentLifecycleEventRecipientsConfig.class), + @DiscriminatorMapping(value = "EDGE_CONNECTION", schema = DefaultNotificationRuleRecipientsConfig.EdgeConnectionRecipientsConfig.class), + @DiscriminatorMapping(value = "EDGE_COMMUNICATION_FAILURE", schema = DefaultNotificationRuleRecipientsConfig.EdgeCommunicationFailureRecipientsConfig.class), + @DiscriminatorMapping(value = "NEW_PLATFORM_VERSION", schema = DefaultNotificationRuleRecipientsConfig.NewPlatformVersionRecipientsConfig.class), + @DiscriminatorMapping(value = "ENTITIES_LIMIT", schema = DefaultNotificationRuleRecipientsConfig.EntitiesLimitRecipientsConfig.class), + @DiscriminatorMapping(value = "API_USAGE_LIMIT", schema = DefaultNotificationRuleRecipientsConfig.ApiUsageLimitRecipientsConfig.class), + @DiscriminatorMapping(value = "RATE_LIMITS", schema = DefaultNotificationRuleRecipientsConfig.RateLimitsRecipientsConfig.class), + @DiscriminatorMapping(value = "TASK_PROCESSING_FAILURE", schema = DefaultNotificationRuleRecipientsConfig.TaskProcessingFailureRecipientsConfig.class), + @DiscriminatorMapping(value = "RESOURCES_SHORTAGE", schema = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class) + }) @JsonIgnoreProperties(ignoreUnknown = true) -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType", visible = true, include = JsonTypeInfo.As.EXISTING_PROPERTY, defaultImpl = DefaultNotificationRuleRecipientsConfig.class) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType", include = JsonTypeInfo.As.EXISTING_PROPERTY) @JsonSubTypes({ @Type(name = "ALARM", value = EscalatedNotificationRuleRecipientsConfig.class), + @Type(name = "ENTITY_ACTION", value = DefaultNotificationRuleRecipientsConfig.EntityActionRecipientsConfig.class), + @Type(name = "ALARM_COMMENT", value = DefaultNotificationRuleRecipientsConfig.AlarmCommentRecipientsConfig.class), + @Type(name = "ALARM_ASSIGNMENT", value = DefaultNotificationRuleRecipientsConfig.AlarmAssignmentRecipientsConfig.class), + @Type(name = "DEVICE_ACTIVITY", value = DefaultNotificationRuleRecipientsConfig.DeviceActivityRecipientsConfig.class), + @Type(name = "RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT", value = DefaultNotificationRuleRecipientsConfig.RuleEngineComponentLifecycleEventRecipientsConfig.class), + @Type(name = "EDGE_CONNECTION", value = DefaultNotificationRuleRecipientsConfig.EdgeConnectionRecipientsConfig.class), + @Type(name = "EDGE_COMMUNICATION_FAILURE", value = DefaultNotificationRuleRecipientsConfig.EdgeCommunicationFailureRecipientsConfig.class), + @Type(name = "NEW_PLATFORM_VERSION", value = DefaultNotificationRuleRecipientsConfig.NewPlatformVersionRecipientsConfig.class), + @Type(name = "ENTITIES_LIMIT", value = DefaultNotificationRuleRecipientsConfig.EntitiesLimitRecipientsConfig.class), + @Type(name = "API_USAGE_LIMIT", value = DefaultNotificationRuleRecipientsConfig.ApiUsageLimitRecipientsConfig.class), + @Type(name = "RATE_LIMITS", value = DefaultNotificationRuleRecipientsConfig.RateLimitsRecipientsConfig.class), + @Type(name = "TASK_PROCESSING_FAILURE", value = DefaultNotificationRuleRecipientsConfig.TaskProcessingFailureRecipientsConfig.class), + @Type(name = "RESOURCES_SHORTAGE", value = DefaultNotificationRuleRecipientsConfig.ResourceShortageRecipientsConfig.class) }) -@Data -public abstract class NotificationRuleRecipientsConfig implements Serializable { +public interface NotificationRuleRecipientsConfig extends Serializable { - @NotNull - @JsonProperty("triggerType") - private NotificationRuleTriggerType triggerType; + NotificationRuleTriggerType getTriggerType(); @JsonIgnore - public abstract Map> getTargetsTable(); + Map> getTargetsTable(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java index fd6b089a42..7511fd7f2a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; @@ -66,6 +67,7 @@ public class ResourcesShortageTrigger implements NotificationRuleTrigger { return NotificationRuleTriggerType.RESOURCES_SHORTAGE; } + @Schema public enum Resource { CPU, RAM, STORAGE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java index e6deee9838..0cd2e441ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; @@ -30,15 +32,20 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { @Serial private static final long serialVersionUID = -5313556049809972096L; + @ArraySchema(schema = @Schema(implementation = String.class)) private Set alarmTypes; + @ArraySchema(schema = @Schema(implementation = AlarmSeverity.class)) private Set alarmSeverities; + @ArraySchema(schema = @Schema(implementation = AlarmSearchStatus.class)) private Set alarmStatuses; @NotEmpty + @ArraySchema(schema = @Schema(implementation = Action.class)) private Set notifyOn; @Override @@ -46,6 +53,7 @@ public class AlarmAssignmentNotificationRuleTriggerConfig implements Notificatio return NotificationRuleTriggerType.ALARM_ASSIGNMENT; } + @Schema public enum Action { ASSIGNED, UNASSIGNED } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java index 3aa3e27638..c651077bed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -29,15 +31,21 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class AlarmCommentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { @Serial private static final long serialVersionUID = -9164282098882339645L; + @ArraySchema(schema = @Schema(implementation = String.class)) private Set alarmTypes; + @ArraySchema(schema = @Schema(implementation = AlarmSeverity.class)) private Set alarmSeverities; + @ArraySchema(schema = @Schema(implementation = AlarmSearchStatus.class)) private Set alarmStatuses; + @Schema private boolean onlyUserComments; + @Schema private boolean notifyOnCommentUpdate; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java index 20d484efa7..f2be38d6f9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; @@ -31,16 +33,20 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { @Serial private static final long serialVersionUID = -7382883720381542344L; + @ArraySchema(schema = @Schema(implementation = String.class)) private Set alarmTypes; + @ArraySchema(schema = @Schema(implementation = AlarmSeverity.class)) private Set alarmSeverities; @NotEmpty + @ArraySchema(schema = @Schema(implementation = AlarmAction.class)) private Set notifyOn; - + @Schema private ClearRule clearRule; @Override @@ -55,6 +61,7 @@ public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTrigg private Set alarmStatuses; } + @Schema public enum AlarmAction { CREATED, SEVERITY_CHANGED, ACKNOWLEDGED, CLEARED } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java index e7051778b2..77c75dfce0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ApiUsageLimitNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,9 +30,12 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class ApiUsageLimitNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = ApiFeature.class)) private Set apiFeatures; + @ArraySchema(schema = @Schema(implementation = ApiUsageStateValue.class)) private Set notifyOn; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java index 2ad8586f75..568ec50d51 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/DeviceActivityNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Builder; @@ -28,11 +30,15 @@ import java.util.UUID; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class DeviceActivityNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set devices; + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set deviceProfiles; // set either devices or profiles @NotEmpty + @ArraySchema(schema = @Schema(implementation = DeviceEvent.class)) private Set notifyOn; @Override @@ -40,6 +46,7 @@ public class DeviceActivityNotificationRuleTriggerConfig implements Notification return NotificationRuleTriggerType.DEVICE_ACTIVITY; } + @Schema public enum DeviceEvent { ACTIVE, INACTIVE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java index 6def4c1799..8d180ae5bf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -27,8 +29,10 @@ import java.util.UUID; @NoArgsConstructor @AllArgsConstructor @Builder +@Schema public class EdgeCommunicationFailureNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set edges; // if empty - all edges @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java index 264fa9aa55..3caa45f32a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -27,9 +29,12 @@ import java.util.UUID; @NoArgsConstructor @AllArgsConstructor @Builder +@Schema public class EdgeConnectionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set edges; // if empty - all edges + @ArraySchema(schema = @Schema(implementation = EdgeConnectivityEvent.class)) private Set notifyOn; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java index 7c8997dae5..9b51687a6c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntitiesLimitNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; import lombok.AllArgsConstructor; import lombok.Builder; @@ -28,8 +30,10 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class EntitiesLimitNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = EntityType.class)) private Set entityTypes; @Max(1) private float threshold; // in percents, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java index bdc871ad96..d838d899f3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EntityActionNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -27,8 +29,10 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class EntityActionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = EntityType.class)) private Set entityTypes; // maybe add name filter ? private boolean created; private boolean updated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java index b625efb506..64154b6841 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NewPlatformVersionNotificationRuleTriggerConfig.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data +@Schema public class NewPlatformVersionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java index 6fb4bbd48b..a58b3d5eef 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java @@ -20,9 +20,32 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import java.io.Serializable; +@Schema( + name = "NotificationRuleTriggerConfig", + description = "Configuration for notification rule trigger", + discriminatorProperty = "triggerType", + discriminatorMapping = { + @DiscriminatorMapping(value = "ALARM", schema = AlarmNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "DEVICE_ACTIVITY", schema = DeviceActivityNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "ENTITY_ACTION", schema = EntityActionNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "ALARM_COMMENT", schema = AlarmCommentNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT", schema = RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "ALARM_ASSIGNMENT", schema = AlarmAssignmentNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "NEW_PLATFORM_VERSION", schema = NewPlatformVersionNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "ENTITIES_LIMIT", schema = EntitiesLimitNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "API_USAGE_LIMIT", schema = ApiUsageLimitNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "RATE_LIMITS", schema = RateLimitsNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "EDGE_CONNECTION", schema = EdgeConnectionNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "EDGE_COMMUNICATION_FAILURE", schema = EdgeCommunicationFailureNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "TASK_PROCESSING_FAILURE", schema = TaskProcessingFailureNotificationRuleTriggerConfig.class), + @DiscriminatorMapping(value = "RESOURCES_SHORTAGE", schema = ResourcesShortageNotificationRuleTriggerConfig.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType") @JsonSubTypes({ @@ -43,6 +66,7 @@ import java.io.Serializable; }) public interface NotificationRuleTriggerConfig extends Serializable { + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) NotificationRuleTriggerType getTriggerType(); @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java index 982e20284c..8f2ae1cdc9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RateLimitsNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,8 +30,10 @@ import java.util.stream.Collectors; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class RateLimitsNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = LimitedApi.class)) private Set apis; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java index 77782a73cf..f62ed241ca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/ResourcesShortageNotificationRuleTriggerConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; import lombok.AllArgsConstructor; import lombok.Builder; @@ -27,6 +28,7 @@ import java.io.Serial; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class ResourcesShortageNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { @Serial diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java index 4b46c26d65..7e987f1d0b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,14 +30,17 @@ import java.util.UUID; @AllArgsConstructor @NoArgsConstructor @Builder +@Schema public class RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set ruleChains; // if empty - all rule chains - + @ArraySchema(schema = @Schema(implementation = ComponentLifecycleEvent.class)) private Set ruleChainEvents; // available options: STARTED, UPDATED, STOPPED. if empty - all events private boolean onlyRuleChainLifecycleFailures; private boolean trackRuleNodeEvents; + @ArraySchema(schema = @Schema(implementation = ComponentLifecycleEvent.class)) private Set ruleNodeEvents; // available options: STARTED, UPDATED, STOPPED. if empty - all events private boolean onlyRuleNodeLifecycleFailures; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java index 6a88000124..59fd219705 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/TaskProcessingFailureNotificationRuleTriggerConfig.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger.config; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @Data @Builder +@Schema public class TaskProcessingFailureNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileAppNotificationDeliveryMethodConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileAppNotificationDeliveryMethodConfig.java index b5b573a452..2a708ed998 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileAppNotificationDeliveryMethodConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileAppNotificationDeliveryMethodConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.notification.settings; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +@Schema @Data public class MobileAppNotificationDeliveryMethodConfig implements NotificationDeliveryMethodConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java index 5eecb63b1e..eb3236ec00 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java @@ -20,10 +20,19 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import java.io.Serializable; +@Schema( + discriminatorProperty = "method", + discriminatorMapping = { + @DiscriminatorMapping(value = "SLACK", schema = SlackNotificationDeliveryMethodConfig.class), + @DiscriminatorMapping(value = "MOBILE_APP", schema = MobileAppNotificationDeliveryMethodConfig.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java index fbfc9f9d95..d5facf4d00 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.settings; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -23,6 +24,7 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho import java.io.Serializable; import java.util.Map; +@Schema @Data public class NotificationSettings implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/SlackNotificationDeliveryMethodConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/SlackNotificationDeliveryMethodConfig.java index f2a8a0d6da..283c09bbd8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/SlackNotificationDeliveryMethodConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/SlackNotificationDeliveryMethodConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.notification.settings; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +@Schema @Data public class SlackNotificationDeliveryMethodConfig implements NotificationDeliveryMethodConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java index a241616712..d551aab4c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.notification.targets; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class MicrosoftTeamsNotificationTargetConfig extends NotificationTargetConfig implements NotificationRecipient { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java index 74a2544d11..932d954d58 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.targets; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class NotificationTarget extends BaseData implements HasTenantId, HasName, ExportableEntity { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java index 98468b9521..2f7f2615bd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java @@ -20,12 +20,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "PLATFORM_USERS", schema = PlatformUsersNotificationTargetConfig.class), + @DiscriminatorMapping(value = "SLACK", schema = SlackNotificationTargetConfig.class), + @DiscriminatorMapping(value = "MICROSOFT_TEAMS", schema = MicrosoftTeamsNotificationTargetConfig.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedTenantAdministratorsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedTenantAdministratorsFilter.java index 7dc10cd6ee..96dda12884 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedTenantAdministratorsFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedTenantAdministratorsFilter.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class AffectedTenantAdministratorsFilter implements UsersFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedUserFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedUserFilter.java index c1e600d5bb..b2c5348d3b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedUserFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AffectedUserFilter.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class AffectedUserFilter implements UsersFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java index fbe42938f0..119859db7c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class AllUsersFilter implements SystemLevelUsersFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/CustomerUsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/CustomerUsersFilter.java index 34df18244f..bc63801124 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/CustomerUsersFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/CustomerUsersFilter.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.UUID; +@Schema @Data public class CustomerUsersFilter implements UsersFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/OriginatorEntityOwnerUsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/OriginatorEntityOwnerUsersFilter.java index a7dfdff074..375b47cc03 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/OriginatorEntityOwnerUsersFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/OriginatorEntityOwnerUsersFilter.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class OriginatorEntityOwnerUsersFilter implements UsersFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/PlatformUsersNotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/PlatformUsersNotificationTargetConfig.java index 1d97552c2e..df26d6ca65 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/PlatformUsersNotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/PlatformUsersNotificationTargetConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -22,10 +23,12 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class PlatformUsersNotificationTargetConfig extends NotificationTargetConfig { + @Schema(implementation = UsersFilter.class) @NotNull @Valid private UsersFilter usersFilter; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java index 8c5330d165..8daf518089 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class SystemAdministratorsFilter implements SystemLevelUsersFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java index 6f73f45e08..3b551b4fbf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java @@ -15,15 +15,20 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.Set; import java.util.UUID; +@Schema @Data public class TenantAdministratorsFilter implements SystemLevelUsersFilter { + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set tenantsIds; + @ArraySchema(schema = @Schema(implementation = UUID.class)) private Set tenantProfilesIds; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UserListFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UserListFilter.java index e0adb26d59..2534bc8dbc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UserListFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UserListFilter.java @@ -15,15 +15,19 @@ */ package org.thingsboard.server.common.data.notification.targets.platform; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import java.util.List; import java.util.UUID; +@Schema @Data public class UserListFilter implements UsersFilter { + @ArraySchema(schema = @Schema(implementation = UUID.class)) @NotEmpty private List usersIds; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UsersFilter.java index 596972be80..2b3628012c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UsersFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/UsersFilter.java @@ -20,7 +20,22 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "USER_LIST", schema = UserListFilter.class), + @DiscriminatorMapping(value = "CUSTOMER_USERS", schema = CustomerUsersFilter.class), + @DiscriminatorMapping(value = "TENANT_ADMINISTRATORS", schema = TenantAdministratorsFilter.class), + @DiscriminatorMapping(value = "AFFECTED_TENANT_ADMINISTRATORS", schema = AffectedTenantAdministratorsFilter.class), + @DiscriminatorMapping(value = "SYSTEM_ADMINISTRATORS", schema = SystemAdministratorsFilter.class), + @DiscriminatorMapping(value = "ALL_USERS", schema = AllUsersFilter.class), + @DiscriminatorMapping(value = "ORIGINATOR_ENTITY_OWNER_USERS", schema = OriginatorEntityOwnerUsersFilter.class), + @DiscriminatorMapping(value = "AFFECTED_USER", schema = AffectedUserFilter.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java index 4febcc1d72..0b8992309e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.notification.targets.slack; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.notification.targets.NotificationRecip import static org.apache.commons.lang3.StringUtils.isEmpty; +@Schema @Data @NoArgsConstructor @AllArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackNotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackNotificationTargetConfig.java index 11c84549d2..ba21291465 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackNotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackNotificationTargetConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.targets.slack; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -22,6 +23,7 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class SlackNotificationTargetConfig extends NotificationTargetConfig { @@ -31,6 +33,7 @@ public class SlackNotificationTargetConfig extends NotificationTargetConfig { @Valid private SlackConversation conversation; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of the notification target") @Override public NotificationTargetType getType() { return NotificationTargetType.SLACK; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java index a679d0e6e7..e8a9784310 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.NoArgsConstructor; @@ -27,6 +29,18 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho import java.util.List; +@Schema( + description = "Base template for different delivery methods", + discriminatorProperty = "method", + discriminatorMapping = { + @DiscriminatorMapping(value = "WEB", schema = WebDeliveryMethodNotificationTemplate.class), + @DiscriminatorMapping(value = "EMAIL", schema = EmailDeliveryMethodNotificationTemplate.class), + @DiscriminatorMapping(value = "SMS", schema = SmsDeliveryMethodNotificationTemplate.class), + @DiscriminatorMapping(value = "SLACK", schema = SlackDeliveryMethodNotificationTemplate.class), + @DiscriminatorMapping(value = "MICROSOFT_TEAMS", schema = MicrosoftTeamsDeliveryMethodNotificationTemplate.class), + @DiscriminatorMapping(value = "MOBILE_APP", schema = MobileAppDeliveryMethodNotificationTemplate.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method") @JsonSubTypes({ diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java index 59293e9429..261c127ce4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.template; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; @@ -30,6 +31,7 @@ import java.util.List; @NoArgsConstructor @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) +@Schema public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { @NoXss(fieldName = "email subject") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java index 97def8310e..4d03afd7e2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.notification.template; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -30,8 +31,11 @@ import java.util.UUID; @ToString(callSuper = true) public class MicrosoftTeamsDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + @Schema private String subject; + @Schema private String themeColor; + @Schema private Button button; private final List templatableValues = List.of( @@ -82,6 +86,7 @@ public class MicrosoftTeamsDeliveryMethodNotificationTemplate extends DeliveryMe this.setEntityIdInState = other.setEntityIdInState; } + @Schema public enum LinkType { LINK, DASHBOARD } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileAppDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileAppDeliveryMethodNotificationTemplate.java index 248d2dedf5..c7d8689f45 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileAppDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileAppDeliveryMethodNotificationTemplate.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.notification.template; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; @@ -31,8 +32,10 @@ import java.util.List; @ToString(callSuper = true) public class MobileAppDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + @Schema(description = "Subject line for the mobile notification", example = "New Message Received") @NotEmpty private String subject; + @Schema(description = "Additional JSON configuration for web buttons/actions", type = "object") private JsonNode additionalConfig; private final List templatableValues = List.of( diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java index 9385c00abf..c0229678f6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; @@ -37,10 +38,12 @@ import java.util.Optional; @ToString(callSuper = true) public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + @Schema(description = "Subject line for the web notification", example = "New Message Received") @NoXss(fieldName = "web notification subject") @Length(fieldName = "web notification subject", max = 150, message = "cannot be longer than 150 chars") @NotEmpty private String subject; + @Schema(description = "Additional JSON configuration for web buttons/actions") private JsonNode additionalConfig; private final List templatableValues = List.of( diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java index 23c0241d51..d5f61fdbeb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Client.java @@ -93,7 +93,10 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo imp @Schema(description = "List of platforms for which usage of the OAuth2 client is allowed (empty for all allowed)") @Length(fieldName = "platforms") private List platforms; - @Schema(description = "Additional info of OAuth2 client (e.g. providerName)", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "Additional info of OAuth2 client. " + + "Must include: 'providerName' (string, name of the OAuth2 provider).", + requiredMode = Schema.RequiredMode.REQUIRED, + example = "{\"providerName\":\"Google\"}") private JsonNode additionalInfo; public OAuth2Client() { @@ -128,4 +131,5 @@ public class OAuth2Client extends BaseDataWithAdditionalInfo imp public String getName() { return title; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java index 68f06d94d4..25cd734d77 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.page; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data @@ -36,6 +37,7 @@ public class SortOrder { return new SortOrder(property, direction); } + @Schema(name = "SortOrderDirection") public static enum Direction { ASC, DESC } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java index f9d2e8d3d8..8431d7a303 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -31,6 +32,7 @@ import java.util.List; @AllArgsConstructor @Data @ToString +@Schema public class AlarmCountQuery extends EntityCountQuery { private long startTs; private long endTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/BooleanFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/BooleanFilterPredicate.java index 74c4f87c25..c469a7ef75 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/BooleanFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/BooleanFilterPredicate.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class BooleanFilterPredicate implements SimpleKeyFilterPredicate { @@ -28,6 +30,7 @@ public class BooleanFilterPredicate implements SimpleKeyFilterPredicate return FilterPredicateType.BOOLEAN; } + @Schema public enum BooleanOperation { EQUAL, NOT_EQUAL diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java index 4fb663c4c1..557f08eb2b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexFilterPredicate.java @@ -15,14 +15,18 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; @Data +@Schema public class ComplexFilterPredicate implements KeyFilterPredicate { private ComplexOperation operation; + @ArraySchema(schema = @Schema(ref = "#/components/schemas/KeyFilterPredicate")) private List predicates; @Override @@ -30,6 +34,7 @@ public class ComplexFilterPredicate implements KeyFilterPredicate { return FilterPredicateType.COMPLEX; } + @Schema public enum ComplexOperation { AND, OR diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java index 97fe9972ba..5451e557e7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; +@Schema @Data @RequiredArgsConstructor public class DynamicValue implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataSortOrder.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataSortOrder.java index dae84fa598..c7b933add5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataSortOrder.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataSortOrder.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class EntityDataSortOrder { @@ -34,6 +36,7 @@ public class EntityDataSortOrder { this.direction = direction; } + @Schema public enum Direction { ASC, DESC } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java index 4263a4aeea..bf75f7e9ca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java @@ -19,12 +19,34 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import static org.thingsboard.server.common.data.query.AliasEntityId.resolveAliasEntityId; +@Schema( + description = "Filter for selecting entities", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "singleEntity", schema = SingleEntityFilter.class), + @DiscriminatorMapping(value = "entityList", schema = EntityListFilter.class), + @DiscriminatorMapping(value = "entityName", schema = EntityNameFilter.class), + @DiscriminatorMapping(value = "entityType", schema = EntityTypeFilter.class), + @DiscriminatorMapping(value = "assetType", schema = AssetTypeFilter.class), + @DiscriminatorMapping(value = "deviceType", schema = DeviceTypeFilter.class), + @DiscriminatorMapping(value = "edgeType", schema = EdgeTypeFilter.class), + @DiscriminatorMapping(value = "entityViewType", schema = EntityViewTypeFilter.class), + @DiscriminatorMapping(value = "apiUsageState", schema = ApiUsageStateFilter.class), + @DiscriminatorMapping(value = "relationsQuery", schema = RelationsQueryFilter.class), + @DiscriminatorMapping(value = "assetSearchQuery", schema = AssetSearchQueryFilter.class), + @DiscriminatorMapping(value = "deviceSearchQuery", schema = DeviceSearchQueryFilter.class), + @DiscriminatorMapping(value = "entityViewSearchQuery", schema = EntityViewSearchQueryFilter.class), + @DiscriminatorMapping(value = "edgeSearchQuery", schema = EdgeSearchQueryFilter.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java index 8501ddca12..3103a1a161 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityViewTypeFilter.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java index 6a080e3b28..cb844c5c44 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/FilterPredicateValue.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.query; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.Data; import lombok.Getter; @@ -25,6 +26,7 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; +@Schema @Data public class FilterPredicateValue implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java index 949ccc9bc5..cb5c7a835f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilterPredicate.java @@ -18,9 +18,21 @@ package org.thingsboard.server.common.data.query; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import java.io.Serializable; +@Schema( + description = "Filter predicate for key-based filtering", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "STRING", schema = StringFilterPredicate.class), + @DiscriminatorMapping(value = "NUMERIC", schema = NumericFilterPredicate.class), + @DiscriminatorMapping(value = "BOOLEAN", schema = BooleanFilterPredicate.class), + @DiscriminatorMapping(value = "COMPLEX", schema = ComplexFilterPredicate.class) + } +) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/NumericFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/NumericFilterPredicate.java index 0b19d56ea6..0d7953defc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/NumericFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/NumericFilterPredicate.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class NumericFilterPredicate implements SimpleKeyFilterPredicate { @@ -28,6 +30,7 @@ public class NumericFilterPredicate implements SimpleKeyFilterPredicate return FilterPredicateType.NUMERIC; } + @Schema public enum NumericOperation { EQUAL, NOT_EQUAL, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java index 8c063201f7..027c4a7c5c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java @@ -15,15 +15,17 @@ */ package org.thingsboard.server.common.data.query; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.List; import java.util.Set; +@Schema @Data public class RelationsQueryFilter implements EntityFilter { @@ -33,6 +35,7 @@ public class RelationsQueryFilter implements EntityFilter { } private AliasEntityId rootEntity; + @JsonProperty("multiRoot") private boolean isMultiRoot; private EntityType multiRootEntitiesType; private Set multiRootEntityIds; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/SimpleKeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/SimpleKeyFilterPredicate.java index 2a06eb57c2..f85c259034 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/SimpleKeyFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/SimpleKeyFilterPredicate.java @@ -15,8 +15,12 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Base interface for simple key filter predicates") public interface SimpleKeyFilterPredicate extends KeyFilterPredicate { + @Schema(description = "The value associated with the filter predicate") FilterPredicateValue getValue(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java index 046f86f414..c0bbd43378 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java @@ -16,7 +16,6 @@ package org.thingsboard.server.common.data.query; import lombok.Data; -import org.thingsboard.server.common.data.id.EntityId; @Data public class SingleEntityFilter implements EntityFilter { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java index 915dc6ef5f..da127a58b3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/StringFilterPredicate.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.Data; @Data +@Schema public class StringFilterPredicate implements SimpleKeyFilterPredicate { private StringOperation operation; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index 76918cc8d9..fc24327a6d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -103,7 +103,9 @@ public class EntityRelation implements HasVersion, Serializable, EdqsObject { this.version = entityRelation.getVersion(); } - @Schema(description = "Additional parameters of the relation", implementation = JsonNode.class) + @Schema(description = "Additional parameters of the relation.", + implementation = JsonNode.class, + example = "{\"description\":\"Power supply connection\"}") public JsonNode getAdditionalInfo() { return BaseDataWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index 4d8bb35919..f81bf841e5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -41,7 +41,7 @@ public class RuleChain extends BaseDataWithAdditionalInfo implement private static final long serialVersionUID = -5656679015121935465L; - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY) private TenantId tenantId; @NoXss @Length(fieldName = "name") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java index 981d5c2598..ac1e85dbeb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java @@ -105,7 +105,12 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements return super.getCreatedTime(); } - @Schema(description = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", implementation = JsonNode.class) + @Schema(description = "Additional parameters of the rule node. " + + "May include: 'layoutX' (number, X coordinate for visualization), " + + "'layoutY' (number, Y coordinate for visualization), " + + "'description' (string).", + implementation = JsonNode.class, + example = "{\"layoutX\":320,\"layoutY\":160,\"description\":\"Filter temperature data\"}") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java index 90b777c11c..e26287708b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java @@ -16,6 +16,8 @@ package org.thingsboard.server.common.data.security.model.mfa; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; @@ -29,9 +31,11 @@ import java.util.List; import java.util.Optional; @Data +@Schema(description = "Platform Two-Factor Authentication settings") @JsonIgnoreProperties(ignoreUnknown = true) public class PlatformTwoFaSettings { + @ArraySchema(schema = @Schema(implementation = TwoFaProviderConfig.class)) @Valid @NotNull private List providers; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java index b24bd68c52..f613b37220 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import java.util.LinkedHashMap; @Data +@Schema(description = "Account Two-Factor Authentication Settings") public class AccountTwoFaSettings { private LinkedHashMap configs; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/BackupCodeTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/BackupCodeTwoFaAccountConfig.java index 3c673c1e8e..74a804688d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/BackupCodeTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/BackupCodeTwoFaAccountConfig.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.security.model.mfa.account; import com.fasterxml.jackson.annotation.JsonGetter; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.EqualsAndHashCode; @@ -23,6 +24,7 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi import java.util.Set; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class BackupCodeTwoFaAccountConfig extends TwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java index 5960fa8a34..5e87cf5ce7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class EmailTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java index 574a8e01b3..e5903fecf5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +@Schema @Data @EqualsAndHashCode(callSuper = true) public abstract class OtpBasedTwoFaAccountConfig extends TwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java index 97755f6195..1a9b2b718e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +@Schema @EqualsAndHashCode(callSuper = true) @Data public class SmsTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java index 384ca39e3b..799e381b4f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.security.model.mfa.account; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class TotpTwoFaAccountConfig extends TwoFaAccountConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java index d8783178e0..06a5088117 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; @@ -35,6 +37,16 @@ import java.io.Serializable; @Type(name = "EMAIL", value = EmailTwoFaAccountConfig.class), @Type(name = "BACKUP_CODE", value = BackupCodeTwoFaAccountConfig.class) }) +@Schema( + description = "Base configuration for two-factor authentication accounts", + discriminatorProperty = "providerType", + discriminatorMapping = { + @DiscriminatorMapping(value = "TOTP", schema = TotpTwoFaAccountConfig.class), + @DiscriminatorMapping(value = "SMS", schema = SmsTwoFaAccountConfig.class), + @DiscriminatorMapping(value = "EMAIL", schema = EmailTwoFaAccountConfig.class), + @DiscriminatorMapping(value = "BACKUP_CODE", schema = BackupCodeTwoFaAccountConfig.class) + } +) @Data public abstract class TwoFaAccountConfig implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java index 18da5115f2..5346f996fd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; import lombok.Data; @Data +@Schema public class BackupCodeTwoFaProviderConfig implements TwoFaProviderConfig { @Min(value = 1, message = "must be greater than 0") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java index 56527ebf8a..f9b6d2e618 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @Data +@Schema @EqualsAndHashCode(callSuper = true) public class EmailTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java index 35f18d50e0..bd6b8f4332 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; import lombok.Data; @Data +@Schema public abstract class OtpBasedTwoFaProviderConfig implements TwoFaProviderConfig { @Min(value = 1, message = "is required") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java index e848c7b942..dba2372989 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import lombok.Data; @@ -22,6 +23,7 @@ import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) @Data +@Schema public class SmsTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig { @NotBlank(message = "is required") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java index de91a5b732..64cabcbbbd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.security.model.mfa.provider; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; @Data +@Schema public class TotpTwoFaProviderConfig implements TwoFaProviderConfig { @NotBlank diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java index 42c3556b08..1bb883b375 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java @@ -20,7 +20,19 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; +@Schema( + description = "Two-factor authentication provider configuration", + discriminatorProperty = "providerType", + discriminatorMapping = { + @DiscriminatorMapping(value = "TOTP", schema = TotpTwoFaProviderConfig.class), + @DiscriminatorMapping(value = "SMS", schema = SmsTwoFaProviderConfig.class), + @DiscriminatorMapping(value = "EMAIL", schema = EmailTwoFaProviderConfig.class), + @DiscriminatorMapping(value = "BACKUP_CODE", schema = BackupCodeTwoFaProviderConfig.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java index 02f1db557b..e35f8da2e8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.sms.config; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema @Data public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { @Schema(description = "SMPP version", allowableValues = "3.3, 3.4", requiredMode = Schema.RequiredMode.REQUIRED) @@ -89,7 +90,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { @Schema(description = "Address range", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String addressRange; - @Schema(allowableValues = {"0-10" ,"13-14"}, + @Schema(minimum = "0", maximum = "14", description = "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free, used as default)\n" + "1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))\n" + "2 - Octet Unspecified (8-bit binary)\n" + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java index 5bba9a578b..5b6bceec35 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( @@ -30,6 +32,15 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = TwilioSmsProviderConfiguration.class, name = "TWILIO"), @JsonSubTypes.Type(value = SmppSmsProviderConfiguration.class, name = "SMPP") }) +@Schema( + description = "Base configuration for SMS providers", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "AWS_SNS", schema = AwsSnsSmsProviderConfiguration.class), + @DiscriminatorMapping(value = "TWILIO", schema = TwilioSmsProviderConfiguration.class), + @DiscriminatorMapping(value = "SMPP", schema = SmppSmsProviderConfiguration.class) + } +) public interface SmsProviderConfiguration { @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java index 883a8e383b..2373f53283 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java @@ -18,17 +18,23 @@ 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 io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.security.DeviceCredentials; +@Schema @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Data public class DeviceExportData extends EntityExportData { + @Override + public EntityType getEntityType() { return EntityType.DEVICE; } + @JsonProperty(index = 3) @JsonIgnoreProperties({"id", "deviceId", "createdTime", "version"}) private DeviceCredentials credentials; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java index 01b07026fa..3e04074236 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java @@ -23,11 +23,25 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.ai.AiModel; +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.id.EntityId; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.sync.JsonTbEntity; @@ -36,17 +50,50 @@ import java.util.List; import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "entityType", include = As.EXISTING_PROPERTY, visible = true, defaultImpl = EntityExportData.class) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "entityType", include = As.EXISTING_PROPERTY, visible = true) @JsonSubTypes({ @Type(name = "DEVICE", value = DeviceExportData.class), @Type(name = "RULE_CHAIN", value = RuleChainExportData.class), @Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class), @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class), - @Type(name = "OTA_PACKAGE", value = OtaPackageExportData.class) + @Type(name = "OTA_PACKAGE", value = OtaPackageExportData.class), + @Type(name = "CUSTOMER", value = EntityExportData.CustomerExportData.class), + @Type(name = "TB_RESOURCE", value = EntityExportData.TbResourceExportData.class), + @Type(name = "DASHBOARD", value = EntityExportData.DashboardExportData.class), + @Type(name = "ASSET_PROFILE", value = EntityExportData.AssetProfileExportData.class), + @Type(name = "ASSET", value = EntityExportData.AssetExportData.class), + @Type(name = "DEVICE_PROFILE", value = EntityExportData.DeviceProfileExportData.class), + @Type(name = "ENTITY_VIEW", value = EntityExportData.EntityViewExportData.class), + @Type(name = "NOTIFICATION_TEMPLATE", value = EntityExportData.NotificationTemplateExportData.class), + @Type(name = "NOTIFICATION_TARGET", value = EntityExportData.NotificationTargetExportData.class), + @Type(name = "NOTIFICATION_RULE", value = EntityExportData.NotificationRuleExportData.class), + @Type(name = "AI_MODEL", value = EntityExportData.AiModelExportData.class) }) @JsonInclude(JsonInclude.Include.NON_NULL) +@Schema( + description = "Base export container for ThingsBoard entities", + discriminatorProperty = "entityType", + discriminatorMapping = { + @DiscriminatorMapping(value = "CUSTOMER", schema = EntityExportData.CustomerExportData.class), + @DiscriminatorMapping(value = "DEVICE", schema = DeviceExportData.class), + @DiscriminatorMapping(value = "RULE_CHAIN", schema = RuleChainExportData.class), + @DiscriminatorMapping(value = "WIDGET_TYPE", schema = WidgetTypeExportData.class), + @DiscriminatorMapping(value = "WIDGETS_BUNDLE", schema = WidgetsBundleExportData.class), + @DiscriminatorMapping(value = "OTA_PACKAGE", schema = OtaPackageExportData.class), + @DiscriminatorMapping(value = "TB_RESOURCE", schema = EntityExportData.TbResourceExportData.class), + @DiscriminatorMapping(value = "DASHBOARD", schema = EntityExportData.DashboardExportData.class), + @DiscriminatorMapping(value = "ASSET_PROFILE", schema = EntityExportData.AssetProfileExportData.class), + @DiscriminatorMapping(value = "ASSET", schema = EntityExportData.AssetExportData.class), + @DiscriminatorMapping(value = "DEVICE_PROFILE", schema = EntityExportData.DeviceProfileExportData.class), + @DiscriminatorMapping(value = "ENTITY_VIEW", schema = EntityExportData.EntityViewExportData.class), + @DiscriminatorMapping(value = "NOTIFICATION_TEMPLATE", schema = EntityExportData.NotificationTemplateExportData.class), + @DiscriminatorMapping(value = "NOTIFICATION_TARGET", schema = EntityExportData.NotificationTargetExportData.class), + @DiscriminatorMapping(value = "NOTIFICATION_RULE", schema = EntityExportData.NotificationRuleExportData.class), + @DiscriminatorMapping(value = "AI_MODEL", schema = EntityExportData.AiModelExportData.class) + } +) @Data -public class EntityExportData> { +public abstract class EntityExportData> { public static final Comparator relationsComparator = Comparator .comparing(EntityRelation::getFrom, Comparator.comparing(EntityId::getId)) @@ -61,18 +108,45 @@ public class EntityExportData> { @JsonProperty(index = 2) @JsonTbEntity + @Schema(implementation = ExportableEntity.class) private E entity; @JsonProperty(index = 1) - private EntityType entityType; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + public abstract EntityType getEntityType(); @JsonProperty(index = 100) + @ArraySchema(schema = @Schema(implementation = EntityRelation.class)) private List relations; @JsonProperty(index = 101) + @Schema(description = "Map of attributes where key is the scope of attributes and value is the list of attributes for that scope") private Map> attributes; @JsonProperty(index = 102) @JsonIgnoreProperties({"id", "entityId", "createdTime", "version"}) + @ArraySchema(schema = @Schema(implementation = CalculatedField.class)) private List calculatedFields; + public static EntityExportData newInstance(EntityType entityType) { + return switch (entityType) { + case DEVICE -> new DeviceExportData(); + case RULE_CHAIN -> new RuleChainExportData(); + case WIDGET_TYPE -> new WidgetTypeExportData(); + case WIDGETS_BUNDLE -> new WidgetsBundleExportData(); + case OTA_PACKAGE -> new OtaPackageExportData(); + case CUSTOMER -> new CustomerExportData(); + case TB_RESOURCE -> new TbResourceExportData(); + case DASHBOARD -> new DashboardExportData(); + case ASSET_PROFILE -> new AssetProfileExportData(); + case ASSET -> new AssetExportData(); + case DEVICE_PROFILE -> new DeviceProfileExportData(); + case ENTITY_VIEW -> new EntityViewExportData(); + case NOTIFICATION_TEMPLATE -> new NotificationTemplateExportData(); + case NOTIFICATION_TARGET -> new NotificationTargetExportData(); + case NOTIFICATION_RULE -> new NotificationRuleExportData(); + case AI_MODEL -> new AiModelExportData(); + default -> throw new IllegalArgumentException("Unsupported entity type: " + entityType); + }; + } + public EntityExportData sort() { if (relations != null && !relations.isEmpty()) { relations.sort(relationsComparator); @@ -111,4 +185,92 @@ public class EntityExportData> { return calculatedFields != null && !calculatedFields.isEmpty(); } + @Schema + public static class CustomerExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.CUSTOMER; + } + } + + @Schema + public static class TbResourceExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.TB_RESOURCE; + } + } + + @Schema + public static class DashboardExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.DASHBOARD; + } + } + + @Schema + public static class AssetProfileExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.ASSET_PROFILE; + } + } + + @Schema + public static class AssetExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.ASSET; + } + } + + @Schema + public static class DeviceProfileExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.DEVICE_PROFILE; + } + } + + @Schema + public static class EntityViewExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.ENTITY_VIEW; + } + } + + @Schema + public static class NotificationTemplateExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_TEMPLATE; + } + } + + @Schema + public static class NotificationTargetExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_TARGET; + } + } + + @Schema + public static class NotificationRuleExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_RULE; + } + } + + @Schema + public static class AiModelExportData extends EntityExportData { + @Override + public EntityType getEntityType() { + return EntityType.AI_MODEL; + } + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java index ebab6164b3..727c026bc3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java @@ -16,12 +16,18 @@ package org.thingsboard.server.common.data.sync.ie; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; +@Schema @EqualsAndHashCode(callSuper = true) public class OtaPackageExportData extends EntityExportData { + @Override + public EntityType getEntityType() { return EntityType.OTA_PACKAGE; } + /* * OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking) * We override both methods to ensure 'version' is not ignored during (de)serialization. diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java index c00e6a59a2..7b9ec8e0a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java @@ -17,17 +17,23 @@ package org.thingsboard.server.common.data.sync.ie; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +@Schema @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Data public class RuleChainExportData extends EntityExportData { + @Override + public EntityType getEntityType() { return EntityType.RULE_CHAIN; } + @JsonProperty(index = 3) @JsonIgnoreProperties({"ruleChainId", "version"}) private RuleChainMetaData metaData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java index 1da9a64eb1..f69b3603db 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetTypeExportData.java @@ -15,12 +15,18 @@ */ package org.thingsboard.server.common.data.sync.ie; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class WidgetTypeExportData extends EntityExportData { + @Override + public EntityType getEntityType() { return EntityType.WIDGET_TYPE; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java index 978b44357a..62140abdd1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/WidgetsBundleExportData.java @@ -16,18 +16,27 @@ package org.thingsboard.server.common.data.sync.ie; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.widget.WidgetsBundle; import java.util.ArrayList; import java.util.List; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class WidgetsBundleExportData extends EntityExportData { + @Override + public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; } + + @ArraySchema(arraySchema = @Schema(description = "List of widgets in the bundle"), schema = @Schema(implementation = JsonNode.class)) @JsonProperty(index = 3) private List widgets; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataDiff.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataDiff.java index 71d715d586..c3ca21cdaf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataDiff.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityDataDiff.java @@ -15,13 +15,17 @@ */ package org.thingsboard.server.common.data.sync.vc; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.sync.ie.EntityExportData; +@Schema @Data @AllArgsConstructor public class EntityDataDiff { + @Schema(implementation = EntityExportData.class) private EntityExportData currentVersion; + @Schema(implementation = EntityExportData.class) private EntityExportData otherVersion; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/ComplexVersionCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/ComplexVersionCreateRequest.java index d9ba1817cf..13fb3f08d6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/ComplexVersionCreateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/ComplexVersionCreateRequest.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.sync.vc.request.create; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.EntityType; import java.util.Map; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class ComplexVersionCreateRequest extends VersionCreateRequest { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/SingleEntityVersionCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/SingleEntityVersionCreateRequest.java index 8a1ebb06e0..4798393cb4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/SingleEntityVersionCreateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/SingleEntityVersionCreateRequest.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.sync.vc.request.create; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.EntityId; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class SingleEntityVersionCreateRequest extends VersionCreateRequest { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java index fe30c1d6ae..81b4aca9a0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.sync.vc.request.create; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.io.Serial; import java.io.Serializable; +@Schema @Data public class VersionCreateConfig implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateRequest.java index 1939f2e222..a3e3071474 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateRequest.java @@ -18,8 +18,18 @@ package org.thingsboard.server.common.data.sync.vc.request.create; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +@Schema( + description = "Request for creating a version", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SINGLE_ENTITY", schema = SingleEntityVersionCreateRequest.class), + @DiscriminatorMapping(value = "COMPLEX", schema = ComplexVersionCreateRequest.class) + } +) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @Type(name = "SINGLE_ENTITY", value = SingleEntityVersionCreateRequest.class), @@ -31,6 +41,7 @@ public abstract class VersionCreateRequest { private String versionName; private String branch; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of the version to create") public abstract VersionCreateRequestType getType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadConfig.java index 7156232eba..dfc783bffb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadConfig.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.sync.vc.request.load; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @Data +@Schema @EqualsAndHashCode(callSuper = true) public class EntityTypeVersionLoadConfig extends VersionLoadConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java index 8bb4902f43..c3a1260fb0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/EntityTypeVersionLoadRequest.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data.sync.vc.request.load; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.EntityType; import java.util.Map; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class EntityTypeVersionLoadRequest extends VersionLoadRequest { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/SingleEntityVersionLoadRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/SingleEntityVersionLoadRequest.java index 5379fc69e9..46c760a60f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/SingleEntityVersionLoadRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/SingleEntityVersionLoadRequest.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.sync.vc.request.load; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.EntityId; +@Schema @Data @EqualsAndHashCode(callSuper = true) public class SingleEntityVersionLoadRequest extends VersionLoadRequest { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java index d7620d9ea5..7f0e3158d8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.sync.vc.request.load; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data +@Schema public class VersionLoadConfig { private boolean loadRelations; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadRequest.java index dffa3bdf3d..85b2def3e1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadRequest.java @@ -17,10 +17,20 @@ package org.thingsboard.server.common.data.sync.vc.request.load; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import static com.fasterxml.jackson.annotation.JsonSubTypes.Type; +@Schema( + description = "Request for loading a version", + discriminatorProperty = "type", + discriminatorMapping = { + @DiscriminatorMapping(value = "SINGLE_ENTITY", schema = SingleEntityVersionLoadRequest.class), + @DiscriminatorMapping(value = "ENTITY_TYPE", schema = EntityTypeVersionLoadRequest.class) + } +) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @Type(name = "SINGLE_ENTITY", value = SingleEntityVersionLoadRequest.class), @@ -31,6 +41,7 @@ public abstract class VersionLoadRequest { private String versionId; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Type of the version to load") public abstract VersionLoadRequestType getType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index a04816365b..297f92cffb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.tenant.profile; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -173,6 +174,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura @Schema(example = "10") private long maxArgumentsPerCF = 10; @Schema(example = "10") + @PositiveOrZero private int minAllowedScheduledUpdateIntervalInSecForCF = 10; @Builder.Default @Schema(example = "2") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java index 973b4807d3..008988e6cb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.transport.resource; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(hidden = true) public enum ResourceType { LWM2M_MODEL, JKS, PKCS_12 } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java index 0fa4d8f053..d38733af24 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec; import org.thingsboard.server.common.data.transport.snmp.SnmpMapping; import org.thingsboard.server.common.data.transport.snmp.SnmpMethod; @@ -31,6 +33,17 @@ import org.thingsboard.server.common.data.transport.snmp.config.impl.ToDeviceRpc import java.io.Serializable; import java.util.List; +@Schema( + description = "SNMP communication configuration", + discriminatorProperty = "spec", + discriminatorMapping = { + @DiscriminatorMapping(value = "TELEMETRY_QUERYING", schema = TelemetryQueryingSnmpCommunicationConfig.class), + @DiscriminatorMapping(value = "CLIENT_ATTRIBUTES_QUERYING", schema = ClientAttributesQueryingSnmpCommunicationConfig.class), + @DiscriminatorMapping(value = "SHARED_ATTRIBUTES_SETTING", schema = SharedAttributesSettingSnmpCommunicationConfig.class), + @DiscriminatorMapping(value = "TO_DEVICE_RPC_REQUEST", schema = ToDeviceRpcRequestSnmpCommunicationConfig.class), + @DiscriminatorMapping(value = "TO_SERVER_RPC_REQUEST", schema = ToServerRpcRequestSnmpCommunicationConfig.class) + } +) @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "spec") @JsonSubTypes({ @@ -42,6 +55,7 @@ import java.util.List; }) public interface SnmpCommunicationConfig extends Serializable { + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Specification of the SNMP communication") SnmpCommunicationSpec getSpec(); @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java index 398f4b6929..1cbaa957f0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java @@ -33,7 +33,7 @@ import java.util.List; @Data @EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"fqn", "name", "deprecated", "image", "description", "descriptor", "externalId", "resources"}) +@JsonPropertyOrder({"id", "createdTime", "tenantId", "fqn", "name", "deprecated", "scada", "version", "descriptor", "image", "description", "tags", "externalId", "resources"}) public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity { @Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java index 23e0f1add3..a2df32b483 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java @@ -47,8 +47,8 @@ public class ScheduledUpdateSupportedCalculatedFieldConfigurationTest { assertThatThrownBy(() -> cfg.validate(minAllowedInterval)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Scheduled update interval is less than configured " + - "minimum allowed interval in tenant profile: " + minAllowedInterval); + .hasMessage("Scheduled update interval (1 seconds) is less than " + + "minimum allowed interval in tenant profile: " + minAllowedInterval + " seconds"); } } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java index 2f391435b2..2c68bcd8c5 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java @@ -28,6 +28,7 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; @@ -103,4 +104,17 @@ public class GeofencingCalculatedFieldConfigurationTest { assertThat(allowedZonesArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("perimeter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); } + @Test + void validateShouldThrowWhenScheduledUpdateEnabledButIntervalNotSet() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setEntityCoordinates(mock(EntityCoordinates.class)); + cfg.setZoneGroups(Map.of("zone", mock(ZoneGroupConfiguration.class))); + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(null); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Refresh interval is required when periodic zone group refresh is enabled."); + } + } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java index d645d40dcc..9d6434a7c2 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java @@ -15,8 +15,18 @@ */ package org.thingsboard.server.common.data.id; +import io.swagger.v3.oas.annotations.media.DiscriminatorMapping; +import io.swagger.v3.oas.annotations.media.Schema; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.EntityType; + +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; public class EntityIdTest { @@ -25,4 +35,44 @@ public class EntityIdTest { Assertions.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString()); } + @Test + public void allEntityIdImplementors_shouldBeInDiscriminatorMapping() { + Schema schemaAnnotation = EntityId.class.getAnnotation(Schema.class); + assertThat(schemaAnnotation).as("EntityId must have @Schema annotation").isNotNull(); + + DiscriminatorMapping[] mappings = schemaAnnotation.discriminatorMapping(); + Map> discriminatorMap = Arrays.stream(mappings) + .collect(Collectors.toMap(DiscriminatorMapping::value, DiscriminatorMapping::schema)); + + UUID testUuid = UUID.randomUUID(); + for (EntityType entityType : EntityType.values()) { + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, testUuid); + String typeName = entityType.name(); + + assertThat(discriminatorMap) + .as("EntityId @Schema discriminatorMapping is missing entry for EntityType." + typeName) + .containsKey(typeName); + assertThat(discriminatorMap.get(typeName)) + .as("Discriminator mapping for " + typeName + " should point to " + entityId.getClass().getSimpleName()) + .isEqualTo(entityId.getClass()); + } + } + + @Test + public void allEntityIdImplementors_shouldHaveAllOfEntityId() { + UUID testUuid = UUID.randomUUID(); + for (EntityType entityType : EntityType.values()) { + EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, testUuid); + Class idClass = entityId.getClass(); + Schema schemaAnnotation = idClass.getAnnotation(Schema.class); + + assertThat(schemaAnnotation) + .as(idClass.getSimpleName() + " must have @Schema annotation") + .isNotNull(); + assertThat(schemaAnnotation.allOf()) + .as(idClass.getSimpleName() + " @Schema must include allOf = EntityId.class") + .contains(EntityId.class); + } + } + } \ No newline at end of file diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java new file mode 100644 index 0000000000..dd932fa2c5 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/sync/ie/EntityExportDataTest.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2026 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.JsonSubTypes; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.EntityType; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EntityExportDataTest { + + @Test + public void newInstance_shouldSupportAllJsonSubTypes() { + JsonSubTypes subTypes = EntityExportData.class.getAnnotation(JsonSubTypes.class); + assertThat(subTypes).as("EntityExportData must have @JsonSubTypes annotation").isNotNull(); + + Set jsonSubTypeNames = Arrays.stream(subTypes.value()) + .map(JsonSubTypes.Type::name) + .collect(Collectors.toSet()); + + for (String typeName : jsonSubTypeNames) { + EntityType entityType = EntityType.valueOf(typeName); + EntityExportData instance = EntityExportData.newInstance(entityType); + + assertThat(instance) + .as("newInstance(%s) should not return null", typeName) + .isNotNull(); + assertThat(instance.getEntityType()) + .as("newInstance(%s).getEntityType() should return %s", typeName, entityType) + .isEqualTo(entityType); + } + } + +} diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index 9a7cfe43ff..56e90e4b7c 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -213,7 +213,7 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } - @Operation(summary = "Save claiming information (claimDevice)", + @Operation(summary = "Save claiming information (saveClaimingInfo)", description = "Saves the information required for user to claim the device. " + "See more info about claiming in the corresponding 'Claiming devices' platform documentation." + "\n Example of the request payload: " @@ -224,7 +224,7 @@ public class DeviceApiController implements TbTransportService { "In case the secretKey is not specified, the empty string as a default value is used. In case the durationMs is not specified, the system parameter device.claim.duration is used.\n\n" + REQUIRE_ACCESS_TOKEN) @RequestMapping(value = "/{deviceToken}/claim", method = RequestMethod.POST) - public DeferredResult claimDevice( + public DeferredResult saveClaimingInfo( @Parameter(description = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true , schema = @Schema(defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")) @PathVariable("deviceToken") String deviceToken, @RequestBody(required = false) String json) { diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 2cf758885f..cf547cb9d4 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -243,6 +243,14 @@ public class JacksonUtil { } } + public static T treeToValue(JsonNode node, TypeReference type) { + try { + return OBJECT_MAPPER.treeToValue(node, type); + } catch (IOException e) { + throw new IllegalArgumentException("Can't convert value: " + node.toString(), e); + } + } + public static JsonNode toJsonNode(String value) { return toJsonNode(value, OBJECT_MAPPER); } 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 00c5ec372e..075e2d8a9d 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 @@ -26,18 +26,21 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; 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.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityService; 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.usagerecord.ApiLimitService; import java.util.EnumSet; import java.util.List; @@ -62,6 +65,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements private final EntityService entityService; private final CalculatedFieldDao calculatedFieldDao; private final CalculatedFieldDataValidator calculatedFieldDataValidator; + private final ApiLimitService apiLimitService; @Override public CalculatedField save(CalculatedField calculatedField) { @@ -70,6 +74,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements @Override public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { + setConfigurationDefaults(calculatedField); CalculatedField oldCalculatedField = null; if (doValidate) { oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); @@ -79,6 +84,15 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return doSave(calculatedField, oldCalculatedField); } + private void setConfigurationDefaults(CalculatedField calculatedField) { + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config + && config.getScheduledUpdateInterval() == null) { + int minScheduledUpdateInterval = (int) apiLimitService.getLimit( + calculatedField.getTenantId(), DefaultTenantProfileConfiguration::getMinAllowedScheduledUpdateIntervalInSecForCF + ); + config.setScheduledUpdateInterval(minScheduledUpdateInterval); + } + } private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) { try { diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index b46540aa55..fb8d5eb1d8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -511,12 +511,10 @@ public class DefaultNotifications { rule.setTriggerConfig(defaultRule.getTriggerConfig()); if (rule.getTriggerType() == NotificationRuleTriggerType.ALARM) { EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(rule.getTriggerType()); recipientsConfig.setEscalationTable(Map.of(0, toUUIDs(List.of(targets)))); rule.setRecipientsConfig(recipientsConfig); } else { - DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(rule.getTriggerType()); + DefaultNotificationRuleRecipientsConfig recipientsConfig = DefaultNotificationRuleRecipientsConfig.forTriggerType(rule.getTriggerType()); recipientsConfig.setTargets(toUUIDs(List.of(targets))); rule.setRecipientsConfig(recipientsConfig); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 9a467071ef..5ee9af6292 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -93,11 +93,13 @@ public abstract class AbstractServiceTest { @Autowired protected EntityServiceRegistry entityServiceRegistry; + protected Tenant tenant; protected TenantId tenantId; @Before public void beforeAbstractService() { - tenantId = createTenant().getId(); + tenant = createTenant(); + tenantId = tenant.getId(); } @After 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 d157783bf1..3caf4bb5f4 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,11 +15,13 @@ */ package org.thingsboard.server.dao.service; +import org.apache.commons.lang3.RandomUtils; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; @@ -30,6 +32,10 @@ import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; @@ -40,6 +46,7 @@ import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; @@ -60,6 +67,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { private DeviceService deviceService; @Autowired private TbTenantProfileCache tbTenantProfileCache; + @Autowired + private TenantProfileService tenantProfileService; @Test public void testSaveCalculatedField() { @@ -85,8 +94,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); assertThat(updatedCalculatedField.getAdditionalInfo()).isEqualTo(savedCalculatedField.getAdditionalInfo()); assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); - - calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId()); } @Test @@ -116,11 +123,11 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { int min = tbTenantProfileCache.get(tenantId) .getDefaultProfileConfiguration() .getMinAllowedScheduledUpdateIntervalInSecForCF(); - int valueFromConfig = min - 10; // Enable scheduling with an interval below tenant min cfg.setScheduledUpdateEnabled(true); - cfg.setScheduledUpdateInterval(valueFromConfig); + int invalidInterval = RandomUtils.insecure().randomInt(1, min); + cfg.setScheduledUpdateInterval(invalidInterval); // Create & save Calculated Field CalculatedField cf = new CalculatedField(); @@ -134,8 +141,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThatThrownBy(() -> calculatedFieldService.save(cf)) .isInstanceOf(DataValidationException.class) .hasCauseInstanceOf(IllegalArgumentException.class) - .hasMessageStartingWith("Scheduled update interval is less than configured " + - "minimum allowed interval in tenant profile: "); + .hasMessage("Scheduled update interval (" + invalidInterval + + " seconds) is less than minimum allowed interval in tenant profile: " + min + " seconds"); } @Test @@ -236,8 +243,67 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); assertThat(savedInterval).isEqualTo(valueFromConfig); + } - calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + @Test + public void testSaveGeofencingCalculatedField_shouldAcceptZeroScheduledUpdateIntervalWhenTenantProfileAllows() { + // GIVEN + var device = createTestDevice(); + + // Store original value and update tenant profile to allow 0 as min scheduled update interval + TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId()); + int originalMinScheduledUpdateInterval = tenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF(); + tenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(0); + tenantProfileService.saveTenantProfile(tenantId, tenantProfile); + tbTenantProfileCache.evict(tenantProfile.getId()); + + try { + // Build a valid Geofencing configuration + var cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + var entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + var 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); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + // Enable scheduling with interval = 0 + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(0); + + // Create Calculated Field + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF zero scheduled update interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + var out = new AttributesOutput(); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + // WHEN + CalculatedField saved = calculatedFieldService.save(cf); + + // THEN + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + + var savedConfig = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); + assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(0); + } finally { + // Restore original tenant profile value + tenantProfile.getProfileConfiguration().orElseThrow().setMinAllowedScheduledUpdateIntervalInSecForCF(originalMinScheduledUpdateInterval); + tenantProfileService.saveTenantProfile(tenantId, tenantProfile); + tbTenantProfileCache.evict(tenantProfile.getId()); + } } @Test @@ -257,8 +323,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { CalculatedField fetchedCalculatedField = calculatedFieldService.findById(tenantId, savedCalculatedField.getId()); assertThat(fetchedCalculatedField).isEqualTo(savedCalculatedField); - - calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId()); } @Test @@ -270,6 +334,156 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThat(calculatedFieldService.findById(tenantId, savedCalculatedField.getId())).isNull(); } + @Test + public void testSaveRelatedEntitiesAggregationCF_shouldUseMinScheduledUpdateIntervalFromTenantProfileWhenNotSet() { + // GIVEN + var device = createTestDevice(); + + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temp", argument)); + + var metric = new AggMetric(); + metric.setFunction(AggFunction.AVG); + metric.setInput(new AggKeyInput("temp")); + cfg.setMetrics(Map.of("avgTemp", metric)); + + var output = new TimeSeriesOutput(); + output.setName("avgTemperature"); + cfg.setOutput(output); + + int minDeduplicationInterval = (int) tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedDeduplicationIntervalInSecForCF(); + cfg.setDeduplicationIntervalInSec(minDeduplicationInterval); + + // Do NOT set scheduledUpdateInterval - it should default to tenant profile min value + + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + cf.setName("Related Entities Aggregation CF - default scheduled interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + // WHEN + CalculatedField saved = calculatedFieldService.save(cf); + + // THEN + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(RelatedEntitiesAggregationCalculatedFieldConfiguration.class); + + var savedConfig = (RelatedEntitiesAggregationCalculatedFieldConfiguration) saved.getConfiguration(); + int expectedMinScheduledUpdateInterval = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + + assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(expectedMinScheduledUpdateInterval); + } + + @Test + public void testSaveRelatedEntitiesAggregationCF_shouldThrowWhenScheduledUpdateIntervalLessThanMinAllowed() { + // GIVEN + var device = createTestDevice(); + + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temp", argument)); + + var metric = new AggMetric(); + metric.setFunction(AggFunction.AVG); + metric.setInput(new AggKeyInput("temp")); + cfg.setMetrics(Map.of("avgTemp", metric)); + + var output = new TimeSeriesOutput(); + output.setName("avgTemperature"); + cfg.setOutput(output); + + int minDeduplicationInterval = (int) tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedDeduplicationIntervalInSecForCF(); + cfg.setDeduplicationIntervalInSec(minDeduplicationInterval); + + int minScheduledUpdateInterval = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int invalidInterval = RandomUtils.insecure().randomInt(1, minScheduledUpdateInterval); + cfg.setScheduledUpdateInterval(invalidInterval); + + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + cf.setName("Related Entities Aggregation CF - invalid scheduled interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + // WHEN-THEN + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update interval (" + invalidInterval + + " seconds) is less than minimum allowed interval in tenant profile: " + minScheduledUpdateInterval + " seconds"); + } + + @Test + public void testSaveRelatedEntitiesAggregationCF_shouldAcceptValidScheduledUpdateInterval() { + // GIVEN + var device = createTestDevice(); + + var cfg = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE)); + + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temp", argument)); + + var metric = new AggMetric(); + metric.setFunction(AggFunction.AVG); + metric.setInput(new AggKeyInput("temp")); + cfg.setMetrics(Map.of("avgTemp", metric)); + + var output = new TimeSeriesOutput(); + output.setName("avgTemperature"); + cfg.setOutput(output); + + int minDeduplicationInterval = (int) tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedDeduplicationIntervalInSecForCF(); + cfg.setDeduplicationIntervalInSec(minDeduplicationInterval); + + int minScheduledUpdateInterval = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int customScheduledUpdateInterval = minScheduledUpdateInterval + 100; + cfg.setScheduledUpdateInterval(customScheduledUpdateInterval); + + var cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + cf.setName("Related Entities Aggregation CF - valid scheduled interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + // WHEN + CalculatedField saved = calculatedFieldService.save(cf); + + // THEN + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(RelatedEntitiesAggregationCalculatedFieldConfiguration.class); + + var savedConfig = (RelatedEntitiesAggregationCalculatedFieldConfiguration) saved.getConfiguration(); + assertThat(savedConfig.getScheduledUpdateInterval()).isEqualTo(customScheduledUpdateInterval); + } + private CalculatedField saveValidCalculatedField() { Device device = createTestDevice(); CalculatedField calculatedField = getCalculatedField(device.getId(), device.getId()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java index 170b452fee..373344cb6d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidatorTest.java @@ -16,29 +16,35 @@ package org.thingsboard.server.dao.service.validator; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; 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.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.tenant.TenantProfileDao; import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.exception.DataValidationException; import java.util.UUID; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.verify; @SpringBootTest(classes = TenantProfileDataValidator.class) class TenantProfileDataValidatorTest { - @MockBean + @MockitoBean TenantProfileDao tenantProfileDao; - @MockBean + @MockitoBean TenantProfileService tenantProfileService; - @SpyBean + @MockitoSpyBean TenantProfileDataValidator validator; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); @Test @@ -53,4 +59,44 @@ class TenantProfileDataValidatorTest { verify(validator).validateString("Tenant profile name", tenantProfile.getName()); } + @ParameterizedTest + @ValueSource(ints = {-1, -100, Integer.MIN_VALUE}) + void minAllowedScheduledUpdateIntervalInSecForCF_shouldRejectNegativeValues(int value) { + // GIVEN + var config = new DefaultTenantProfileConfiguration(); + config.setMinAllowedScheduledUpdateIntervalInSecForCF(value); + + var tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(config); + + var tenantProfile = new TenantProfile(); + tenantProfile.setName("Test"); + tenantProfile.setProfileData(tenantProfileData); + + // WHEN/THEN + assertThatThrownBy(() -> validator.validate(tenantProfile, __ -> TenantId.SYS_TENANT_ID)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("minAllowedScheduledUpdateIntervalInSecForCF") + .hasMessageContaining("must be greater than or equal to 0"); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 60, Integer.MAX_VALUE}) + void minAllowedScheduledUpdateIntervalInSecForCF_shouldAcceptValidValues(int value) { + // GIVEN + var config = new DefaultTenantProfileConfiguration(); + config.setMinAllowedScheduledUpdateIntervalInSecForCF(value); + + var tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(config); + + var tenantProfile = new TenantProfile(); + tenantProfile.setName("Test"); + tenantProfile.setProfileData(tenantProfileData); + + // WHEN/THEN + assertThatCode(() -> validator.validate(tenantProfile, __ -> TenantId.SYS_TENANT_ID)) + .doesNotThrowAnyException(); + } + } diff --git a/dao/src/test/resources/sql/system-data.sql b/dao/src/test/resources/sql/system-data.sql index 0b17d4f108..2cd038aae7 100644 --- a/dao/src/test/resources/sql/system-data.sql +++ b/dao/src/test/resources/sql/system-data.sql @@ -66,3 +66,5 @@ VALUES ( '6eaaefa6-4612-11e7-a919-92ebcb67fe33', 1592576748000 ,'13814000-1dd2-1 '{"type": "BURST", "batchSize": 1000}', '{"type": "SKIP_ALL_FAILURES", "retries": 3, "failurePercentage": 0.0, "pauseBetweenRetries": 3, "maxPauseBetweenRetries": 3}' ); + +INSERT INTO tb_schema_settings (schema_version, product) VALUES (999999000000, 'CE'); diff --git a/pom.xml b/pom.xml index a4aa43688d..dfa1506bbe 100755 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ ${project.name} /var/log/${pkg.name} /usr/share/${pkg.name} + 4.3.1.2-SNAPSHOT 3.5.13 2.4.0-b180830.0359 0.12.5 @@ -1177,6 +1178,12 @@ ${project.version} test
+ + org.thingsboard.client + thingsboard-ce-client + ${thingsboard.client.version} + test + org.thingsboard.msa js-executor diff --git a/ui-ngx/src/app/core/http/alarm-rules.service.ts b/ui-ngx/src/app/core/http/alarm-rules.service.ts new file mode 100644 index 0000000000..18a51115f3 --- /dev/null +++ b/ui-ngx/src/app/core/http/alarm-rules.service.ts @@ -0,0 +1,73 @@ +/// +/// Copyright © 2016-2026 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. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, defaultHttpOptionsFromParams, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageData } from '@shared/models/page/page-data'; +import { + CalculatedFieldAlarmRule, + CalculatedFieldAlarmRuleInfo, + CalculatedFieldsQuery, + CalculatedFieldTestScriptInputParams +} from '@shared/models/calculated-field.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityTestScriptResult } from '@shared/models/entity.models'; +import { CalculatedFieldEventBody } from '@shared/models/event.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AlarmRulesService { + + constructor( + private http: HttpClient + ) { } + + public getAlarmRuleById(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveAlarmRule(alarmRule: CalculatedFieldAlarmRule, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule', alarmRule, defaultHttpOptionsFromConfig(config)); + } + + public deleteAlarmRule(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRules(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + } + + public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule/testScript', inputParams, defaultHttpOptionsFromConfig(config)); + } + + public getLatestAlarmRuleDebugEvent(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${id}/debug`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRuleNames(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules/names${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts index 62cee00513..10793723db 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -21,11 +21,15 @@ import { AppState } from '@core/core.state'; import { FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; -import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models'; +import { + CalculatedFieldAlarmRule, + CalculatedFieldArgument, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; -import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { AlarmRulesService } from '@core/http/alarm-rules.service'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes"; @@ -49,13 +53,13 @@ import { AssetInfo } from '@shared/models/asset.models'; import { DeviceInfo } from '@shared/models/device.models'; export interface AlarmRuleDialogData { - value?: CalculatedField; + value?: CalculatedFieldAlarmRule; buttonTitle: string; entityId: EntityId; tenantId: string; entityName?: string; ownerId: EntityId; - additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; + additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedFieldAlarmRule) => void>; isDirty?: boolean; getTestScriptDialogFn: AlarmRuleTestScriptFn, } @@ -67,7 +71,7 @@ export interface AlarmRuleDialogData { encapsulation: ViewEncapsulation.None, standalone: false }) -export class AlarmRuleDialogComponent extends DialogComponent { +export class AlarmRuleDialogComponent extends DialogComponent { @ViewChild('entitySelect') entitySelect!: EntitySelectComponent; @@ -96,8 +100,8 @@ export class AlarmRuleDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, - protected dialogRef: MatDialogRef, - private calculatedFieldsService: CalculatedFieldsService, + protected dialogRef: MatDialogRef, + private alarmRulesService: AlarmRulesService, private destroyRef: DestroyRef, private cfFormService: CalculatedFieldFormService) { super(store, router, dialogRef); @@ -159,8 +163,8 @@ export class AlarmRuleDialogComponent extends DialogComponent this.dialogRef.close(calculatedField), diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 039ee7d5fd..f5043bdcee 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -21,7 +21,7 @@ import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { Direction } from '@shared/models/page/sort-order'; import { MatDialog } from '@angular/material/dialog'; @@ -35,15 +35,14 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { AlarmRulesService } from '@core/http/alarm-rules.service'; import { catchError, filter, first, switchMap, tap } from 'rxjs/operators'; import { ArgumentEntityType, ArgumentType, - CalculatedField, CalculatedFieldAlarmRule, + CalculatedFieldAlarmRuleInfo, CalculatedFieldEventArguments, - CalculatedFieldInfo, CalculatedFieldsQuery, CalculatedFieldType, getCalculatedFieldArgumentsEditorCompleter, @@ -72,7 +71,7 @@ import { Router } from '@angular/router'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { AlarmRulesComponent } from '@home/components/alarm-rules/alarm-rules.component'; -type AlarmRuleTableEntity = CalculatedField | CalculatedFieldInfo; +export type AlarmRuleTableEntity = CalculatedFieldAlarmRule | CalculatedFieldAlarmRuleInfo; export class AlarmRulesTableConfig extends EntityTableConfig { @@ -84,7 +83,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.fetchCalculatedFields(pageLink); this.addEntity = this.getCalculatedAlarmDialog.bind(this); - this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id); - this.saveEntity = (alarmRule) => this.calculatedFieldsService.saveCalculatedField(alarmRule); + this.loadEntity = id => this.alarmRulesService.getAlarmRuleById(id.id); + this.saveEntity = (alarmRule) => this.alarmRulesService.saveAlarmRule(alarmRule); this.deleteEntityTitle = (field) => this.translate.instant('alarm-rule.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text'); this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count}); this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text'); - this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); + this.deleteEntity = id => this.alarmRulesService.deleteAlarmRule(id.id); this.onEntityAction = action => this.onCFAction(action); @@ -159,9 +158,9 @@ export class AlarmRulesTableConfig extends EntityTableConfig('name', 'alarm-rule.alarm-type', '33%', entity => this.utilsService.customTranslation(entity.name, entity.name))); if (this.pageMode) { - this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', + this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); - this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', + this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', entity => this.utilsService.customTranslation(entity.entityName, entity.entityName), entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); } @@ -216,8 +215,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig> { return this.pageMode ? - this.calculatedFieldsService.getCalculatedFields(pageLink, {types: [CalculatedFieldType.ALARM], ...this.alarmRuleFilterConfig}) : - this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink, CalculatedFieldType.ALARM); + this.alarmRulesService.getAlarmRules(pageLink, this.alarmRuleFilterConfig) : + this.alarmRulesService.getAlarmRulesByEntityId(this.entityId, pageLink); } onOpenDebugConfig($event: Event, calculatedField: AlarmRuleTableEntity): void { @@ -260,7 +259,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { + private getCalculatedAlarmDialog(value?: AlarmRuleTableEntity, buttonTitle = 'action.add', isDirty = false): Observable { const entityId = this.entityId || value?.entityId; - const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName; - return this.dialog.open(AlarmRuleDialogComponent, { + const entityName = this.entityName || (value as CalculatedFieldAlarmRuleInfo)?.entityName; + return this.dialog.open(AlarmRuleDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { @@ -353,14 +352,14 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), filter(Boolean), - switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), + switchMap(calculatedField => this.alarmRulesService.saveAlarmRule(calculatedField)), filter(Boolean), takeUntilDestroyed(this.destroyRef) ) .subscribe(() => this.updateData()); } - private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField { + private updateImportedCalculatedField(calculatedField: CalculatedFieldAlarmRule): CalculatedFieldAlarmRule { if (calculatedField.type === CalculatedFieldType.ALARM) { calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { const arg = calculatedField.configuration.arguments[key]; @@ -375,48 +374,44 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })), + this.alarmRulesService.getAlarmRuleById(id).pipe( + switchMap(field => this.alarmRulesService.saveAlarmRule({ ...field, debugSettings })), catchError(() => of(null)), takeUntilDestroyed(this.destroyRef), ).subscribe(() => this.updateData()); } - private getTestScriptDialog(calculatedField: AlarmRuleTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { - if (calculatedField.type === CalculatedFieldType.ALARM) { - const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { - const type = calculatedField.configuration.arguments[key].refEntityKey.type; - acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) - ? {...argumentsObj[key], type} - : type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()}; - return acc; - }, {}); - return this.dialog.open(CalculatedFieldScriptTestDialogComponent, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], - data: { - arguments: resultArguments, - expression, - argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments), - argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments), - openCalculatedFieldEdit + getTestScriptDialog(calculatedField: AlarmRuleTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { + const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const type = calculatedField.configuration.arguments[key].refEntityKey.type; + acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) + ? {...argumentsObj[key], type} + : type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()}; + return acc; + }, {}); + return this.dialog.open(CalculatedFieldScriptTestDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], + data: { + arguments: resultArguments, + expression, + argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments), + argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments), + openCalculatedFieldEdit + } + }).afterClosed() + .pipe( + filter(Boolean), + tap(expression => { + if (openCalculatedFieldEdit) { + this.editCalculatedField(null, { + entityId: this.entityId, ...calculatedField, + configuration: {...calculatedField.configuration, expression} as any + }, true) } - }).afterClosed() - .pipe( - filter(Boolean), - tap(expression => { - if (openCalculatedFieldEdit) { - this.editCalculatedField(null, { - entityId: this.entityId, ...calculatedField, - configuration: {...calculatedField.configuration, expression} as any - }, true) - } - }), - ); - } else { - return of(null); - } + }), + ); } private openCalculatedField($event: Event, entity: AlarmRuleTableEntity) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts index a13aa2fac7..ce0aecb598 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -30,7 +30,7 @@ import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { AlarmRulesService } from '@core/http/alarm-rules.service'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; @@ -59,7 +59,7 @@ export class AlarmRulesTableComponent { pageMode: boolean = false; - constructor(private calculatedFieldsService: CalculatedFieldsService, + constructor(private alarmRulesService: AlarmRulesService, private translate: TranslateService, private dialog: MatDialog, private store: Store, @@ -77,7 +77,7 @@ export class AlarmRulesTableComponent { effect(() => { if (this.active() || this.pageMode) { this.alarmRulesTableConfig = new AlarmRulesTableConfig( - this.calculatedFieldsService, + this.alarmRulesService, this.translate, this.dialog, this.datePipe, diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts index 13c388675c..740dd30440 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts @@ -22,19 +22,19 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { EntityType } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { + CalculatedFieldAlarmRuleConfiguration, + CalculatedFieldAlarmRuleInfo, CalculatedFieldArgument, - CalculatedFieldConfiguration, - CalculatedFieldInfo, CalculatedFieldType } from '@shared/models/calculated-field.models'; import { EntityId } from '@shared/models/id/entity-id'; import { BaseData } from '@shared/models/base-data'; import { Observable } from 'rxjs'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; -import { - CalculatedFieldsTableConfig, - CalculatedFieldsTableEntity -} from '@home/components/calculated-fields/calculated-fields-table-config'; +import type { + AlarmRulesTableConfig, + AlarmRuleTableEntity +} from '@home/components/alarm-rules/alarm-rules-table-config'; import { TenantId } from '@shared/models/id/tenant-id'; import { StringItemsOption } from '@shared/components/string-items-list.component'; import { RelationTypes } from '@shared/models/relation.models'; @@ -56,7 +56,7 @@ import { EntityService } from '@core/http/entity.service'; styleUrls: ['./alarm-rule-dialog.component.scss'], standalone: false }) -export class AlarmRulesComponent extends EntityComponent { +export class AlarmRulesComponent extends EntityComponent { @Input() standalone = false; @@ -75,8 +75,8 @@ export class AlarmRulesComponent extends EntityComponent, protected translate: TranslateService, - @Inject('entity') protected entityValue: CalculatedFieldInfo, - @Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig, + @Inject('entity') protected entityValue: CalculatedFieldAlarmRuleInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: AlarmRulesTableConfig, protected fb: FormBuilder, protected cd: ChangeDetectorRef, private entityService: EntityService) { @@ -93,21 +93,14 @@ export class AlarmRulesComponent extends EntityComponent this.entitiesTableConfig.additionalDebugActionConfig.action( - { id: this.entity.id, ...this.entityFormValue() }, false, - (expression) => { - if (expression) { - this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression}); - this.entityForm.get('configuration').markAsDirty(); - } - }), + action: () => this.entitiesTableConfig.additionalDebugActionConfig.action({id: this.entity.id, ...this.entityFormValue()}) }; get entityId(): EntityId { return this.entityForm.get('entityId').value; } - get entitiesTableConfig(): CalculatedFieldsTableConfig { + get entitiesTableConfig(): AlarmRulesTableConfig { return this.entitiesTableConfigValue; } @@ -115,12 +108,12 @@ export class AlarmRulesComponent extends EntityComponent = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot, - calculatedFieldsService = inject(CalculatedFieldsService), + alarmRulesService = inject(AlarmRulesService), translate = inject(TranslateService), dialog = inject(MatDialog), store = inject(Store), @@ -52,7 +52,7 @@ export const AlarmRulesTableConfigResolver: ResolveFn = router = inject(Router), ) => { return new AlarmRulesTableConfig( - calculatedFieldsService, + alarmRulesService, translate, dialog, datePipe, diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts index ae4ee4001d..c26624bebc 100644 --- a/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts @@ -18,9 +18,9 @@ import { Component } from '@angular/core'; import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; import type { - CalculatedFieldsTableConfig, - CalculatedFieldsTableEntity -} from '@home/components/calculated-fields/calculated-fields-table-config'; + AlarmRulesTableConfig, + AlarmRuleTableEntity +} from '@home/components/alarm-rules/alarm-rules-table-config'; import { debugCfActionEnabled } from '@shared/models/calculated-field.models'; @Component({ @@ -29,7 +29,7 @@ import { debugCfActionEnabled } from '@shared/models/calculated-field.models'; styleUrls: [], standalone: false }) -export class AlarmRulesTabsComponent extends EntityTabsComponent { +export class AlarmRulesTabsComponent extends EntityTabsComponent { readonly DebugEventType = DebugEventType; readonly EventType = EventType; @@ -43,8 +43,7 @@ export class AlarmRulesTabsComponent extends EntityTabsComponent { - }); + (this.entitiesTableConfig as AlarmRulesTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments)) + .subscribe((_expression) => { }); }; } diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index c01083d078..552d2a3a95 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -80,7 +80,9 @@ export type CalculatedField = | CalculatedFieldRelatedEntityAggregation | CalculatedFieldAlarmRule; -export type CalculatedFieldInfo = CalculatedField & {entityName: string}; +export type WithCalculatedFieldInfo = T & {entityName: string}; +export type CalculatedFieldInfo = WithCalculatedFieldInfo; +export type CalculatedFieldAlarmRuleInfo = WithCalculatedFieldInfo; export enum CalculatedFieldType { SIMPLE = 'SIMPLE', @@ -192,7 +194,7 @@ interface BasePropagationConfiguration { output: CalculatedFieldOutput; } -interface CalculatedFieldAlarmRuleConfiguration { +export interface CalculatedFieldAlarmRuleConfiguration { type: CalculatedFieldType.ALARM; arguments: Record; createRules: Record;