diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java index 001350246e68e7e75bbfc781695b284138c01ace..84b24b66c789d3769358bd823c1661999548fa51 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/ViewEndpoint.java @@ -270,6 +270,10 @@ public class ViewEndpoint extends AbstractEndpoint { // TODO improve with a single operation that checks if user xyz has access to view abc final PrivilegedViewDto view = metadataServiceGateway.getViewById(databaseId, viewId); if (!view.getIsPublic()) { + if (principal == null) { + log.error("Failed to get data from view: unauthorized"); + throw new NotAllowedException("Failed to get data from view: unauthorized"); + } metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); } try { diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java index 5d33c28a51de128dd101c48aee35525fea2b184b..b374bfe7aa27adf4980d4dc8156f227a9abd0426 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/ViewEndpointUnitTest.java @@ -25,7 +25,6 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.sql.SQLException; -import java.time.Instant; import java.util.List; import java.util.Map; diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SubsetServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SubsetServiceIntegrationTest.java index d0e04f2c518fc69c7e70d7269c8a704d8c6becef..0eab9c6ff3e012950dd011f472ca65e67c4912e9 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SubsetServiceIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SubsetServiceIntegrationTest.java @@ -165,7 +165,6 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { /* test */ final QueryDto response = queryService.findById(DATABASE_1_PRIVILEGED_DTO, queryId); assertEquals(QUERY_1_ID, response.getId()); - assertEquals(DATABASE_1_ID, response.getDatabaseId()); } protected List<QueryDto> findAll_generic(Boolean filterPersisted) throws SQLException, QueryNotFoundException, diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/DataMapper.java b/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/DataMapper.java index f3a83c910f1acbf9f55a219d74af3411dff01237..b89e6b9c0feb3a282c38afbf39f07ef2d85fb546 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/DataMapper.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/DataMapper.java @@ -21,7 +21,6 @@ import at.tuwien.api.database.table.constraints.primary.PrimaryKeyDto; import at.tuwien.api.database.table.constraints.unique.UniqueDto; import at.tuwien.api.user.UserBriefDto; import at.tuwien.config.QueryConfig; -import at.tuwien.exception.QueryNotFoundException; import at.tuwien.exception.TableNotFoundException; import org.apache.hadoop.shaded.com.google.common.hash.Hashing; import org.apache.hadoop.shaded.org.apache.commons.io.FileUtils; @@ -198,13 +197,10 @@ public interface DataMapper { return view; } - default QueryDto resultSetToQueryDto(@NotNull ResultSet data) throws SQLException, QueryNotFoundException { + default QueryDto resultSetToQueryDto(@NotNull ResultSet data) throws SQLException { /* note that next() is called outside this mapping function */ - return QueryDto.builder() + final QueryDto subset = QueryDto.builder() .id(data.getLong(1)) - .owner(UserBriefDto.builder() - .id(UUID.fromString(data.getString(3))) - .build()) .query(data.getString(4)) .queryHash(data.getString(5)) .resultHash(data.getString(6)) @@ -214,6 +210,12 @@ public interface DataMapper { .atZone(ZoneId.of("UTC")) .toInstant()) .build(); + if (data.getString(3) != null) { + subset.setOwner(UserBriefDto.builder() + .id(UUID.fromString(data.getString(3))) + .build()); + } + return subset; } default List<TableHistoryDto> resultSetToTableHistory(ResultSet resultSet) throws SQLException { @@ -318,7 +320,7 @@ public interface DataMapper { if (!resultSet.next()) { throw new TableNotFoundException("Failed to find table in the information schema"); } - return TableDto.builder() + final TableDto table = TableDto.builder() .name(resultSet.getString(1)) .internalName(resultSet.getString(1)) .isVersioned(resultSet.getString(2).equals("SYSTEM VERSIONED")) @@ -333,7 +335,6 @@ public interface DataMapper { .columns(new LinkedList<>()) .identifiers(new LinkedList<>()) .owner(database.getOwner()) - .owner(database.getOwner()) .constraints(ConstraintsDto.builder() .foreignKeys(new LinkedList<>()) .primaryKey(new LinkedHashSet<>()) @@ -342,6 +343,7 @@ public interface DataMapper { .build()) .isPublic(database.getIsPublic()) .build(); + return table; } default void prepareStatementWithColumnTypeObject(PreparedStatement ps, ColumnTypeDto columnType, int idx, Object value) throws SQLException { diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java b/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java index d918fc966f98c51d90688c48ca9cc3907b164bdb..ec34942d8db402ebfe357a637452f2ef089f97d5 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java @@ -119,7 +119,7 @@ public interface MariaDbMapper { } default String queryStoreCreateTableRawQuery() { - final String statement = "CREATE TABLE `qs_queries` ( `id` bigint not null primary key default nextval(`qs_queries_seq`), `created` datetime not null default now(), `executed` datetime not null default now(), `created_by` varchar(36) not null, `query` text not null, `query_normalized` text not null, `is_persisted` boolean not null, `query_hash` varchar(255) not null, `result_hash` varchar(255), `result_number` bigint) WITH SYSTEM VERSIONING;"; + final String statement = "CREATE TABLE `qs_queries` ( `id` bigint not null primary key default nextval(`qs_queries_seq`), `created` datetime not null default now(), `executed` datetime not null default now(), `created_by` varchar(36), `query` text not null, `query_normalized` text not null, `is_persisted` boolean not null, `query_hash` varchar(255) not null, `result_hash` varchar(255), `result_number` bigint) WITH SYSTEM VERSIONING;"; log.trace("mapped create query store table statement: {}", statement); return statement; } diff --git a/dbrepo-metadata-service/pom.xml b/dbrepo-metadata-service/pom.xml index 27fc34e111f2951fb1a621cca5673032cfa3e236..27dd83121f6b83d311872023b306be181ebd409d 100644 --- a/dbrepo-metadata-service/pom.xml +++ b/dbrepo-metadata-service/pom.xml @@ -55,6 +55,7 @@ <keycloak.version>21.0.2</keycloak.version> <springdoc-openapi.version>2.3.0</springdoc-openapi.version> <testcontainers.version>1.19.1</testcontainers.version> + <jackson.version>2.15.2</jackson.version> <keycloak-testcontainer.version>3.2.0</keycloak-testcontainer.version> <aws-s3.version>2.25.23</aws-s3.version> <jackson.version>2.15.2</jackson.version> diff --git a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java index 3b39857ee614cf55bc53aa9acec0a5e40a3afdc7..812ec7bc21f1d5f080ba874b863d06ab2da5c4f2 100644 --- a/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java +++ b/dbrepo-metadata-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java @@ -20,7 +20,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; @@ -92,7 +91,7 @@ public class AccessEndpoint { schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<DatabaseAccessDto> create(@NotNull @PathVariable("databaseId") Long databaseId, - @org.hibernate.validator.constraints.UUID @PathVariable("userId") UUID userId, + @PathVariable("userId") UUID userId, @Valid @RequestBody UpdateDatabaseAccessDto data, @NotNull Principal principal) throws NotAllowedException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, @@ -155,7 +154,7 @@ public class AccessEndpoint { schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<Void> update(@NotNull @PathVariable("databaseId") Long databaseId, - @org.hibernate.validator.constraints.UUID @PathVariable("userId") UUID userId, + @PathVariable("userId") UUID userId, @Valid @RequestBody UpdateDatabaseAccessDto data, @NotNull Principal principal) throws NotAllowedException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, @@ -200,7 +199,7 @@ public class AccessEndpoint { schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<DatabaseAccessDto> find(@NotNull @PathVariable("databaseId") Long databaseId, - @org.hibernate.validator.constraints.UUID @PathVariable("userId") UUID userId, + @PathVariable("userId") UUID userId, @NotNull Principal principal) throws DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, NotAllowedException { log.debug("endpoint get database access, databaseId={}, userId={}, principal.name={}", databaseId, userId, @@ -257,7 +256,7 @@ public class AccessEndpoint { schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<Void> revoke(@NotNull @PathVariable("databaseId") Long databaseId, - @org.hibernate.validator.constraints.UUID @PathVariable("userId") UUID userId, + @PathVariable("userId") UUID userId, @NotNull Principal principal) throws NotAllowedException, DataServiceException, DataServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, SearchServiceException, SearchServiceConnectionException { diff --git a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java index a1b42d7360d57e5ecf61e0e7e221c82171af4fd7..a7e7cb7a29bedcac63987df21a055718efa63566 100644 --- a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java +++ b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/BaseTest.java @@ -5510,7 +5510,7 @@ public abstract class BaseTest { .query(VIEW_5_QUERY) .queryHash(VIEW_5_QUERY_HASH) .owner(USER_1_BRIEF_DTO) - .columns(null) + .columns(new LinkedList<>()) .build(); public final static ViewBriefDto VIEW_5_BRIEF_DTO = ViewBriefDto.builder() @@ -5596,6 +5596,40 @@ public abstract class BaseTest { .isNullAllowed(true) .build()); + public final static List<ViewColumnDto> VIEW_5_COLUMNS_DTO = List.of( + ViewColumnDto.builder() + .id(29L) + .ordinalPosition(0) + .name("location") + .internalName("location") + .ordinalPosition(0) + .columnType(ColumnTypeDto.VARCHAR) + .size(255L) + .isNullAllowed(false) + .build(), + ViewColumnDto.builder() + .id(30L) + .ordinalPosition(1) + .name("lat") + .internalName("lat") + .ordinalPosition(1) + .columnType(ColumnTypeDto.DECIMAL) + .size(10L) + .d(0L) + .isNullAllowed(true) + .build(), + ViewColumnDto.builder() + .id(31L) + .ordinalPosition(2) + .name("lng") + .internalName("lng") + .ordinalPosition(2) + .columnType(ColumnTypeDto.DECIMAL) + .size(10L) + .d(0L) + .isNullAllowed(true) + .build()); + public final static Long QUERY_1_RESULT_ID = 1L; public final static List<Map<String, Object>> QUERY_1_RESULT_DTO = new LinkedList<>(List.of( new HashMap<>() {{ diff --git a/dbrepo-ui/components/view/ViewToolbar.vue b/dbrepo-ui/components/view/ViewToolbar.vue index e3ba6807bc979638a4152d1e33cc291a0478333f..1f122cc7a49a92ff9e7d71db3620f204c227d7b8 100644 --- a/dbrepo-ui/components/view/ViewToolbar.vue +++ b/dbrepo-ui/components/view/ViewToolbar.vue @@ -69,11 +69,11 @@ :text="$t('navigation.info')" :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/info`" /> <v-tab - v-if="canViewData" + v-if="canReadData" :text="$t('navigation.data')" :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/data`" /> <v-tab - v-if="canViewSchema" + v-if="canReadData" :text="$t('navigation.schema')" :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/schema`" /> </v-tabs> @@ -174,18 +174,30 @@ export default { access () { return this.userStore.getAccess }, - hasReadAccess () { - if (!this.access) { - return false - } - return this.access.type === 'read' || this.access.type === 'write_all' || this.access.type === 'write_own' - }, user () { return this.userStore.getUser }, roles () { return this.userStore.getRoles }, + hasReadAccess () { + if (!this.access) { + return false + } + return this.access.type === 'read' || this.access.type === 'write_own' || this.access.type === 'write_all' + }, + canReadData () { + if (!this.view) { + return false + } + if (this.cachedView.is_public) { + return true + } + if (!this.user) { + return false + } + return this.cachedView.owner.id === this.user.id || this.hasReadAccess + }, identifiers () { if (!this.cachedView) { return [] 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 b9ed74a04287e3f8ecb76b88309399b31d234cd4..764d1b55ffa6b134c07e86290325525caa8919b2 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 @@ -30,7 +30,7 @@ width="50%" /> </v-list> <v-list - v-else + v-else-if="subset" lines="two" dense> <v-list-item @@ -40,6 +40,7 @@ {{ database.is_public ? $t('toolbars.database.public') : $t('toolbars.database.private') }} </v-list-item> <v-list-item + v-if="subset.creator" :title="$t('pages.subset.creator.title')" density="compact"> <UserBadge :user="subset.creator" :other-user="user" /> 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 287f25e45441abe1d706e2c03719d97eb7ad30e9..cac7b4ab3dfca87c882b5032d9ed87dbd8ee356b 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 @@ -66,7 +66,7 @@ id="query-results" ref="queryResults" type="table" - :timestamp="this.versionISO || this.lastReload.toISOString()" + :timestamp="versionISO || lastReload.toISOString()" class="mt-0 mb-0" /> </v-card> <v-dialog diff --git a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/data.vue b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/data.vue index 11e91baabccca6b90241b56c75e396c380acba38..6aed9307c4f9d8cbebfd6bc85d0a80be40405f63 100644 --- a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/data.vue +++ b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/data.vue @@ -1,6 +1,6 @@ <template> <div - v-if="canViewData"> + v-if="canReadData"> <ViewToolbar v-if="cachedView" /> <v-toolbar @@ -8,7 +8,6 @@ :title="$t('toolbars.database.current')" flat> <v-btn - v-if="canDownload" :prepend-icon="$vuetify.display.lgAndUp ? 'mdi-download' : null" variant="flat" :loading="downloadLoading" @@ -94,35 +93,23 @@ export default { if (!this.access) { return false } - return this.access.type === 'read' || this.access.type === 'write_all' || this.access.type === 'write_own' + return this.access.type === 'read' || this.access.type === 'write_own' || this.access.type === 'write_all' }, - canDownload () { + canReadData () { if (!this.view) { return false } if (this.cachedView.is_public) { return true } - if (!this.access) { - return false - } - return this.access.type === 'read' || this.access.type === 'write_own' || this.access.type === 'write_all' - }, - canViewData () { - if (!this.cachedView) { - return false - } - if (this.cachedView.is_public) { - return true - } if (!this.user) { return false } - return this.hasReadAccess || this.cachedView.owned_by === this.user.id || this.database.owner.id === this.user.id + return this.view.owner.id === this.user.id || this.hasReadAccess }, }, mounted () { - if (!this.canViewData) { + if (!this.canReadData) { return } this.reload() diff --git a/lib/python/dbrepo/RestClient.py b/lib/python/dbrepo/RestClient.py index e5edc03649c14698ef6964995a2dca4b1ecd9f7c..4ec7569929257e755ccdf099c1a9b319e844382f 100644 --- a/lib/python/dbrepo/RestClient.py +++ b/lib/python/dbrepo/RestClient.py @@ -974,8 +974,7 @@ class RestClient: raise ResponseCodeError(f'Failed to delete view: response code: {response.status_code} is not ' f'202 (ACCEPTED): {response.text}') - def get_view_data(self, database_id: int, view_id: int, page: int = 0, size: int = 10, - df: bool = False) -> Result | DataFrame: + def get_view_data(self, database_id: int, view_id: int, page: int = 0, size: int = 10) -> DataFrame: """ Get data of a view in a database with given database id and view id. @@ -983,9 +982,8 @@ class RestClient: :param view_id: The view id. :param page: The result pagination number. Optional. Default: 0. :param size: The result pagination size. Optional. Default: 10. - :param df: If true, the result is returned as Pandas DataFrame. Optional. Default: False. - :returns: The result of the view query, if successful. + :returns: The view data, if successful. :raises MalformedError: If the payload is rejected by the service. :raises ForbiddenError: If something went wrong with the authorization. @@ -1001,11 +999,7 @@ class RestClient: params.append(('size', size)) response = self._wrapper(method="get", url=url, params=params) if response.status_code == 200: - body = response.json() - res = Result.model_validate(body) - if df: - return DataFrame.from_records(res.result) - return res + return DataFrame.from_records(response.json()) if response.status_code == 400: raise MalformedError(f'Failed to get view data: {response.text}') if response.status_code == 403: @@ -1043,7 +1037,7 @@ class RestClient: f'200 (OK): {response.text}') def get_table_data(self, database_id: int, table_id: int, page: int = 0, size: int = 10, - timestamp: datetime.datetime = None, df: bool = False) -> Result | DataFrame: + timestamp: datetime.datetime = None) -> DataFrame: """ Get data of a table in a database with given database id and table id. @@ -1052,9 +1046,8 @@ class RestClient: :param page: The result pagination number. Optional. Default: 0. :param size: The result pagination size. Optional. Default: 10. :param timestamp: The query execution time. Optional. - :param df: If true, the result is returned as Pandas DataFrame. Optional. Default: False. - :returns: The result of the view query, if successful. + :returns: The table data, if successful. :raises MalformedError: If the payload is rejected by the service. :raises ForbiddenError: If something went wrong with the authorization. @@ -1071,11 +1064,7 @@ class RestClient: params.append(('timestamp', timestamp)) response = self._wrapper(method="get", url=url, params=params) if response.status_code == 200: - body = response.json() - res = Result.model_validate(body) - if df: - return DataFrame.from_records(res.result) - return res + return DataFrame.from_records(response.json()) if response.status_code == 400: raise MalformedError(f'Failed to get table data: {response.text}') if response.status_code == 403: @@ -1568,7 +1557,7 @@ class RestClient: f'201 (CREATED): {response.text}') def create_subset(self, database_id: int, query: str, page: int = 0, size: int = 10, - timestamp: datetime.datetime = None, df: bool = False) -> Result | DataFrame: + timestamp: datetime.datetime = None) -> DataFrame: """ Executes a SQL query in a database where the current user has at least read access with given database id. The result set can be paginated with setting page and size (both). Historic data can be queried by setting @@ -1579,7 +1568,6 @@ class RestClient: :param page: The result pagination number. Optional. Default: `0`. :param size: The result pagination size. Optional. Default: `10`. :param timestamp: The timestamp at which the data validity is set. Optional. Default: <current timestamp>. - :param df: If true, the result is returned as Pandas DataFrame. Optional. Default: `False`. :returns: The result set, if successful. @@ -1602,11 +1590,8 @@ class RestClient: response = self._wrapper(method="post", url=url, headers={"Accept": "application/json"}, payload=ExecuteQuery(statement=query)) if response.status_code == 201: - body = response.json() - res = Result.model_validate(body) - if df: - return DataFrame.from_records(res.result) - return res + logging.info(f'Created subset with id: {response.headers["X-Id"]}') + return DataFrame.from_records(response.json()) if response.status_code == 400: raise MalformedError(f'Failed to create subset: {response.text}') if response.status_code == 403: @@ -1622,8 +1607,7 @@ class RestClient: raise ResponseCodeError(f'Failed to create subset: response code: {response.status_code} is not ' f'201 (CREATED): {response.text}') - def get_subset_data(self, database_id: int, subset_id: int, page: int = 0, size: int = 10, - df: bool = False) -> Result | DataFrame: + def get_subset_data(self, database_id: int, subset_id: int, page: int = 0, size: int = 10) -> DataFrame: """ Re-executes a query in a database with given database id and query id. @@ -1632,9 +1616,8 @@ class RestClient: :param page: The result pagination number. Optional. Default: `0`. :param size: The result pagination size. Optional. Default: `10`. :param size: The result pagination size. Optional. Default: `10`. - :param df: If true, the result is returned as Pandas DataFrame. Optional.Optional. Default: `False`. - :returns: The result set, if successful. + :returns: The subset data, if successful. :raises MalformedError: If the payload is rejected by the service. :raises ForbiddenError: If something went wrong with the authorization. @@ -1648,11 +1631,7 @@ class RestClient: url += f'?page={page}&size={size}' response = self._wrapper(method="get", url=url, headers=headers) if response.status_code == 200: - body = response.json() - res = Result.model_validate(body) - if df: - return DataFrame.from_records(res.result) - return res + return DataFrame.from_records(response.json()) if response.status_code == 400: raise MalformedError(f'Failed to get query data: {response.text}') if response.status_code == 403: diff --git a/lib/python/dbrepo/api/dto.py b/lib/python/dbrepo/api/dto.py index 79c4279ed0124c251b6ce0442799100c5d5401a9..fabb834647e5c450c31134c97ce6baea8565881f 100644 --- a/lib/python/dbrepo/api/dto.py +++ b/lib/python/dbrepo/api/dto.py @@ -1,9 +1,10 @@ from __future__ import annotations +import datetime from dataclasses import field from enum import Enum -import datetime -from typing import List, Optional, Any, Annotated +from typing import List, Optional, Annotated + from pydantic import BaseModel, PlainSerializer, Field Timestamp = Annotated[ @@ -642,12 +643,6 @@ class CreateView(BaseModel): is_schema_public: bool -class Result(BaseModel): - result: Any - headers: Any - id: Optional[int] = None - - class ViewBrief(BaseModel): id: int database_id: int diff --git a/lib/python/tests/test_unit_query.py b/lib/python/tests/test_unit_query.py index bc1f7e481457629779f341de44b4673a007c7346..6856644eadec9882e38074b4867c36686a858668 100644 --- a/lib/python/tests/test_unit_query.py +++ b/lib/python/tests/test_unit_query.py @@ -1,3 +1,4 @@ +import json import unittest import requests_mock @@ -6,25 +7,23 @@ import datetime from dbrepo.RestClient import RestClient from pandas import DataFrame -from dbrepo.api.dto import Result, Query, User, UserAttributes, QueryType, UserBrief -from dbrepo.api.exceptions import MalformedError, NotExistsError, ForbiddenError, QueryStoreError, \ - MetadataConsistencyError, AuthenticationError +from dbrepo.api.dto import Query, User, UserAttributes, QueryType +from dbrepo.api.exceptions import MalformedError, NotExistsError, ForbiddenError class QueryUnitTest(unittest.TestCase): def test_create_subset_succeeds(self): with requests_mock.Mocker() as mock: - exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], - headers=[{'id': 0, 'username': 1}], - id=None) + exp = [{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}] + df = DataFrame.from_records(json.dumps(exp)) # mock - mock.post('/api/database/1/subset', json=exp.model_dump(), status_code=201) + mock.post('/api/database/1/subset', json=json.dumps(exp), headers={'X-Id': '1'}, status_code=201) # test client = RestClient(username="a", password="b") response = client.create_subset(database_id=1, page=0, size=10, query="SELECT id, username FROM some_table WHERE id IN (1,2)") - self.assertEqual(exp, response) + self.assertTrue(DataFrame.equals(df, response)) def test_create_subset_malformed_fails(self): with requests_mock.Mocker() as mock: @@ -64,17 +63,16 @@ class QueryUnitTest(unittest.TestCase): def test_create_subset_not_auth_succeeds(self): with requests_mock.Mocker() as mock: - exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], - headers=[{'id': 0, 'username': 1}], - id=None) + exp = [{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}] + df = DataFrame.from_records(json.dumps(exp)) # mock - mock.post('/api/database/1/subset', json=exp.model_dump(), status_code=201) + mock.post('/api/database/1/subset', json=json.dumps(exp), headers={'X-Id': '1'}, status_code=201) # test client = RestClient() response = client.create_subset(database_id=1, page=0, size=10, query="SELECT id, username FROM some_table WHERE id IN (1,2)") - self.assertEqual(exp, response) + self.assertTrue(DataFrame.equals(df, response)) def test_find_query_succeeds(self): with requests_mock.Mocker() as mock: @@ -167,27 +165,24 @@ class QueryUnitTest(unittest.TestCase): def test_get_subset_data_succeeds(self): with requests_mock.Mocker() as mock: - exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], - headers=[{'id': 0, 'username': 1}], - id=6) + exp = [{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}] + df = DataFrame.from_records(json.dumps(exp)) # mock - mock.get('/api/database/1/subset/6/data', json=exp.model_dump()) + mock.get('/api/database/1/subset/6/data', json=json.dumps(exp)) # test response = RestClient().get_subset_data(database_id=1, subset_id=6) - self.assertEqual(exp, response) + self.assertTrue(DataFrame.equals(df, response)) def test_get_subset_data_dataframe_succeeds(self): with requests_mock.Mocker() as mock: - res = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], - headers=[{'id': 0, 'username': 1}], - id=6) - exp = DataFrame.from_records(res.model_dump()['result']) + exp = [{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}] + df = DataFrame.from_records(json.dumps(exp)) # mock - mock.get('/api/database/1/subset/6/data', json=res.model_dump()) + mock.get('/api/database/1/subset/6/data', json=json.dumps(exp)) # test - response = RestClient().get_subset_data(database_id=1, subset_id=6, df=True) - self.assertEqual(exp.shape, response.shape) - self.assertTrue(DataFrame.equals(exp, response)) + response = RestClient().get_subset_data(database_id=1, subset_id=6) + self.assertEqual(df.shape, response.shape) + self.assertTrue(DataFrame.equals(df, response)) def test_get_subset_data_not_allowed_fails(self): with requests_mock.Mocker() as mock: diff --git a/lib/python/tests/test_unit_table.py b/lib/python/tests/test_unit_table.py index f149b607818458ac82f15e9eeb8ea9ff1541c62b..5d9e35294812c08adab7931c32337754fa17e3ee 100644 --- a/lib/python/tests/test_unit_table.py +++ b/lib/python/tests/test_unit_table.py @@ -1,3 +1,4 @@ +import json import unittest import requests_mock @@ -6,9 +7,9 @@ import datetime from dbrepo.RestClient import RestClient from pandas import DataFrame -from dbrepo.api.dto import Table, CreateTableConstraints, Column, Constraints, ColumnType, Result, Concept, Unit, \ - TableStatistics, ColumnStatistic, PrimaryKey, TableMinimal, ColumnMinimal, TableBrief, UserBrief -from dbrepo.api.exceptions import MalformedError, ForbiddenError, NotExistsError, NameExistsError, \ +from dbrepo.api.dto import Table, CreateTableConstraints, UserAttributes, User, Column, Constraints, ColumnType, \ + Concept, Unit, TableStatistics, ColumnStatistic, PrimaryKey, TableMinimal, ColumnMinimal, TableBrief, UserBrief +from dbrepo.api.exceptions import MalformedError, ForbiddenError, NotExistsError, NameExistsError, QueryStoreError, \ AuthenticationError, ExternalSystemError @@ -240,27 +241,24 @@ class TableUnitTest(unittest.TestCase): def test_get_table_data_succeeds(self): with requests_mock.Mocker() as mock: - exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], - headers=[{'id': 0, 'username': 1}], - id=None) + exp = [{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}] + df = DataFrame.from_records(json.dumps(exp)) # mock - mock.get('/api/database/1/table/9/data', json=exp.model_dump()) + mock.get('/api/database/1/table/9/data', json=json.dumps(exp)) # test response = RestClient().get_table_data(database_id=1, table_id=9) - self.assertEqual(exp, response) + self.assertTrue(DataFrame.equals(df, response)) def test_get_table_data_dataframe_succeeds(self): with requests_mock.Mocker() as mock: - res = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], - headers=[{'id': 0, 'username': 1}], - id=None) - exp = DataFrame.from_records(res.model_dump()['result']) + exp = [{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}] + df = DataFrame.from_records(json.dumps(exp)) # mock - mock.get('/api/database/1/table/9/data', json=res.model_dump()) + mock.get('/api/database/1/table/9/data', json=json.dumps(exp)) # test - response = RestClient().get_table_data(database_id=1, table_id=9, df=True) - self.assertEqual(exp.shape, response.shape) - self.assertTrue(DataFrame.equals(exp, response)) + response = RestClient().get_table_data(database_id=1, table_id=9) + self.assertEqual(df.shape, response.shape) + self.assertTrue(DataFrame.equals(df, response)) def test_get_table_data_malformed_fails(self): with requests_mock.Mocker() as mock: diff --git a/lib/python/tests/test_unit_view.py b/lib/python/tests/test_unit_view.py index 1bb46bc37c5b619eae1eadcc0818688d71edc735..a2f21247591aeb7d9ae39059250a2c868da0a84d 100644 --- a/lib/python/tests/test_unit_view.py +++ b/lib/python/tests/test_unit_view.py @@ -1,10 +1,10 @@ +import json import unittest import requests_mock from pandas import DataFrame -from dbrepo.RestClient import RestClient -from dbrepo.api.dto import View, Result, ViewColumn, ColumnType, UserBrief +from dbrepo.api.dto import UserAttributes, User, View, ViewColumn, ColumnType from dbrepo.api.exceptions import ForbiddenError, NotExistsError, MalformedError, AuthenticationError @@ -202,27 +202,24 @@ class ViewUnitTest(unittest.TestCase): def test_get_view_data_succeeds(self): with requests_mock.Mocker() as mock: - exp = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], - headers=[{'id': 0, 'username': 1}], - id=None) + exp = [{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}] + df = DataFrame.from_records(json.dumps(exp)) # mock - mock.get('/api/database/1/view/3/data', json=exp.model_dump()) + mock.get('/api/database/1/view/3/data', json=json.dumps(exp)) # test response = RestClient().get_view_data(database_id=1, view_id=3) - self.assertEqual(exp, response) + self.assertTrue(DataFrame.equals(df, response)) def test_get_view_data_dataframe_succeeds(self): with requests_mock.Mocker() as mock: - res = Result(result=[{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}], - headers=[{'id': 0, 'username': 1}], - id=None) - exp = DataFrame.from_records(res.model_dump()['result']) + exp = [{'id': 1, 'username': 'foo'}, {'id': 2, 'username': 'bar'}] + df = DataFrame.from_records(json.dumps(exp)) # mock - mock.get('/api/database/1/view/3/data', json=res.model_dump()) + mock.get('/api/database/1/view/3/data', json=json.dumps(exp)) # test - response: DataFrame = RestClient().get_view_data(database_id=1, view_id=3, df=True) - self.assertEqual(exp.shape, response.shape) - self.assertTrue(DataFrame.equals(exp, response)) + response: DataFrame = RestClient().get_view_data(database_id=1, view_id=3) + self.assertEqual(df.shape, response.shape) + self.assertTrue(DataFrame.equals(df, response)) def test_get_view_data_malformed_fails(self): with requests_mock.Mocker() as mock: