diff --git a/.docs/api/data-service.md b/.docs/api/data-service.md index 257c68c1957fa697957c9dfc3f6b681693ac3f9d..bc43a5d3631f7a9b4ecd85c64472bd25095a9603 100644 --- a/.docs/api/data-service.md +++ b/.docs/api/data-service.md @@ -24,25 +24,32 @@ author: Martin Weise ## Overview -The Data Service is responsible for inserting AMQP tuples from the Broker Service into the Data DB +The Data Service is responsible for inserting AMQP tuples from the Broker Service into the Data DB via [Spring AMQP](https://docs.spring.io/spring-amqp/reference/html/). To increase the number of consumers, scale the Data Service up. ## Data Processing -The Data Service uses [Apache Spark](https://spark.apache.org/), a data engine to load data from/into +The Data Service uses [Apache Spark](https://spark.apache.org/), a data engine to load data from/into the [Data Database](../data-db) with a wide range of open-source connectors. The default deployment uses a local mode of -embedded processing directly in the service until there exists +embedded processing directly in the service until there exists a [Bitnami Chart](https://artifacthub.io/packages/helm/bitnami/spark) for Spark 4. Retrieving data from a subset internally generates a view with the 64-character hash of the query. This view is not automatically deleted currently. +## Caching + +The Data Service uses [Caffeine](https://github.com/ben-manes/caffeine), a caching solution that is used to temporarily +cache the connection details from the [Metadata Service](../metadata-service) such that they don't have to be queried +everytime e.g. a sensor measurement is inserted. By default, this information is stored for 60 minutes. System +administrators can disable this behavior by setting `CREDENTIAL_CACHE_TIMEOUT=0` (cache is deleted after 0 seconds). + ## Limitations -* Views in DBRepo can only have 63-character length (it is assumed only internal views have the maximum length of 64 +* Views in DBRepo can only have 63-character length (it is assumed only internal views have the maximum length of 64 characters). -* Local mode of embedded processing of Apache Spark directly in the service using +* Local mode of embedded processing of Apache Spark directly in the service using a [`local[2]`](https://spark.apache.org/docs/latest/#running-the-examples-and-shell) configuration. !!! question "Do you miss functionality? Do these limitations affect you?" diff --git a/.docs/concepts/data-visibility.md b/.docs/concepts/data-visibility.md index e76448ec3eaddb09ecf7d601d37433f641f19751..04f37c6979bd9019d954859535a757c72da4b63e 100644 --- a/.docs/concepts/data-visibility.md +++ b/.docs/concepts/data-visibility.md @@ -2,32 +2,34 @@ author: Martin Weise --- -There are several ways to set the visibility of (meta-)data in DBRepo. It is possible to set the data to public/private -and the schema to be public/private for each database and separately for each table, each view and each subset of a -database. +There are several ways to set the visibility of (meta-)data in DBRepo. It is possible to set the data visibility to +visible/hidden and the schema to be visible/hidden for each database and separately for each table, each view and each +subset of a database. + +## Visibility In total there are three possible scenarios: -## Public +#### Public !!! info "Possible use-case: data publication supplement to an open-access publication" -Where the database's data and metadata is set to be *public*. This means everything in the database (tables, views, +Where the database's data and metadata is set to be *visible*. This means everything in the database (tables, views, subsets) are visible by anyone from the public. -## Mixed +#### Private !!! info "Possible use-case: private sensor measurements with timed embargo" -Where the database's data and metadata is set to be *private*. This means everything in the database (tables, views, -subsets) are by default not visible by anyone from the public. You can however make specific views that join tables -and/or filter certain columns and apply a 14-day delay-embargo. +Where the database's data set to be *hidden* but the schema to be *visible*. This means everything in the database +(tables, views, subsets) are by default not visible by anyone from the public. You can however make specific views that +join tables and/or filter certain columns and apply a 14-day delay-embargo. <figure markdown>  <figcaption>Figure 1: Public view that joins two private tables and applies a time-embargo</figcaption> </figure> -## Private +#### Draft -!!! info "Possible use-case: data storage for trusted-/virtual research environments" \ No newline at end of file +!!! info "Possible use-case: project data storage before publication" \ No newline at end of file diff --git a/dbrepo-analyse-service/Pipfile.lock b/dbrepo-analyse-service/Pipfile.lock index 80237c91cfb007d4da0a9bad42220c798622604f..99d70b613229b3fcc81638d99d038034b58ef9d6 100644 --- a/dbrepo-analyse-service/Pipfile.lock +++ b/dbrepo-analyse-service/Pipfile.lock @@ -412,7 +412,7 @@ }, "dbrepo": { "hashes": [ - "sha256:79ae690b125dcc6ba7ada28f07af47bca6e943ae3079232b3b4ae2d369095bb5" + "sha256:7cddcbdcb3eade84f67db01fa32e0649ecc01d4c3cc5e7542d3c402ad52efc19" ], "path": "./lib/dbrepo-1.6.1.tar.gz" }, @@ -1612,7 +1612,7 @@ "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" ], - "markers": "python_version >= '3.9'", + "markers": "python_version >= '3.10'", "version": "==2.3.0" }, "werkzeug": { @@ -2236,7 +2236,7 @@ "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" ], - "markers": "python_version >= '3.9'", + "markers": "python_version >= '3.10'", "version": "==2.3.0" }, "wrapt": { diff --git a/dbrepo-analyse-service/lib/dbrepo-1.6.1.tar.gz b/dbrepo-analyse-service/lib/dbrepo-1.6.1.tar.gz index 6e6884a28d818166eec6ded367274d57b137f4af..a1197ff6465d77567108f3389aae9d54dc9c8ab6 100644 Binary files a/dbrepo-analyse-service/lib/dbrepo-1.6.1.tar.gz and b/dbrepo-analyse-service/lib/dbrepo-1.6.1.tar.gz differ diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java index 8194ca624cde4b55f407e01ed716b899eb6c69b7..8a106b6cfdd3b6d3117617348f5d592df5144181 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/SubsetEndpoint.java @@ -76,7 +76,7 @@ public class SubsetEndpoint extends AbstractEndpoint { @GetMapping @Observed(name = "dbrepo_subset_list") @Operation(summary = "Find subsets", - description = "Finds subsets in the query store. The result can be optionally filtered by setting `persisted`. When set to *true*, only persisted queries are returned, otherwise only non-persisted queries are returned.", + description = "Finds subsets in the query store. When the database schema is marked as hidden, the user needs to be authorized, have at least read-access to the database and have the role `list-queries`. The result can be optionally filtered by setting `persisted`. When set to *true*, only persisted queries are returned, otherwise only non-persisted queries are returned.", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", @@ -107,13 +107,8 @@ public class SubsetEndpoint extends AbstractEndpoint { QueryNotFoundException, NotAllowedException, MetadataServiceException { log.debug("endpoint find subsets in database, databaseId={}, filterPersisted={}", databaseId, filterPersisted); final PrivilegedDatabaseDto database = credentialService.getDatabase(databaseId); - if (!database.getIsPublic() || !database.getIsSchemaPublic()) { - if (principal == null) { - log.error("Failed to find subset: database is private & missing authentication"); - throw new NotAllowedException("Failed to find subset: database is private & missing authentication"); - } - metadataServiceGateway.getAccess(databaseId, getId(principal)); - } + endpointValidator.validateOnlyPrivateSchemaAccess(database, principal); + endpointValidator.validateOnlyPrivateSchemaHasRole(database, principal, "list-queries"); final List<QueryDto> queries; try { queries = subsetService.findAll(database, filterPersisted); @@ -128,7 +123,7 @@ public class SubsetEndpoint extends AbstractEndpoint { @GetMapping("/{subsetId}") @Observed(name = "dbrepo_subset_find") @Operation(summary = "Find subset", - description = "Finds a subset in the data database. Requests with HTTP header `Accept=application/json` return the metadata, requests with HTTP header `Accept=text/csv` return the data as downloadable file.", + description = "Finds a subset in the data database. When the database schema is marked as hidden, the user needs to be authorized, have at least read-access to the database and have the role `find-query`. Requests with HTTP header `Accept=application/json` return the metadata, requests with HTTP header `Accept=text/csv` return the data as downloadable file.", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", @@ -175,13 +170,8 @@ public class SubsetEndpoint extends AbstractEndpoint { log.debug("endpoint find subset in database, databaseId={}, subsetId={}, accept={}, timestamp={}", databaseId, subsetId, accept, timestamp); final PrivilegedDatabaseDto database = credentialService.getDatabase(databaseId); - if (!database.getIsPublic() || !database.getIsSchemaPublic()) { - if (principal == null) { - log.error("Failed to find subset: database is private & missing authentication"); - throw new NotAllowedException("Failed to find subset: database is private & missing authentication"); - } - metadataServiceGateway.getAccess(databaseId, getId(principal)); - } + endpointValidator.validateOnlyPrivateSchemaAccess(database, principal); + endpointValidator.validateOnlyPrivateSchemaHasRole(database, principal, "find-query"); final QueryDto subset; try { subset = subsetService.findById(database, subsetId); @@ -194,7 +184,7 @@ public class SubsetEndpoint extends AbstractEndpoint { timestamp = Instant.now(); log.debug("timestamp not set: default to {}", timestamp); } - if (accept == null) { + if (accept == null || accept.isEmpty() || accept.isBlank()) { accept = MediaType.APPLICATION_JSON_VALUE; log.debug("accept header not set: default to {}", accept); } @@ -219,7 +209,7 @@ public class SubsetEndpoint extends AbstractEndpoint { throw new DatabaseUnavailableException("Failed to find data: " + e.getMessage(), e); } } - throw new FormatNotAvailableException("Must provide either application/json or text/csv headers"); + throw new FormatNotAvailableException("Must provide either application/json or text/csv value for header 'Accept': provided " + accept + " instead"); } @PostMapping @@ -303,13 +293,7 @@ public class SubsetEndpoint extends AbstractEndpoint { } /* create */ final PrivilegedDatabaseDto database = credentialService.getDatabase(databaseId); - if (!database.getIsPublic() || !database.getIsSchemaPublic()) { - if (principal == null) { - log.error("Failed to find subset: database is private & missing authentication"); - throw new NotAllowedException("Failed to find subset: database is private & missing authentication"); - } - metadataServiceGateway.getAccess(databaseId, getId(principal)); - } + endpointValidator.validateOnlyPrivateSchemaAccess(database, principal); try { final Long subsetId = subsetService.create(database, data.getStatement(), timestamp, userId); return getData(databaseId, subsetId, principal, request, page, size); @@ -375,6 +359,7 @@ public class SubsetEndpoint extends AbstractEndpoint { } credentialService.getAccess(databaseId, getId(principal)); } + log.trace("visibility for database: is_public={}, is_schema_public={}", database.getIsPublic(), database.getIsSchemaPublic()); /* parameters */ if (page == null) { page = 0L; diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java index 1c6adfd6a5b87355e76f9b40147fe91f5de4d4e2..25b858f51bd3f643f7879b9eac2dbb648b00aabb 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java @@ -1,14 +1,17 @@ package at.tuwien.validation; import at.tuwien.api.database.AccessTypeDto; +import at.tuwien.api.database.DatabaseAccessDto; +import at.tuwien.api.database.internal.PrivilegedDatabaseDto; import at.tuwien.config.QueryConfig; -import at.tuwien.exception.NotAllowedException; -import at.tuwien.exception.PaginationException; -import at.tuwien.exception.QueryNotSupportedException; +import at.tuwien.endpoints.AbstractEndpoint; +import at.tuwien.exception.*; +import at.tuwien.gateway.MetadataServiceGateway; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.security.Principal; import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -18,13 +21,15 @@ import java.util.regex.Pattern; @Log4j2 @Component -public class EndpointValidator { +public class EndpointValidator extends AbstractEndpoint { private final QueryConfig queryConfig; + private final MetadataServiceGateway metadataServiceGateway; @Autowired - public EndpointValidator(QueryConfig queryConfig) { + public EndpointValidator(QueryConfig queryConfig, MetadataServiceGateway metadataServiceGateway) { this.queryConfig = queryConfig; + this.metadataServiceGateway = metadataServiceGateway; } public void validateDataParams(Long page, Long size) throws PaginationException { @@ -43,6 +48,56 @@ public class EndpointValidator { } } + public void validateOnlyPrivateSchemaAccess(PrivilegedDatabaseDto database, Principal principal) + throws NotAllowedException, RemoteUnavailableException, MetadataServiceException { + validateOnlyPrivateSchemaAccess(database, principal, false); + } + + public void validateOnlyPrivateSchemaAccess(PrivilegedDatabaseDto database, Principal principal, + boolean writeAccessOnly) throws NotAllowedException, + RemoteUnavailableException, MetadataServiceException { + if (database.getIsSchemaPublic()) { + log.trace("database schema with id {} is public: no access needed", database.getId()); + return; + } + validateOnlyAccess(database, principal, writeAccessOnly); + } + + public void validateOnlyPrivateSchemaHasRole(PrivilegedDatabaseDto database, Principal principal, String role) + throws NotAllowedException { + if (database.getIsSchemaPublic()) { + log.trace("database with id {} has public schema: no access needed", database.getId()); + return; + } + log.trace("database with id {} has private schema", database.getId()); + if (principal == null) { + log.error("Access not allowed: no authorization provided"); + throw new NotAllowedException("Access not allowed: no authorization provided"); + } + log.trace("principal: {}", principal.getName()); + if (!hasRole(principal, role)) { + log.error("Access not allowed: role {} missing", role); + throw new NotAllowedException("Access not allowed: role " + role + " missing"); + } + log.trace("principal has role '{}': access granted", role); + } + + public void validateOnlyAccess(PrivilegedDatabaseDto database, Principal principal, boolean writeAccessOnly) + throws NotAllowedException, RemoteUnavailableException, MetadataServiceException { + if (principal == null) { + throw new NotAllowedException("No principal provided"); + } + if (isSystem(principal)) { + return; + } + final DatabaseAccessDto access = metadataServiceGateway.getAccess(database.getId(), getId(principal)); + log.trace("found access: {}", access); + if (writeAccessOnly && !(access.getType().equals(AccessTypeDto.WRITE_OWN) || access.getType().equals(AccessTypeDto.WRITE_ALL))) { + log.error("Access not allowed: no write access"); + throw new NotAllowedException("Access not allowed: no write access"); + } + } + public void validateForbiddenStatements(String query) throws QueryNotSupportedException { final List<String> words = new LinkedList<>(); Arrays.stream(queryConfig.getForbiddenKeywords()) diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SubsetServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SubsetServiceMariaDbImpl.java index 4d688393cb8b0aa3054bea77c9ad0cc534aa5e65..fb244bb30113ab90f4f2209bf4e80b5d11cd9185 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SubsetServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SubsetServiceMariaDbImpl.java @@ -90,13 +90,14 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs @Override public Dataset<Row> getData(PrivilegedDatabaseDto database, QueryDto subset, Long page, Long size) throws ViewMalformedException, SQLException, QueryMalformedException, TableNotFoundException { - if (!viewService.existsByName(database, metadataMapper.queryDtoToViewName(subset))) { - log.warn("Missing internal view {} for subset with id {}: create it from subset query", metadataMapper.queryDtoToViewName(subset), subset.getId()); + final String viewName = metadataMapper.queryDtoToViewName(subset); + if (!viewService.existsByName(database, viewName)) { + log.warn("Missing internal view {} for subset with id {}: create it from subset query", viewName, subset.getId()); viewService.create(database, subset); } else { - log.debug("internal view {} for subset with id {} exists", metadataMapper.queryDtoToViewName(subset), subset.getId()); + log.debug("internal view {}.{} for subset with id {} exists", database.getInternalName(), viewName, subset.getId()); } - return tableService.getData(database, metadataMapper.queryDtoToViewName(subset), subset.getExecution(), page, size, null, null); + return tableService.getData(database, viewName, subset.getExecution(), page, size, null, null); } @Override diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index 21a1659923c89b1a054c34fd89f4351ba1798417..6ed865ca25923bea14c9ed39c3d8b18bacbc7f63 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java @@ -203,7 +203,7 @@ public class TableEndpoint extends AbstractEndpoint { principal.getName()); final Database database = databaseService.findById(databaseId); final Table table = tableService.findById(database, tableId); - if (!table.getOwner().getId().equals(getId(principal))) { + if (!table.getOwner().getId().equals(getId(principal)) && !isSystem(principal)) { log.error("Failed to update table statistics: not owner"); throw new NotAllowedException("Failed to update table statistics: not owner"); } @@ -480,16 +480,24 @@ public class TableEndpoint extends AbstractEndpoint { UserNotFoundException, NotAllowedException, AccessNotFoundException { log.debug("endpoint find table, databaseId={}, tableId={}", databaseId, tableId); final Database database = databaseService.findById(databaseId); - endpointValidator.validateOnlyPrivateDataAccess(database, principal); - endpointValidator.validateOnlyPrivateDataHasRole(database, principal, "find-table"); final Table table = tableService.findById(database, tableId); boolean isOwner = false; if (principal != null) { isOwner = table.getOwner().getId().equals(getId(principal)); - try { - accessService.find(table.getDatabase(), userService.findById(getId(principal))); - } catch (UserNotFoundException | AccessNotFoundException e) { - /* ignore */ + if (table.getIsSchemaPublic()) { + try { + accessService.find(table.getDatabase(), userService.findById(getId(principal))); + } catch (UserNotFoundException | AccessNotFoundException e) { + if (!isOwner && !isSystem(principal)) { + log.error("Failed to find table with id {}: private and not authorized", table); + throw new NotAllowedException("Failed to find table with id " + tableId + ": private and not authorized"); + } + } + } + } else { + if (!table.getIsSchemaPublic()) { + log.error("Failed to find table with id {}: private and not authorized", table); + throw new NotAllowedException("Failed to find table with id " + tableId + ": private and not authorized"); } } if (!table.getIsSchemaPublic() && !isOwner && !isSystem(principal)) { diff --git a/dbrepo-search-service/Pipfile.lock b/dbrepo-search-service/Pipfile.lock index 92168e4d68c3931a1c84e22912f2fa3784d0bad2..9a33916adfdf47848c302c03e7110bba5dc8d09c 100644 --- a/dbrepo-search-service/Pipfile.lock +++ b/dbrepo-search-service/Pipfile.lock @@ -360,7 +360,7 @@ }, "dbrepo": { "hashes": [ - "sha256:79ae690b125dcc6ba7ada28f07af47bca6e943ae3079232b3b4ae2d369095bb5" + "sha256:7cddcbdcb3eade84f67db01fa32e0649ecc01d4c3cc5e7542d3c402ad52efc19" ], "path": "./lib/dbrepo-1.6.1.tar.gz" }, diff --git a/dbrepo-search-service/init/Pipfile.lock b/dbrepo-search-service/init/Pipfile.lock index 79939b60d160089bf3f51116242e508b7712b3f7..93c8b4291603d19deb3fbc79bd23e8d55e8379e4 100644 --- a/dbrepo-search-service/init/Pipfile.lock +++ b/dbrepo-search-service/init/Pipfile.lock @@ -254,7 +254,7 @@ }, "dbrepo": { "hashes": [ - "sha256:79ae690b125dcc6ba7ada28f07af47bca6e943ae3079232b3b4ae2d369095bb5" + "sha256:7cddcbdcb3eade84f67db01fa32e0649ecc01d4c3cc5e7542d3c402ad52efc19" ], "path": "./lib/dbrepo-1.6.1.tar.gz" }, diff --git a/dbrepo-search-service/init/database.json b/dbrepo-search-service/init/database.json index fb175700c614599daab44fa0fb77e51a05151b62..363624ff059daa02d4edb58f3f6bce1b5b4664dd 100644 --- a/dbrepo-search-service/init/database.json +++ b/dbrepo-search-service/init/database.json @@ -244,7 +244,7 @@ "created": { "type": "date" }, - "creator": { + "owner": { "properties": { "id": { "type": "text", @@ -310,7 +310,7 @@ "created": { "type": "date" }, - "creator": { + "owner": { "properties": { "id": { "type": "text", @@ -359,9 +359,9 @@ } } }, - "creators": { + "owners": { "properties": { - "creator_name": { + "owner_name": { "type": "text", "fields": { "keyword": { @@ -610,7 +610,7 @@ "auto_generated": { "type": "boolean" }, - "column_type": { + "type": { "type": "text", "fields": { "keyword": { @@ -640,9 +640,6 @@ "is_null_allowed": { "type": "boolean" }, - "is_public": { - "type": "boolean" - }, "mean": { "type": "float" }, @@ -733,7 +730,7 @@ "created": { "type": "date" }, - "creator": { + "owner": { "properties": { "id": { "type": "text", @@ -906,7 +903,7 @@ "auto_generated": { "type": "boolean" }, - "column_type": { + "type": { "type": "text", "fields": { "keyword": { @@ -950,7 +947,7 @@ "created": { "type": "date" }, - "creator": { + "owner": { "properties": { "id": { "type": "text", @@ -1010,7 +1007,7 @@ "created": { "type": "date" }, - "creator": { + "owner": { "properties": { "id": { "type": "text", @@ -1059,9 +1056,9 @@ } } }, - "creators": { + "owners": { "properties": { - "creator_name": { + "owner_name": { "type": "text", "fields": { "keyword": { diff --git a/dbrepo-search-service/init/lib/dbrepo-1.6.1.tar.gz b/dbrepo-search-service/init/lib/dbrepo-1.6.1.tar.gz index 6e6884a28d818166eec6ded367274d57b137f4af..a1197ff6465d77567108f3389aae9d54dc9c8ab6 100644 Binary files a/dbrepo-search-service/init/lib/dbrepo-1.6.1.tar.gz and b/dbrepo-search-service/init/lib/dbrepo-1.6.1.tar.gz differ diff --git a/dbrepo-search-service/lib/dbrepo-1.6.1.tar.gz b/dbrepo-search-service/lib/dbrepo-1.6.1.tar.gz index 6e6884a28d818166eec6ded367274d57b137f4af..a1197ff6465d77567108f3389aae9d54dc9c8ab6 100644 Binary files a/dbrepo-search-service/lib/dbrepo-1.6.1.tar.gz and b/dbrepo-search-service/lib/dbrepo-1.6.1.tar.gz differ diff --git a/dbrepo-ui/components/ResourceStatus.vue b/dbrepo-ui/components/ResourceStatus.vue index 9ff310ce1b391ad40da8a1dea1af0883a9b0235a..36544b1c299ac09ba0fcf339ff2bf04d418980ee 100644 --- a/dbrepo-ui/components/ResourceStatus.vue +++ b/dbrepo-ui/components/ResourceStatus.vue @@ -44,6 +44,9 @@ export default { } return 'private' } + if (!this.resource.is_schema_public) { + return 'private' + } return 'public' }, status () { diff --git a/dbrepo-ui/components/dialogs/EditAccess.vue b/dbrepo-ui/components/dialogs/EditAccess.vue index b3bd8dbf96454ae44bd207803fa736838bd10473..856a442592bf9b40000b9295c8b7ed9bc17f6b78 100644 --- a/dbrepo-ui/components/dialogs/EditAccess.vue +++ b/dbrepo-ui/components/dialogs/EditAccess.vue @@ -229,7 +229,7 @@ export default { const userService = useUserService() userService.findAll() .then((users) => { - this.users = users.filter(u => u.username !== this.database.owner.username) + this.users = users.filter(u => u.id !== this.database.owner.id) }) .catch(({code}) => { const toast = useToastInstance() diff --git a/dbrepo-ui/components/dialogs/EditTuple.vue b/dbrepo-ui/components/dialogs/EditTuple.vue index 893504331618bed1a928b1e4ee533453cc6916f8..3290d230d1bf216bd2f212c4fbe9c683901e4b96 100644 --- a/dbrepo-ui/components/dialogs/EditTuple.vue +++ b/dbrepo-ui/components/dialogs/EditTuple.vue @@ -27,7 +27,7 @@ type="number"> <template v-slot:append> - {{ column.column_type.toUpperCase() }} + {{ column.type.toUpperCase() }} <NuxtLink target="_blank" class="ml-2" @@ -61,7 +61,7 @@ type="text"> <template v-slot:append> - {{ column.column_type.toUpperCase() }} + {{ column.type.toUpperCase() }} <NuxtLink target="_blank" class="ml-2" @@ -94,7 +94,7 @@ type="number"> <template v-slot:append> - {{ column.column_type.toUpperCase() }} + {{ column.type.toUpperCase() }} <NuxtLink target="_blank" class="ml-2" @@ -126,7 +126,7 @@ :hint="hint(column)"> <template v-slot:append> - {{ column.column_type.toUpperCase() }} + {{ column.type.toUpperCase() }} <NuxtLink target="_blank" class="ml-2" @@ -161,7 +161,7 @@ :items="isSet(column) ? column.sets : column.enums"> <template v-slot:append> - {{ column.column_type.toUpperCase() }} + {{ column.type.toUpperCase() }} <NuxtLink target="_blank" class="ml-2" @@ -192,7 +192,7 @@ :clearable="!required(column)"> <template v-slot:append> - {{ column.column_type.toUpperCase() }} + {{ column.type.toUpperCase() }} <NuxtLink target="_blank" class="ml-2" @@ -221,7 +221,7 @@ :hint="hint(column)"> <template v-slot:append> - {{ column.column_type.toUpperCase() }} + {{ column.type.toUpperCase() }} <NuxtLink target="_blank" class="ml-2" @@ -313,6 +313,8 @@ export default { oldTuple: null, error: false, menu: false, + loadContainer: false, + container: null, bools: [ { title: 'true', value: true }, { title: 'false', value: false } @@ -321,6 +323,7 @@ export default { } }, mounted() { + this.fetchContainer() this.$refs.form.validate() this.oldTuple = Object.assign({}, this.tuple) }, @@ -328,11 +331,20 @@ export default { database () { return this.cacheStore.getDatabase }, + table () { + return this.cacheStore.getTable + }, columnTypes () { - if (!this.database) { + if (!this.container) { + return [] + } + return this.container.image.data_types + }, + primaryKeyColumns () { + if (!this.table) { return [] } - return this.database.container.image.data_types + return this.table.constraints.primary_key.map(pk => pk.column) }, title () { return (this.edit ? this.$t('toolbars.table.data.edit') : this.$t('toolbars.table.data.add')) + ' ' + this.$t('toolbars.table.data.tuple') @@ -360,7 +372,7 @@ export default { if (!is_null_allowed) { hint += this.$t('pages.table.subpages.data.required.hint') } - if (column.column_type === 'sequence') { + if (column.type === 'sequence') { hint += ' ' + this.$t('pages.table.subpages.data.auto.hint') } if (is_primary_key) { @@ -371,47 +383,47 @@ export default { } return hint }, - documentationLink ({column_type}) { - const filter = this.columnTypes.filter(t => t.value === column_type) + documentationLink ({type}) { + const filter = this.columnTypes.filter(t => t.value === type) if (filter.length !== 1) { return null } return filter[0].documentation }, - formatHint ({column_type}) { - const filter = this.columnTypes.filter(t => t.value === column_type) + formatHint ({type}) { + const filter = this.columnTypes.filter(t => t.value === type) if (filter.length !== 1) { return null } return filter[0].data_hint }, isTextField (column) { - const { column_type } = column - return ['char', 'varchar', 'tinytext', 'mediumtext'].includes(column_type) + const { type } = column + return ['char', 'varchar', 'tinytext', 'mediumtext'].includes(type) }, isTextArea (column) { - return ['text'].includes(column.column_type) + return ['text'].includes(column.type) }, isFileField (column) { - return ['blob', 'longblob', 'mediumblob', 'tinyblob'].includes(column.column_type) + return ['blob', 'longblob', 'mediumblob', 'tinyblob'].includes(column.type) }, isBoolean (column) { - return ['bool'].includes(column.column_type) + return ['bool'].includes(column.type) }, isNumber (column) { - return ['int', 'binary', 'bit', 'tinyint', 'smallint', 'mediumint', 'bigint'].includes(column.column_type) + return ['int', 'binary', 'bit', 'tinyint', 'smallint', 'mediumint', 'bigint', 'serial'].includes(column.type) }, isFloatingPoint (column) { - return ['float', 'double', 'decimal'].includes(column.column_type) + return ['float', 'double', 'decimal'].includes(column.type) }, isEnum (column) { - return column.column_type === 'enum' + return column.type === 'enum' }, isSet (column) { - return column.column_type === 'set' + return column.type === 'set' }, isTimeField (column) { - return ['date', 'datetime', 'timestamp', 'time', 'year'].includes(column.column_type) + return ['date', 'datetime', 'timestamp', 'time', 'year'].includes(column.type) }, rules (column) { if (column.is_null_allowed) { @@ -419,7 +431,7 @@ export default { } const rules = [] rules.push(v => v !== null || this.$t('validation.required')) - if (column.column_type === 'decimal' || column.column_type === 'double') { + if (column.type === 'decimal' || column.type === 'double') { rules.push(v => !(!v || v.split('.')[0].length > column.size) || `${this.$t('pages.table.subpages.data.float.max')} ${column.size} ${this.$t('pages.table.subpages.data.float.before')}`) rules.push(v => !(!v || (column.d && v.split('.')[1].length > column.d)) || `${this.$t('pages.table.subpages.data.float.max')} ${column.d} ${this.$t('pages.table.subpages.data.float.after')}`) } @@ -439,19 +451,11 @@ export default { }, updateTuple () { const constraints = {} - if (this.table.constraints.primary_key.length > 0) { - this.table.constraints.primary_key - .forEach((pk) => { - constraints[pk.column.internal_name] = this.oldTuple[pk.column.internal_name] - }) - console.debug('table has primary key: set update tuple constraints', constraints) - } else { - this.table.columns - .forEach((column) => { - constraints[column.internal_name] = this.oldTuple[column.internal_name] - }) - console.debug('table does not have a primary key: set update tuple constraints', constraints) - } + this.primaryKeyColumns + .forEach((pk) => { + constraints[pk.internal_name] = this.oldTuple[pk.internal_name] + }) + console.debug('table has primary key: set update tuple constraints', constraints) const tupleService = useTupleService() this.loading = true tupleService.update(this.$route.params.database_id, this.$route.params.table_id, { data: this.tuple, keys: constraints }) @@ -494,7 +498,7 @@ export default { this.$emit('close', { success: true }) this.loading = false }) - .catch(({message}) => { + .catch(({code, message}) => { this.loading = false const toast = useToastInstance() if (typeof code !== 'string') { @@ -510,6 +514,29 @@ export default { const toast = useToastInstance() toast.success(this.$t('success.upload.blob')) this.tuple[column.internal_name] = s3key + }, + fetchContainer () { + if (!this.database) { + return + } + this.loadContainer = true + const containerService = useContainerService() + containerService.findOne(this.database.container.id) + .then((container) => { + this.container = container + this.loadContainer = false + }) + .catch(({code, message}) => { + this.loadContainer = false + const toast = useToastInstance() + if (typeof code !== 'string') { + return + } + toast.error(message) + }) + .finally(() => { + this.loadContainer = false + }) } } } diff --git a/dbrepo-ui/components/search/AdvancedSearch.vue b/dbrepo-ui/components/search/AdvancedSearch.vue index 13de402e019f2a791f0233c4fc611164e46e20d8..b312b2dc5291812fd98930e8585b50e389c341c5 100644 --- a/dbrepo-ui/components/search/AdvancedSearch.vue +++ b/dbrepo-ui/components/search/AdvancedSearch.vue @@ -75,14 +75,14 @@ :variant="inputVariant" :label="field.attr_friendly_name" /> <v-text-field - v-if="(field.type === 'keyword' && field.attr_name !== 'column_type') || field.type === 'text' || field.type === 'date'" + v-if="(field.type === 'keyword' && field.attr_name !== 'type') || field.type === 'text' || field.type === 'date'" v-model="advancedSearchData[field.attr_name]" type="text" :variant="inputVariant" :label="field.attr_friendly_name" clearable /> <v-select - v-if="field.type === 'keyword' && field.attr_name === 'column_type'" + v-if="field.type === 'keyword' && field.attr_name === 'type'" v-model="advancedSearchData[field.attr_name]" :items="columnTypes" item-value="value" diff --git a/dbrepo-ui/components/subset/Results.vue b/dbrepo-ui/components/subset/Results.vue index e9e3c0deb5cddf119cab770014ff085d71b6df94..e558186daf15c186dfdc085a844799815e3756a4 100644 --- a/dbrepo-ui/components/subset/Results.vue +++ b/dbrepo-ui/components/subset/Results.vue @@ -2,12 +2,15 @@ <div> <v-data-table-server flat + v-model="selection" :headers="headers" :loading="loading || loadingCount || loadingExecute" :options="options" :items="result.rows" :items-length="total" :footer-props="footerProps" + :show-select="select" + return-object :items-per-page-options="footerProps.itemsPerPageOptions" @update:options="updateOptions" /> </div> @@ -24,6 +27,10 @@ export default { type: Boolean, default: () => false }, + select: { + type: Boolean, + default: () => false + }, timestamp: { type: String, default: () => new Date().toISOString() @@ -35,6 +42,7 @@ export default { loadingExecute: false, resultId: null, id: null, + selection: null, result: { headers: [], rows: [] @@ -64,6 +72,11 @@ export default { this.reExecute(this.id) }, deep: true + }, + selection: { + handler () { + this.$emit('selection', this.selection) + } } }, methods: { @@ -224,6 +237,9 @@ export default { this.options.page = page this.options.itemsPerPage = itemsPerPage this.reExecute(this.id) + }, + resetSelection () { + this.selection = [] } } } diff --git a/dbrepo-ui/components/subset/SubsetList.vue b/dbrepo-ui/components/subset/SubsetList.vue index ae8f2bf448753d4d7b6b4454f0b7e1671d7eb25d..6d84a6a48ea91bbbbbf7ef6fa869c2230ecbcd29 100644 --- a/dbrepo-ui/components/subset/SubsetList.vue +++ b/dbrepo-ui/components/subset/SubsetList.vue @@ -88,7 +88,11 @@ export default { queryService.findAll(this.$route.params.database_id, true) .then((subsets) => { this.loadingSubsets = false - this.subsets = subsets + this.subsets = subsets.map(subset => { + subset.is_public = this.database.is_public + subset.is_schema_public = this.database.is_schema_public + return subset + }) }) .catch(({code}) => { this.loadingSubsets = false diff --git a/dbrepo-ui/components/table/TableList.vue b/dbrepo-ui/components/table/TableList.vue index 2cb42a1633e47ca9d117e4449318582666975d35..234470076db8bb400542fdebe4274b2eefad8e62 100644 --- a/dbrepo-ui/components/table/TableList.vue +++ b/dbrepo-ui/components/table/TableList.vue @@ -56,7 +56,7 @@ export default { dialogDelete: false, headers: [ { value: 'name', title: 'Name' }, - { value: 'column_type', title: 'Type' }, + { value: 'type', title: 'Type' }, { value: 'column_concept', title: 'Concept' }, { value: 'column_unit', title: 'Unit' }, { value: 'is_primary_key', title: 'Primary Key' }, diff --git a/dbrepo-ui/composables/container-service.ts b/dbrepo-ui/composables/container-service.ts index 9aaf116e7efeed12f03170ec22340119d7d3341f..f1280517566165b8f190b9bd05eea3b1d4ac98f1 100644 --- a/dbrepo-ui/composables/container-service.ts +++ b/dbrepo-ui/composables/container-service.ts @@ -17,5 +17,21 @@ export const useContainerService = (): any => { }) } - return {findAll} + async function findOne(containerId: number): Promise<ContainerDto> { + const axios = useAxiosInstance(); + console.debug('find containers'); + return new Promise<ContainerDto>((resolve, reject) => { + axios.get<ContainerDto>(`/api/container/${containerId}`) + .then((response) => { + console.info(`Find container with id ${containerId}`) + resolve(response.data) + }) + .catch((error) => { + console.error('Failed to find container', error) + reject(axiosErrorToApiError(error)) + }) + }) + } + + return {findAll, findOne} } diff --git a/dbrepo-ui/composables/table-service.ts b/dbrepo-ui/composables/table-service.ts index f2e7a3a2f9f07cf9ed25b1220190e22879ef7a83..4abaebec937ecdfc2f965a8184b2982ef776ddce 100644 --- a/dbrepo-ui/composables/table-service.ts +++ b/dbrepo-ui/composables/table-service.ts @@ -287,6 +287,7 @@ export const useTableService = (): any => { exportData, create, remove, + updateSemantics, removeTuple, history, suggest, diff --git a/dbrepo-ui/locales/en-US.json b/dbrepo-ui/locales/en-US.json index 737e880820923f810ba04c91b82e9be349056e76..c4d5ef5eba6f9d6592ef2f47c86644530d82c1e7 100644 --- a/dbrepo-ui/locales/en-US.json +++ b/dbrepo-ui/locales/en-US.json @@ -1099,16 +1099,16 @@ }, "error": { "permission": { - "title": "You do not have permission to view this resource", - "text": "This a default-fallback error message since this resource marked as private. Please try logging in or request read-access permissions from the owner." + "title": "You do not have permission to view this {resource}", + "text": "This a default-fallback error message since this {resource} marked as private. Please try logging in or request read-access permissions from the owner." }, "missing": { - "title": "The requested resource was not found", - "text": "We could not find the requested resource anywhere." + "title": "The requested {resource} was not found", + "text": "We could not find the requested {resource} anywhere." }, "gone": { - "title": "The requested resource does not exist anymore", - "text": "We could not find the requested resource anymore." + "title": "The requested {resource} does not exist anymore", + "text": "We could not find the requested {resource} anymore." }, "auth": { "connection": "Failed to contact auth service", diff --git a/dbrepo-ui/pages/database/[database_id]/info.vue b/dbrepo-ui/pages/database/[database_id]/info.vue index 840e31bf505ff86a3b0df23dfaab7df2eb138ef7..4d88b2c6c86dfdb86e9e61c7a601212e0c7293ad 100644 --- a/dbrepo-ui/pages/database/[database_id]/info.vue +++ b/dbrepo-ui/pages/database/[database_id]/info.vue @@ -1,5 +1,6 @@ <template> - <div> + <div + v-if="canViewSchema"> <DatabaseToolbar /> <v-window v-model="tab"> @@ -160,6 +161,12 @@ :items="items" class="pa-0 mt-2" /> </div> + <JumboBox + v-if="error" + :title="$t(errorCodeKey(error).title, { resource: 'database' })" + :subtitle="$t(errorCodeKey(error).subtitle)" + :text="$t(errorCodeKey(error).text, { resource: 'database' })" /> + <pre v-if="error">{{ error }}</pre> </template> <script> @@ -168,7 +175,7 @@ import Summary from '@/components/identifier/Summary.vue' import Select from '@/components/identifier/Select.vue' import UserBadge from '@/components/user/UserBadge.vue' import JumboBox from '@/components/JumboBox.vue' -import { sizeToHumanLabel } from '@/utils' +import { sizeToHumanLabel, errorCodeKey } from '@/utils' import { useUserStore } from '@/stores/user' import { useCacheStore } from '@/stores/cache' @@ -198,7 +205,6 @@ export default { useServerSeoMeta(identifierService.databaseToServerSeoMeta(data.value)) } return { - database: data, error } }, @@ -356,7 +362,16 @@ export default { return null } return this.database.preview_image + }, + canViewSchema () { + if (this.error) { + return false + } + return this.database } + }, + methods: { + errorCodeKey } } </script> diff --git a/dbrepo-ui/pages/database/[database_id]/settings.vue b/dbrepo-ui/pages/database/[database_id]/settings.vue index 0e833914a266810967e41f40f9c13da46fef2953..5e52a9081603653da7847b7029f077761219870a 100644 --- a/dbrepo-ui/pages/database/[database_id]/settings.vue +++ b/dbrepo-ui/pages/database/[database_id]/settings.vue @@ -1,5 +1,6 @@ <template> - <div> + <div + v-if="canView"> <DatabaseToolbar ref="toolbar" /> <v-window @@ -241,6 +242,11 @@ </v-window> <v-breadcrumbs :items="items" class="pa-0 mt-2" /> </div> + <JumboBox + v-if="error" + :title="$t(errorCodeKey(error).title, { resource: 'database' })" + :subtitle="$t(errorCodeKey(error).subtitle)" + :text="$t(errorCodeKey(error).text, { resource: 'database' })" /> </template> <script> @@ -248,12 +254,30 @@ import DatabaseToolbar from '@/components/database/DatabaseToolbar.vue' import EditAccess from '@/components/dialogs/EditAccess.vue' import { useUserStore } from '@/stores/user' import { useCacheStore } from '@/stores/cache' +import { errorCodeKey } from '@/utils' export default { components: { DatabaseToolbar, EditAccess }, + setup () { + const config = useRuntimeConfig() + const userStore = useUserStore() + const { database_id } = useRoute().params + const { error } = useFetch(`${config.public.api.server}/api/database/${database_id}`, { + immediate: true, + method: 'HEAD', + timeout: 90_000, + headers: { + Accept: 'application/json', + Authorization: userStore.getToken ? `Bearer ${userStore.getToken}` : null + } + }) + return { + error + } + }, data () { return { dialogDelete: false, @@ -405,6 +429,12 @@ export default { } return this.roles.includes('modify-database-image') }, + canView () { + if (this.error) { + return false + } + return this.database + }, previewImage () { if (this.file) { return URL.createObjectURL(this.file) @@ -456,6 +486,7 @@ export default { this.modifyOwner.id = this.database.owner.id }, methods: { + errorCodeKey, submit () { this.$refs.form.validate() }, diff --git a/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/info.vue b/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/info.vue index 33f370e2f275371bb72b34eed23adda2b4085181..88add5804efd050bca3c9cd73011724832d3d3cd 100644 --- a/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/info.vue +++ b/dbrepo-ui/pages/database/[database_id]/subset/[subset_id]/info.vue @@ -38,8 +38,13 @@ :title="$t('pages.subset.visibility.title')" density="compact"> <ResourceStatus + v-if="!identifier" :inline="true" - :resource="subset" /> + :resource="database" /> + <ResourceStatus + v-else + :inline="true" + :resource="identifier" /> </v-list-item> <v-list-item v-if="subset.creator" diff --git a/dbrepo-ui/pages/database/[database_id]/subset/index.vue b/dbrepo-ui/pages/database/[database_id]/subset/index.vue index 8d454ce83d11a4f6143093ecb20d268b1897a9d2..7866e6d5de310d95729268dc3c1f264be20e540b 100644 --- a/dbrepo-ui/pages/database/[database_id]/subset/index.vue +++ b/dbrepo-ui/pages/database/[database_id]/subset/index.vue @@ -1,18 +1,43 @@ <template> - <div> + <div + v-if="canViewSchema"> <DatabaseToolbar /> <SubsetList /> <v-breadcrumbs :items="items" class="pa-0 mt-2" /> </div> + <JumboBox + v-if="error" + :title="$t(errorCodeKey(error).title, { resource: 'subset' })" + :subtitle="$t(errorCodeKey(error).subtitle)" + :text="$t(errorCodeKey(error).text, { resource: 'subset' })" /> </template> <script> import SubsetList from '@/components/subset/SubsetList.vue' +import { errorCodeKey } from '@/utils' +import { useCacheStore } from '@/stores/cache' export default { components: { SubsetList }, + setup () { + const config = useRuntimeConfig() + const userStore = useUserStore() + const { database_id } = useRoute().params + const { error } = useFetch(`${config.public.api.server}/api/database/${database_id}`, { + immediate: true, + method: 'HEAD', + timeout: 90_000, + headers: { + Accept: 'application/json', + Authorization: userStore.getToken ? `Bearer ${userStore.getToken}` : null + } + }) + return { + error + } + }, data () { return { items: [ @@ -29,8 +54,23 @@ export default { to: `/database/${this.$route.params.database_id}/subset`, disabled: true } - ] + ], + cacheStore: useCacheStore() } + }, + computed: { + database () { + return this.cacheStore.getDatabase + }, + canViewSchema () { + if (this.error) { + return false + } + return this.database + } + }, + methods: { + errorCodeKey } } </script> diff --git a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/data.vue b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/data.vue index fc4b046df8226fa3e14eaac3a4b9af94e4f2aced..6d84d8a6d63049aa2c5e6ef07edf7771800a01df 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/data.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/data.vue @@ -9,14 +9,14 @@ <v-spacer /> <v-btn v-if="canAddTuple" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-plus' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-plus' : null" variant="flat" :text="$t('toolbars.table.data.add')" class="ml-2" @click="addTuple" /> <v-btn v-if="canEditTuple" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-pencil' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-pencil' : null" color="warning" variant="flat" :text="$t('toolbars.table.data.edit')" @@ -24,7 +24,7 @@ @click="editTuple" /> <v-btn v-if="canDeleteTuple" - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-delete' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-delete' : null" color="error" variant="flat" :text="$t('toolbars.table.data.delete')" @@ -32,14 +32,14 @@ :loading="loadingDelete" @click="deleteItems" /> <v-btn - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-download' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-download' : null" variant="flat" :loading="downloadLoading" :text="$t('toolbars.table.data.download')" class="ml-2" @click.stop="download" /> <v-btn - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-refresh' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-refresh' : null" variant="flat" :text="$t('toolbars.table.data.refresh')" class="ml-2" @@ -47,7 +47,7 @@ :loading="loadingData" @click="reload" /> <v-btn - :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-update' : null" + :prepend-icon="$vuetify.display.mdAndUp ? 'mdi-update' : null" variant="flat" :text="$t('toolbars.table.data.version')" class="ml-2 mr-2" @@ -61,13 +61,16 @@ {{ $t('error.table.connection') }} </v-card-text> </v-card> - <v-card tile> + <v-card + tile> <QueryResults id="query-results" ref="queryResults" + class="mt-0 mb-0" type="table" + :select="canSelectTuples" :timestamp="versionISO || lastReload.toISOString()" - class="mt-0 mb-0" /> + @selection="updateSelect" /> </v-card> <v-dialog v-model="pickVersionDialog" @@ -99,18 +102,24 @@ </v-dialog> <v-breadcrumbs :items="items" class="pa-0 mt-2" /> </div> + <JumboBox + v-if="error" + :title="$t(errorCodeKey(error).title, { resource: 'table' })" + :subtitle="$t(errorCodeKey(error).subtitle)" + :text="$t(errorCodeKey(error).text, { resource: 'table' })" /> </template> <script> import TableHistory from '@/components/table/TableHistory.vue' import TimeDrift from '@/components/TimeDrift.vue' import TableToolbar from '@/components/table/TableToolbar.vue' -import { formatTimestamp } from '@/utils' +import { errorCodeKey, formatTimestamp } from '@/utils' import { useUserStore } from '@/stores/user' import { useCacheStore } from '@/stores/cache' import EditTuple from '@/components/dialogs/EditTuple.vue' import BlobDownload from '@/components/table/BlobDownload.vue' import QueryResults from '@/components/subset/Results.vue' +import JumboBox from '@/components/JumboBox.vue' export default { components: { @@ -119,7 +128,29 @@ export default { EditTuple, TableHistory, TableToolbar, - TimeDrift + TimeDrift, + JumboBox + }, + setup () { + const config = useRuntimeConfig() + const userStore = useUserStore() + const { database_id, table_id } = useRoute().params + const { error, data } = useFetch(`${config.public.api.server}/api/database/${database_id}/table/${table_id}`, { + immediate: true, + timeout: 90_000, + headers: { + Accept: 'application/json', + Authorization: userStore.getToken ? `Bearer ${userStore.getToken}` : null + } + }) + if (data.value) { + const identifierService = useIdentifierService() + useServerHead(identifierService.databaseToServerHead(data.value)) + useServerSeoMeta(identifierService.databaseToServerSeoMeta(data.value)) + } + return { + error + } }, data () { return { @@ -144,7 +175,6 @@ export default { version: null, lastReload: new Date(), tab: null, - error: false, tuple: null, options: { page: 1, @@ -193,9 +223,6 @@ export default { user () { return this.userStore.getUser }, - tables () { - return this.cacheStore.getTable - }, access () { return this.userStore.getAccess }, @@ -238,8 +265,16 @@ export default { } return this.access.type === 'write_all' }, + primaryKeyColumns () { + if (!this.table) { + return [] + } + return this.table.constraints.primary_key.map(pk => pk.column) + }, canViewTableData () { - /* view when database is public or when private: 1) view-table-data role present 2) access is at least read */ + if (this.error) { + return false + } if (!this.table) { return false } @@ -258,6 +293,13 @@ export default { const userService = useUserService() return userService.hasWriteAccess(this.table, this.access, this.user) && this.roles.includes('insert-table-data') }, + canSelectTuples () { + if (!this.roles) { + return false + } + const userService = useUserService() + return userService.hasWriteAccess(this.table, this.access, this.user) && this.roles.includes('insert-table-data') + }, canEditTuple () { if (!this.roles || this.selection === null || this.selection.length !== 1) { return false @@ -282,6 +324,7 @@ export default { this.reload() }, methods: { + errorCodeKey, addTuple () { this.tuple = {} this.columns.forEach((c) => { @@ -299,8 +342,7 @@ export default { for (const select of this.selection) { /* remove in container */ const constraints = {} - this.columns - .filter(c => c.is_primary_key) + this.primaryKeyColumns .forEach((c) => { constraints[c.internal_name] = select[c.internal_name] }) @@ -327,6 +369,7 @@ export default { toast.success(`Deleted ${this.selection.length} row(s)`) this.$emit('modified', { success: true, action: 'delete' }) this.selection = [] + this.$refs.queryResults.resetSelection() this.reload() }) this.loadingDelete = false @@ -399,11 +442,14 @@ export default { }, reload () { this.lastReload = new Date() + if (!this.canViewTableData) { + return + } this.$refs.queryResults.reExecute(Number(this.$route.params.table_id)) this.$refs.queryResults.reExecuteCount(Number(this.$route.params.table_id)) }, isFileField (column) { - return ['blob', 'longblob', 'mediumblob', 'tinyblob'].includes(column.column_type) + return ['blob', 'longblob', 'mediumblob', 'tinyblob'].includes(column.type) }, close ({ success }) { console.debug('closed edit/create tuple dialog') @@ -412,7 +458,11 @@ export default { if (success) { this.reload() this.selection = [] + this.$refs.queryResults.resetSelection() } + }, + updateSelect (selection) { + this.selection = selection } } } diff --git a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/info.vue b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/info.vue index 5c258e75eccb654d2a3ecbd9bdb50151ee95d1a3..93c16d67925ed584e0427a14f5fd53ede7c0826d 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/info.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/info.vue @@ -1,6 +1,6 @@ <template> <div - v-if="table"> + v-if="canViewSchema"> <TableToolbar :selection="selection" /> <v-card @@ -116,6 +116,11 @@ </v-card> <v-breadcrumbs :items="items" class="pa-0 mt-2" /> </div> + <JumboBox + v-if="error" + :title="$t(errorCodeKey(error).title, { resource: 'table' })" + :subtitle="$t(errorCodeKey(error).subtitle)" + :text="$t(errorCodeKey(error).text, { resource: 'table' })" /> </template> <script> @@ -125,6 +130,7 @@ import Summary from '@/components/identifier/Summary.vue' import UserBadge from '@/components/user/UserBadge.vue' import { useUserStore } from '@/stores/user' import { useCacheStore } from '@/stores/cache' +import { errorCodeKey } from '@/utils' export default { components: { @@ -151,7 +157,6 @@ export default { useServerSeoMeta(identifierService.databaseToServerSeoMeta(data.value)) } return { - table: data, error } }, @@ -201,6 +206,9 @@ export default { database () { return this.cacheStore.getDatabase }, + table () { + return this.cacheStore.getTable + }, roles () { return this.userStore.getRoles }, @@ -213,6 +221,21 @@ export default { } return this.access.type === 'read' || this.access.type === 'write_own' || this.access.type === 'write_all' }, + canViewSchema () { + if (this.error) { + return false + } + if (!this.table) { + return false + } + if (this.table.is_schema_public || this.table.is_public) { + return true + } + if (!this.user) { + return false + } + return this.hasReadAccess || this.table.owner.id === this.user.id || this.database.owner.id === this.user.id + }, canWrite () { if (!this.table || !this.user || !this.access) { return false @@ -288,6 +311,9 @@ export default { return this.$t('pages.table.connection.permissions.read') } } - } + }, + methods: { + errorCodeKey + }, } </script> diff --git a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/schema.vue b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/schema.vue index 5364b6c8997e4e336852f02747f2b7bfc6b08ee7..e0d4f9555fc780b7bfcad2275b71261861eb1b3e 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/schema.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/schema.vue @@ -118,17 +118,44 @@ :items="items" class="pa-0 mt-2" /> </div> + <JumboBox + v-if="error" + :title="$t(errorCodeKey(error).title, { resource: 'table' })" + :subtitle="$t(errorCodeKey(error).subtitle)" + :text="$t(errorCodeKey(error).text, { resource: 'table' })" /> </template> <script> import TableToolbar from '@/components/table/TableToolbar.vue' import { useUserStore } from '@/stores/user' import { useCacheStore } from '@/stores/cache' +import { errorCodeKey } from '@/utils' export default { components: { TableToolbar }, + setup () { + const config = useRuntimeConfig() + const userStore = useUserStore() + const { database_id, table_id } = useRoute().params + const { error, data } = useFetch(`${config.public.api.server}/api/database/${database_id}/table/${table_id}`, { + immediate: true, + timeout: 90_000, + headers: { + Accept: 'application/json', + Authorization: userStore.getToken ? `Bearer ${userStore.getToken}` : null + } + }) + if (data.value) { + const identifierService = useIdentifierService() + useServerHead(identifierService.databaseToServerHead(data.value)) + useServerSeoMeta(identifierService.databaseToServerSeoMeta(data.value)) + } + return { + error + } + }, data () { return { selection: [], @@ -160,7 +187,7 @@ export default { ], headers: [ { value: 'internal_name', title: this.$t('pages.table.subpages.schema.internal-name.title') }, - { value: 'column_type', title: this.$t('pages.table.subpages.schema.column-type.title') }, + { value: 'type', title: this.$t('pages.table.subpages.schema.column-type.title') }, { value: 'extra', title: this.$t('pages.table.subpages.schema.extra.title') }, { value: 'column_concept', title: this.$t('pages.table.subpages.schema.concept.title') }, { value: 'column_unit', title: this.$t('pages.table.subpages.schema.unit.title') }, @@ -195,6 +222,9 @@ export default { return this.userStore.getRoles }, canViewSchema () { + if (this.error) { + return false + } if (!this.table) { return false } @@ -204,7 +234,7 @@ export default { if (!this.user) { return false } - return this.hasReadAccess || this.table.owned_by === this.user.id || this.database.owner.id === this.user.id + return this.hasReadAccess || this.table.owner.id === this.user.id || this.database.owner.id === this.user.id }, primaryKeysColumns () { return this.table.constraints.primary_key.map(pk => pk.column.internal_name).join(', ') @@ -235,10 +265,11 @@ export default { } }, methods: { + errorCodeKey, extra (column) { - if (column.column_type === 'float') { + if (column.type === 'float') { return `precision=${column.size}` - } else if (['decimal', 'double'].includes(column.column_type)) { + } else if (['decimal', 'double'].includes(column.type)) { let extra = '' if (column.size !== null) { extra += `size=${column.size}` @@ -250,11 +281,11 @@ export default { extra += `d=${column.d}` } return extra - } else if (column.column_type === 'enum') { + } else if (column.type === 'enum') { return `(${column.enums.join(', ')})` - } else if (column.column_type === 'set') { + } else if (column.type === 'set') { return `(${column.sets.join(', ')})` - } else if (['int', 'char', 'varchar', 'binary', 'varbinary', 'tinyint', 'size="small"int', 'mediumint', 'bigint'].includes(column.column_type)) { + } else if (['int', 'char', 'varchar', 'binary', 'varbinary', 'tinyint', 'size="small"int', 'mediumint', 'bigint'].includes(column.type)) { return column.size !== null ? `size=${column.size}` : '' } return null diff --git a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/settings.vue b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/settings.vue index 936db0ab73e70d9ca11d4bd0a8ee80c5618a8fe9..45a8a3a5f2d701801f9ca1fb6f6f7e2055a04e1e 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/[table_id]/settings.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/[table_id]/settings.vue @@ -167,7 +167,7 @@ export default { ], headers: [ { value: 'internal_name', title: this.$t('pages.table.subpages.schema.internal-name.title') }, - { value: 'column_type', title: this.$t('pages.table.subpages.schema.column-type.title') }, + { value: 'type', title: this.$t('pages.table.subpages.schema.column-type.title') }, { value: 'extra', title: this.$t('pages.table.subpages.schema.extra.title') }, { value: 'column_concept', title: this.$t('pages.table.subpages.schema.concept.title') }, { value: 'column_unit', title: this.$t('pages.table.subpages.schema.unit.title') }, @@ -258,9 +258,9 @@ export default { this.$refs.form.validate() }, extra (column) { - if (column.column_type === 'float') { + if (column.type === 'float') { return `precision=${column.size}` - } else if (['decimal', 'double'].includes(column.column_type)) { + } else if (['decimal', 'double'].includes(column.type)) { let extra = '' if (column.size !== null) { extra += `size=${column.size}` @@ -272,11 +272,11 @@ export default { extra += `d=${column.d}` } return extra - } else if (column.column_type === 'enum') { + } else if (column.type === 'enum') { return `(${column.enums.join(', ')})` - } else if (column.column_type === 'set') { + } else if (column.type === 'set') { return `(${column.sets.join(', ')})` - } else if (['int', 'char', 'varchar', 'binary', 'varbinary', 'tinyint', 'size="small"int', 'mediumint', 'bigint'].includes(column.column_type)) { + } else if (['int', 'char', 'varchar', 'binary', 'varbinary', 'tinyint', 'size="small"int', 'mediumint', 'bigint'].includes(column.type)) { return column.size !== null ? `size=${column.size}` : '' } return null diff --git a/dbrepo-ui/pages/database/[database_id]/table/index.vue b/dbrepo-ui/pages/database/[database_id]/table/index.vue index 7198c88cf31ab8203f711cd8e7982805524ecff9..9616074fddd6fa61e7033ad8de5dd9fd387e5691 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/index.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/index.vue @@ -1,15 +1,27 @@ <template> - <div> + <div + v-if="canViewSchema"> <DatabaseToolbar /> - <v-window v-model="tab"> + <v-window + v-model="tab"> <TableList /> </v-window> - <v-breadcrumbs :items="items" class="pa-0 mt-2" /> + <v-breadcrumbs + :items="items" + class="pa-0 mt-2" /> </div> + <JumboBox + v-if="error" + :title="$t(errorCodeKey(error).title, { resource: 'table' })" + :subtitle="$t(errorCodeKey(error).subtitle)" + :text="$t(errorCodeKey(error).text, { resource: 'table' })" /> </template> + <script> import TableList from '@/components/table/TableList.vue' import DatabaseToolbar from '@/components/database/DatabaseToolbar.vue' +import { useCacheStore } from '@/stores/cache' +import { errorCodeKey } from '@/utils' export default { name: 'Tables', @@ -17,9 +29,26 @@ export default { TableList, DatabaseToolbar }, + setup () { + const config = useRuntimeConfig() + const userStore = useUserStore() + const { database_id } = useRoute().params + const { error } = useFetch(`${config.public.api.server}/api/database/${database_id}`, { + immediate: true, + method: 'HEAD', + timeout: 90_000, + headers: { + Accept: 'application/json', + Authorization: userStore.getToken ? `Bearer ${userStore.getToken}` : null + } + }) + return { + error + } + }, data () { return { - db: null, + tab: 0, items: [ { title: this.$t('navigation.databases'), @@ -34,13 +63,23 @@ export default { to: `/database/${this.$route.params.database_id}/table`, disabled: true } - ] + ], + cacheStore: useCacheStore() } }, computed: { - tab () { - return 1 + database () { + return this.cacheStore.getDatabase + }, + canViewSchema () { + if (this.error) { + return false + } + return this.database } + }, + methods: { + errorCodeKey } } </script> diff --git a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/schema.vue b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/schema.vue index e9559bca7a27e2b6088f1b619ac516844733a386..e126e19d9050c23b4299f9799b2895490813f33f 100644 --- a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/schema.vue +++ b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/schema.vue @@ -88,7 +88,7 @@ export default { ], headers: [ { value: 'internal_name', title: this.$t('pages.table.subpages.schema.internal-name.title') }, - { value: 'column_type', title: this.$t('pages.table.subpages.schema.column-type.title') }, + { value: 'type', title: this.$t('pages.table.subpages.schema.column-type.title') }, { value: 'extra', title: this.$t('pages.table.subpages.schema.extra.title') }, { value: 'column_concept', title: this.$t('pages.table.subpages.schema.concept.title') }, { value: 'column_unit', title: this.$t('pages.table.subpages.schema.unit.title') }, @@ -144,9 +144,9 @@ export default { }, methods: { extra (column) { - if (column.column_type === 'float') { + if (column.type === 'float') { return `precision=${column.size}` - } else if (['decimal', 'double'].includes(column.column_type)) { + } else if (['decimal', 'double'].includes(column.type)) { let extra = '' if (column.size !== null) { extra += `size=${column.size}` @@ -158,11 +158,11 @@ export default { extra += `d=${column.d}` } return extra - } else if (column.column_type === 'enum') { + } else if (column.type === 'enum') { return `(${column.enums.join(', ')})` - } else if (column.column_type === 'set') { + } else if (column.type === 'set') { return `(${column.sets.join(', ')})` - } else if (['int', 'char', 'varchar', 'binary', 'varbinary', 'tinyint', 'size="small"int', 'mediumint', 'bigint'].includes(column.column_type)) { + } else if (['int', 'char', 'varchar', 'binary', 'varbinary', 'tinyint', 'size="small"int', 'mediumint', 'bigint'].includes(column.type)) { return column.size !== null ? `size=${column.size}` : '' } return null diff --git a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/settings.vue b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/settings.vue index 80c746292b7ecd6bf2904a9159d92cf534db8aea..f9c905ffd4de036c8ccd1157d14bcda1504a10a0 100644 --- a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/settings.vue +++ b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/settings.vue @@ -140,7 +140,7 @@ export default { ], headers: [ { value: 'internal_name', title: this.$t('pages.table.subpages.schema.internal-name.title') }, - { value: 'column_type', title: this.$t('pages.table.subpages.schema.column-type.title') }, + { value: 'type', title: this.$t('pages.table.subpages.schema.column-type.title') }, { value: 'extra', title: this.$t('pages.table.subpages.schema.extra.title') }, { value: 'column_concept', title: this.$t('pages.table.subpages.schema.concept.title') }, { value: 'column_unit', title: this.$t('pages.table.subpages.schema.unit.title') }, @@ -226,9 +226,9 @@ export default { this.$refs.form.validate() }, extra (column) { - if (column.column_type === 'float') { + if (column.type === 'float') { return `precision=${column.size}` - } else if (['decimal', 'double'].includes(column.column_type)) { + } else if (['decimal', 'double'].includes(column.type)) { let extra = '' if (column.size !== null) { extra += `size=${column.size}` @@ -240,11 +240,11 @@ export default { extra += `d=${column.d}` } return extra - } else if (column.column_type === 'enum') { + } else if (column.type === 'enum') { return `(${column.enums.join(', ')})` - } else if (column.column_type === 'set') { + } else if (column.type === 'set') { return `(${column.sets.join(', ')})` - } else if (['int', 'char', 'varchar', 'binary', 'varbinary', 'tinyint', 'size="small"int', 'mediumint', 'bigint'].includes(column.column_type)) { + } else if (['int', 'char', 'varchar', 'binary', 'varbinary', 'tinyint', 'size="small"int', 'mediumint', 'bigint'].includes(column.type)) { return column.size !== null ? `size=${column.size}` : '' } return null diff --git a/dbrepo-ui/pages/database/[database_id]/view/index.vue b/dbrepo-ui/pages/database/[database_id]/view/index.vue index dc87510ae887285ac36fc647e8f2a172cb708784..5d8cee483626d54afb8dbb082d489313a30f3aad 100644 --- a/dbrepo-ui/pages/database/[database_id]/view/index.vue +++ b/dbrepo-ui/pages/database/[database_id]/view/index.vue @@ -1,16 +1,26 @@ <template> - <div> + <div + v-if="canViewSchema"> <DatabaseToolbar /> - <v-window v-model="tab"> + <v-window + v-model="tab"> <ViewList /> </v-window> - <v-breadcrumbs :items="items" class="pa-0 mt-2" /> + <v-breadcrumbs + :items="items" + class="pa-0 mt-2" /> </div> + <JumboBox + v-if="error" + :title="$t(errorCodeKey(error).title, { resource: 'view' })" + :subtitle="$t(errorCodeKey(error).subtitle)" + :text="$t(errorCodeKey(error).text, { resource: 'view' })" /> </template> <script> import DatabaseToolbar from '@/components/database/DatabaseToolbar.vue' import ViewList from '@/components/view/ViewList.vue' +import { useCacheStore } from '@/stores/cache' export default { name: 'Views', @@ -18,9 +28,26 @@ export default { ViewList, DatabaseToolbar }, + setup () { + const config = useRuntimeConfig() + const userStore = useUserStore() + const { database_id } = useRoute().params + const { error } = useFetch(`${config.public.api.server}/api/database/${database_id}`, { + immediate: true, + method: 'HEAD', + timeout: 90_000, + headers: { + Accept: 'application/json', + Authorization: userStore.getToken ? `Bearer ${userStore.getToken}` : null + } + }) + return { + error + } + }, data () { return { - db: null, + tab: 0, items: [ { title: this.$t('navigation.databases'), @@ -35,20 +62,23 @@ export default { to: `/database/${this.$route.params.database_id}/view`, disabled: true } - ] + ], + cacheStore: useCacheStore() } }, computed: { - tab () { - return 1 + database () { + return this.cacheStore.getDatabase + }, + canViewSchema () { + if (this.error) { + return false + } + return this.database } }, - mounted () { - }, methods: { + errorCodeKey } } </script> - -<style scoped> -</style> diff --git a/dbrepo-ui/pages/search.vue b/dbrepo-ui/pages/search.vue index c437dff5564557a1e29f1c05289e7e1a7d42ad0b..04f94a2d8e19e7cfe8747a7bc8bf5d64ad541ae0 100644 --- a/dbrepo-ui/pages/search.vue +++ b/dbrepo-ui/pages/search.vue @@ -230,7 +230,7 @@ export default { } return description } else if (this.isColumn(item)) { - let text = item.column_type + let text = item.type if (item.size) { text += `(${item.size}${item.d ? ',' + item.d : ''})` } diff --git a/dbrepo-ui/utils/index.ts b/dbrepo-ui/utils/index.ts index 4a9b2394979cd2066de446ef566071f9994d91d5..46a5e9e5550cc3860d4e1fc69ba7a010a68ff33d 100644 --- a/dbrepo-ui/utils/index.ts +++ b/dbrepo-ui/utils/index.ts @@ -1,6 +1,7 @@ import {format} from 'date-fns' import moment from 'moment' import type {AxiosError} from 'axios' +import type {H3Error} from "h3"; export function notEmpty(str: string) { @@ -24,6 +25,29 @@ export function notFile(files: [File[]]) { return files.length === 1 } +export function errorCodeKey(error: H3Error): any { + switch (error.statusCode) { + case 404: + return { + title: 'error.missing.title', + subtitle: 'ERR_NOT_FOUND', + text: 'error.missing.text' + } + case 403: + return { + title: 'error.permission.title', + subtitle: 'ERR_NOT_AUTHORIZED', + text: 'error.permission.text' + } + default: + return { + title: 'error.gone.title', + subtitle: 'ERR_GONE', + text: 'error.gone.text' + } + } +} + export function castNumberOptional(str: string): string | number { const num = Number(str) const ss = String(num) @@ -1055,6 +1079,14 @@ export function isActiveMessage(message: any) { } export function axiosErrorToApiError(error: AxiosError): ApiErrorDto { + if (!error || !('data' in error.response)) { + const errorObj: ApiErrorDto = { + status: 'NOT_SET', + code: 'error.axios.connection', + message: '' + } + return errorObj + } if (error.code === 'ECONNABORTED') { /* timeout */ const errorObj: ApiErrorDto = { @@ -1064,15 +1096,7 @@ export function axiosErrorToApiError(error: AxiosError): ApiErrorDto { } return errorObj } - if ('data' in error.response) { - const errorObj: ApiErrorDto = (error.response.data as ApiErrorDto) - return errorObj - } - const errorObj: ApiErrorDto = { - status: error.code ? error.code : 'NOT_SET', - code: 'error.axios.connection', - message: error.message - } + const errorObj: ApiErrorDto = (error.response.data as ApiErrorDto) return errorObj } diff --git a/lib/python/dbrepo/api/dto.py b/lib/python/dbrepo/api/dto.py index c6188098f92b7f33a7b76c08de0ae5beac7355c1..b1caa578cad6fa8c64266dbfe430323d8d5dc4da 100644 --- a/lib/python/dbrepo/api/dto.py +++ b/lib/python/dbrepo/api/dto.py @@ -117,7 +117,7 @@ class ColumnBrief(BaseModel): database_id: int table_id: int internal_name: str - column_type: ColumnType + type: ColumnType class TableBrief(BaseModel): @@ -889,8 +889,6 @@ class Column(BaseModel): ord: int internal_name: str type: ColumnType - is_public: bool - is_null_allowed: bool alias: Optional[str] = None description: Optional[str] = None size: Optional[int] = None