From a25cc7123bdeb356c5f48dc6a2d4630d30081a33 Mon Sep 17 00:00:00 2001 From: Martin Weise <martin.weise@tuwien.ac.at> Date: Sun, 9 Jun 2024 19:43:24 +0000 Subject: [PATCH] Hotfix foreign key --- .../docker-compose.yml | 44 +- .docs/.swagger/api.yaml | 9815 +++++++++++++---- .docs/.swagger/openapi-merge.json | 23 + .docs/.swagger/swagger-ui.html | 2 +- .docs/api/analyse-service.md | 14 +- .docs/api/data-service.md | 4 +- .docs/api/gateway-service.md | 4 +- .docs/api/index.md | 24 +- .docs/api/metadata-service.md | 20 +- .docs/api/open-api.md | 12 +- .docs/api/storage-service.md | 2 +- .docs/deployment-docker-compose.md | 8 +- .docs/deployment-helm.md | 6 +- .docs/docker/_header.md | 6 +- .docs/publications.md | 6 +- .docs/usage-storage.md | 2 +- .docs/usage-upload.md | 2 +- .gitlab-ci.yml | 23 +- Makefile | 3 +- build-docs.sh | 5 +- dbrepo-analyse-service/Pipfile.lock | 90 +- dbrepo-analyse-service/app.py | 86 +- .../as-yml/analyse_datatypes.yml | 48 +- .../as-yml/analyse_keys.yml | 42 +- .../as-yml/analyse_table_stat.yml | 47 - dbrepo-analyse-service/as-yml/health.yml | 18 - dbrepo-analyse-service/determine_stats.py | 28 - .../lib/dbrepo-1.4.4-py3-none-any.whl | Bin 27387 -> 27881 bytes .../lib/dbrepo-1.4.4.tar.gz | Bin 37662 -> 38911 bytes dbrepo-auth-service/dbrepo-realm.json | 58 +- dbrepo-data-db/sidecar/app.py | 1 - dbrepo-data-db/sidecar/ds-yml/import.yml | 4 +- dbrepo-data-service/Dockerfile | 2 + dbrepo-data-service/metrics.md | 8 +- dbrepo-data-service/pom.xml | 12 +- .../at/tuwien/endpoints/AccessEndpoint.java | 22 +- .../at/tuwien/endpoints/DatabaseEndpoint.java | 10 +- .../at/tuwien/endpoints/SubsetEndpoint.java | 12 +- .../at/tuwien/endpoints/TableEndpoint.java | 127 +- .../at/tuwien/endpoints/ViewEndpoint.java | 21 +- .../src/main/resources/application-local.yml | 6 +- .../src/main/resources/application.yml | 6 +- .../java/at/tuwien/config/MariaDbConfig.java | 67 +- .../endpoint/AccessEndpointUnitTest.java | 35 +- .../endpoint/DatabaseEndpointUnitTest.java | 18 +- .../endpoint/SubsetEndpointUnitTest.java | 41 +- .../endpoint/TableEndpointUnitTest.java | 132 +- .../tuwien/endpoint/ViewEndpointUnitTest.java | 23 +- .../gateway/DataDatabaseGatewayUnitTest.java | 151 + .../tuwien/gateway/InterceptorUnitTest.java | 61 + .../KeycloakSidecarGatewayUnitTest.java | 101 + .../MetadataServiceGatewayUnitTest.java | 933 ++ .../DefaultListenerIntegrationTest.java | 7 +- .../listener/DefaultListenerUnitTest.java | 5 +- .../tuwien/mvc/PrometheusEndpointMvcTest.java | 2 +- .../service/AccessServiceIntegrationTest.java | 145 + .../DatabaseServiceIntegrationTest.java | 119 + .../service/QueueServiceIntegrationTest.java | 7 +- .../service/SchemaServiceIntegrationTest.java | 288 +- .../StorageServiceIntegrationTest.java | 171 + .../service/SubsetServiceIntegrationTest.java | 50 +- .../service/TableServiceIntegrationTest.java | 499 +- .../service/ViewServiceIntegrationTest.java | 18 +- .../java/at/tuwien/utils/MariaDbUtilTest.java | 42 + .../src/test/resources/application.properties | 6 +- .../src/test/resources/init/weather.sql | 19 +- .../auth/BasicAuthenticationProvider.java | 3 +- .../java/at/tuwien/config/GatewayConfig.java | 8 +- .../tuwien/gateway/AnalyseServiceGateway.java | 10 - .../gateway/DataDatabaseSidecarGateway.java | 10 +- .../at/tuwien/gateway/KeycloakGateway.java | 5 +- .../gateway/MetadataServiceGateway.java | 101 +- .../impl/AnalyseServiceGatewayImpl.java | 50 - .../impl/DataDatabaseSidecarGatewayImpl.java | 39 +- .../gateway/impl/KeycloakGatewayImpl.java | 42 +- .../impl/MetadataServiceGatewayImpl.java | 294 +- .../at/tuwien/listener/DefaultListener.java | 2 +- .../java/at/tuwien/mapper/MariaDbMapper.java | 327 +- .../java/at/tuwien/mapper/MetadataMapper.java | 4 + .../java/at/tuwien/service/AccessService.java | 29 +- .../at/tuwien/service/AnalyseService.java | 11 - .../at/tuwien/service/DatabaseService.java | 15 + .../java/at/tuwien/service/SubsetService.java | 8 +- .../java/at/tuwien/service/TableService.java | 100 +- .../java/at/tuwien/service/ViewService.java | 2 +- .../impl/AccessServiceMariaDbImpl.java | 23 +- .../service/impl/AnalyseServiceImpl.java | 30 - .../impl/DatabaseServiceMariaDbImpl.java | 15 +- .../impl/SchemaServiceMariaDbImpl.java | 1 + .../impl/SubsetServiceMariaDbImpl.java | 13 +- .../service/impl/TableServiceMariaDbImpl.java | 121 +- .../service/impl/ViewServiceMariaDbImpl.java | 16 +- .../java/at/tuwien/utils/MariaDbUtil.java | 32 +- dbrepo-gateway-service/dbrepo.conf | 2 +- dbrepo-metadata-service/Dockerfile | 2 + .../at/tuwien/api/database/ViewColumnDto.java | 4 + .../api/database/table/TableBriefDto.java | 6 - .../api/database/table/TableStatisticDto.java | 5 + .../table/columns/ColumnCreateDto.java | 6 + .../api/database/table/columns/ColumnDto.java | 2 +- .../foreign/ForeignKeyBriefDto.java | 16 + .../constraints/foreign/ForeignKeyDto.java | 5 +- .../foreign/ForeignKeyReferenceDto.java | 8 +- .../constraints/foreign/ReferenceTypeDto.java | 11 + .../container/image/ContainerImage.java | 3 + dbrepo-metadata-service/pom.xml | 19 +- .../java/at/tuwien/mapper/MetadataMapper.java | 100 +- .../at/tuwien/repository/ImageRepository.java | 3 + .../at/tuwien/endpoints/TableEndpoint.java | 27 +- .../src/main/resources/application-local.yml | 7 +- .../src/main/resources/application.yml | 9 +- .../endpoints/TableEndpointUnitTest.java | 2 +- .../tuwien/mapper/MetadataMapperUnitTest.java | 7 +- .../tuwien/mvc/PrometheusEndpointMvcTest.java | 2 +- .../service/TableServicePersistenceTest.java | 3 +- .../tuwien/service/TableServiceUnitTest.java | 9 +- .../service/ViewServicePersistenceTest.java | 28 + .../java/at/tuwien/config/GatewayConfig.java | 19 +- .../at/tuwien/gateway/DataServiceGateway.java | 3 + .../impl/BrokerServiceGatewayImpl.java | 11 +- .../gateway/impl/DataServiceGatewayImpl.java | 94 +- .../impl/SearchServiceGatewayImpl.java | 8 +- .../at/tuwien/service/ConceptService.java | 2 + .../java/at/tuwien/service/TableService.java | 7 +- .../java/at/tuwien/service/UnitService.java | 3 + .../java/at/tuwien/service/ViewService.java | 3 +- .../service/impl/ConceptServiceImpl.java | 6 + .../service/impl/DatabaseServiceImpl.java | 13 +- .../tuwien/service/impl/TableServiceImpl.java | 149 +- .../tuwien/service/impl/UnitServiceImpl.java | 6 + .../tuwien/service/impl/ViewServiceImpl.java | 8 +- .../java/at/tuwien/test/AbstractUnitTest.java | 8 +- .../main/java/at/tuwien/test/BaseTest.java | 306 +- dbrepo-search-service/Dockerfile | 2 +- dbrepo-search-service/Pipfile.lock | 80 +- dbrepo-search-service/app.py | 86 +- dbrepo-search-service/init/database.json | 1722 +-- .../lib/dbrepo-1.4.4-py3-none-any.whl | Bin 27387 -> 27881 bytes dbrepo-search-service/lib/dbrepo-1.4.4.tar.gz | Bin 37662 -> 38911 bytes dbrepo-search-service/os-yml/get_fields.yml | 18 +- .../os-yml/get_fuzzy_search.yml | 16 +- dbrepo-search-service/os-yml/get_index.yml | 11 +- dbrepo-search-service/os-yml/health.yml | 24 - .../os-yml/post_general_search.yml | 8 +- .../os-yml/update_database.yml | 9 +- dbrepo-ui/components/subset/Results.vue | 15 - .../TimeTravel.vue => table/TableHistory.vue} | 32 +- dbrepo-ui/components/table/TableSchema.vue | 16 +- dbrepo-ui/components/view/ViewToolbar.vue | 83 +- dbrepo-ui/composables/table-service.ts | 7 +- dbrepo-ui/locales/de-AT.json | 375 +- dbrepo-ui/locales/en-US.json | 409 +- .../[database_id]/table/[table_id]/data.vue | 16 +- .../[database_id]/table/[table_id]/schema.vue | 13 +- .../database/[database_id]/table/create.vue | 2 +- .../[database_id]/view/[view_id]/data.vue | 13 +- .../[database_id]/view/[view_id]/info.vue | 10 +- docker-compose.yml | 19 +- helm/dbrepo/.helmignore | 1 + helm/dbrepo/Makefile | 8 - helm/dbrepo/README.md | 2 + helm/{ => dbrepo}/artifacthub-repo.yml | 0 helm/dbrepo/templates/data-secret.yaml | 1 + helm/dbrepo/templates/metadata-configmap.yaml | 92 +- helm/dbrepo/templates/metadata-secret.yaml | 4 +- helm/dbrepo/values.prod.yaml | 514 - helm/dbrepo/values.yaml | 7 +- install.sh | 2 +- lib/python/Pipfile.lock | 717 +- lib/python/dbrepo/RestClient.py | 14 +- lib/python/dbrepo/api/dto.py | 71 +- lib/python/tests/test_component_user.py | 52 - lib/python/tests/test_unit_table.py | 44 +- make/gen.mk | 6 + make/rel.mk | 66 +- values.schema.json | 1459 +++ 176 files changed, 15755 insertions(+), 6147 deletions(-) rename docker-compose.prod.yml => .docker/docker-compose.yml (92%) create mode 100644 .docs/.swagger/openapi-merge.json delete mode 100644 dbrepo-analyse-service/as-yml/health.yml delete mode 100644 dbrepo-analyse-service/determine_stats.py create mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/DataDatabaseGatewayUnitTest.java create mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/InterceptorUnitTest.java create mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakSidecarGatewayUnitTest.java create mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/MetadataServiceGatewayUnitTest.java create mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/AccessServiceIntegrationTest.java create mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java create mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java create mode 100644 dbrepo-data-service/rest-service/src/test/java/at/tuwien/utils/MariaDbUtilTest.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/gateway/AnalyseServiceGateway.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/AnalyseServiceGatewayImpl.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/service/AnalyseService.java delete mode 100644 dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/AnalyseServiceImpl.java create mode 100644 dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyBriefDto.java delete mode 100644 dbrepo-search-service/os-yml/health.yml rename dbrepo-ui/components/{dialogs/TimeTravel.vue => table/TableHistory.vue} (86%) delete mode 100644 helm/dbrepo/Makefile rename helm/{ => dbrepo}/artifacthub-repo.yml (100%) delete mode 100644 helm/dbrepo/values.prod.yaml delete mode 100644 lib/python/tests/test_component_user.py create mode 100644 values.schema.json diff --git a/docker-compose.prod.yml b/.docker/docker-compose.yml similarity index 92% rename from docker-compose.prod.yml rename to .docker/docker-compose.yml index 3f24092344..d51b00551a 100644 --- a/docker-compose.prod.yml +++ b/.docker/docker-compose.yml @@ -14,7 +14,7 @@ services: restart: "no" container_name: dbrepo-metadata-db hostname: metadata-db - image: docker.io/dbrepo/metadata-db:latest + image: docker.io/dbrepo/metadata-db:1.4.4 volumes: - metadata-db-data:/bitnami/mariadb - ./dist/2_setup-data.sql:/docker-entrypoint-initdb.d/2_setup-data.sql @@ -76,7 +76,7 @@ services: restart: "no" container_name: dbrepo-auth-service hostname: auth-service - image: docker.io/dbrepo/auth-service:latest + image: docker.io/dbrepo/auth-service:1.4.4 healthcheck: test: curl -sSL 'http://0.0.0.0:8080/realms/dbrepo' | grep "dbrepo" || exit 1 interval: 10s @@ -98,18 +98,19 @@ services: restart: "no" container_name: dbrepo-metadata-service hostname: metadata-service - image: docker.io/dbrepo/metadata-service:latest + image: docker.io/dbrepo/metadata-service:1.4.4 volumes: - "${SHARED_VOLUME:-/tmp}:/tmp" environment: - ADMIN_MAIL: "${ADMIN_MAIL:-noreply@localhost}" + ADMIN_EMAIL: "${ADMIN_EMAIL:-noreply@localhost}" ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}" ADMIN_USERNAME: "${ADMIN_USERNAME:-admin}" + ANALYSE_SERVICE_ENDPOINT: "${ANALYSE_SERVICE_ENDPOINT:-http://gateway-service}" AUTH_SERVICE_ADMIN: ${AUTH_SERVICE_ADMIN:-fda} AUTH_SERVICE_ADMIN_PASSWORD: ${AUTH_SERVICE_ADMIN_PASSWORD:-fda} AUTH_SERVICE_CLIENT: ${AUTH_SERVICE_CLIENT:-dbrepo-client} AUTH_SERVICE_CLIENT_SECRET: ${AUTH_SERVICE_CLIENT:-MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG} - AUTH_SERVICE_ENDPOINT: ${AUTH_SERVICE_ENDPOINT:-http://auth-service:8080} + AUTH_SERVICE_ENDPOINT: ${AUTH_SERVICE_ENDPOINT:-http://gateway-service/api/auth} BASE_URL: "${BASE_URL:-http://localhost}" BROKER_EXCHANGE_NAME: ${BROKER_EXCHANGE_NAME:-dbrepo} BROKER_QUEUE_NAME: ${BROKER_QUEUE_NAME:-dbrepo} @@ -121,10 +122,9 @@ services: BROKER_VIRTUALHOST: "${BROKER_VIRTUALHOST:-dbrepo}" DATA_SERVICE_ENDPOINT: ${DATA_SERVICE_ENDPOINT:-http://data-service:8080} DELETED_RECORD: "${DELETED_RECORD:-persistent}" - GATEWAY_SERVICE_ENDPOINT: ${GATEWAY_SERVICE_ENDPOINT:-http://gateway-service} GRANULARITY: "${GRANULARITY:-YYYY-MM-DDThh:mm:ssZ}" JWT_PUBKEY: "${JWT_PUBKEY:-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB}" - LOG_LEVEL: ${LOG_LEVEL:-info} + LOG_LEVEL: "${LOG_LEVEL:-info}" METADATA_DB: "${METADATA_DB:-dbrepo}" METADATA_HOST: "${METADATA_HOST:-metadata-db}" METADATA_JDBC_EXTRA_ARGS: "${METADATA_JDBC_EXTRA_ARGS:-}" @@ -132,9 +132,9 @@ services: METADATA_PASSWORD: "${METADATA_PASSWORD:-dbrepo}" PID_BASE: ${PID_BASE:-http://localhost/pid/} REPOSITORY_NAME: "${REPOSITORY_NAME:-Database Repository}" - SEARCH_SERVICE_ENDPOINT: "${SEARCH_SERVICE_ENDPOINT:-http://search-service:8080}" + SEARCH_SERVICE_ENDPOINT: "${SEARCH_SERVICE_ENDPOINT:-http://gateway-service}" S3_ACCESS_KEY_ID: "${S3_ACCESS_KEY_ID:-seaweedfsadmin}" - S3_ENDPOINT: "${S3_ENDPOINT:-http://storage-service:9000}" + S3_ENDPOINT: "${S3_ENDPOINT:-http://gateway-service/api/storage}" S3_EXPORT_BUCKET: "${S3_EXPORT_BUCKET:-dbrepo-download}" S3_IMPORT_BUCKET: "${S3_IMPORT_BUCKET:-dbrepo-upload}" S3_SECRET_ACCESS_KEY: "${S3_SECRET_ACCESS_KEY:-seaweedfsadmin}" @@ -160,7 +160,7 @@ services: restart: "no" container_name: dbrepo-analyse-service hostname: analyse-service - image: docker.io/dbrepo/analyse-service:latest + image: docker.io/dbrepo/analyse-service:1.4.4 environment: ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}" ADMIN_USERNAME: "${ADMIN_USERNAME:-admin}" @@ -211,7 +211,7 @@ services: restart: "no" container_name: dbrepo-search-db hostname: search-db - image: docker.io/dbrepo/search-db:latest + image: docker.io/dbrepo/search-db:1.4.4 healthcheck: test: curl -sSL localhost:9200/_plugins/_security/health | jq .status | grep UP interval: 10s @@ -235,7 +235,7 @@ services: restart: "no" container_name: dbrepo-search-service hostname: search-service - image: docker.io/dbrepo/search-service:latest + image: docker.io/dbrepo/search-service:1.4.4 environment: ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}" ADMIN_USERNAME: "${ADMIN_USERNAME:-admin}" @@ -253,11 +253,12 @@ services: restart: "no" container_name: dbrepo-data-db-sidecar hostname: data-db-sidecar - image: docker.io/dbrepo/data-db-sidecar:latest + image: docker.io/dbrepo/data-db-sidecar:1.4.4 environment: S3_ACCESS_KEY_ID: "${S3_ACCESS_KEY_ID:-seaweedfsadmin}" S3_ENDPOINT: "${S3_ENDPOINT:-http://storage-service:9000}" S3_EXPORT_BUCKET: "${S3_EXPORT_BUCKET:-dbrepo-download}" + S3_FILE_PATH: "${S3_FILE_PATH:-/tmp}" S3_IMPORT_BUCKET: "${S3_IMPORT_BUCKET:-dbrepo-upload}" S3_SECRET_ACCESS_KEY: "${S3_SECRET_ACCESS_KEY:-seaweedfsadmin}" volumes: @@ -274,7 +275,7 @@ services: restart: "no" container_name: dbrepo-ui hostname: ui - image: docker.io/dbrepo/ui:latest + image: docker.io/dbrepo/ui:1.4.4 depends_on: dbrepo-search-service: condition: service_started @@ -318,7 +319,7 @@ services: restart: "no" container_name: dbrepo-search-service-init hostname: search-service-init - image: docker.io/dbrepo/search-service-init:latest + image: docker.io/dbrepo/search-service-init:1.4.4 environment: GATEWAY_SERVICE_ENDPOINT: ${GATEWAY_SERVICE_ENDPOINT:-http://gateway-service} OPENSEARCH_HOST: ${OPENSEARCH_HOST:-search-db} @@ -353,7 +354,7 @@ services: restart: "no" container_name: dbrepo-storage-service-init hostname: storage-service-init - image: docker.io/dbrepo/storage-service-init:latest + image: docker.io/dbrepo/storage-service-init:1.4.4 environment: SEAWEEDFS_ENDPOINT: "${STORAGE_SEAWEEDFS_ENDPOINT:-storage-service:9333}" depends_on: @@ -390,7 +391,7 @@ services: restart: "no" container_name: dbrepo-data-service hostname: data-service - image: docker.io/dbrepo/data-service:latest + image: docker.io/dbrepo/data-service:1.4.4 volumes: - "${SHARED_VOLUME:-/tmp}:/tmp" environment: @@ -411,7 +412,7 @@ services: BROKER_VIRTUALHOST: "${BROKER_VIRTUALHOST:-dbrepo}" CONNECTION_TIMEOUT: ${CONNECTION_TIMEOUT:-60000} EXCHANGE_NAME: ${EXCHANGE_NAME:-dbrepo} - GATEWAY_SERVICE_ENDPOINT: ${GATEWAY_SERVICE_ENDPOINT:-http://gateway-service} + METADATA_SERVICE_ENDPOINT: ${METADATA_SERVICE_ENDPOINT:-http://gateway-service} GRANT_DEFAULT_READ: "${GRANT_DEFAULT_READ:-SELECT}" GRANT_DEFAULT_WRITE: "${GRANT_DEFAULT_WRITE:-SELECT, CREATE, CREATE VIEW, CREATE ROUTINE, CREATE TEMPORARY TABLES, LOCK TABLES, INDEX, TRIGGER, INSERT, UPDATE, DELETE}" JWT_PUBKEY: "${JWT_PUBKEY:-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB}" @@ -421,7 +422,12 @@ services: QUEUE_NAME: ${QUEUE_NAME:-dbrepo} REQUEUE_REJECTED: ${REQUEUE_REJECTED:-false} ROUTING_KEY: "${ROUTING_KEY:-dbrepo.#}" - STORAGE_SERVICE_ENDPOINT: ${BROKER_SERVICE_ENDPOINT:-http://storage-service:9000} + S3_ACCESS_KEY_ID: "${S3_ACCESS_KEY_ID:-seaweedfsadmin}" + S3_ENDPOINT: "${S3_ENDPOINT:-http://storage-service:9000}" + S3_EXPORT_BUCKET: "${S3_EXPORT_BUCKET:-dbrepo-download}" + S3_FILE_PATH: "${S3_FILE_PATH:-/tmp}" + S3_IMPORT_BUCKET: "${S3_IMPORT_BUCKET:-dbrepo-upload}" + S3_SECRET_ACCESS_KEY: "${S3_SECRET_ACCESS_KEY:-seaweedfsadmin}" healthcheck: test: wget -qO- localhost:8080/actuator/health/readiness | grep -q "UP" || exit 1 interval: 10s diff --git a/.docs/.swagger/api.yaml b/.docs/.swagger/api.yaml index 607276e81a..7ba582ed3f 100644 --- a/.docs/.swagger/api.yaml +++ b/.docs/.swagger/api.yaml @@ -1,17 +1,4 @@ -components: - securitySchemes: - basicAuth: - in: header - scheme: basic - type: http - bearerAuth: - bearerFormat: JWT - in: header - scheme: bearer - type: http -externalDocs: - description: Sourcecode Documentation - url: https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/ +openapi: 3.0.3 info: contact: email: andreas.rauber@tuwien.ac.at @@ -19,67 +6,142 @@ info: description: The REST API license: name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' title: DBRepo REST API version: 1.4.4 -openapi: 3.1.0 servers: - description: Test Instance - url: https://test.dbrepo.tuwien.ac.at + url: 'https://test.dbrepo.tuwien.ac.at' - description: Local Instance - url: http://localhost + url: 'http://localhost' +externalDocs: + description: Sourcecode Documentation + url: 'https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/' paths: - /api/database: - post: - tags: - - database-endpoint - summary: Create database - operationId: create - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/CreateDatabaseDto' - required: true + /api/analyse/datatypes: + get: + consumes: + - application/json + description: This is a simple API which returns the datatypes of a (path) csv file + operationId: analyse_datatypes + parameters: + - example: filename_s3_key + in: query + name: filename + required: true + schema: + type: string + - example: ',' + in: query + name: separator + required: true + schema: + type: string + - example: 'false' + in: query + name: enum + required: false + schema: + type: boolean + - example: '2.5' + in: query + name: enum_tol + required: false + schema: + type: float + produces: + - application/json responses: - "201": - description: Created a database + '202': content: application/json: schema: - $ref: '#/components/schemas/DatabaseDto' - "400": - description: Database create query is malformed or image is not supported + $ref: '#/components/schemas/DataTypesDto' + description: Determined data types successfully + '400': content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Failed to create query store in database + $ref: '#/components/schemas/ErrorDto' + description: Failed to determine data types + '404': content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to communicate with database + $ref: '#/components/schemas/ErrorDto' + description: Failed to find file in Storage Service + '500': content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find container in metadata database + $ref: '#/components/schemas/ErrorDto' + description: Unexpected system error + security: + - bearerAuth: [] + - basicAuth: [] + summary: Determine datatypes + tags: + - analyse-endpoint + /api/analyse/keys: + get: + consumes: + - application/json + description: >- + This is a simple API which returns the primary keys + ranking of a + (path) csv file + operationId: analyse_keys + parameters: + - example: filename_s3_key + in: query + name: filename + required: true + schema: + type: string + - example: ',' + in: query + name: separator + required: true + schema: + type: string + produces: + - application/json + responses: + '202': content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' + $ref: '#/components/schemas/KeysDto' + description: Determined keys successfully + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' + description: Failed to determine keys + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' + description: Failed to find file in Storage Service or is empty + '500': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDto' + description: Unexpected system error security: + - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/access/{userId}: - put: + summary: Determine primary keys tags: - - access-endpoint - summary: Update access to some database - operationId: update_1 + - analyse-endpoint + '/api/database/{databaseId}/view/{viewId}/data': + get: + tags: + - view-endpoint + summary: Retrieve view data + operationId: getData parameters: - name: databaseId in: path @@ -87,58 +149,75 @@ paths: schema: type: integer format: int64 - - name: userId + - name: viewId in: path required: true + schema: + type: integer + format: int64 + - name: page + in: query + required: false + schema: + type: integer + format: int64 + - name: size + in: query + required: false + schema: + type: integer + format: int64 + - name: timestamp + in: query + required: false schema: type: string - format: uuid - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateDatabaseAccessDto' - required: true + format: date-time responses: - "400": - description: Update access query or database connection is malformed + '200': + description: Retrieved view data + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResultDto' + '400': + description: Request pagination is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Failed to update access in database + '403': + description: Not allowed to retrieve view data content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with metadata service + '404': + description: Failed to find view in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to update access + '409': + description: View schema could not be mapped content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database/user in metadata database + '503': + description: Failed to establish connection with the metadata service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Update access succeeded security: - basicAuth: [] - post: + - bearerAuth: [] + head: tags: - - access-endpoint - summary: Give access to some database - operationId: create_4 + - view-endpoint + summary: Retrieve view data + operationId: getData_1 parameters: - name: databaseId in: path @@ -146,62 +225,76 @@ paths: schema: type: integer format: int64 - - name: userId + - name: viewId in: path required: true + schema: + type: integer + format: int64 + - name: page + in: query + required: false + schema: + type: integer + format: int64 + - name: size + in: query + required: false + schema: + type: integer + format: int64 + - name: timestamp + in: query + required: false schema: type: string - format: uuid - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateDatabaseAccessDto' - required: true + format: date-time responses: - "417": - description: Failed to give access in the database + '200': + description: Retrieved view data content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Granting access succeeded + $ref: '#/components/schemas/QueryResultDto' + '400': + description: Request pagination is malformed content: - '*/*': + application/json: schema: - type: object - "503": - description: Failed to establish connection to metadata service + $ref: '#/components/schemas/ApiErrorDto' + '403': + description: Not allowed to retrieve view data content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to give access + '404': + description: Failed to find view in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database/user in metadata database + '409': + description: View schema could not be mapped content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Granting access query or database connection is malformed + '503': + description: Failed to establish connection with the metadata service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - basicAuth: [] - delete: + - bearerAuth: [] + '/api/database/{databaseId}/table/{tableId}/data': + get: tags: - - access-endpoint - summary: Revoke access to some database - operationId: revoke + - table-endpoint + summary: Retrieve table data + operationId: getData_2 parameters: - name: databaseId in: path @@ -209,297 +302,442 @@ paths: schema: type: integer format: int64 - - name: userId + - name: tableId in: path required: true + schema: + type: integer + format: int64 + - name: timestamp + in: query + required: false schema: type: string - format: uuid + format: date-time + - name: page + in: query + required: false + schema: + type: integer + format: int64 + - name: size + in: query + required: false + schema: + type: integer + format: int64 responses: - "400": - description: Revoke access query or database connection is malformed + '200': + description: Retrieved table data content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Failed to revoke access in database + $ref: '#/components/schemas/QueryResultDto' + '400': + description: Request pagination or table data select query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to revoke access + '404': + description: Failed to find table in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": + '503': description: Failed to establish connection with the metadata service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database/user in metadata database - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Revoked access successfully - content: - '*/*': - schema: - type: object security: - basicAuth: [] - /api/user/{userId}: - get: + - bearerAuth: [] + put: tags: - - user-endpoint - summary: Get a user info - operationId: find_2 + - table-endpoint + summary: Update a raw data tuple + description: >- + Updates a raw data tuple in a table with at least WRITE_OWN access. Then + update the table statistics. + operationId: updateRawTuple parameters: - - name: userId + - name: databaseId in: path required: true schema: - type: string - format: uuid + type: integer + format: int64 + - name: tableId + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TupleUpdateDto' + required: true responses: - "403": - description: Find user is not permitted + '202': + description: Updated table data + '400': + description: Request pagination or table data select query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found user + '403': + description: Update table data not allowed content: application/json: schema: - $ref: '#/components/schemas/UserDto' - "404": - description: User was not found + $ref: '#/components/schemas/ApiErrorDto' + '404': + description: Failed to find table in metadata database + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to establish connection with the metadata service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - bearerAuth: [] - basicAuth: [] - put: + - bearerAuth: [] + post: tags: - - user-endpoint - summary: Modify user information - operationId: modify + - table-endpoint + summary: Insert a raw data tuple + description: >- + Inserts a raw data tuple into a table with at least WRITE_OWN access. + Then update the table statistics. + operationId: insertRawTuple parameters: - - name: userId + - name: databaseId in: path required: true schema: - type: string - format: uuid + type: integer + format: int64 + - name: tableId + in: path + required: true + schema: + type: integer + format: int64 requestBody: content: application/json: schema: - $ref: '#/components/schemas/UserUpdateDto' + $ref: '#/components/schemas/TupleDto' required: true responses: - "404": - description: Failed to find database/user in metadata database + '201': + description: Created table data + '400': + description: Request pagination or table data select query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to modify user metadata + '403': + description: Create table data not allowed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Modify user query is malformed + '404': + description: Failed to find table in metadata database or blob in storage service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Modified user information + '503': + description: >- + Failed to establish connection with the metadata service or storage + service content: application/json: schema: - $ref: '#/components/schemas/UserDto' + $ref: '#/components/schemas/ApiErrorDto' security: - - bearerAuth: [] - basicAuth: [] - /api/user/{userId}/password: - put: + - bearerAuth: [] + delete: tags: - - user-endpoint - summary: Modify user password - operationId: password + - table-endpoint + summary: Delete table data + description: >- + Deletes a raw data tuple in a table with at least WRITE_OWN access. Then + update the table statistics. + operationId: deleteRawTuple parameters: - - name: userId + - name: databaseId in: path required: true schema: - type: string - format: uuid + type: integer + format: int64 + - name: tableId + in: path + required: true + schema: + type: integer + format: int64 requestBody: content: application/json: schema: - $ref: '#/components/schemas/UserPasswordDto' + $ref: '#/components/schemas/TupleDeleteDto' required: true responses: - "403": - description: Not allowed to change foreign user password + '202': + description: Deleted table data + '400': + description: Request pagination or table data select query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database/user in metadata database + '403': + description: Delete table data not allowed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to get user in auth service + '404': + description: Failed to find table in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Modified user password - content: - application/json: - schema: - $ref: '#/components/schemas/UserDto' - "502": - description: Connection to auth service failed + '503': + description: Failed to establish connection with the metadata service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - bearerAuth: [] - basicAuth: [] - /api/user/token: - put: + - bearerAuth: [] + head: tags: - - user-endpoint - summary: Refresh user token - operationId: refreshToken - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/RefreshTokenRequestDto' - required: true + - table-endpoint + summary: Retrieve table data + operationId: getData_3 + parameters: + - name: databaseId + in: path + required: true + schema: + type: integer + format: int64 + - name: tableId + in: path + required: true + schema: + type: integer + format: int64 + - name: timestamp + in: query + required: false + schema: + type: string + format: date-time + - name: page + in: query + required: false + schema: + type: integer + format: int64 + - name: size + in: query + required: false + schema: + type: integer + format: int64 responses: - "202": - description: Refreshed user token + '200': + description: Retrieved table data content: application/json: schema: - $ref: '#/components/schemas/TokenDto' - "502": - description: Connection to auth service failed + $ref: '#/components/schemas/QueryResultDto' + '400': + description: Request pagination or table data select query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Invalid refresh token + '404': + description: Failed to find table in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - post: - tags: - - user-endpoint - summary: Obtain user token - operationId: getToken - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/LoginRequestDto' - required: true - responses: - "404": - description: Failed to find user in auth database + '503': + description: Failed to establish connection with the metadata service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to get user in auth service + security: + - basicAuth: [] + - bearerAuth: [] + '/api/database/{databaseId}/subset/{subsetId}/data': + get: + tags: + - subset-endpoint + summary: Retrieved subset data + operationId: getData_4 + parameters: + - name: databaseId + in: path + required: true + schema: + type: integer + format: int64 + - name: subsetId + in: path + required: true + schema: + type: integer + format: int64 + - name: page + in: query + required: false + schema: + type: integer + format: int64 + - name: size + in: query + required: false + schema: + type: integer + format: int64 + responses: + '200': + description: Retrieved subset data content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Obtained user token + $ref: '#/components/schemas/QueryResultDto' + '400': + description: Malformed select query content: application/json: schema: - $ref: '#/components/schemas/TokenDto' - "403": - description: Not allowed to get token + $ref: '#/components/schemas/ApiErrorDto' + '403': + description: Not allowed to retrieve subset data content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "428": - description: Account is not fully setup in auth service (requires password change?) + '404': + description: >- + Failed to find database in metadata database or query in query store + of the data database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to auth service failed + '503': + description: Failed to communicate with database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - /api/ontology/{ontologyId}: - get: + security: + - bearerAuth: [] + - basicAuth: [] + head: tags: - - ontology-endpoint - summary: Find one ontology - operationId: find_3 + - subset-endpoint + summary: Retrieved subset data + operationId: getData_5 parameters: - - name: ontologyId + - name: databaseId + in: path + required: true + schema: + type: integer + format: int64 + - name: subsetId in: path required: true schema: type: integer format: int64 + - name: page + in: query + required: false + schema: + type: integer + format: int64 + - name: size + in: query + required: false + schema: + type: integer + format: int64 responses: - "404": - description: Could not find ontology + '200': + description: Retrieved subset data + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResultDto' + '400': + description: Malformed select query content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Find one ontology + '403': + description: Not allowed to retrieve subset data content: application/json: schema: - $ref: '#/components/schemas/OntologyDto' + $ref: '#/components/schemas/ApiErrorDto' + '404': + description: >- + Failed to find database in metadata database or query in query store + of the data database + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to communicate with database + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + security: + - bearerAuth: [] + - basicAuth: [] + '/api/database/{databaseId}/subset/{queryId}': put: tags: - - ontology-endpoint - summary: Update an ontology - operationId: update + - subset-endpoint + summary: Persist subset + operationId: persist parameters: - - name: ontologyId + - name: databaseId + in: path + required: true + schema: + type: integer + format: int64 + - name: queryId in: path required: true schema: @@ -509,58 +747,67 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OntologyModifyDto' + $ref: '#/components/schemas/QueryPersistDto' required: true responses: - "404": - description: Could not find ontology + '202': + description: Persisted subset + content: + application/json: + schema: + $ref: '#/components/schemas/QueryDto' + '400': + description: Malformed select query + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '403': + description: Not allowed to persist subset + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '404': + description: >- + Failed to find database in metadata database or query in query store + of the data database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Updated ontology successfully + '417': + description: Failed to persist subset content: application/json: schema: - $ref: '#/components/schemas/OntologyDto' + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to communicate with database + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] - delete: + '/api/database/{databaseId}/table/{tableId}/data/import': + post: tags: - - ontology-endpoint - summary: Delete an ontology - operationId: delete + - table-endpoint + summary: Import data from a dataset + description: >- + Deletes a raw data tuple in a table with at least WRITE_OWN access. Then + update the table statistics. + operationId: importDataset parameters: - - name: ontologyId + - name: databaseId in: path required: true schema: type: integer format: int64 - responses: - "404": - description: Could not find ontology - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Deleted ontology successfully - content: - application/json: {} - security: - - bearerAuth: [] - - basicAuth: [] - /api/message/{messageId}: - put: - tags: - - message-endpoint - summary: Update maintenance message - operationId: update_1 - parameters: - - name: messageId + - name: tableId in: path required: true schema: @@ -570,377 +817,476 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BannerMessageUpdateDto' + $ref: '#/components/schemas/ImportCsvDto' required: true responses: - "202": - description: Updated message + '202': + description: Imported dataset successfully + '400': + description: Dataset query is malformed content: application/json: schema: - $ref: '#/components/schemas/BannerMessageBriefDto' - "404": - description: Could not find message + $ref: '#/components/schemas/ApiErrorDto' + '403': + description: Import table dataset not allowed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - security: - - bearerAuth: [] - - basicAuth: [] - delete: - tags: - - message-endpoint - summary: Delete maintenance message - operationId: delete_1 - parameters: - - name: messageId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "202": - description: Deleted message + '404': + description: Failed to find table in metadata database content: - application/json: {} - "404": - description: Could not find message + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to establish connection with the metadata service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - bearerAuth: [] - basicAuth: [] - /api/image/{imageId}: + - bearerAuth: [] + '/api/database/{databaseId}/subset': get: tags: - - image-endpoint - summary: Find some image - operationId: findById + - subset-endpoint + summary: Find subsets + operationId: list parameters: - - name: imageId + - name: databaseId in: path required: true schema: type: integer format: int64 + - name: persisted + in: query + required: false + schema: + type: boolean responses: - "404": - description: Image could not be found + '200': + description: Found subsets + content: + application/json: + schema: + type: string + '403': + description: Not allowed to find subsets content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found image + '404': + description: >- + Failed to find database in metadata database or query in query store + of the data database content: application/json: schema: - $ref: '#/components/schemas/ImageDto' - put: + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to communicate with database + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + security: + - basicAuth: [] + - bearerAuth: [] + post: tags: - - image-endpoint - summary: Update some image - operationId: update_2 + - subset-endpoint + summary: Create subset + operationId: create parameters: - - name: imageId + - name: databaseId in: path required: true schema: type: integer format: int64 + - name: page + in: query + required: false + schema: + type: integer + format: int64 + - name: size + in: query + required: false + schema: + type: integer + format: int64 + - name: timestamp + in: query + required: false + schema: + type: string + format: date-time requestBody: content: application/json: schema: - $ref: '#/components/schemas/ImageChangeDto' + $ref: '#/components/schemas/ExecuteStatementDto' required: true responses: - "404": - description: Image could not be found + '201': + description: Created subset + content: + application/json: + schema: + $ref: '#/components/schemas/QueryResultDto' + '400': + description: Malformed select query content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Updated image successfully + '403': + description: Not allowed to find subset content: application/json: schema: - $ref: '#/components/schemas/ImageDto' - security: - - bearerAuth: [] - - basicAuth: [] - delete: - tags: - - image-endpoint - summary: Delete some image - operationId: delete_2 - parameters: - - name: imageId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "202": - description: Deleted image successfully - "404": - description: Image could not be found + $ref: '#/components/schemas/ApiErrorDto' + '404': + description: >- + Failed to find database in metadata database or query in query store + of the data database + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '417': + description: Failed to insert query into query store of data database + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '501': + description: Failed to execute query as it contains non-supported keywords + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to communicate with database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - bearerAuth: [] - basicAuth: [] - /api/identifier/{identifierId}: + - bearerAuth: [] + '/api/database/{databaseId}/table/{tableId}/statistic': get: tags: - - identifier-endpoint - summary: Find some identifier - operationId: find_6 + - table-endpoint + summary: Generate table statistic + operationId: statistic parameters: - - name: identifierId + - name: databaseId in: path required: true schema: type: integer format: int64 - - name: Accept - in: header + - name: tableId + in: path required: true schema: - type: string + type: integer + format: int64 responses: - "406": - description: Failed to find acceptable representation + '200': + description: Generated table statistic content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Failed to retrieve from database sidecar + $ref: '#/components/schemas/TableStatisticDto' + '400': + description: Failed to obtain column statistic content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to find in data service + '404': + description: Failed to find table in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found identifier successfully + '503': + description: Failed to establish connection with the metadata service content: application/json: schema: - $ref: '#/components/schemas/IdentifierDto' - application/ld+json: - schema: - $ref: '#/components/schemas/LdDatasetDto' - text/csv: {} - text/xml: {} - text/bibliography: {} - text/bibliography; style=apa: {} - text/bibliography; style=ieee: {} - text/bibliography; style=bibtex: {} - "404": - description: Identifier could not be found + $ref: '#/components/schemas/ApiErrorDto' + '/api/database/{databaseId}/table/{tableId}/history': + get: + tags: + - table-endpoint + summary: Find table history + description: >- + Lists the insert/delete operations performed. Authentication is only + required for tables in private databases + operationId: getHistory + parameters: + - name: databaseId + in: path + required: true + schema: + type: integer + format: int64 + - name: tableId + in: path + required: true + schema: + type: integer + format: int64 + - name: size + in: query + required: false + schema: + type: integer + format: int64 + responses: + '200': + description: Found table history content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to data service failed + type: string + '400': + description: Invalid pagination request content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: "Identifier could not be exported, the requested style is not known" + '403': + description: Find table history not allowed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "410": - description: Failed to retrieve from S3 endpoint + '404': + description: Failed to find table history in data database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "409": - description: Exported resource was not found + '503': + description: Failed to establish connection with the metadata service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - put: + security: + - basicAuth: [] + - bearerAuth: [] + '/api/database/{databaseId}/table/{tableId}/export': + get: tags: - - identifier-endpoint - summary: Save identifier - operationId: save + - table-endpoint + summary: Export table data + operationId: exportData parameters: - - name: identifierId + - name: databaseId in: path required: true schema: type: integer format: int64 - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/IdentifierSaveDto' - required: true + - name: tableId + in: path + required: true + schema: + type: integer + format: int64 + - name: timestamp + in: query + required: false + schema: + type: string + format: date-time responses: - "403": - description: Insufficient access rights or authorities - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to search service failed + '200': + description: Exported table data content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Creating identifier not permitted + type: string + format: binary + '400': + description: Request pagination or table data select query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Identifier form contains invalid request data + '403': + description: Export table data not allowed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Failed to find database, table or view" + '404': + description: Failed to find table in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Saved identifier - content: - application/json: - schema: - $ref: '#/components/schemas/IdentifierDto' - "503": - description: Failed to save in search service + '503': + description: Failed to establish connection with the metadata service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - bearerAuth: [] - basicAuth: [] - delete: + - bearerAuth: [] + '/api/database/{databaseId}/subset/{subsetId}': + get: tags: - - identifier-endpoint - summary: Delete some identifier - operationId: delete_3 + - subset-endpoint + summary: Find subset + operationId: findById parameters: - - name: identifierId + - name: databaseId + in: path + required: true + schema: + type: integer + format: int64 + - name: subsetId in: path required: true schema: type: integer format: int64 + - name: timestamp + in: query + required: false + schema: + type: string + format: date-time responses: - "502": - description: Connection to search service failed + '200': + description: Found subset + content: + application/json: + schema: + $ref: '#/components/schemas/QueryDto' + text/csv: {} + '400': + description: Malformed select query content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Deleting identifier not permitted + '403': + description: Not allowed to find subset content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to delete in search service + '404': + description: >- + Failed to find database in metadata database or query in query store + of the data database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Deleted identifier + '406': + description: Failed to find acceptable representation content: - '*/*': + application/json: schema: - type: object - "404": - description: Identifier or database could not be found + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to communicate with database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - bearerAuth: [] - basicAuth: [] - /api/identifier/{identifierId}/publish: - put: + - bearerAuth: [] + /api/database: + get: tags: - - identifier-endpoint - summary: Publish identifier - operationId: publish + - database-endpoint + summary: List databases + operationId: list1 parameters: - - name: identifierId - in: path - required: true + - name: internal_name + in: query + required: false schema: - type: integer - format: int64 + type: string responses: - "403": - description: Insufficient access rights or authorities + '200': + description: List of databases content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to search service failed + type: array + items: + $ref: '#/components/schemas/DatabaseDto' + post: + tags: + - database-endpoint + summary: Create database + operationId: create_5 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseCreateDto' + required: true + responses: + '201': + description: Created a new database + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseDto' + '400': + description: Database create query is malformed or image is not supported content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": - description: Creating identifier not permitted + '403': + description: >- + Database create permission is missing or grant permissions at broker + service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Identifier form contains invalid request data + '404': + description: Failed to fin container/user/database in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Failed to find database, table or view" + '409': + description: Query store could not be created content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Published identifier + '502': + description: Connection to search service failed content: application/json: schema: - $ref: '#/components/schemas/IdentifierDto' - "503": + $ref: '#/components/schemas/ApiErrorDto' + '503': description: Failed to save in search service content: application/json: @@ -949,12 +1295,32 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/visibility: - put: + head: tags: - database-endpoint - summary: Update database visibility - operationId: visibility + summary: List databases + operationId: list_1 + parameters: + - name: internal_name + in: query + required: false + schema: + type: string + responses: + '200': + description: List of databases + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DatabaseDto' + '/api/database/{databaseId}/access/{userId}': + get: + tags: + - access-endpoint + summary: Check access to some database + operationId: find parameters: - name: databaseId in: path @@ -962,39 +1328,27 @@ paths: schema: type: integer format: int64 - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/DatabaseModifyVisibilityDto' - required: true + - name: userId + in: path + required: true + schema: + type: string + format: uuid responses: - "202": - description: Visibility modified successfully - content: - application/json: - schema: - $ref: '#/components/schemas/DatabaseDto' - "403": - description: Visibility modification is not permitted - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to search service failed + '200': + description: Found database access content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database in metadata database + $ref: '#/components/schemas/DatabaseAccessDto' + '403': + description: No access to this database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to save in search service + '404': + description: Database not found content: application/json: schema: @@ -1002,12 +1356,11 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/table/{tableId}: - delete: + put: tags: - - table-endpoint - summary: Delete table - operationId: delete_1 + - access-endpoint + summary: Modify access to some database + operationId: update_4 parameters: - name: databaseId in: path @@ -1015,45 +1368,63 @@ paths: schema: type: integer format: int64 - - name: tableId + - name: userId in: path required: true schema: - type: integer - format: int64 + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateDatabaseAccessDto' + required: true responses: - "404": - description: Failed to find table in metadata database + '202': + description: Modify access succeeded + '400': + description: Modify access query or database connection is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Deleted table + '403': + description: >- + Modify access not permitted when no access is granted in the first + place content: application/json: schema: - $ref: '#/components/schemas/DatabaseDto' - "503": - description: Failed to establish connection with the metadata service + $ref: '#/components/schemas/ApiErrorDto' + '404': + description: Database or user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: >- + Access could not be updated due to connection error in the data + service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Deletion query is malformed + '503': + description: Access could not be updated in the data service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: + - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/table/{tableId}/column/{columnId}: - put: + post: tags: - - table-endpoint - summary: Update a table column semantic mapping - operationId: update_3 + - access-endpoint + summary: Give access to some database + operationId: create_8 parameters: - name: databaseId in: path @@ -1061,57 +1432,53 @@ paths: schema: type: integer format: int64 - - name: tableId - in: path - required: true - schema: - type: integer - format: int64 - - name: columnId + - name: userId in: path required: true schema: - type: integer - format: int64 + type: string + format: uuid requestBody: content: application/json: schema: - $ref: '#/components/schemas/ColumnSemanticsUpdateDto' + $ref: '#/components/schemas/UpdateDatabaseAccessDto' required: true responses: - "400": - description: Update semantic concept query is malformed or update unit of measurement query is malformed + '202': + description: Granting access succeeded + '400': + description: Granting access query or database connection is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to search service failed + '403': + description: Failed giving access content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Access to the database is forbidden + '404': + description: Database or user not found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Updated column semantics successfully + '405': + description: Granting access not permitted content: application/json: schema: - $ref: '#/components/schemas/ColumnDto' - "404": - description: Failed to find user/table/database/ontology in metadata database + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: Access could not be created due to connection error content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to save in search service + '503': + description: Access could not be created in the data service content: application/json: schema: @@ -1119,12 +1486,11 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/owner: - put: + delete: tags: - - database-endpoint - summary: Update database owner - operationId: transfer + - access-endpoint + summary: Revoke access to some database + operationId: revoke parameters: - name: databaseId in: path @@ -1132,39 +1498,41 @@ paths: schema: type: integer format: int64 - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/DatabaseTransferDto' - required: true + - name: userId + in: path + required: true + schema: + type: string + format: uuid responses: - "403": - description: Transfer of ownership is not permitted + '202': + description: Revoked access successfully + '400': + description: Modify access query or database connection is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Transfer of ownership was successful + '403': + description: Revoke of access not permitted as no access was found content: application/json: schema: - $ref: '#/components/schemas/DatabaseDto' - "404": - description: Database or user could not be found + $ref: '#/components/schemas/ApiErrorDto' + '404': + description: 'User, database with access was not found' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to search service failed + '502': + description: Access could not be created due to connection error content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to save in search service + '503': + description: Access could not be revoked in the data service content: application/json: schema: @@ -1172,12 +1540,11 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/metadata/view: - put: + head: tags: - - database-endpoint - summary: Refresh database views metadata - operationId: refreshViewMetadata + - access-endpoint + summary: Check access to some database + operationId: find_1 parameters: - name: databaseId in: path @@ -1185,33 +1552,62 @@ paths: schema: type: integer format: int64 + - name: userId + in: path + required: true + schema: + type: string + format: uuid responses: - "403": - description: Refresh view metadata is not permitted + '200': + description: Found database access + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseAccessDto' + '403': + description: No access to this database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to search service failed + '404': + description: Database not found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Refreshed database views metadata + security: + - bearerAuth: [] + - basicAuth: [] + '/api/user/{userId}': + get: + tags: + - user-endpoint + summary: Get a user info + operationId: find_2 + parameters: + - name: userId + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Found user content: application/json: schema: - $ref: '#/components/schemas/DatabaseDto' - "404": - description: Failed to find database in metadata database + $ref: '#/components/schemas/UserDto' + '403': + description: Find user is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to save in search service + '404': + description: User was not found content: application/json: schema: @@ -1219,52 +1615,45 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/metadata/table: put: tags: - - database-endpoint - summary: Refresh database tables metadata - operationId: refreshTableMetadata + - user-endpoint + summary: Modify user information + operationId: modify parameters: - - name: databaseId + - name: userId in: path required: true schema: - type: integer - format: int64 + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserUpdateDto' + required: true responses: - "200": - description: Refreshed database tables metadata - content: - application/json: - schema: - $ref: '#/components/schemas/DatabaseDto' - "400": - description: Failed to parse payload at search service - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to refresh table metadata + '202': + description: Modified user information content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to search service failed + $ref: '#/components/schemas/UserDto' + '400': + description: Modify user query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to fin user/database in metadata database + '403': + description: Not allowed to modify user metadata content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to save in search service + '404': + description: Failed to find database/user in metadata database content: application/json: schema: @@ -1272,58 +1661,52 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/image: + '/api/user/{userId}/password': put: tags: - - database-endpoint - summary: Update database image - operationId: modifyImage + - user-endpoint + summary: Modify user password + operationId: password parameters: - - name: databaseId + - name: userId in: path required: true schema: - type: integer - format: int64 + type: string + format: uuid requestBody: content: application/json: schema: - $ref: '#/components/schemas/DatabaseModifyImageDto' + $ref: '#/components/schemas/UserPasswordDto' required: true responses: - "404": - description: Database or user could not be found + '202': + description: Modified user password content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to search service failed + $ref: '#/components/schemas/UserDto' + '403': + description: Not allowed to change foreign user password content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Modify of image was successful - content: - application/json: - schema: - $ref: '#/components/schemas/DatabaseDto' - "410": - description: File was not found in the Storage Service + '404': + description: Failed to find database/user in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Modify of image is not permitted + '502': + description: Connection to auth service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to save in search service + '503': + description: Failed to get user in auth service content: application/json: schema: @@ -1331,192 +1714,311 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/user: - get: + /api/user/token: + put: tags: - user-endpoint - summary: Find all users - operationId: findAll + summary: Refresh user token + operationId: refreshToken + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RefreshTokenRequestDto' + required: true responses: - "200": - description: List users + '202': + description: Refreshed user token content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/UserBriefDto' + $ref: '#/components/schemas/TokenDto' + '403': + description: Invalid refresh token + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: Connection to auth service failed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' post: tags: - user-endpoint - summary: Create user - operationId: create + summary: Obtain user token + operationId: getToken requestBody: content: application/json: schema: - $ref: '#/components/schemas/SignupRequestDto' + $ref: '#/components/schemas/LoginRequestDto' required: true responses: - "503": - description: Failed to create in auth service + '202': + description: Obtained user token content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "417": - description: User with e-mail already exists + $ref: '#/components/schemas/TokenDto' + '403': + description: Not allowed to get token content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Failed to create in auth service + '404': + description: Failed to find user in auth database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: default role not found + '428': + description: >- + Account is not fully setup in auth service (requires password + change?) content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Parameters are not well-formed (likely email) - content: - application/json: {} - "201": - description: Created user + '502': + description: Connection to auth service failed content: application/json: schema: - $ref: '#/components/schemas/UserBriefDto' - "409": - description: User with username already exists + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to get user in auth service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - /api/ontology: + '/api/ontology/{ontologyId}': get: tags: - ontology-endpoint - summary: List all ontologies - operationId: findAll_2 + summary: Find one ontology + operationId: find_3 + parameters: + - name: ontologyId + in: path + required: true + schema: + type: integer + format: int64 responses: - "200": - description: List all ontologies + '200': + description: Find one ontology content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/OntologyDto' - post: + $ref: '#/components/schemas/OntologyDto' + '404': + description: Could not find ontology + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + put: tags: - ontology-endpoint - summary: Register a new ontology - operationId: create_1 + summary: Update an ontology + operationId: update + parameters: + - name: ontologyId + in: path + required: true + schema: + type: integer + format: int64 requestBody: content: application/json: schema: - $ref: '#/components/schemas/OntologyCreateDto' + $ref: '#/components/schemas/OntologyModifyDto' required: true responses: - "201": - description: Registered ontology successfully + '202': + description: Updated ontology successfully content: application/json: schema: $ref: '#/components/schemas/OntologyDto' + '404': + description: Could not find ontology + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] - /api/message: - get: + delete: tags: - - message-endpoint - summary: Find maintenance messages - operationId: list_2 + - ontology-endpoint + summary: Delete an ontology + operationId: delete parameters: - - name: filter - in: query - required: false + - name: ontologyId + in: path + required: true schema: - type: string + type: integer + format: int64 responses: - "200": - description: List messages + '202': + description: Deleted ontology successfully + content: + application/json: {} + '404': + description: Could not find ontology content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/BannerMessageDto' - post: + $ref: '#/components/schemas/ApiErrorDto' + security: + - bearerAuth: [] + - basicAuth: [] + '/api/message/{messageId}': + put: tags: - message-endpoint - summary: Create maintenance message - operationId: create_2 + summary: Update maintenance message + operationId: update_1 + parameters: + - name: messageId + in: path + required: true + schema: + type: integer + format: int64 requestBody: content: application/json: schema: - $ref: '#/components/schemas/BannerMessageCreateDto' + $ref: '#/components/schemas/BannerMessageUpdateDto' required: true responses: - "201": - description: Created message + '202': + description: Updated message content: application/json: schema: $ref: '#/components/schemas/BannerMessageBriefDto' + '404': + description: Could not find message + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] - /api/image: + delete: + tags: + - message-endpoint + summary: Delete maintenance message + operationId: delete_1 + parameters: + - name: messageId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '202': + description: Deleted message + content: + application/json: {} + '404': + description: Could not find message + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + security: + - bearerAuth: [] + - basicAuth: [] + '/api/image/{imageId}': get: tags: - image-endpoint - summary: Find all images - operationId: findAll_3 + summary: Find some image + operationId: findById1 + parameters: + - name: imageId + in: path + required: true + schema: + type: integer + format: int64 responses: - "200": - description: List images + '200': + description: Found image content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/ContainerImage' - post: + $ref: '#/components/schemas/ImageDto' + '404': + description: Image could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + put: tags: - image-endpoint - summary: Create image - operationId: create_3 + summary: Update some image + operationId: update_2 + parameters: + - name: imageId + in: path + required: true + schema: + type: integer + format: int64 requestBody: content: application/json: schema: - $ref: '#/components/schemas/ImageCreateDto' + $ref: '#/components/schemas/ImageChangeDto' required: true responses: - "201": - description: Created image + '202': + description: Updated image successfully content: application/json: schema: $ref: '#/components/schemas/ImageDto' - "400": - description: Image specification is invalid + '404': + description: Image could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "409": - description: Image already exists + security: + - bearerAuth: [] + - basicAuth: [] + delete: + tags: + - image-endpoint + summary: Delete some image + operationId: delete_2 + parameters: + - name: imageId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '202': + description: Deleted image successfully + '404': + description: Image could not be found content: application/json: schema: @@ -1524,34 +2026,16 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/identifier: + '/api/identifier/{identifierId}': get: tags: - identifier-endpoint - summary: Find all identifiers - operationId: findAll_4 + summary: Find some identifier + operationId: find_6 parameters: - - name: dbid - in: query - required: false - schema: - type: integer - format: int64 - - name: qid - in: query - required: false - schema: - type: integer - format: int64 - - name: vid - in: query - required: false - schema: - type: integer - format: int64 - - name: tid - in: query - required: false + - name: identifierId + in: path + required: true schema: type: integer format: int64 @@ -1561,71 +2045,172 @@ paths: schema: type: string responses: - "200": - description: Found identifiers successfully + '200': + description: Found identifier successfully content: application/json: schema: - type: string + $ref: '#/components/schemas/IdentifierDto' application/ld+json: schema: - type: string - "406": - description: "Identifier could not be exported, the requested style is not known" + $ref: '#/components/schemas/LdDatasetDto' + text/csv: {} + text/xml: {} + text/bibliography: {} + text/bibliography; style=apa: {} + text/bibliography; style=ieee: {} + text/bibliography; style=bibtex: {} + '400': + description: 'Identifier could not be exported, the requested style is not known' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - post: + '404': + description: Identifier could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '406': + description: Failed to find acceptable representation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '409': + description: Exported resource was not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '410': + description: Failed to retrieve from S3 endpoint + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '422': + description: Failed to retrieve from database sidecar + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: Connection to data service failed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to find in data service + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + put: tags: - identifier-endpoint - summary: Draft identifier - operationId: create_4 + summary: Save identifier + operationId: save + parameters: + - name: identifierId + in: path + required: true + schema: + type: integer + format: int64 requestBody: content: application/json: schema: - $ref: '#/components/schemas/IdentifierCreateDto' + $ref: '#/components/schemas/IdentifierSaveDto' required: true responses: - "403": + '202': + description: Saved identifier + content: + application/json: + schema: + $ref: '#/components/schemas/IdentifierDto' + '400': + description: Identifier form contains invalid request data + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '403': description: Insufficient access rights or authorities content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "502": - description: Connection to search service failed + '404': + description: 'Failed to find database, table or view' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "405": + '405': description: Creating identifier not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Identifier form contains invalid request data + '502': + description: Connection to search service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: "Failed to find database, table or view" + '503': + description: Failed to save in search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Drafted identifier + security: + - bearerAuth: [] + - basicAuth: [] + delete: + tags: + - identifier-endpoint + summary: Delete some identifier + operationId: delete_3 + parameters: + - name: identifierId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '202': + description: Deleted identifier + content: + '*/*': + schema: + type: object + '403': + description: Deleting identifier not permitted content: application/json: schema: - $ref: '#/components/schemas/IdentifierDto' - "503": - description: Failed to save in search service + $ref: '#/components/schemas/ApiErrorDto' + '404': + description: Identifier or database could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: Connection to search service failed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to delete in search service content: application/json: schema: @@ -1633,61 +2218,71 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/view: - get: + '/api/identifier/{identifierId}/publish': + put: tags: - - view-endpoint - summary: Find view schemas - operationId: getSchema + - identifier-endpoint + summary: Publish identifier + operationId: publish parameters: - - name: databaseId + - name: identifierId in: path required: true schema: type: integer format: int64 responses: - "417": - description: View schema could not be retrieved + '202': + description: Published identifier + content: + application/json: + schema: + $ref: '#/components/schemas/IdentifierDto' + '400': + description: Identifier form contains invalid request data content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '403': + description: Insufficient access rights or authorities content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "409": - description: View schema could not be mapped to known columns + '404': + description: 'Failed to find database, table or view' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database/view in metadata database + '405': + description: Creating identifier not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Database schema is malformed + '502': + description: Connection to search service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found view schemas + '503': + description: Failed to save in search service content: application/json: schema: - type: string - post: + $ref: '#/components/schemas/ApiErrorDto' + security: + - bearerAuth: [] + - basicAuth: [] + '/api/database/{databaseId}/visibility': + put: tags: - - view-endpoint - summary: Create view - operationId: create_1 + - database-endpoint + summary: Update database visibility + operationId: visibility parameters: - name: databaseId in: path @@ -1699,48 +2294,48 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ViewCreateDto' + $ref: '#/components/schemas/DatabaseModifyVisibilityDto' required: true responses: - "400": - description: View schema is malformed + '202': + description: Visibility modified successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database in metadata database + $ref: '#/components/schemas/DatabaseDto' + '403': + description: Visibility modification is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "409": - description: View schema could not be mapped + '404': + description: Failed to find database in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '502': + description: Connection to search service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Created view + '503': + description: Failed to save in search service content: application/json: schema: - $ref: '#/components/schemas/ViewDto' + $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - /api/database/{databaseId}/table: + - basicAuth: [] + '/api/database/{databaseId}/table/{tableId}': get: tags: - table-endpoint - summary: Find table schemas - operationId: getSchema_1 + summary: Get information about table + operationId: findById_2 parameters: - name: databaseId in: path @@ -1748,48 +2343,51 @@ paths: schema: type: integer format: int64 + - name: tableId + in: path + required: true + schema: + type: integer + format: int64 responses: - "403": - description: Find table schema not allowed + '200': + description: Find table successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database in metadata database + $ref: '#/components/schemas/TableDto' + '403': + description: Access to the database is forbidden content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Failed to parse table schema + '404': + description: 'Table, database or container could not be found' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Got table schemas - content: - application/json: - schema: - type: string - "503": - description: Failed to establish connection with the metadata service + '502': + description: Connection to search service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Schema data malformed + '503': + description: Failed to save in search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - post: + security: + - bearerAuth: [] + - basicAuth: [] + put: tags: - table-endpoint - summary: Create table - operationId: create_2 + summary: Update table statistics + operationId: updateStatistic parameters: - name: databaseId in: path @@ -1797,167 +2395,162 @@ paths: schema: type: integer format: int64 - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TableCreateDto' - required: true + - name: tableId + in: path + required: true + schema: + type: integer + format: int64 responses: - "400": - description: Table schema or query is malformed + '202': + description: Updated table statistics successfully + '400': + description: Failed to map column statistic to known columns content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "409": - description: Table name already exists in database + '404': + description: Failed to find database/table in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database in metadata database or table in data database + '502': + description: Connection to search service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '503': + description: Failed to save in search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Created table - content: - application/json: - schema: - $ref: '#/components/schemas/TableDto' security: + - bearerAuth: [] - basicAuth: [] - /api/container: - get: + delete: tags: - - container-endpoint - summary: Find all containers - operationId: findAll_6 + - table-endpoint + summary: Delete a table + operationId: delete_5 parameters: - - name: limit - in: query - required: false + - name: databaseId + in: path + required: true schema: type: integer - format: int32 + format: int64 + - name: tableId + in: path + required: true + schema: + type: integer + format: int64 responses: - "200": - description: List containers + '202': + description: Delete table successfully + '400': + description: Delete table query resulted in an invalid query statement content: application/json: schema: - type: array - items: - type: string - post: - tags: - - container-endpoint - summary: Create container - operationId: create_9 - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ContainerCreateDto' - required: true - responses: - "404": - description: Container image or user could not be found + $ref: '#/components/schemas/ApiErrorDto' + '403': + description: Access to the database is forbidden content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "409": - description: Container name already exists + '404': + description: 'Table, database or container could not be found' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Created a new container + '502': + description: Connection to search service failed content: application/json: schema: - $ref: '#/components/schemas/ContainerBriefDto' - security: - - bearerAuth: [] - - basicAuth: [] - /api/unit: - get: - tags: - - unit-endpoint - summary: List semantic units - operationId: findAll_1 - responses: - "200": - description: Find all semantic units + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to save in search service content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/UnitDto' - /api/ontology/{ontologyId}/entity: - get: + $ref: '#/components/schemas/ApiErrorDto' + security: + - bearerAuth: [] + - basicAuth: [] + '/api/database/{databaseId}/table/{tableId}/column/{columnId}': + put: tags: - - ontology-endpoint - summary: Find entities - operationId: find_4 + - table-endpoint + summary: Update a table column semantic mapping + operationId: update_3 parameters: - - name: ontologyId + - name: databaseId in: path required: true schema: type: integer format: int64 - - name: label - in: query - required: false + - name: tableId + in: path + required: true schema: - type: string - - name: uri - in: query - required: false + type: integer + format: int64 + - name: columnId + in: path + required: true schema: - type: string + type: integer + format: int64 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ColumnSemanticsUpdateDto' + required: true responses: - "404": - description: Could not find ontology + '202': + description: Updated column semantics successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ColumnDto' + '400': + description: >- + Update semantic concept query is malformed or update unit of + measurement query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Ontology does not have rdf or sparql endpoint + '403': + description: Access to the database is forbidden content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found entities + '404': + description: Failed to find user/table/database/ontology in metadata database content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/EntityDto' - "400": - description: Filter params are invalid + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: Connection to search service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Generated query or uri is malformed + '503': + description: Failed to save in search service content: application/json: schema: @@ -1965,97 +2558,65 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/oai: - get: - tags: - - metadata-endpoint - summary: Get the record - operationId: identify_1_1_1_1 - parameters: - - name: verb - in: query - - name: parameters - in: query - required: true - schema: - $ref: '#/components/schemas/OaiListIdentifiersParameters' - responses: - "200": - description: List containers - content: - text/xml: {} - /api/message/message/{messageId}: - get: + '/api/database/{databaseId}/owner': + put: tags: - - message-endpoint - summary: Find one maintenance message - operationId: find_5 + - database-endpoint + summary: Update database owner + operationId: transfer parameters: - - name: messageId + - name: databaseId in: path required: true schema: type: integer format: int64 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseTransferDto' + required: true responses: - "200": - description: Get messages + '202': + description: Transfer of ownership was successful content: application/json: schema: - $ref: '#/components/schemas/BannerMessageDto' - "404": - description: Could not find message + $ref: '#/components/schemas/DatabaseDto' + '403': + description: Transfer of ownership is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - /api/license: - get: - tags: - - license-endpoint - summary: Get all licenses - operationId: list_3 - responses: - "200": - description: List of licenses + '404': + description: Database or user could not be found content: application/json: schema: - type: array - items: - type: string - /api/identifier/retrieve: - get: - tags: - - identifier-endpoint - summary: Retrieve metadata from identifier - operationId: retrieve - parameters: - - name: url - in: query - required: true - schema: - type: string - responses: - "200": - description: Retrieved metadata from identifier + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: Connection to search service failed content: application/json: schema: - $ref: '#/components/schemas/IdentifierDto' - "404": - description: Failed to find metadata for identifier + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to save in search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - /api/database/{databaseId}: + security: + - bearerAuth: [] + - basicAuth: [] + '/api/database/{databaseId}/metadata/view': put: tags: - database-endpoint - summary: Update user password in database - operationId: update + summary: Refresh database views metadata + operationId: refreshViewMetadata parameters: - name: databaseId in: path @@ -2063,41 +2624,46 @@ paths: schema: type: integer format: int64 - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateUserPasswordDto' - required: true responses: - "404": + '200': + description: Refreshed database views metadata + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseDto' + '403': + description: Refresh view metadata is not permitted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '404': description: Failed to find database in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Updated user password in database - "503": - description: Failed to communicate with database + '502': + description: Connection to search service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Failed to update user password in database + '503': + description: Failed to save in search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: + - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/view/{viewId}: - delete: + '/api/database/{databaseId}/metadata/table': + put: tags: - - view-endpoint - summary: Delete view - operationId: delete + - database-endpoint + summary: Refresh database tables metadata + operationId: refreshTableMetadata parameters: - name: databaseId in: path @@ -2105,48 +2671,52 @@ paths: schema: type: integer format: int64 - - name: viewId - in: path - required: true - schema: - type: integer - format: int64 responses: - "202": - description: Deleted view - "409": - description: View schema could not be mapped + '200': + description: Refreshed database tables metadata + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseDto' + '400': + description: Failed to parse payload at search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '403': + description: Not allowed to refresh table metadata content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find view in metadata database + '404': + description: Failed to fin user/database in metadata database + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: Connection to search service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Database schema is malformed + '503': + description: Failed to save in search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - /api/database/{databaseId}/table/{tableId}/suggest: - get: + - basicAuth: [] + '/api/database/{databaseId}/image': + put: tags: - - table-endpoint - summary: Suggest table semantics - operationId: analyseTable + - database-endpoint + summary: Update database image + operationId: modifyImage parameters: - name: databaseId in: path @@ -2154,41 +2724,45 @@ paths: schema: type: integer format: int64 - - name: tableId - in: path - required: true - schema: - type: integer - format: int64 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DatabaseModifyImageDto' + required: true responses: - "200": - description: Suggested table semantics successfully + '202': + description: Modify of image was successful content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/TableColumnEntityDto' - "400": - description: Failed to parse statistic in search service + $ref: '#/components/schemas/DatabaseDto' + '403': + description: Modify of image is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "422": - description: Ontology does not have rdf or sparql endpoint + '404': + description: Database or user could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database/table in metadata database + '410': + description: File was not found in the Storage Service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Generated query is malformed + '502': + description: Connection to search service failed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to save in search service content: application/json: schema: @@ -2196,289 +2770,314 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/table/{tableId}/column/{columnId}/suggest: + /api/user: get: tags: - - table-endpoint - summary: Suggest table column semantics - operationId: analyseTableColumn - parameters: - - name: databaseId - in: path - required: true - schema: - type: integer - format: int64 - - name: tableId - in: path - required: true - schema: - type: integer - format: int64 - - name: columnId - in: path - required: true - schema: - type: integer - format: int64 + - user-endpoint + summary: Find all users + operationId: findAll responses: - "200": - description: Suggested table column semantics successfully + '200': + description: List users content: application/json: schema: type: array items: - $ref: '#/components/schemas/TableColumnEntityDto' - "422": - description: Ontology does not have rdf or sparql endpoint + $ref: '#/components/schemas/UserBriefDto' + post: + tags: + - user-endpoint + summary: Create user + operationId: create1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SignupRequestDto' + required: true + responses: + '201': + description: Created user content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database/table in metadata database + $ref: '#/components/schemas/UserBriefDto' + '400': + description: Parameters are not well-formed (likely email) + content: + application/json: {} + '404': + description: default role not found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Generated query is malformed + '409': + description: User with username already exists content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - security: - - bearerAuth: [] - - basicAuth: [] - /api/container/{containerId}: - get: - tags: - - container-endpoint - summary: Find some container - operationId: findById_3 - parameters: - - name: containerId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "200": - description: Found container + '417': + description: User with e-mail already exists content: application/json: schema: - $ref: '#/components/schemas/ContainerDto' - "404": - description: Container image could not be found + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: Failed to create in auth service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - delete: - tags: - - container-endpoint - summary: Delete some container - operationId: delete_6 - parameters: - - name: containerId - in: path - required: true - schema: - type: integer - format: int64 - responses: - "404": - description: Container not found + '503': + description: Failed to create in auth service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Deleted container successfully - content: - '*/*': - schema: - type: object - security: - - bearerAuth: [] - - basicAuth: [] - /api/concept: + /api/ontology: get: tags: - - concept-endpoint - summary: List semantic concepts - operationId: findAll_7 + - ontology-endpoint + summary: List all ontologies + operationId: findAll_2 responses: - "200": - description: Find all semantic concepts + '200': + description: List all ontologies content: application/json: schema: type: array items: - $ref: '#/components/schemas/ConceptDto' - /api/database/{databaseId}/view/{viewId}/data: - get: + $ref: '#/components/schemas/OntologyDto' + post: tags: - - view-endpoint - summary: Retrieve view data - operationId: getData - parameters: - - name: databaseId - in: path - required: true - schema: - type: integer - format: int64 - - name: viewId - in: path - required: true - schema: - type: integer - format: int64 - - name: page - in: query - required: false - schema: - type: integer - format: int64 - - name: size - in: query - required: false - schema: - type: integer - format: int64 - - name: timestamp + - ontology-endpoint + summary: Register a new ontology + operationId: create_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OntologyCreateDto' + required: true + responses: + '201': + description: Registered ontology successfully + content: + application/json: + schema: + $ref: '#/components/schemas/OntologyDto' + security: + - bearerAuth: [] + - basicAuth: [] + /api/message: + get: + tags: + - message-endpoint + summary: Find maintenance messages + operationId: list_2 + parameters: + - name: filter in: query required: false schema: type: string - format: date-time responses: - "403": - description: Not allowed to retrieve view data + '200': + description: List messages content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "409": - description: View schema could not be mapped + type: array + items: + $ref: '#/components/schemas/BannerMessageDto' + post: + tags: + - message-endpoint + summary: Create maintenance message + operationId: create_2 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BannerMessageCreateDto' + required: true + responses: + '201': + description: Created message content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + $ref: '#/components/schemas/BannerMessageBriefDto' + security: + - bearerAuth: [] + - basicAuth: [] + /api/image: + get: + tags: + - image-endpoint + summary: Find all images + operationId: findAll_3 + responses: + '200': + description: List images content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find view in metadata database + type: array + items: + $ref: '#/components/schemas/ContainerImage' + post: + tags: + - image-endpoint + summary: Create image + operationId: create_3 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ImageCreateDto' + required: true + responses: + '201': + description: Created image content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Request pagination is malformed + $ref: '#/components/schemas/ImageDto' + '400': + description: Image specification is invalid content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Retrieved view data + '409': + description: Image already exists content: application/json: schema: - $ref: '#/components/schemas/QueryResultDto' + $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - head: + - basicAuth: [] + /api/identifier: + get: tags: - - view-endpoint - summary: Retrieve view data - operationId: getData_1 + - identifier-endpoint + summary: Find all identifiers + operationId: findAll_4 parameters: - - name: databaseId - in: path - required: true + - name: dbid + in: query + required: false schema: type: integer format: int64 - - name: viewId - in: path - required: true + - name: qid + in: query + required: false schema: type: integer format: int64 - - name: page + - name: vid in: query required: false schema: type: integer format: int64 - - name: size + - name: tid in: query required: false schema: type: integer format: int64 - - name: timestamp - in: query - required: false + - name: Accept + in: header + required: true schema: type: string - format: date-time responses: - "403": - description: Not allowed to retrieve view data + '200': + description: Found identifiers successfully + content: + application/json: + schema: + type: string + application/ld+json: + schema: + type: string + '406': + description: 'Identifier could not be exported, the requested style is not known' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "409": - description: View schema could not be mapped + post: + tags: + - identifier-endpoint + summary: Draft identifier + operationId: create_4 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IdentifierCreateDto' + required: true + responses: + '201': + description: Drafted identifier + content: + application/json: + schema: + $ref: '#/components/schemas/IdentifierDto' + '400': + description: Identifier form contains invalid request data content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '403': + description: Insufficient access rights or authorities content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find view in metadata database + '404': + description: 'Failed to find database, table or view' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Request pagination is malformed + '405': + description: Creating identifier not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Retrieved view data + '502': + description: Connection to search service failed content: application/json: schema: - $ref: '#/components/schemas/QueryResultDto' + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to save in search service + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - /api/database/{databaseId}/table/{tableId}/data: + - basicAuth: [] + '/api/database/{databaseId}/view': get: tags: - - table-endpoint - summary: Retrieve table data - operationId: getData_2 + - view-endpoint + summary: Find all views + operationId: findAll_5 parameters: - name: databaseId in: path @@ -2486,63 +3085,29 @@ paths: schema: type: integer format: int64 - - name: tableId - in: path - required: true - schema: - type: integer - format: int64 - - name: timestamp - in: query - required: false - schema: - type: string - format: date-time - - name: page - in: query - required: false - schema: - type: integer - format: int64 - - name: size - in: query - required: false - schema: - type: integer - format: int64 responses: - "200": - description: Retrieved table data - content: - application/json: - schema: - $ref: '#/components/schemas/QueryResultDto' - "404": - description: Failed to find table in metadata database - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Request pagination or table data select query is malformed + '200': + description: Find views successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + type: array + items: + $ref: '#/components/schemas/ViewBriefDto' + '404': + description: Database or user could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - put: + - basicAuth: [] + post: tags: - - table-endpoint - summary: Update table data - operationId: updateTuple + - view-endpoint + summary: Create a view + operationId: create_6 parameters: - name: databaseId in: path @@ -2550,53 +3115,76 @@ paths: schema: type: integer format: int64 - - name: tableId - in: path - required: true - schema: - type: integer - format: int64 requestBody: content: application/json: schema: - $ref: '#/components/schemas/TupleUpdateDto' + $ref: '#/components/schemas/ViewCreateDto' required: true responses: - "403": - description: Update table data not allowed + '201': + description: Create view successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ViewBriefDto' + '400': + description: Create view query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find table in metadata database + '401': + description: Credentials missing content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Updated table data - "400": - description: Request pagination or table data select query is malformed + '403': + description: Credentials missing content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '404': + description: Failed to find database/user in metadata database. + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '405': + description: Create view is not permitted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '423': + description: Create view resulted in an invalid query statement + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: Connection to search service failed + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorDto' + '503': + description: Failed to save in search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - post: + - basicAuth: [] + '/api/database/{databaseId}/table': + get: tags: - table-endpoint - summary: Create table data - operationId: createTuple + summary: List all tables + operationId: list_4 parameters: - name: databaseId in: path @@ -2604,53 +3192,35 @@ paths: schema: type: integer format: int64 - - name: tableId - in: path - required: true - schema: - type: integer - format: int64 - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TupleDto' - required: true responses: - "403": - description: Create table data not allowed - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find table in metadata database + '200': + description: List tables content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Created table data - "400": - description: Request pagination or table data select query is malformed + type: array + items: + $ref: '#/components/schemas/TableBriefDto' + '403': + description: List tables not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '404': + description: Database could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - delete: + - basicAuth: [] + post: tags: - table-endpoint - summary: Delete table data - operationId: deleteTuple + summary: Create a table + operationId: create_7 parameters: - name: databaseId in: path @@ -2658,312 +3228,278 @@ paths: schema: type: integer format: int64 - - name: tableId - in: path - required: true - schema: - type: integer - format: int64 requestBody: content: application/json: schema: - $ref: '#/components/schemas/TupleDeleteDto' + $ref: '#/components/schemas/TableCreateDto' required: true responses: - "403": - description: Delete table data not allowed + '201': + description: Created a new table content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find table in metadata database + $ref: '#/components/schemas/TableBriefDto' + '400': + description: Create table query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Deleted table data - "400": - description: Request pagination or table data select query is malformed + '403': + description: Create table not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '404': + description: 'Database, container or user could not be found' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - security: - - basicAuth: [] - - bearerAuth: [] - head: - tags: - - table-endpoint - summary: Retrieve table data - operationId: getData_3 - parameters: - - name: databaseId - in: path - required: true - schema: - type: integer - format: int64 - - name: tableId - in: path - required: true - schema: - type: integer - format: int64 - - name: timestamp - in: query - required: false - schema: - type: string - format: date-time - - name: page - in: query - required: false - schema: - type: integer - format: int64 - - name: size - in: query - required: false - schema: - type: integer - format: int64 - responses: - "200": - description: Retrieved table data - content: - application/json: - schema: - $ref: '#/components/schemas/QueryResultDto' - "404": - description: Failed to find table in metadata database + '409': + description: Create table conflicts with existing table name content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Request pagination or table data select query is malformed + '502': + description: Connection to search service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '503': + description: Failed to save in search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - /api/database/{databaseId}/subset/{subsetId}/data: + - basicAuth: [] + /api/container: get: tags: - - subset-endpoint - summary: Retrieved subset data - operationId: getData_4 + - container-endpoint + summary: Find all containers + operationId: findAll_6 parameters: - - name: databaseId - in: path - required: true - schema: - type: integer - format: int64 - - name: subsetId - in: path - required: true - schema: - type: integer - format: int64 - - name: page - in: query - required: false - schema: - type: integer - format: int64 - - name: size + - name: limit in: query required: false schema: type: integer - format: int64 + format: int32 responses: - "400": - description: Malformed select query + '200': + description: List containers content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to retrieve subset data + type: array + items: + type: string + post: + tags: + - container-endpoint + summary: Create container + operationId: create_9 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContainerCreateDto' + required: true + responses: + '201': + description: Created a new container content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database in metadata database or query in query store of the data database + $ref: '#/components/schemas/ContainerBriefDto' + '404': + description: Container image or user could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to communicate with database + '409': + description: Container name already exists content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Retrieved subset data - content: - application/json: - schema: - $ref: '#/components/schemas/QueryResultDto' security: - bearerAuth: [] - basicAuth: [] - head: + /api/unit: + get: tags: - - subset-endpoint - summary: Retrieved subset data - operationId: getData_5 + - unit-endpoint + summary: List semantic units + operationId: findAll_1 + responses: + '200': + description: Find all semantic units + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UnitDto' + '/api/ontology/{ontologyId}/entity': + get: + tags: + - ontology-endpoint + summary: Find entities + operationId: find_4 parameters: - - name: databaseId - in: path - required: true - schema: - type: integer - format: int64 - - name: subsetId + - name: ontologyId in: path required: true schema: type: integer format: int64 - - name: page + - name: label in: query required: false schema: - type: integer - format: int64 - - name: size + type: string + - name: uri in: query required: false schema: - type: integer - format: int64 + type: string responses: - "400": - description: Malformed select query + '200': + description: Found entities content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to retrieve subset data + type: array + items: + $ref: '#/components/schemas/EntityDto' + '400': + description: Filter params are invalid content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database in metadata database or query in query store of the data database + '404': + description: Could not find ontology content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to communicate with database + '417': + description: Generated query or uri is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Retrieved subset data + '422': + description: Ontology does not have rdf or sparql endpoint content: application/json: schema: - $ref: '#/components/schemas/QueryResultDto' + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] - /api/database/{databaseId}/subset/{queryId}: - put: + /api/oai: + get: tags: - - subset-endpoint - summary: Persist subset - operationId: persist + - metadata-endpoint + summary: Get the record + operationId: identify_1_1_1_1 parameters: - - name: databaseId - in: path + - name: verb + in: query + - name: parameters + in: query required: true schema: - type: integer - format: int64 - - name: queryId + $ref: '#/components/schemas/OaiListIdentifiersParameters' + responses: + '200': + description: List containers + content: + text/xml: {} + '/api/message/message/{messageId}': + get: + tags: + - message-endpoint + summary: Find one maintenance message + operationId: find_5 + parameters: + - name: messageId in: path required: true schema: type: integer format: int64 - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/QueryPersistDto' - required: true responses: - "202": - description: Persisted subset - content: - application/json: - schema: - $ref: '#/components/schemas/QueryDto' - "400": - description: Malformed select query + '200': + description: Get messages content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database in metadata database or query in query store of the data database + $ref: '#/components/schemas/BannerMessageDto' + '404': + description: Could not find message content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to communicate with database + /api/license: + get: + tags: + - license-endpoint + summary: Get all licenses + operationId: list_3 + responses: + '200': + description: List of licenses content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to persist subset + type: array + items: + type: string + /api/identifier/retrieve: + get: + tags: + - identifier-endpoint + summary: Retrieve metadata from identifier + operationId: retrieve + parameters: + - name: url + in: query + required: true + schema: + type: string + responses: + '200': + description: Retrieved metadata from identifier content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "417": - description: Failed to persist subset + $ref: '#/components/schemas/IdentifierDto' + '404': + description: Failed to find metadata for identifier content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - security: - - bearerAuth: [] - - basicAuth: [] - /api/database/{databaseId}/table/{tableId}/data/import: - post: + '/api/database/{databaseId}': + get: tags: - - table-endpoint - summary: Import dataset - operationId: importData + - database-endpoint + summary: Find some database + operationId: findById_1 parameters: - name: databaseId in: path @@ -2971,54 +3507,40 @@ paths: schema: type: integer format: int64 - - name: tableId - in: path - required: true - schema: - type: integer - format: int64 - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ImportCsvDto' - required: true responses: - "403": - description: Import table dataset not allowed + '200': + description: Database found successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find table in metadata database + $ref: '#/components/schemas/DatabaseDto' + '404': + description: Database or exchange could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '502': + description: Connection to the broker service could not be established content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "400": - description: Import dataset query is malformed + '503': + description: Failed to find queue information in broker service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "202": - description: Import dataset successfully security: - - basicAuth: [] - bearerAuth: [] - /api/database/{databaseId}/subset: + - basicAuth: [] + '/api/database/{databaseId}/view/{viewId}': get: tags: - - subset-endpoint - summary: Find subsets - operationId: list + - view-endpoint + summary: Find one view + operationId: find_7 parameters: - name: databaseId in: path @@ -3026,44 +3548,39 @@ paths: schema: type: integer format: int64 - - name: persisted - in: query - required: false + - name: viewId + in: path + required: true schema: - type: boolean + type: integer + format: int64 responses: - "404": - description: Failed to find database in metadata database or query in query store of the data database + '200': + description: Find view successfully content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to communicate with database + $ref: '#/components/schemas/ViewDto' + '403': + description: Find view is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to find subsets + '404': + description: 'Database, view or user could not be found' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found subsets - content: - application/json: - schema: - type: string security: - - basicAuth: [] - bearerAuth: [] - post: + - basicAuth: [] + delete: tags: - - subset-endpoint - summary: Create subset - operationId: create_3 + - view-endpoint + summary: Delete one view + operationId: delete_4 parameters: - name: databaseId in: path @@ -3071,82 +3588,66 @@ paths: schema: type: integer format: int64 - - name: page - in: query - required: false - schema: - type: integer - format: int64 - - name: size - in: query - required: false + - name: viewId + in: path + required: true schema: type: integer format: int64 - - name: timestamp - in: query - required: false - schema: - type: string - format: date-time - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ExecuteStatementDto' - required: true responses: - "400": - description: Malformed select query + '202': + description: Delete view successfully + '400': + description: Delete view query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database in metadata database or query in query store of the data database + '403': + description: Deletion not allowed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to communicate with database + '404': + description: 'Database, view or user could not be found' content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to find subset + '405': + description: Delete view is not permitted content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "201": - description: Created subset + '423': + description: Delete view resulted in an invalid query statement content: application/json: schema: - $ref: '#/components/schemas/QueryResultDto' - "417": - description: Failed to insert query into query store of data database + $ref: '#/components/schemas/ApiErrorDto' + '502': + description: Connection to search service failed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "501": - description: Failed to execute query as it contains non-supported keywords + '503': + description: Failed to save in search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - /api/database/{databaseId}/table/{tableId}/history: + - basicAuth: [] + '/api/database/{databaseId}/table/{tableId}/suggest': get: tags: - table-endpoint - summary: Find table history - operationId: getHistory + summary: Suggest table semantics + operationId: analyseTable parameters: - name: databaseId in: path @@ -3161,39 +3662,47 @@ paths: type: integer format: int64 responses: - "404": - description: Failed to find table history in data database + '200': + description: Suggested table semantics successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TableColumnEntityDto' + '400': + description: Failed to parse statistic in search service content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found table history + '404': + description: Failed to find database/table in metadata database content: application/json: schema: - type: string - "403": - description: Find table history not allowed + $ref: '#/components/schemas/ApiErrorDto' + '417': + description: Generated query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '422': + description: Ontology does not have rdf or sparql endpoint content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - /api/database/{databaseId}/table/{tableId}/export: + - basicAuth: [] + '/api/database/{databaseId}/table/{tableId}/column/{columnId}/suggest': get: tags: - table-endpoint - summary: Export table data - operationId: exportData + summary: Suggest table column semantics + operationId: analyseTableColumn parameters: - name: databaseId in: path @@ -3207,264 +3716,138 @@ paths: schema: type: integer format: int64 - - name: timestamp - in: query - required: false + - name: columnId + in: path + required: true schema: - type: string - format: date-time + type: integer + format: int64 responses: - "404": - description: Failed to find table in metadata database - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Exported table data + '200': + description: Suggested table column semantics successfully content: application/json: schema: - type: string - format: binary - "400": - description: Request pagination or table data select query is malformed + type: array + items: + $ref: '#/components/schemas/TableColumnEntityDto' + '400': + description: Generated query is malformed content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to establish connection with the metadata service + '404': + description: Failed to find database/table in metadata database content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Export table data not allowed + '422': + description: Ontology does not have rdf or sparql endpoint content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' security: - - basicAuth: [] - bearerAuth: [] - /api/database/{databaseId}/subset/{subsetId}: + - basicAuth: [] + '/api/container/{containerId}': get: tags: - - subset-endpoint - summary: Find subset - operationId: findById + - container-endpoint + summary: Find some container + operationId: findById_3 parameters: - - name: databaseId - in: path - required: true - schema: - type: integer - format: int64 - - name: subsetId + - name: containerId in: path required: true schema: type: integer format: int64 - - name: timestamp - in: query - required: false - schema: - type: string - format: date-time responses: - "400": - description: Malformed select query - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "404": - description: Failed to find database in metadata database or query in query store of the data database - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "503": - description: Failed to communicate with database - content: - application/json: - schema: - $ref: '#/components/schemas/ApiErrorDto' - "403": - description: Not allowed to find subset + '200': + description: Found container content: application/json: schema: - $ref: '#/components/schemas/ApiErrorDto' - "406": - description: Failed to find acceptable representation + $ref: '#/components/schemas/ContainerDto' + '404': + description: Container image could not be found content: application/json: schema: $ref: '#/components/schemas/ApiErrorDto' - "200": - description: Found subset - content: - application/json: - schema: - $ref: '#/components/schemas/QueryDto' - text/csv: {} - security: - - basicAuth: [] - - bearerAuth: [] - /api/search: - get: - consumes: - - application/json - description: Performs a fuzzy search - operationId: post_fuzzy_search + delete: + tags: + - container-endpoint + summary: Delete some container + operationId: delete_6 parameters: - - in: query + - name: containerId + in: path required: true schema: - properties: - q: - example: air quality - type: string - type: string - produces: - - application/json - responses: - "200": - content: - application/json: - schema: - properties: - results: - items: - type: object - type: array - type: object - description: OK, contains the elements formatted as an array of JSON arrays - "415": - description: Wrong accept type - summary: Performs a fuzzy search - tags: - - search-endpoint - /api/search/database/{database_id}: - delete: - consumes: - - application/json - description: Deletes a database - operationId: delete_database - produces: - - application/json + type: integer + format: int64 responses: - "202": + '202': + description: Deleted container successfully content: - application/json: + '*/*': schema: - properties: - id: - example: 1 - implementation: int64 - type: integer - required: - - id type: object - description: Deleted database successfully - "404": + '404': + description: Container not found content: application/json: schema: - properties: - message: - example: Message - type: string - success: - example: false - type: boolean - required: - - success - - message - type: object - description: Database not found + $ref: '#/components/schemas/ApiErrorDto' security: - bearerAuth: [] - basicAuth: [] - summary: Deletes a database + /api/concept: + get: tags: - - database-endpoint - put: + - concept-endpoint + summary: List semantic concepts + operationId: findAll_7 + responses: + '200': + description: Find all semantic concepts + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ConceptDto' + /api/search: + get: consumes: - application/json - description: Updates a database - operationId: update_database + description: Performs a fuzzy search + operationId: post_fuzzy_search parameters: - - in: body - name: body + - in: query + name: q required: true schema: - properties: - internal_name: - example: air_quality_abcd - type: string - name: - example: Air Quality - type: string - type: object + type: string produces: - application/json responses: - "202": - content: - application/json: - schema: - properties: - id: - example: 1 - implementation: int64 - type: integer - required: - - id - type: object - description: Updated database successfully - "400": - content: - application/json: - schema: - properties: - message: - example: Message - type: string - success: - example: false - type: boolean - required: - - success - - message - type: object - description: Invalid schema - "404": + '200': content: application/json: schema: - properties: - message: - example: Message - type: string - success: - example: false - type: boolean - required: - - success - - message - type: object - description: Database not found - security: - - bearerAuth: [] - - basicAuth: [] - summary: Updates a database + $ref: '#/components/schemas/SearchResultDto' + description: 'OK, contains the elements formatted as an array of JSON arrays' + '415': + description: Wrong accept type + summary: Performs a fuzzy search tags: - - database-endpoint - /api/search/{index}: + - search-endpoint + '/api/search/{index}': get: consumes: - application/json @@ -3506,33 +3889,16 @@ paths: produces: - application/json responses: - "200": + '200': content: application/json: schema: - properties: - results: - items: - type: object - type: array - type: - description: Same as the requested type - enum: - - database - - table - - view - - column - - user - - identifier - - concept - - unit - type: string - type: object - description: OK, contains the elements formatted as an array of JSON arrays + $ref: '#/components/schemas/IndexDto' + description: 'OK, contains the elements formatted as an array of JSON arrays' summary: Gets the index tags: - search-endpoint - /api/search/{type}: + '/api/search/{type}': post: consumes: - application/json @@ -3566,17 +3932,11 @@ paths: name: body required: true schema: - properties: - field_value_pairs: - type: object - search_term: - example: air quality - type: string - type: object + $ref: '#/components/schemas/SearchRequestDto' produces: - application/json responses: - "200": + '200': content: application/json: schema: @@ -3598,11 +3958,11 @@ paths: - unit type: string type: object - description: OK, contains the elements formatted as an array of JSON arrays + description: 'OK, contains the elements formatted as an array of JSON arrays' summary: Performs a general search tags: - search-endpoint - /api/search/{type}/fields: + '/api/search/{type}/fields': get: operationId: get_fields parameters: @@ -3622,207 +3982,18 @@ paths: - unit type: string responses: - "200": + '200': content: application/json: schema: - properties: - results: - items: - properties: - attr_friendly_name: - example: Name - type: string - attr_name: - example: name - type: string - type: - description: OpenSearch data types. - example: string - type: string - type: object - type: array - type: object + $ref: '#/components/schemas/IndexFieldsDto' description: List of fields - "404": + '404': description: Invalid type. summary: Get searchable fields tags: - search-endpoint - /health: - get: - consumes: - - application/json - description: | - Return UP if the instance is ready to serve connections. - produces: - - application/json - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/Health' - description: OK, service is up and running - "404": - description: Service is not yet ready - summary: Return a healthcheck - tags: - - actuator - /api/analyse/database/{database_id}/table/{table_id}/statistics: - get: - operationId: analyse_table_stat - parameters: - - example: 1 - in: path - name: database_id - required: true - schema: - format: int64 - type: integer - - example: 1 - in: path - name: table_id - required: true - schema: - format: int64 - type: integer - responses: - "202": - content: - application/json: - schema: - $ref: '#/components/schemas/TableStats' - description: Determined statistics - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorDto' - description: Missing parameters - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorDto' - description: Table not found - security: - - bearerAuth: [] - - basicAuth: [] - summary: Determine table statistics - tags: - - analyse-endpoint - /api/analyse/datatypes: - get: - consumes: - - application/json - description: This is a simple API which returns the datatypes of a (path) csv file - operationId: analyse_datatypes - parameters: - - example: filename_s3_key - in: query - name: filename - required: true - schema: - type: string - - example: ',' - in: query - name: separator - required: true - schema: - type: string - - example: "false" - in: query - name: enum - required: false - schema: - type: boolean - - example: "2.5" - in: query - name: enum_tol - required: false - schema: - type: float - produces: - - application/json - responses: - "202": - content: - application/json: - schema: - $ref: '#/components/schemas/DataTypesDto' - description: Determined data types successfully - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorDto' - description: Failed to determine data types - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorDto' - description: Failed to find file in Storage Service - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorDto' - description: Unexpected system error - summary: Determine datatypes - tags: - - analyse-endpoint - /api/analyse/keys: - get: - consumes: - - application/json - description: This is a simple API which returns the primary keys + ranking of a (path) csv file - operationId: analyse_keys - parameters: - - example: filename_s3_key - in: query - name: filename - required: true - schema: - type: string - - example: ',' - in: query - name: separator - required: true - schema: - type: string - produces: - - application/json - responses: - "202": - content: - application/json: - schema: - $ref: '#/components/schemas/KeysDto' - description: Determined keys successfully - "400": - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorDto' - description: Failed to determine keys - "404": - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorDto' - description: Failed to find file in Storage Service or is empty - "500": - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorDto' - description: Unexpected system error - summary: Determine primary keys - tags: - - analyse-endpoint - /sidecar/export/{filename}: + '/sidecar/export/{filename}': post: consumes: - application/json @@ -3836,10 +4007,10 @@ paths: produces: - application/json responses: - "202": + '202': content: {} description: Exported the .csv - "400": + '400': description: The Storage Service could not be contacted or .csv was not found. security: - bearerAuth: [] @@ -3847,7 +4018,7 @@ paths: summary: Exports a .csv to the Storage Service tags: - sidecar - /sidecar/import/{filename}: + '/sidecar/import/{filename}': post: consumes: - application/json @@ -3861,10 +4032,10 @@ paths: produces: - application/json responses: - "202": + '202': content: {} description: Imported the .csv - "400": + '400': description: The Storage Service could not be contacted or .csv was not found. security: - bearerAuth: [] @@ -3872,3 +4043,5627 @@ paths: summary: Imports a .csv from the Storage Service tags: - sidecar +components: + securitySchemes: + basicAuth: + in: header + scheme: basic + type: http + bearerAuth: + bearerFormat: JWT + in: header + scheme: bearer + type: http + schemas: + DataTypesDto: + properties: + columns: + $ref: '#/components/schemas/SuggestedColumnDto' + line_termination: + example: "\r\n" + type: string + separator: + example: ',' + type: string + type: object + ErrorDto: + properties: + message: + example: Message + type: string + success: + example: false + type: boolean + type: object + KeysDto: + properties: + keys: + items: + properties: + column_name: + format: int64 + type: integer + type: array + required: + - keys + type: object + SuggestedColumnDto: + properties: + column_name: + type: string + type: object + QueryResultDto: + required: + - headers + - id + - result + type: object + properties: + result: + type: array + items: + type: object + additionalProperties: + type: object + headers: + type: array + items: + type: object + additionalProperties: + type: integer + format: int32 + id: + type: integer + format: int64 + ApiErrorDto: + required: + - code + - message + - status + type: object + properties: + status: + type: string + example: NOT_FOUND + enum: + - 100 CONTINUE + - 101 SWITCHING_PROTOCOLS + - 102 PROCESSING + - 103 EARLY_HINTS + - 103 CHECKPOINT + - 200 OK + - 201 CREATED + - 202 ACCEPTED + - 203 NON_AUTHORITATIVE_INFORMATION + - 204 NO_CONTENT + - 205 RESET_CONTENT + - 206 PARTIAL_CONTENT + - 207 MULTI_STATUS + - 208 ALREADY_REPORTED + - 226 IM_USED + - 300 MULTIPLE_CHOICES + - 301 MOVED_PERMANENTLY + - 302 FOUND + - 302 MOVED_TEMPORARILY + - 303 SEE_OTHER + - 304 NOT_MODIFIED + - 305 USE_PROXY + - 307 TEMPORARY_REDIRECT + - 308 PERMANENT_REDIRECT + - 400 BAD_REQUEST + - 401 UNAUTHORIZED + - 402 PAYMENT_REQUIRED + - 403 FORBIDDEN + - 404 NOT_FOUND + - 405 METHOD_NOT_ALLOWED + - 406 NOT_ACCEPTABLE + - 407 PROXY_AUTHENTICATION_REQUIRED + - 408 REQUEST_TIMEOUT + - 409 CONFLICT + - 410 GONE + - 411 LENGTH_REQUIRED + - 412 PRECONDITION_FAILED + - 413 PAYLOAD_TOO_LARGE + - 413 REQUEST_ENTITY_TOO_LARGE + - 414 URI_TOO_LONG + - 414 REQUEST_URI_TOO_LONG + - 415 UNSUPPORTED_MEDIA_TYPE + - 416 REQUESTED_RANGE_NOT_SATISFIABLE + - 417 EXPECTATION_FAILED + - 418 I_AM_A_TEAPOT + - 419 INSUFFICIENT_SPACE_ON_RESOURCE + - 420 METHOD_FAILURE + - 421 DESTINATION_LOCKED + - 422 UNPROCESSABLE_ENTITY + - 423 LOCKED + - 424 FAILED_DEPENDENCY + - 425 TOO_EARLY + - 426 UPGRADE_REQUIRED + - 428 PRECONDITION_REQUIRED + - 429 TOO_MANY_REQUESTS + - 431 REQUEST_HEADER_FIELDS_TOO_LARGE + - 451 UNAVAILABLE_FOR_LEGAL_REASONS + - 500 INTERNAL_SERVER_ERROR + - 501 NOT_IMPLEMENTED + - 502 BAD_GATEWAY + - 503 SERVICE_UNAVAILABLE + - 504 GATEWAY_TIMEOUT + - 505 HTTP_VERSION_NOT_SUPPORTED + - 506 VARIANT_ALSO_NEGOTIATES + - 507 INSUFFICIENT_STORAGE + - 508 LOOP_DETECTED + - 509 BANDWIDTH_LIMIT_EXCEEDED + - 510 NOT_EXTENDED + - 511 NETWORK_AUTHENTICATION_REQUIRED + message: + type: string + example: Error message + code: + type: string + example: error.service.code + TupleUpdateDto: + required: + - data + - keys + type: object + properties: + data: + type: object + additionalProperties: + type: object + keys: + type: object + additionalProperties: + type: object + QueryPersistDto: + required: + - persist + type: object + properties: + persist: + type: boolean + example: true + CreatorDto: + required: + - creator_name + - id + type: object + properties: + id: + type: integer + format: int64 + firstname: + type: string + example: Josiah + lastname: + type: string + example: Carberry + affiliation: + type: string + example: Brown University + creator_name: + type: string + example: 'Carberry, Josiah' + name_type: + type: string + example: Personal + enum: + - Personal + - Organizational + name_identifier: + type: string + example: 0000-0002-1825-0097 + name_identifier_scheme: + type: string + example: ORCID + enum: + - ORCID + - ROR + - ISNI + - GRID + name_identifier_scheme_uri: + type: string + example: 'https://orcid.org/' + affiliation_identifier: + type: string + example: 'https://ror.org/05gq02987' + affiliation_identifier_scheme: + type: string + example: ROR + enum: + - ROR + - GRID + - ISNI + affiliation_identifier_scheme_uri: + type: string + example: 'https://ror.org/' + IdentifierDescriptionDto: + required: + - id + type: object + properties: + id: + type: integer + format: int64 + description: + type: string + example: 'Air quality reports at Stephansplatz, Vienna' + language: + type: string + example: en + enum: + - ab + - aa + - af + - ak + - sq + - am + - ar + - an + - hy + - as + - av + - ae + - ay + - az + - bm + - ba + - eu + - be + - bn + - bh + - bi + - bs + - br + - bg + - my + - ca + - km + - ch + - ce + - ny + - zh + - cu + - cv + - kw + - co + - cr + - hr + - cs + - da + - dv + - nl + - dz + - en + - eo + - et + - ee + - fo + - fj + - fi + - fr + - ff + - gd + - gl + - lg + - ka + - de + - ki + - el + - kl + - gn + - gu + - ht + - ha + - he + - hz + - hi + - ho + - hu + - is + - io + - ig + - id + - ia + - ie + - iu + - ik + - ga + - it + - ja + - jv + - kn + - kr + - ks + - kk + - rw + - kv + - kg + - ko + - kj + - ku + - ky + - lo + - la + - lv + - lb + - li + - ln + - lt + - lu + - mk + - mg + - ms + - ml + - mt + - gv + - mi + - mr + - mh + - ro + - mn + - na + - nv + - nd + - ng + - ne + - se + - 'no' + - nb + - nn + - ii + - oc + - oj + - or + - om + - os + - pi + - pa + - ps + - fa + - pl + - pt + - qu + - rm + - rn + - ru + - sm + - sg + - sa + - sc + - sr + - sn + - sd + - si + - sk + - sl + - so + - st + - nr + - es + - su + - sw + - ss + - sv + - tl + - ty + - tg + - ta + - tt + - te + - th + - bo + - ti + - to + - ts + - tn + - tr + - tk + - tw + - ug + - uk + - ur + - uz + - ve + - vi + - vo + - wa + - cy + - fy + - wo + - xh + - yi + - yo + - za + - zu + type: + type: string + example: Abstract + enum: + - Abstract + - Methods + - SeriesInformation + - TableOfContents + - TechnicalInfo + - Other + IdentifierDto: + required: + - created + - created_by + - creator + - creators + - database_id + - execution + - id + - last_modified + - publication_year + - publisher + - query + - query_hash + - query_normalized + - titles + - type + type: object + properties: + id: + type: integer + format: int64 + type: + type: string + enum: + - database + - subset + - table + - view + titles: + type: array + items: + $ref: '#/components/schemas/IdentifierTitleDto' + descriptions: + type: array + items: + $ref: '#/components/schemas/IdentifierDescriptionDto' + funders: + type: array + items: + $ref: '#/components/schemas/IdentifierFunderDto' + query: + type: string + example: >- + SELECT `id`, `value`, `location` FROM `air_quality` WHERE `location` + = "09:STEF" + execution: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + doi: + type: string + example: 10.1038/nphys1170 + publisher: + type: string + example: TU Wien + creator: + $ref: '#/components/schemas/UserDto' + language: + type: string + enum: + - ab + - aa + - af + - ak + - sq + - am + - ar + - an + - hy + - as + - av + - ae + - ay + - az + - bm + - ba + - eu + - be + - bn + - bh + - bi + - bs + - br + - bg + - my + - ca + - km + - ch + - ce + - ny + - zh + - cu + - cv + - kw + - co + - cr + - hr + - cs + - da + - dv + - nl + - dz + - en + - eo + - et + - ee + - fo + - fj + - fi + - fr + - ff + - gd + - gl + - lg + - ka + - de + - ki + - el + - kl + - gn + - gu + - ht + - ha + - he + - hz + - hi + - ho + - hu + - is + - io + - ig + - id + - ia + - ie + - iu + - ik + - ga + - it + - ja + - jv + - kn + - kr + - ks + - kk + - rw + - kv + - kg + - ko + - kj + - ku + - ky + - lo + - la + - lv + - lb + - li + - ln + - lt + - lu + - mk + - mg + - ms + - ml + - mt + - gv + - mi + - mr + - mh + - ro + - mn + - na + - nv + - nd + - ng + - ne + - se + - 'no' + - nb + - nn + - ii + - oc + - oj + - or + - om + - os + - pi + - pa + - ps + - fa + - pl + - pt + - qu + - rm + - rn + - ru + - sm + - sg + - sa + - sc + - sr + - sn + - sd + - si + - sk + - sl + - so + - st + - nr + - es + - su + - sw + - ss + - sv + - tl + - ty + - tg + - ta + - tt + - te + - th + - bo + - ti + - to + - ts + - tn + - tr + - tk + - tw + - ug + - uk + - ur + - uz + - ve + - vi + - vo + - wa + - cy + - fy + - wo + - xh + - yi + - yo + - za + - zu + licenses: + type: array + items: + $ref: '#/components/schemas/LicenseDto' + creators: + type: array + items: + $ref: '#/components/schemas/CreatorDto' + status: + type: string + enum: + - draft + - published + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + database_id: + type: integer + format: int64 + example: 1 + query_id: + type: integer + format: int64 + example: 1 + table_id: + type: integer + format: int64 + example: 1 + view_id: + type: integer + format: int64 + example: 1 + query_normalized: + type: string + example: >- + SELECT `id`, `value`, `location` FROM `air_quality` WHERE `location` + = "09:STEF" + related_identifiers: + type: array + items: + $ref: '#/components/schemas/RelatedIdentifierDto' + query_hash: + type: string + description: query hash in sha512 + result_hash: + type: string + example: 34fe82cda2c53f13f8d90cfd7a3469e3a939ff311add50dce30d9136397bf8e5 + result_number: + type: integer + format: int64 + example: 1 + publication_day: + type: integer + format: int32 + example: 15 + publication_month: + type: integer + format: int32 + example: 12 + publication_year: + type: integer + format: int32 + example: 2022 + created_by: + type: string + format: uuid + last_modified: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + IdentifierFunderDto: + required: + - funder_name + - id + type: object + properties: + id: + type: integer + format: int64 + funder_name: + type: string + example: European Commission + funder_identifier: + type: string + example: 'http://doi.org/10.13039/501100000780' + funder_identifier_type: + type: string + example: Crossref Funder ID + enum: + - Crossref Funder ID + - ROR + - GND + - ISNI + - Other + scheme_uri: + type: string + example: 'http://doi.org/' + award_number: + type: string + example: '824087' + award_title: + type: string + example: EOSC-Life + IdentifierTitleDto: + required: + - id + type: object + properties: + id: + type: integer + format: int64 + title: + type: string + example: Airquality Demonstrator + language: + type: string + example: en + enum: + - ab + - aa + - af + - ak + - sq + - am + - ar + - an + - hy + - as + - av + - ae + - ay + - az + - bm + - ba + - eu + - be + - bn + - bh + - bi + - bs + - br + - bg + - my + - ca + - km + - ch + - ce + - ny + - zh + - cu + - cv + - kw + - co + - cr + - hr + - cs + - da + - dv + - nl + - dz + - en + - eo + - et + - ee + - fo + - fj + - fi + - fr + - ff + - gd + - gl + - lg + - ka + - de + - ki + - el + - kl + - gn + - gu + - ht + - ha + - he + - hz + - hi + - ho + - hu + - is + - io + - ig + - id + - ia + - ie + - iu + - ik + - ga + - it + - ja + - jv + - kn + - kr + - ks + - kk + - rw + - kv + - kg + - ko + - kj + - ku + - ky + - lo + - la + - lv + - lb + - li + - ln + - lt + - lu + - mk + - mg + - ms + - ml + - mt + - gv + - mi + - mr + - mh + - ro + - mn + - na + - nv + - nd + - ng + - ne + - se + - 'no' + - nb + - nn + - ii + - oc + - oj + - or + - om + - os + - pi + - pa + - ps + - fa + - pl + - pt + - qu + - rm + - rn + - ru + - sm + - sg + - sa + - sc + - sr + - sn + - sd + - si + - sk + - sl + - so + - st + - nr + - es + - su + - sw + - ss + - sv + - tl + - ty + - tg + - ta + - tt + - te + - th + - bo + - ti + - to + - ts + - tn + - tr + - tk + - tw + - ug + - uk + - ur + - uz + - ve + - vi + - vo + - wa + - cy + - fy + - wo + - xh + - yi + - yo + - za + - zu + type: + type: string + enum: + - AlternativeTitle + - Subtitle + - TranslatedTitle + - Other + LicenseDto: + required: + - identifier + - uri + type: object + properties: + identifier: + type: string + example: MIT + uri: + type: string + example: 'https://opensource.org/licenses/MIT' + description: + type: string + example: >- + A short and simple permissive license with conditions only requiring + preservation of copyright and license notices. Licensed works, + modifications, and larger works may be distributed under different + terms and without source code. + QueryDto: + required: + - created + - creator + - database_id + - execution + - id + - identifiers + - is_persisted + - last_modified + - query + - query_hash + - query_normalized + type: object + properties: + id: + type: integer + format: int64 + creator: + $ref: '#/components/schemas/UserDto' + execution: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + query: + type: string + example: SELECT `id` FROM `air_quality` + type: + type: string + example: query + enum: + - query + - view + identifiers: + type: array + items: + $ref: '#/components/schemas/IdentifierDto' + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + database_id: + type: integer + format: int64 + query_normalized: + type: string + example: SELECT `id` FROM `air_quality` + query_hash: + type: string + example: 17e682f060b5f8e47ea04c5c4855908b0a5ad612022260fe50e11ecb0cc0ab76 + is_persisted: + type: boolean + example: true + result_hash: + type: string + example: 17e682f060b5f8e47ea04c5c4855908b0a5ad612022260fe50e11ecb0cc0ab76 + result_number: + type: integer + format: int64 + example: 1 + last_modified: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + RelatedIdentifierDto: + required: + - id + - relation + - type + - value + type: object + properties: + id: + type: integer + format: int64 + value: + type: string + example: 10.70124/dc4zh-9ce78 + type: + type: string + example: DOI + enum: + - DOI + - URL + - URN + - ARK + - arXiv + - bibcode + - EAN13 + - EISSN + - Handle + - IGSN + - ISBN + - ISTC + - LISSN + - LSID + - PMID + - PURL + - UPC + - w3id + relation: + type: string + example: Cites + enum: + - IsCitedBy + - Cites + - IsSupplementTo + - IsSupplementedBy + - IsContinuedBy + - Continues + - IsDescribedBy + - Describes + - HasMetadata + - IsMetadataFor + - HasVersion + - IsVersionOf + - IsNewVersionOf + - IsPreviousVersionOf + - IsPartOf + - HasPart + - IsPublishedIn + - IsReferencedBy + - References + - IsDocumentedBy + - Documents + - IsCompiledBy + - Compiles + - IsVariantFormOf + - IsOriginalFormOf + - IsIdenticalTo + - IsReviewedBy + - Reviews + - IsDerivedFrom + - IsSourceOf + - IsRequiredBy + - Requires + - IsObsoletedBy + - Obsoletes + UserAttributesDto: + required: + - language + - theme + type: object + properties: + theme: + type: string + example: light + orcid: + type: string + example: 'https://orcid.org/0000-0002-1825-0097' + affiliation: + type: string + example: Brown University + language: + type: string + example: en + UserDto: + required: + - attributes + - id + - username + type: object + properties: + id: + type: string + format: uuid + example: 1ffc7b0e-9aeb-4e8b-b8f1-68f3936155b4 + username: + type: string + description: Only contains lowercase characters + example: jcarberry + name: + type: string + example: Josiah Carberry + attributes: + $ref: '#/components/schemas/UserAttributesDto' + qualified_name: + type: string + example: Josiah Carberry — @jcarberry + given_name: + type: string + example: Josiah + family_name: + type: string + example: Carberry + TupleDto: + required: + - data + type: object + properties: + data: + type: object + additionalProperties: + type: object + ImportCsvDto: + required: + - location + - separator + type: object + properties: + location: + type: string + example: file.csv + separator: + type: string + example: ',' + quote: + type: string + example: '"' + skip_lines: + minimum: 0 + type: integer + format: int64 + false_element: + type: string + true_element: + type: string + null_element: + type: string + example: NA + line_termination: + type: string + example: \r\n + ExecuteStatementDto: + required: + - statement + type: object + properties: + statement: + type: string + example: SELECT `id` FROM `air_quality` + ColumnStatisticDto: + required: + - mean + - median + - std_dev + - val_max + - val_min + type: object + properties: + mean: + type: number + median: + type: number + std_dev: + type: number + val_min: + type: number + val_max: + type: number + TableStatisticDto: + required: + - columns + - rows + type: object + properties: + columns: + type: object + additionalProperties: + $ref: '#/components/schemas/ColumnStatisticDto' + rows: + type: integer + format: int64 + TupleDeleteDto: + required: + - keys + type: object + properties: + keys: + type: object + additionalProperties: + type: object + ColumnBriefDto: + required: + - column_type + - database_id + - id + - internal_name + - name + - table_id + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + example: date + alias: + type: string + database_id: + type: integer + format: int64 + table_id: + type: integer + format: int64 + internal_name: + type: string + example: mdb_date + column_type: + type: string + example: date + enum: + - char + - varchar + - binary + - varbinary + - tinyblob + - tinytext + - text + - blob + - mediumtext + - mediumblob + - longtext + - longblob + - enum + - set + - bit + - tinyint + - bool + - smallint + - mediumint + - int + - bigint + - float + - double + - decimal + - date + - datetime + - timestamp + - time + - year + ColumnDto: + required: + - auto_generated + - column_type + - database_id + - id + - internal_name + - is_null_allowed + - is_public + - name + - ordinal_position + - table_id + type: object + properties: + id: + type: integer + format: int64 + name: + maxLength: 64 + minLength: 0 + type: string + example: Date + alias: + type: string + size: + type: integer + format: int64 + example: 255 + d: + type: integer + format: int64 + example: 0 + mean: + type: number + example: 45.4 + median: + type: number + example: 51 + concept: + $ref: '#/components/schemas/ConceptDto' + unit: + $ref: '#/components/schemas/UnitDto' + description: + maxLength: 2048 + minLength: 0 + type: string + example: Column comment + enums: + type: array + items: + type: string + sets: + type: array + items: + type: string + database_id: + type: integer + format: int64 + table_id: + type: integer + format: int64 + ordinal_position: + type: integer + format: int32 + example: 0 + internal_name: + maxLength: 64 + minLength: 0 + type: string + example: mdb_date + date_format: + $ref: '#/components/schemas/ImageDateDto' + auto_generated: + type: boolean + example: false + index_length: + type: integer + format: int64 + length: + type: integer + format: int64 + column_type: + type: string + example: string + enum: + - char + - varchar + - binary + - varbinary + - tinyblob + - tinytext + - text + - blob + - mediumtext + - mediumblob + - longtext + - longblob + - enum + - set + - bit + - tinyint + - bool + - smallint + - mediumint + - int + - bigint + - float + - double + - decimal + - date + - datetime + - timestamp + - time + - year + data_length: + type: integer + format: int64 + example: 34300 + max_data_length: + type: integer + format: int64 + example: 34300 + num_rows: + type: integer + format: int64 + example: 32 + val_min: + type: number + example: 0 + val_max: + type: number + example: 100 + std_dev: + type: number + example: 5.32 + is_public: + type: boolean + example: true + is_null_allowed: + type: boolean + example: false + ConceptDto: + required: + - columns + - created + - id + - uri + type: object + properties: + id: + type: integer + format: int64 + uri: + type: string + name: + type: string + description: + type: string + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + columns: + type: array + items: + $ref: '#/components/schemas/ColumnBriefDto' + ConstraintsDto: + type: object + properties: + uniques: + type: array + items: + $ref: '#/components/schemas/UniqueDto' + checks: + uniqueItems: true + type: array + items: + type: string + foreign_keys: + type: array + items: + $ref: '#/components/schemas/ForeignKeyDto' + primary_key: + uniqueItems: true + type: array + items: + $ref: '#/components/schemas/PrimaryKeyDto' + ContainerDto: + required: + - created + - host + - id + - image + - internal_name + - name + - sidecar_host + - sidecar_port + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + example: Air Quality + host: + type: string + port: + type: integer + format: int32 + image: + $ref: '#/components/schemas/ImageDto' + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + internal_name: + type: string + example: data-db + sidecar_host: + type: string + sidecar_port: + type: integer + format: int32 + ui_host: + type: string + ui_port: + type: integer + format: int32 + DatabaseAccessDto: + required: + - created + - type + - user + type: object + properties: + user: + $ref: '#/components/schemas/UserDto' + type: + type: string + enum: + - read + - write_own + - write_all + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + DatabaseDto: + required: + - contact + - container + - created + - creator + - exchange_name + - id + - internal_name + - is_public + - name + - owner + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + example: Air Quality + description: + type: string + example: Air Quality + tables: + type: array + items: + $ref: '#/components/schemas/TableDto' + views: + type: array + items: + $ref: '#/components/schemas/ViewDto' + container: + $ref: '#/components/schemas/ContainerDto' + accesses: + type: array + items: + $ref: '#/components/schemas/DatabaseAccessDto' + identifiers: + type: array + items: + $ref: '#/components/schemas/IdentifierDto' + subsets: + type: array + items: + $ref: '#/components/schemas/IdentifierDto' + creator: + $ref: '#/components/schemas/UserDto' + contact: + $ref: '#/components/schemas/UserDto' + owner: + $ref: '#/components/schemas/UserDto' + image: + type: array + items: + type: string + format: byte + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + exchange_name: + type: string + example: dbrepo + exchange_type: + type: string + example: topic + internal_name: + type: string + example: air_quality + is_public: + type: boolean + example: true + ForeignKeyBriefDto: + type: object + properties: + id: + type: integer + format: int64 + ForeignKeyDto: + required: + - name + - referenced_table + - references + - table + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + references: + type: array + items: + $ref: '#/components/schemas/ForeignKeyReferenceDto' + table: + $ref: '#/components/schemas/TableBriefDto' + referenced_table: + $ref: '#/components/schemas/TableBriefDto' + on_update: + type: string + enum: + - restrict + - cascade + - set_null + - no_action + - set_default + on_delete: + type: string + enum: + - restrict + - cascade + - set_null + - no_action + - set_default + ForeignKeyReferenceDto: + required: + - column + - foreign_key + - referenced_column + type: object + properties: + id: + type: integer + format: int64 + column: + $ref: '#/components/schemas/ColumnBriefDto' + foreign_key: + $ref: '#/components/schemas/ForeignKeyBriefDto' + referenced_column: + $ref: '#/components/schemas/ColumnBriefDto' + ImageDateDto: + required: + - created_at + - database_format + - has_time + - id + - unix_format + type: object + properties: + id: + type: integer + format: int64 + database_format: + type: string + example: '%d.%c.%Y' + unix_format: + type: string + example: dd.MM.YYYY + has_time: + type: boolean + example: false + created_at: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + ImageDto: + required: + - default_port + - dialect + - driver_class + - id + - jdbc_method + - name + - registry + - version + type: object + properties: + id: + type: integer + format: int64 + registry: + type: string + example: docker.io/library + name: + type: string + example: mariadb + version: + type: string + example: '10.5' + dialect: + type: string + example: org.hibernate.dialect.MariaDBDialect + driver_class: + type: string + example: org.mariadb.jdbc.Driver + date_formats: + type: array + items: + $ref: '#/components/schemas/ImageDateDto' + jdbc_method: + type: string + example: mariadb + default_port: + type: integer + format: int32 + example: 3306 + PrimaryKeyDto: + required: + - column + - table + type: object + properties: + id: + type: integer + format: int64 + table: + $ref: '#/components/schemas/TableBriefDto' + column: + $ref: '#/components/schemas/ColumnBriefDto' + TableBriefDto: + required: + - database_id + - id + - internal_name + - is_versioned + - name + - owner + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + example: Air Quality + description: + type: string + example: Air Quality in Austria + owner: + $ref: '#/components/schemas/UserBriefDto' + database_id: + type: integer + format: int64 + internal_name: + type: string + example: air_quality + is_versioned: + type: boolean + example: true + TableDto: + required: + - columns + - constraints + - created + - created_by + - creator + - database_id + - id + - internal_name + - is_public + - is_versioned + - name + - owner + - queue_name + - routing_key + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + example: Air Quality + alias: + type: string + identifiers: + type: array + items: + $ref: '#/components/schemas/IdentifierDto' + creator: + $ref: '#/components/schemas/UserDto' + owner: + $ref: '#/components/schemas/UserDto' + description: + maxLength: 2048 + minLength: 0 + type: string + example: Air Quality in Austria + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + columns: + type: array + items: + $ref: '#/components/schemas/ColumnDto' + constraints: + $ref: '#/components/schemas/ConstraintsDto' + database_id: + type: integer + format: int64 + internal_name: + type: string + example: air_quality + is_versioned: + type: boolean + example: true + created_by: + type: string + format: uuid + queue_name: + type: string + example: air_quality + queue_type: + type: string + example: quorum + routing_key: + type: string + example: dbrepo.1.2 + is_public: + type: boolean + example: true + num_rows: + type: integer + format: int64 + example: 5 + data_length: + type: integer + description: in bytes + format: int64 + example: 16384 + max_data_length: + type: integer + description: in bytes + format: int64 + example: 0 + avg_row_length: + type: integer + description: in bytes + format: int64 + example: 3276 + UniqueDto: + required: + - columns + - id + - name + - table + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + table: + $ref: '#/components/schemas/TableBriefDto' + columns: + type: array + items: + $ref: '#/components/schemas/ColumnDto' + UnitDto: + required: + - columns + - created + - id + - uri + type: object + properties: + id: + type: integer + format: int64 + uri: + type: string + name: + type: string + description: + type: string + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + columns: + type: array + items: + $ref: '#/components/schemas/ColumnBriefDto' + UserBriefDto: + required: + - id + - username + type: object + properties: + id: + type: string + format: uuid + example: 1ffc7b0e-9aeb-4e8b-b8f1-68f3936155b4 + username: + type: string + description: Only contains lowercase characters + example: jcarberry + name: + type: string + example: Josiah Carberry + orcid: + type: string + example: 0000-0002-1825-0097 + qualified_name: + type: string + example: Josiah Carberry — @jcarberry + given_name: + type: string + example: Josiah + family_name: + type: string + example: Carberry + ViewColumnDto: + required: + - auto_generated + - column_type + - database_id + - id + - internal_name + - is_null_allowed + - is_public + - name + - ordinal_position + type: object + properties: + id: + type: integer + format: int64 + name: + maxLength: 64 + minLength: 0 + type: string + example: Date + alias: + type: string + size: + type: integer + format: int64 + example: 255 + d: + type: integer + format: int64 + example: 0 + concept: + $ref: '#/components/schemas/ConceptDto' + unit: + $ref: '#/components/schemas/UnitDto' + description: + maxLength: 2048 + minLength: 0 + type: string + example: Column comment + database_id: + type: integer + format: int64 + ordinal_position: + type: integer + format: int32 + example: 0 + internal_name: + maxLength: 64 + minLength: 0 + type: string + example: mdb_date + date_format: + $ref: '#/components/schemas/ImageDateDto' + auto_generated: + type: boolean + example: false + index_length: + type: integer + format: int64 + length: + type: integer + format: int64 + column_type: + type: string + example: string + enum: + - char + - varchar + - binary + - varbinary + - tinyblob + - tinytext + - text + - blob + - mediumtext + - mediumblob + - longtext + - longblob + - enum + - set + - bit + - tinyint + - bool + - smallint + - mediumint + - int + - bigint + - float + - double + - decimal + - date + - datetime + - timestamp + - time + - year + is_public: + type: boolean + example: true + is_null_allowed: + type: boolean + example: false + ViewDto: + required: + - columns + - created + - creator + - database + - database_id + - id + - internal_name + - name + - query + - query_hash + type: object + properties: + id: + type: integer + format: int64 + database: + $ref: '#/components/schemas/DatabaseDto' + name: + type: string + example: Air Quality + identifiers: + type: array + items: + $ref: '#/components/schemas/IdentifierDto' + query: + type: string + example: SELECT `id` FROM `air_quality` ORDER BY `value` DESC + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + creator: + $ref: '#/components/schemas/UserDto' + columns: + type: array + items: + $ref: '#/components/schemas/ViewColumnDto' + database_id: + type: integer + format: int64 + internal_name: + type: string + example: air_quality + is_public: + type: boolean + example: true + initial_view: + type: boolean + description: True if it is the default view for the database + example: true + query_hash: + type: string + example: 7de03e818900b6ea6d58ad0306d4a741d658c6df3d1964e89ed2395d8c7e7916 + last_modified: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + UserUpdateDto: + required: + - language + - theme + type: object + properties: + firstname: + type: string + example: Josiah + lastname: + type: string + example: Carberry + affiliation: + type: string + example: Brown University + orcid: + type: string + example: 0000-0002-1825-0097 + theme: + type: string + example: dark + language: + type: string + example: en + UserPasswordDto: + required: + - password + type: object + properties: + password: + type: string + RefreshTokenRequestDto: + required: + - refresh_token + type: object + properties: + refresh_token: + type: string + example: refresh_token + TokenDto: + required: + - access_token + - expires_in + - id_token + - not-before-policy + - refresh_expires_in + - refresh_token + - scope + - session_state + - token_type + type: object + properties: + scope: + type: string + access_token: + type: string + expires_in: + type: integer + format: int64 + refresh_token: + type: string + refresh_expires_in: + type: integer + format: int64 + id_token: + type: string + session_state: + type: string + token_type: + type: string + not-before-policy: + type: integer + format: int64 + OntologyModifyDto: + required: + - prefix + - uri + type: object + properties: + uri: + type: string + example: Ontology URI + prefix: + type: string + example: Ontology prefix + sparql_endpoint: + type: string + example: Ontology SPARQL endpoint + rdf_path: + type: string + example: rdf/om-2.0.rdf + OntologyDto: + required: + - created + - id + - prefix + - rdf + - sparql + - uri + type: object + properties: + id: + type: integer + format: int64 + uri: + type: string + example: 'http://www.wikidata.org/' + prefix: + type: string + example: wd + sparql: + type: boolean + example: true + rdf: + type: boolean + example: false + creator: + $ref: '#/components/schemas/UserBriefDto' + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + uri_pattern: + type: string + example: 'http://www.wikidata.org/entity/.*' + sparql_endpoint: + type: string + example: 'https://query.wikidata.org/sparql' + rdf_path: + type: string + example: rdf/om-2.0.rdf + BannerMessageUpdateDto: + required: + - message + - type + type: object + properties: + type: + type: string + enum: + - error + - warning + - info + message: + type: string + example: Maintenance starts on 8am on Monday + link: + type: string + example: 'https://example.com' + link_text: + type: string + example: More + display_start: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + display_end: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + BannerMessageBriefDto: + required: + - message + - type + type: object + properties: + type: + type: string + enum: + - error + - warning + - info + message: + type: string + example: Maintenance starts on 8am on Monday + link: + type: string + example: 'https://example.com' + link_text: + type: string + example: More + ImageChangeDto: + required: + - dialect + - driver_class + - jdbc_method + - registry + type: object + properties: + registry: + type: string + example: docker.io/library + defaultPort: + maximum: 65535 + minimum: 1024 + type: integer + format: int32 + example: 5432 + dialect: + type: string + example: Postgres + driver_class: + type: string + example: org.postgresql.Driver + jdbc_method: + type: string + example: postgresql + CreatorSaveDto: + required: + - creator_name + - id + type: object + properties: + id: + type: integer + format: int64 + example: 1 + firstname: + type: string + example: Josiah + lastname: + type: string + example: Carberry + affiliation: + type: string + example: Wesleyan University + creator_name: + type: string + example: 'Carberry, Josiah' + name_type: + type: string + example: Personal + enum: + - Personal + - Organizational + name_identifier: + type: string + example: 0000-0002-1825-0097 + name_identifier_scheme: + type: string + example: ORCID + enum: + - ORCID + - ROR + - ISNI + - GRID + affiliation_identifier: + type: string + example: 'https://ror.org/04d836q62' + affiliation_identifier_scheme: + type: string + example: ROR + enum: + - ROR + - GRID + - ISNI + IdentifierFunderSaveDto: + required: + - funder_name + - id + type: object + properties: + id: + type: integer + format: int64 + example: 1 + funder_name: + type: string + example: European Commission + funder_identifier: + type: string + example: 'http://doi.org/10.13039/501100000780' + funder_identifier_type: + type: string + example: Crossref Funder ID + enum: + - Crossref Funder ID + - ROR + - GND + - ISNI + - Other + scheme_uri: + type: string + example: 'http://doi.org/' + award_number: + type: string + example: '824087' + award_title: + type: string + example: EOSC-Life + IdentifierSaveDescriptionDto: + required: + - description + - id + type: object + properties: + id: + type: integer + format: int64 + example: 1 + description: + type: string + example: 'Air quality reports at Stephansplatz, Vienna' + language: + type: string + example: en + enum: + - ab + - aa + - af + - ak + - sq + - am + - ar + - an + - hy + - as + - av + - ae + - ay + - az + - bm + - ba + - eu + - be + - bn + - bh + - bi + - bs + - br + - bg + - my + - ca + - km + - ch + - ce + - ny + - zh + - cu + - cv + - kw + - co + - cr + - hr + - cs + - da + - dv + - nl + - dz + - en + - eo + - et + - ee + - fo + - fj + - fi + - fr + - ff + - gd + - gl + - lg + - ka + - de + - ki + - el + - kl + - gn + - gu + - ht + - ha + - he + - hz + - hi + - ho + - hu + - is + - io + - ig + - id + - ia + - ie + - iu + - ik + - ga + - it + - ja + - jv + - kn + - kr + - ks + - kk + - rw + - kv + - kg + - ko + - kj + - ku + - ky + - lo + - la + - lv + - lb + - li + - ln + - lt + - lu + - mk + - mg + - ms + - ml + - mt + - gv + - mi + - mr + - mh + - ro + - mn + - na + - nv + - nd + - ng + - ne + - se + - 'no' + - nb + - nn + - ii + - oc + - oj + - or + - om + - os + - pi + - pa + - ps + - fa + - pl + - pt + - qu + - rm + - rn + - ru + - sm + - sg + - sa + - sc + - sr + - sn + - sd + - si + - sk + - sl + - so + - st + - nr + - es + - su + - sw + - ss + - sv + - tl + - ty + - tg + - ta + - tt + - te + - th + - bo + - ti + - to + - ts + - tn + - tr + - tk + - tw + - ug + - uk + - ur + - uz + - ve + - vi + - vo + - wa + - cy + - fy + - wo + - xh + - yi + - yo + - za + - zu + type: + type: string + example: Abstract + enum: + - Abstract + - Methods + - SeriesInformation + - TableOfContents + - TechnicalInfo + - Other + IdentifierSaveDto: + required: + - creators + - database_id + - id + - publication_year + - publisher + - titles + - type + type: object + properties: + id: + type: integer + format: int64 + example: 1 + type: + type: string + example: database + enum: + - database + - subset + - table + - view + doi: + type: string + example: 10.1111/11111111 + titles: + type: array + items: + $ref: '#/components/schemas/IdentifierSaveTitleDto' + descriptions: + type: array + items: + $ref: '#/components/schemas/IdentifierSaveDescriptionDto' + funders: + type: array + items: + $ref: '#/components/schemas/IdentifierFunderSaveDto' + licenses: + type: array + items: + $ref: '#/components/schemas/LicenseDto' + publisher: + type: string + example: TU Wien + language: + type: string + enum: + - ab + - aa + - af + - ak + - sq + - am + - ar + - an + - hy + - as + - av + - ae + - ay + - az + - bm + - ba + - eu + - be + - bn + - bh + - bi + - bs + - br + - bg + - my + - ca + - km + - ch + - ce + - ny + - zh + - cu + - cv + - kw + - co + - cr + - hr + - cs + - da + - dv + - nl + - dz + - en + - eo + - et + - ee + - fo + - fj + - fi + - fr + - ff + - gd + - gl + - lg + - ka + - de + - ki + - el + - kl + - gn + - gu + - ht + - ha + - he + - hz + - hi + - ho + - hu + - is + - io + - ig + - id + - ia + - ie + - iu + - ik + - ga + - it + - ja + - jv + - kn + - kr + - ks + - kk + - rw + - kv + - kg + - ko + - kj + - ku + - ky + - lo + - la + - lv + - lb + - li + - ln + - lt + - lu + - mk + - mg + - ms + - ml + - mt + - gv + - mi + - mr + - mh + - ro + - mn + - na + - nv + - nd + - ng + - ne + - se + - 'no' + - nb + - nn + - ii + - oc + - oj + - or + - om + - os + - pi + - pa + - ps + - fa + - pl + - pt + - qu + - rm + - rn + - ru + - sm + - sg + - sa + - sc + - sr + - sn + - sd + - si + - sk + - sl + - so + - st + - nr + - es + - su + - sw + - ss + - sv + - tl + - ty + - tg + - ta + - tt + - te + - th + - bo + - ti + - to + - ts + - tn + - tr + - tk + - tw + - ug + - uk + - ur + - uz + - ve + - vi + - vo + - wa + - cy + - fy + - wo + - xh + - yi + - yo + - za + - zu + creators: + type: array + items: + $ref: '#/components/schemas/CreatorSaveDto' + database_id: + type: integer + format: int64 + example: 1 + query_id: + type: integer + format: int64 + view_id: + type: integer + format: int64 + table_id: + type: integer + format: int64 + publication_day: + type: integer + format: int32 + example: 15 + publication_month: + type: integer + format: int32 + example: 12 + publication_year: + type: integer + format: int32 + example: 2022 + related_identifiers: + type: array + items: + $ref: '#/components/schemas/RelatedIdentifierSaveDto' + IdentifierSaveTitleDto: + required: + - id + - title + type: object + properties: + id: + type: integer + format: int64 + example: 1 + title: + type: string + example: Airquality Demonstrator + language: + type: string + example: en + enum: + - ab + - aa + - af + - ak + - sq + - am + - ar + - an + - hy + - as + - av + - ae + - ay + - az + - bm + - ba + - eu + - be + - bn + - bh + - bi + - bs + - br + - bg + - my + - ca + - km + - ch + - ce + - ny + - zh + - cu + - cv + - kw + - co + - cr + - hr + - cs + - da + - dv + - nl + - dz + - en + - eo + - et + - ee + - fo + - fj + - fi + - fr + - ff + - gd + - gl + - lg + - ka + - de + - ki + - el + - kl + - gn + - gu + - ht + - ha + - he + - hz + - hi + - ho + - hu + - is + - io + - ig + - id + - ia + - ie + - iu + - ik + - ga + - it + - ja + - jv + - kn + - kr + - ks + - kk + - rw + - kv + - kg + - ko + - kj + - ku + - ky + - lo + - la + - lv + - lb + - li + - ln + - lt + - lu + - mk + - mg + - ms + - ml + - mt + - gv + - mi + - mr + - mh + - ro + - mn + - na + - nv + - nd + - ng + - ne + - se + - 'no' + - nb + - nn + - ii + - oc + - oj + - or + - om + - os + - pi + - pa + - ps + - fa + - pl + - pt + - qu + - rm + - rn + - ru + - sm + - sg + - sa + - sc + - sr + - sn + - sd + - si + - sk + - sl + - so + - st + - nr + - es + - su + - sw + - ss + - sv + - tl + - ty + - tg + - ta + - tt + - te + - th + - bo + - ti + - to + - ts + - tn + - tr + - tk + - tw + - ug + - uk + - ur + - uz + - ve + - vi + - vo + - wa + - cy + - fy + - wo + - xh + - yi + - yo + - za + - zu + type: + type: string + example: Subtitle + enum: + - AlternativeTitle + - Subtitle + - TranslatedTitle + - Other + RelatedIdentifierSaveDto: + required: + - id + - relation + - type + - value + type: object + properties: + id: + type: integer + format: int64 + example: 1 + value: + type: string + example: 10.70124/dc4zh-9ce78 + type: + type: string + example: DOI + enum: + - DOI + - URL + - URN + - ARK + - arXiv + - bibcode + - EAN13 + - EISSN + - Handle + - IGSN + - ISBN + - ISTC + - LISSN + - LSID + - PMID + - PURL + - UPC + - w3id + relation: + type: string + example: Cites + enum: + - IsCitedBy + - Cites + - IsSupplementTo + - IsSupplementedBy + - IsContinuedBy + - Continues + - IsDescribedBy + - Describes + - HasMetadata + - IsMetadataFor + - HasVersion + - IsVersionOf + - IsNewVersionOf + - IsPreviousVersionOf + - IsPartOf + - HasPart + - IsPublishedIn + - IsReferencedBy + - References + - IsDocumentedBy + - Documents + - IsCompiledBy + - Compiles + - IsVariantFormOf + - IsOriginalFormOf + - IsIdenticalTo + - IsReviewedBy + - Reviews + - IsDerivedFrom + - IsSourceOf + - IsRequiredBy + - Requires + - IsObsoletedBy + - Obsoletes + DatabaseModifyVisibilityDto: + required: + - is_public + type: object + properties: + is_public: + type: boolean + example: true + ColumnSemanticsUpdateDto: + type: object + properties: + concept_uri: + type: string + unit_uri: + type: string + DatabaseTransferDto: + required: + - id + type: object + properties: + id: + type: string + format: uuid + DatabaseModifyImageDto: + type: object + properties: + key: + type: string + UpdateDatabaseAccessDto: + required: + - type + type: object + properties: + type: + type: string + enum: + - read + - write_own + - write_all + SignupRequestDto: + required: + - email + - password + - username + type: object + properties: + username: + pattern: '^[a-z0-9]{3,}$' + type: string + example: user + email: + type: string + example: user@example.com + password: + type: string + LoginRequestDto: + required: + - password + - username + type: object + properties: + username: + type: string + example: user + password: + type: string + OntologyCreateDto: + required: + - prefix + - uri + type: object + properties: + uri: + type: string + example: Ontology URI + prefix: + type: string + example: Ontology prefix + sparql_endpoint: + type: string + example: Ontology SPARQL endpoint + BannerMessageCreateDto: + required: + - message + - type + type: object + properties: + type: + type: string + enum: + - error + - warning + - info + message: + type: string + example: Maintenance starts on 8am on Monday + link: + type: string + example: 'https://example.com' + link_text: + type: string + example: More + display_start: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + display_end: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + ImageCreateDto: + required: + - default_port + - dialect + - driver_class + - jdbc_method + - name + - registry + - version + type: object + properties: + registry: + type: string + example: docker.io/library + name: + type: string + example: mariadb + version: + type: string + dialect: + type: string + driver_class: + type: string + jdbc_method: + type: string + default_port: + maximum: 65535 + minimum: 1024 + type: integer + format: int32 + IdentifierCreateDto: + required: + - creators + - database_id + - publication_year + - publisher + - titles + - type + type: object + properties: + type: + type: string + example: database + enum: + - database + - subset + - table + - view + doi: + type: string + example: 10.1111/11111111 + titles: + type: array + items: + $ref: '#/components/schemas/IdentifierSaveTitleDto' + descriptions: + type: array + items: + $ref: '#/components/schemas/IdentifierSaveDescriptionDto' + funders: + type: array + items: + $ref: '#/components/schemas/IdentifierFunderSaveDto' + licenses: + type: array + items: + $ref: '#/components/schemas/LicenseDto' + publisher: + type: string + example: TU Wien + language: + type: string + enum: + - ab + - aa + - af + - ak + - sq + - am + - ar + - an + - hy + - as + - av + - ae + - ay + - az + - bm + - ba + - eu + - be + - bn + - bh + - bi + - bs + - br + - bg + - my + - ca + - km + - ch + - ce + - ny + - zh + - cu + - cv + - kw + - co + - cr + - hr + - cs + - da + - dv + - nl + - dz + - en + - eo + - et + - ee + - fo + - fj + - fi + - fr + - ff + - gd + - gl + - lg + - ka + - de + - ki + - el + - kl + - gn + - gu + - ht + - ha + - he + - hz + - hi + - ho + - hu + - is + - io + - ig + - id + - ia + - ie + - iu + - ik + - ga + - it + - ja + - jv + - kn + - kr + - ks + - kk + - rw + - kv + - kg + - ko + - kj + - ku + - ky + - lo + - la + - lv + - lb + - li + - ln + - lt + - lu + - mk + - mg + - ms + - ml + - mt + - gv + - mi + - mr + - mh + - ro + - mn + - na + - nv + - nd + - ng + - ne + - se + - 'no' + - nb + - nn + - ii + - oc + - oj + - or + - om + - os + - pi + - pa + - ps + - fa + - pl + - pt + - qu + - rm + - rn + - ru + - sm + - sg + - sa + - sc + - sr + - sn + - sd + - si + - sk + - sl + - so + - st + - nr + - es + - su + - sw + - ss + - sv + - tl + - ty + - tg + - ta + - tt + - te + - th + - bo + - ti + - to + - ts + - tn + - tr + - tk + - tw + - ug + - uk + - ur + - uz + - ve + - vi + - vo + - wa + - cy + - fy + - wo + - xh + - yi + - yo + - za + - zu + creators: + type: array + items: + $ref: '#/components/schemas/CreatorSaveDto' + database_id: + type: integer + format: int64 + example: 1 + query_id: + type: integer + format: int64 + view_id: + type: integer + format: int64 + table_id: + type: integer + format: int64 + publication_day: + type: integer + format: int32 + example: 15 + publication_month: + type: integer + format: int32 + example: 12 + publication_year: + type: integer + format: int32 + example: 2022 + related_identifiers: + type: array + items: + $ref: '#/components/schemas/RelatedIdentifierSaveDto' + DatabaseCreateDto: + required: + - container_id + - is_public + - name + type: object + properties: + name: + type: string + example: Air Quality + container_id: + type: integer + format: int64 + example: 1 + is_public: + type: boolean + example: true + ViewCreateDto: + required: + - is_public + - name + - query + type: object + properties: + name: + maxLength: 64 + minLength: 1 + type: string + example: Air Quality + query: + type: string + example: SELECT `id` FROM `air_quality` + is_public: + type: boolean + example: true + ViewBriefDto: + required: + - created + - creator + - database_id + - id + - internal_name + - name + - query + - query_hash + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + example: Air Quality + identifier: + $ref: '#/components/schemas/IdentifierDto' + query: + type: string + example: SELECT `id` FROM `air_quality` ORDER BY `value` DESC + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + creator: + $ref: '#/components/schemas/UserDto' + database_id: + type: integer + format: int64 + internal_name: + type: string + example: air_quality + is_public: + type: boolean + example: true + initial_view: + type: boolean + description: True if it is the default view for the database + example: true + query_hash: + type: string + example: 7de03e818900b6ea6d58ad0306d4a741d658c6df3d1964e89ed2395d8c7e7916 + last_modified: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + ColumnCreateDto: + required: + - name + - null_allowed + - type + type: object + properties: + name: + type: string + example: Date + type: + type: string + example: string + enum: + - char + - varchar + - binary + - varbinary + - tinyblob + - tinytext + - text + - blob + - mediumtext + - mediumblob + - longtext + - longblob + - enum + - set + - bit + - tinyint + - bool + - smallint + - mediumint + - int + - bigint + - float + - double + - decimal + - date + - datetime + - timestamp + - time + - year + size: + type: integer + format: int64 + example: 255 + d: + type: integer + format: int64 + example: 0 + description: + maxLength: 2048 + minLength: 0 + type: string + example: Formatted as YYYY-MM-dd + dfid: + type: integer + description: date format id + format: int64 + enums: + type: array + description: 'enum values, only considered when type = ENUM' + items: + type: string + description: 'enum values, only considered when type = ENUM' + sets: + type: array + description: 'set values, only considered when type = SET' + items: + type: string + description: 'set values, only considered when type = SET' + index_length: + type: integer + format: int64 + null_allowed: + type: boolean + example: true + concept_uri: + type: string + unit_uri: + type: string + ConstraintsCreateDto: + required: + - checks + - foreign_keys + - primary_key + - uniques + type: object + properties: + uniques: + type: array + items: + type: array + items: + type: string + checks: + uniqueItems: true + type: array + items: + type: string + foreign_keys: + type: array + items: + $ref: '#/components/schemas/ForeignKeyCreateDto' + primary_key: + uniqueItems: true + type: array + items: + type: string + ForeignKeyCreateDto: + required: + - columns + - referenced_columns + - referenced_table + type: object + properties: + columns: + type: array + items: + type: string + referenced_table: + type: string + referenced_columns: + type: array + items: + type: string + on_update: + type: string + enum: + - restrict + - cascade + - set_null + - no_action + - set_default + on_delete: + type: string + enum: + - restrict + - cascade + - set_null + - no_action + - set_default + TableCreateDto: + required: + - columns + - constraints + - name + type: object + properties: + name: + maxLength: 64 + minLength: 1 + type: string + example: Air Quality + description: + maxLength: 180 + minLength: 0 + type: string + example: Air Quality in Austria + columns: + type: array + items: + $ref: '#/components/schemas/ColumnCreateDto' + constraints: + $ref: '#/components/schemas/ConstraintsCreateDto' + need_sequence: + type: boolean + ContainerCreateDto: + required: + - host + - image_id + - name + - privileged_password + - privileged_username + - sidecar_host + - sidecar_port + type: object + properties: + name: + type: string + example: Air Quality + host: + type: string + description: Hostname of container + port: + type: integer + description: Port of container + format: int32 + image_id: + type: integer + description: Image ID + format: int64 + sidecar_host: + type: string + sidecar_port: + type: integer + format: int32 + ui_host: + type: string + ui_port: + type: integer + format: int32 + privileged_username: + type: string + description: Username of privileged user + example: root + privileged_password: + type: string + description: Password of privileged user + ContainerBriefDto: + required: + - created + - hash + - id + - image + - internal_name + - name + - running + type: object + properties: + id: + type: integer + format: int64 + hash: + type: string + example: f829dd8a884182d0da846f365dee1221fd16610a14c81b8f9f295ff162749e50 + name: + type: string + example: Air Quality + image: + $ref: '#/components/schemas/ImageBriefDto' + running: + type: boolean + example: true + created: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + internal_name: + type: string + example: air-quality + ImageBriefDto: + required: + - id + - jdbc_method + - name + - version + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + example: mariadb + version: + type: string + example: '10.5' + jdbc_method: + type: string + example: mariadb + EntityDto: + required: + - label + - uri + type: object + properties: + uri: + type: string + example: 'https://www.wikidata.org/entity/Q1686799' + label: + type: string + example: Apache Jena + description: + type: string + example: open source semantic web framework for Java + OaiListIdentifiersParameters: + type: object + properties: + metadataPrefix: + type: string + from: + type: string + until: + type: string + set: + type: string + resumptionToken: + type: string + fromDate: + type: string + format: date-time + untilDate: + type: string + format: date-time + parametersString: + type: string + BannerMessageDto: + required: + - id + - message + - type + type: object + properties: + id: + type: integer + format: int64 + type: + type: string + enum: + - error + - warning + - info + message: + type: string + example: Maintenance starts on 8am on Monday + link: + type: string + example: 'https://example.com' + link_text: + type: string + example: More + display_start: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + display_end: + type: string + format: date-time + example: '2021-03-12T15:26:21.000Z' + Constraints: + type: object + properties: + uniques: + type: array + items: + $ref: '#/components/schemas/Unique' + foreignKeys: + type: array + items: + $ref: '#/components/schemas/ForeignKey' + checks: + uniqueItems: true + type: array + items: + type: string + primaryKey: + type: array + items: + $ref: '#/components/schemas/PrimaryKey' + Container: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + internalName: + type: string + host: + type: string + port: + type: integer + format: int32 + sidecarHost: + type: string + sidecarPort: + type: integer + format: int32 + uiHost: + type: string + uiPort: + type: integer + format: int32 + uiAdditionalFlags: + type: string + databases: + type: array + items: + $ref: '#/components/schemas/Database' + image: + $ref: '#/components/schemas/ContainerImage' + created: + type: string + format: date-time + lastModified: + type: string + format: date-time + privilegedUsername: + type: string + privilegedPassword: + type: string + ContainerImage: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + registry: + type: string + version: + type: string + driverClass: + type: string + dialect: + type: string + jdbcMethod: + type: string + defaultPort: + type: integer + format: int32 + dateFormats: + type: array + items: + $ref: '#/components/schemas/ContainerImageDate' + containers: + type: array + items: + $ref: '#/components/schemas/Container' + created: + type: string + format: date-time + lastModified: + type: string + format: date-time + ContainerImageDate: + type: object + properties: + id: + type: integer + format: int64 + iid: + type: integer + format: int64 + image: + $ref: '#/components/schemas/ContainerImage' + example: + type: string + hasTime: + type: boolean + databaseFormat: + type: string + unixFormat: + type: string + createdAt: + type: string + format: date-time + Creator: + type: object + properties: + id: + type: integer + format: int64 + firstname: + type: string + lastname: + type: string + creatorName: + type: string + nameType: + type: string + enum: + - PERSONAL + - ORGANIZATIONAL + nameIdentifier: + type: string + nameIdentifierScheme: + type: string + enum: + - ORCID + - ROR + - ISNI + - GRID + nameIdentifierSchemeUri: + type: string + affiliation: + type: string + affiliationIdentifier: + type: string + affiliationIdentifierScheme: + type: string + enum: + - ROR + - GRID + - ISNI + affiliationIdentifierSchemeUri: + type: string + identifier: + $ref: '#/components/schemas/Identifier' + apaName: + type: string + bibtexName: + type: string + ieeeName: + type: string + Database: + type: object + properties: + id: + type: integer + format: int64 + createdBy: + type: string + format: uuid + creator: + $ref: '#/components/schemas/User' + ownedBy: + type: string + format: uuid + owner: + $ref: '#/components/schemas/User' + cid: + type: integer + format: int64 + container: + $ref: '#/components/schemas/Container' + name: + type: string + internalName: + type: string + exchangeName: + type: string + description: + type: string + contactPerson: + type: string + format: uuid + contact: + $ref: '#/components/schemas/User' + identifiers: + type: array + items: + $ref: '#/components/schemas/Identifier' + subsets: + type: array + items: + $ref: '#/components/schemas/Identifier' + tables: + type: array + items: + $ref: '#/components/schemas/Table' + views: + type: array + items: + $ref: '#/components/schemas/View' + accesses: + type: array + items: + $ref: '#/components/schemas/DatabaseAccess' + isPublic: + type: boolean + image: + type: array + items: + type: string + format: byte + created: + type: string + format: date-time + lastModified: + type: string + format: date-time + DatabaseAccess: + type: object + properties: + huserid: + type: string + format: uuid + user: + $ref: '#/components/schemas/User' + hdbid: + type: integer + format: int64 + database: + $ref: '#/components/schemas/Database' + type: + type: string + enum: + - AccessType.READ + - AccessType.WRITE_OWN + - AccessType.WRITE_ALL + created: + type: string + format: date-time + ForeignKey: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + table: + $ref: '#/components/schemas/Table' + referencedTable: + $ref: '#/components/schemas/Table' + references: + type: array + items: + $ref: '#/components/schemas/ForeignKeyReference' + onUpdate: + type: string + enum: + - ReferenceType.RESTRICT + - ReferenceType.CASCADE + - ReferenceType.SET_NULL + - ReferenceType.NO_ACTION + - ReferenceType.SET_DEFAULT + onDelete: + type: string + enum: + - ReferenceType.RESTRICT + - ReferenceType.CASCADE + - ReferenceType.SET_NULL + - ReferenceType.NO_ACTION + - ReferenceType.SET_DEFAULT + ForeignKeyReference: + type: object + properties: + id: + type: integer + format: int64 + foreignKey: + $ref: '#/components/schemas/ForeignKey' + column: + $ref: '#/components/schemas/TableColumn' + referencedColumn: + $ref: '#/components/schemas/TableColumn' + Identifier: + type: object + properties: + id: + type: integer + format: int64 + queryId: + type: integer + format: int64 + tableId: + type: integer + format: int64 + viewId: + type: integer + format: int64 + creators: + type: array + items: + $ref: '#/components/schemas/Creator' + publisher: + type: string + status: + type: string + enum: + - DRAFT + - PUBLISHED + language: + type: string + enum: + - ab + - aa + - af + - ak + - sq + - am + - ar + - an + - hy + - as + - av + - ae + - ay + - az + - bm + - ba + - eu + - be + - bn + - bh + - bi + - bs + - br + - bg + - my + - ca + - km + - ch + - ce + - ny + - zh + - cu + - cv + - kw + - co + - cr + - hr + - cs + - da + - dv + - nl + - dz + - en + - eo + - et + - ee + - fo + - fj + - fi + - fr + - ff + - gd + - gl + - lg + - ka + - de + - ki + - el + - kl + - gn + - gu + - ht + - ha + - he + - hz + - hi + - ho + - hu + - is + - io + - ig + - id + - ia + - ie + - iu + - ik + - ga + - it + - ja + - jv + - kn + - kr + - ks + - kk + - rw + - kv + - kg + - ko + - kj + - ku + - ky + - lo + - la + - lv + - lb + - li + - ln + - lt + - lu + - mk + - mg + - ms + - ml + - mt + - gv + - mi + - mr + - mh + - ro + - mn + - na + - nv + - nd + - ng + - ne + - se + - 'no' + - nb + - nn + - ii + - oc + - oj + - or + - om + - os + - pi + - pa + - ps + - fa + - pl + - pt + - qu + - rm + - rn + - ru + - sm + - sg + - sa + - sc + - sr + - sn + - sd + - si + - sk + - sl + - so + - st + - nr + - es + - su + - sw + - ss + - sv + - tl + - ty + - tg + - ta + - tt + - te + - th + - bo + - ti + - to + - ts + - tn + - tr + - tk + - tw + - ug + - uk + - ur + - uz + - ve + - vi + - vo + - wa + - cy + - fy + - wo + - xh + - yi + - yo + - za + - zu + titles: + type: array + items: + $ref: '#/components/schemas/IdentifierTitle' + descriptions: + type: array + items: + $ref: '#/components/schemas/IdentifierDescription' + funders: + type: array + items: + $ref: '#/components/schemas/IdentifierFunder' + licenses: + type: array + items: + $ref: '#/components/schemas/License' + type: + type: string + enum: + - DATABASE + - SUBSET + - TABLE + - VIEW + query: + type: string + queryNormalized: + type: string + queryHash: + type: string + resultHash: + type: string + execution: + type: string + format: date-time + resultNumber: + type: integer + format: int64 + publicationYear: + type: integer + format: int32 + publicationMonth: + type: integer + format: int32 + publicationDay: + type: integer + format: int32 + database: + $ref: '#/components/schemas/Database' + relatedIdentifiers: + type: array + items: + $ref: '#/components/schemas/RelatedIdentifier' + doi: + type: string + createdBy: + type: string + format: uuid + creator: + $ref: '#/components/schemas/User' + created: + type: string + format: date-time + lastModified: + type: string + format: date-time + IdentifierDescription: + type: object + properties: + id: + type: integer + format: int64 + description: + type: string + descriptionType: + type: string + enum: + - Abstract + - Methods + - SeriesInformation + - TableOfContents + - TechnicalInfo + - Other + language: + type: string + enum: + - ab + - aa + - af + - ak + - sq + - am + - ar + - an + - hy + - as + - av + - ae + - ay + - az + - bm + - ba + - eu + - be + - bn + - bh + - bi + - bs + - br + - bg + - my + - ca + - km + - ch + - ce + - ny + - zh + - cu + - cv + - kw + - co + - cr + - hr + - cs + - da + - dv + - nl + - dz + - en + - eo + - et + - ee + - fo + - fj + - fi + - fr + - ff + - gd + - gl + - lg + - ka + - de + - ki + - el + - kl + - gn + - gu + - ht + - ha + - he + - hz + - hi + - ho + - hu + - is + - io + - ig + - id + - ia + - ie + - iu + - ik + - ga + - it + - ja + - jv + - kn + - kr + - ks + - kk + - rw + - kv + - kg + - ko + - kj + - ku + - ky + - lo + - la + - lv + - lb + - li + - ln + - lt + - lu + - mk + - mg + - ms + - ml + - mt + - gv + - mi + - mr + - mh + - ro + - mn + - na + - nv + - nd + - ng + - ne + - se + - 'no' + - nb + - nn + - ii + - oc + - oj + - or + - om + - os + - pi + - pa + - ps + - fa + - pl + - pt + - qu + - rm + - rn + - ru + - sm + - sg + - sa + - sc + - sr + - sn + - sd + - si + - sk + - sl + - so + - st + - nr + - es + - su + - sw + - ss + - sv + - tl + - ty + - tg + - ta + - tt + - te + - th + - bo + - ti + - to + - ts + - tn + - tr + - tk + - tw + - ug + - uk + - ur + - uz + - ve + - vi + - vo + - wa + - cy + - fy + - wo + - xh + - yi + - yo + - za + - zu + identifier: + $ref: '#/components/schemas/Identifier' + IdentifierFunder: + type: object + properties: + id: + type: integer + format: int64 + funderName: + type: string + funderIdentifier: + type: string + funderIdentifierType: + type: string + enum: + - CROSSREF_FUNDER_ID + - ROR + - GND + - ISNI + - OTHER + schemeUri: + type: string + awardNumber: + type: string + awardTitle: + type: string + identifier: + $ref: '#/components/schemas/Identifier' + IdentifierTitle: + type: object + properties: + id: + type: integer + format: int64 + title: + type: string + titleType: + type: string + enum: + - AlternativeTitle + - Subtitle + - TranslatedTitle + - Other + language: + type: string + enum: + - ab + - aa + - af + - ak + - sq + - am + - ar + - an + - hy + - as + - av + - ae + - ay + - az + - bm + - ba + - eu + - be + - bn + - bh + - bi + - bs + - br + - bg + - my + - ca + - km + - ch + - ce + - ny + - zh + - cu + - cv + - kw + - co + - cr + - hr + - cs + - da + - dv + - nl + - dz + - en + - eo + - et + - ee + - fo + - fj + - fi + - fr + - ff + - gd + - gl + - lg + - ka + - de + - ki + - el + - kl + - gn + - gu + - ht + - ha + - he + - hz + - hi + - ho + - hu + - is + - io + - ig + - id + - ia + - ie + - iu + - ik + - ga + - it + - ja + - jv + - kn + - kr + - ks + - kk + - rw + - kv + - kg + - ko + - kj + - ku + - ky + - lo + - la + - lv + - lb + - li + - ln + - lt + - lu + - mk + - mg + - ms + - ml + - mt + - gv + - mi + - mr + - mh + - ro + - mn + - na + - nv + - nd + - ng + - ne + - se + - 'no' + - nb + - nn + - ii + - oc + - oj + - or + - om + - os + - pi + - pa + - ps + - fa + - pl + - pt + - qu + - rm + - rn + - ru + - sm + - sg + - sa + - sc + - sr + - sn + - sd + - si + - sk + - sl + - so + - st + - nr + - es + - su + - sw + - ss + - sv + - tl + - ty + - tg + - ta + - tt + - te + - th + - bo + - ti + - to + - ts + - tn + - tr + - tk + - tw + - ug + - uk + - ur + - uz + - ve + - vi + - vo + - wa + - cy + - fy + - wo + - xh + - yi + - yo + - za + - zu + identifier: + $ref: '#/components/schemas/Identifier' + License: + type: object + properties: + identifier: + type: string + uri: + type: string + description: + type: string + PrimaryKey: + type: object + properties: + id: + type: integer + format: int64 + table: + $ref: '#/components/schemas/Table' + column: + $ref: '#/components/schemas/TableColumn' + RelatedIdentifier: + type: object + properties: + id: + type: integer + format: int64 + value: + type: string + type: + type: string + enum: + - DOI + - URL + - URN + - ARK + - arXiv + - bibcode + - EAN13 + - EISSN + - Handle + - IGSN + - ISBN + - ISTC + - LISSN + - LSID + - PMID + - PURL + - UPC + - w3id + relation: + type: string + enum: + - IsCitedBy + - Cites + - IsSupplementTo + - IsSupplementedBy + - IsContinuedBy + - Continues + - IsDescribedBy + - Describes + - HasMetadata + - IsMetadataFor + - HasVersion + - IsVersionOf + - IsNewVersionOf + - IsPreviousVersionOf + - IsPartOf + - HasPart + - IsPublishedIn + - IsReferencedBy + - References + - IsDocumentedBy + - Documents + - IsCompiledBy + - Compiles + - IsVariantFormOf + - IsOriginalFormOf + - IsIdenticalTo + - IsReviewedBy + - Reviews + - IsDerivedFrom + - IsSourceOf + - IsRequiredBy + - Requires + - IsObsoletedBy + - Obsoletes + identifier: + $ref: '#/components/schemas/Identifier' + Table: + type: object + properties: + id: + type: integer + format: int64 + tdbid: + type: integer + format: int64 + createdBy: + type: string + format: uuid + creator: + $ref: '#/components/schemas/User' + ownedBy: + type: string + format: uuid + owner: + $ref: '#/components/schemas/User' + name: + type: string + internalName: + type: string + queueName: + type: string + description: + type: string + database: + $ref: '#/components/schemas/Database' + columns: + type: array + items: + $ref: '#/components/schemas/TableColumn' + identifiers: + type: array + items: + $ref: '#/components/schemas/Identifier' + constraints: + $ref: '#/components/schemas/Constraints' + isVersioned: + type: boolean + numRows: + type: integer + format: int64 + dataLength: + type: integer + format: int64 + maxDataLength: + type: integer + format: int64 + avgRowLength: + type: integer + format: int64 + created: + type: string + format: date-time + lastModified: + type: string + format: date-time + TableColumn: + type: object + properties: + id: + type: integer + format: int64 + dateFormat: + $ref: '#/components/schemas/ContainerImageDate' + table: + $ref: '#/components/schemas/Table' + name: + type: string + autoGenerated: + type: boolean + internalName: + type: string + description: + type: string + indexLength: + type: integer + format: int64 + alias: + type: string + columnType: + type: string + enum: + - TableColumnType.CHAR + - TableColumnType.VARCHAR + - TableColumnType.BINARY + - TableColumnType.VARBINARY + - TableColumnType.TINYBLOB + - TableColumnType.TINYTEXT + - TableColumnType.TEXT + - TableColumnType.BLOB + - TableColumnType.MEDIUMTEXT + - TableColumnType.MEDIUMBLOB + - TableColumnType.LONGTEXT + - TableColumnType.LONGBLOB + - TableColumnType.ENUM + - TableColumnType.SET + - TableColumnType.BIT + - TableColumnType.TINYINT + - TableColumnType.BOOL + - TableColumnType.SMALLINT + - TableColumnType.MEDIUMINT + - TableColumnType.INT + - TableColumnType.BIGINT + - TableColumnType.FLOAT + - TableColumnType.DOUBLE + - TableColumnType.DECIMAL + - TableColumnType.DATE + - TableColumnType.DATETIME + - TableColumnType.TIMESTAMP + - TableColumnType.TIME + - TableColumnType.YEAR + length: + type: integer + format: int64 + isNullAllowed: + type: boolean + ordinalPosition: + type: integer + format: int32 + created: + type: string + format: date-time + concept: + $ref: '#/components/schemas/TableColumnConcept' + unit: + $ref: '#/components/schemas/TableColumnUnit' + enums: + type: array + items: + type: string + sets: + type: array + items: + type: string + size: + type: integer + format: int64 + d: + type: integer + format: int64 + min: + type: number + max: + type: number + mean: + type: number + median: + type: number + stdDev: + type: number + lastModified: + type: string + format: date-time + TableColumnConcept: + type: object + properties: + id: + type: integer + format: int64 + uri: + type: string + name: + type: string + description: + type: string + created: + type: string + format: date-time + columns: + type: array + items: + $ref: '#/components/schemas/TableColumn' + TableColumnUnit: + type: object + properties: + id: + type: integer + format: int64 + uri: + type: string + name: + type: string + description: + type: string + created: + type: string + format: date-time + columns: + type: array + items: + $ref: '#/components/schemas/TableColumn' + Unique: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + table: + $ref: '#/components/schemas/Table' + columns: + type: array + items: + $ref: '#/components/schemas/TableColumn' + User: + type: object + properties: + id: + type: string + format: uuid + username: + type: string + firstname: + type: string + lastname: + type: string + email: + type: string + orcid: + type: string + affiliation: + type: string + language: + type: string + accesses: + type: array + items: + $ref: '#/components/schemas/DatabaseAccess' + theme: + type: string + mariadbPassword: + type: string + View: + type: object + properties: + id: + type: integer + format: int64 + vdbid: + type: integer + format: int64 + createdBy: + type: string + format: uuid + creator: + $ref: '#/components/schemas/User' + name: + type: string + internalName: + type: string + isPublic: + type: boolean + isInitialView: + type: boolean + query: + type: string + queryHash: + type: string + identifiers: + type: array + items: + $ref: '#/components/schemas/Identifier' + database: + $ref: '#/components/schemas/Database' + columns: + type: array + items: + $ref: '#/components/schemas/ViewColumn' + created: + type: string + format: date-time + lastModified: + type: string + format: date-time + ViewColumn: + type: object + properties: + id: + type: integer + format: int64 + dateFormat: + $ref: '#/components/schemas/ContainerImageDate' + view: + $ref: '#/components/schemas/View' + name: + type: string + autoGenerated: + type: boolean + internalName: + type: string + columnType: + type: string + enum: + - TableColumnType.CHAR + - TableColumnType.VARCHAR + - TableColumnType.BINARY + - TableColumnType.VARBINARY + - TableColumnType.TINYBLOB + - TableColumnType.TINYTEXT + - TableColumnType.TEXT + - TableColumnType.BLOB + - TableColumnType.MEDIUMTEXT + - TableColumnType.MEDIUMBLOB + - TableColumnType.LONGTEXT + - TableColumnType.LONGBLOB + - TableColumnType.ENUM + - TableColumnType.SET + - TableColumnType.BIT + - TableColumnType.TINYINT + - TableColumnType.BOOL + - TableColumnType.SMALLINT + - TableColumnType.MEDIUMINT + - TableColumnType.INT + - TableColumnType.BIGINT + - TableColumnType.FLOAT + - TableColumnType.DOUBLE + - TableColumnType.DECIMAL + - TableColumnType.DATE + - TableColumnType.DATETIME + - TableColumnType.TIMESTAMP + - TableColumnType.TIME + - TableColumnType.YEAR + isNullAllowed: + type: boolean + ordinalPosition: + type: integer + format: int32 + size: + type: integer + format: int64 + d: + type: integer + format: int64 + LdCreatorDto: + required: + - '@type' + - name + type: object + properties: + name: + type: string + sameAs: + type: string + givenName: + type: string + familyName: + type: string + '@type': + type: string + LdDatasetDto: + required: + - '@context' + - '@type' + - citation + - creator + - description + - hasPart + - identifier + - name + - temporalCoverage + - url + - version + type: object + properties: + name: + type: string + description: + type: string + url: + type: string + identifier: + type: array + items: + type: string + license: + type: string + creator: + type: array + items: + $ref: '#/components/schemas/LdCreatorDto' + citation: + type: string + hasPart: + type: array + items: + $ref: '#/components/schemas/LdDatasetDto' + temporalCoverage: + type: string + version: + type: string + format: date-time + '@context': + type: string + '@type': + type: string + TableColumnEntityDto: + required: + - column_id + - database_id + - table_id + - uri + type: object + properties: + uri: + type: string + example: 'https://www.wikidata.org/entity/Q1686799' + label: + type: string + example: Apache Jena + description: + type: string + example: open source semantic web framework for Java + database_id: + type: integer + format: int64 + example: 1 + table_id: + type: integer + format: int64 + example: 1 + column_id: + type: integer + format: int64 + example: 1 + IndexDto: + properties: + results: + items: + type: object + type: array + type: + description: Same as the requested type + enum: + - database + - table + - view + - column + - user + - identifier + - concept + - unit + type: string + required: + - results + - type + IndexFieldDto: + properties: + attr_friendly_name: + example: Name + type: string + attr_name: + example: name + type: string + type: + description: OpenSearch data types. + example: string + type: string + required: + - attr_name + - attr_friendly_name + - type + type: object + IndexFieldsDto: + properties: + results: + items: + $ref: '#/components/schemas/IndexFieldDto' + type: array + required: + - results + type: object + SearchRequestDto: + properties: + field_value_pairs: + type: object + search_term: + type: string + required: + - search_term + - field_value_pairs + type: object + SearchResultDto: + properties: + results: + items: + type: object + type: array + required: + - results + type: object diff --git a/.docs/.swagger/openapi-merge.json b/.docs/.swagger/openapi-merge.json new file mode 100644 index 0000000000..af25a7582f --- /dev/null +++ b/.docs/.swagger/openapi-merge.json @@ -0,0 +1,23 @@ +{ + "inputs": [ + { + "inputFile": "./api.base.yaml" + }, + { + "inputFile": "./api-analyse.yaml" + }, + { + "inputFile": "./api-data.yaml" + }, + { + "inputFile": "./api-metadata.yaml" + }, + { + "inputFile": "./api-search.yaml" + }, + { + "inputFile": "./api-sidecar.yaml" + } + ], + "output": "./api.yaml" +} diff --git a/.docs/.swagger/swagger-ui.html b/.docs/.swagger/swagger-ui.html index 98f7cb441f..0bb08a1c07 100644 --- a/.docs/.swagger/swagger-ui.html +++ b/.docs/.swagger/swagger-ui.html @@ -7,7 +7,7 @@ <title>DBRepo REST API</title> <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.17.12/swagger-ui.css"/> <link rel="stylesheet" href="./custom.css"/> - <link rel="icon" href="https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/master/.docs/images/signet_white.png" /> + <link rel="icon" href="https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/master/.docs/images/logos/favicon.png" /> </head> <body> <div class="swagger-ui"> diff --git a/.docs/api/analyse-service.md b/.docs/api/analyse-service.md index be5efdbf5c..484271bbfe 100644 --- a/.docs/api/analyse-service.md +++ b/.docs/api/analyse-service.md @@ -11,19 +11,19 @@ author: Martin Weise * Ports: 5000/tcp * Prometheus: `http://<hostname>:5000/metrics` * Health: `http://<hostname>:5000/health` - * Swagger UI: `http://<hostname>:5000/swagger-ui/` <a href="../swagger/analyse" target="_blank">:fontawesome-solid-square-up-right: view online</a> + * Swagger UI: `http://<hostname>:5000/swagger-ui/` <a href="./swagger/analyse" target="_blank">:fontawesome-solid-square-up-right: view online</a> ## Overview -It suggests data types for the [User Interface](../system-other-ui) when creating a table from a +It suggests data types for the [User Interface](./system-other-ui) when creating a table from a *comma separated values* (CSV) -file. It recommends enumerations for columns and returns e.g. a list of potential primary key candidates. The researcher is able to confirm these suggestions manually. Moreover, the Analyse Service determines basic statistical properties of numerical columns. ### Analysis -After [uploading](../system-services-storage/#buckets) the CSV-file into the `dbrepo-upload` bucket of -the [Storage Service](../system-services-storage), analysis for data types and primary keys follows the flow: +After [uploading](./system-services-storage/#buckets) the CSV-file into the `dbrepo-upload` bucket of +the [Storage Service](./system-services-storage), analysis for data types and primary keys follows the flow: 1. Retrieve the CSV-file from the `dbrepo-upload` bucket of the Storage Service as data stream (=nothing is stored in the service) with the [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) client. @@ -36,16 +36,16 @@ the [Storage Service](../system-services-storage), analysis for data types and p ### Examples -See the [usage page](../usage-analyse/) for examples. +See the [usage page](./usage-analyse/) for examples. ## Limitations !!! question "Do you miss functionality? Do these limitations affect you?" We strongly encourage you to help us implement it as we are welcoming contributors to open-source software and get - in [contact](../contact) with us, we happily answer requests for collaboration with attached CV and your programming + in [contact](./contact) with us, we happily answer requests for collaboration with attached CV and your programming experience! ## Security -1. Credentials for the [Storage Service](../system-services-storage) are stored in plaintext environment variables. +1. Credentials for the [Storage Service](./system-services-storage) are stored in plaintext environment variables. diff --git a/.docs/api/data-service.md b/.docs/api/data-service.md index df6b87e2f0..41efb21514 100644 --- a/.docs/api/data-service.md +++ b/.docs/api/data-service.md @@ -14,7 +14,7 @@ author: Martin Weise - Readiness: `http://<hostname>:9093/actuator/health/readiness` - Liveness: `http://<hostname>:9093/actuator/health/liveness` * Prometheus: `http://<hostname>:9093/actuator/prometheus` - * Swagger UI: `http://<hostname>:9093/swagger-ui/index.html` <a href="../swagger/data" target="_blank">:fontawesome-solid-square-up-right: view online</a> + * Swagger UI: `http://<hostname>:9093/swagger-ui/index.html` <a href="./swagger/data" target="_blank">:fontawesome-solid-square-up-right: view online</a> ## Overview @@ -27,7 +27,7 @@ Data Service up. !!! question "Do you miss functionality? Do these limitations affect you?" We strongly encourage you to help us implement it as we are welcoming contributors to open-source software and get - in [contact](../contact) with us, we happily answer requests for collaboration with attached CV and your programming + in [contact](./contact) with us, we happily answer requests for collaboration with attached CV and your programming experience! ## Security diff --git a/.docs/api/gateway-service.md b/.docs/api/gateway-service.md index 172892e3bd..cd3be4f73d 100644 --- a/.docs/api/gateway-service.md +++ b/.docs/api/gateway-service.md @@ -41,7 +41,7 @@ If your TLS private key as a password, you need to specify it in the `dbrepo.con ### User Interface -To serve the [User Interface](../system-other-ui/) under different port than `80`, change the port mapping in +To serve the [User Interface](./system-other-ui/) under different port than `80`, change the port mapping in the `docker-compose.yml` to e.g. port `8000`: ```yaml title="docker-compose.yml" @@ -61,7 +61,7 @@ services: !!! question "Do you miss functionality? Do these limitations affect you?" We strongly encourage you to help us implement it as we are welcoming contributors to open-source software and get - in [contact](../contact) with us, we happily answer requests for collaboration with attached CV and your programming + in [contact](./contact) with us, we happily answer requests for collaboration with attached CV and your programming experience! diff --git a/.docs/api/index.md b/.docs/api/index.md index 8b8d7218b5..1468c2b20f 100644 --- a/.docs/api/index.md +++ b/.docs/api/index.md @@ -5,7 +5,7 @@ author: Martin Weise # Overview We developed a Python Library for communicating with DBRepo from e.g. Jupyter Notebooks. See -the [Python Library](../usage-python) page for more details. +the [Python Library](./usage-python) page for more details. We give usage examples of the most important use-cases we identified. @@ -30,7 +30,7 @@ A user wants to create an account in DBRepo. button and provide a valid work e-mail address :material-numeric-1-circle-outline: and a username (in lowercase alphanumeric characters) :material-numeric-2-circle-outline:. Choose a secure password in field :material-numeric-3-circle-outline: and repeat it in field :material-numeric-4-circle-outline:. Click "SUBMIT" and - the system creates a user account in Figure 1 with the [default roles](../system-services-authentication/#roles) + the system creates a user account in Figure 1 with the [default roles](./system-services-authentication/#roles) that your administrator has assigned. <figure markdown> @@ -425,7 +425,7 @@ A user wants to import a database dump in `.sql` (or in `.sql.gz`) format into D Setup a new connection in the MySQL Workbench (c.f. Figure 14) by clicking the small ":material-plus-circle-outline:" button :material-numeric-1-circle-outline: to open the dialog. In the opened dialog fill out the connection parameters (for local deployments the hostname is `127.0.0.1` and port `3307` for the - [Data Database](../system-databases-data/) :material-numeric-2-circle-outline:. + [Data Database](./system-databases-data/) :material-numeric-2-circle-outline:. The default credentials are username `root` and password `dbrepo`, type the password in :material-numeric-3-circle-outline: and click the "OK" button. Then finish the setup of the new connection by @@ -470,8 +470,8 @@ A user wants to import a database dump in `.sql` (or in `.sql.gz`) format into D gunzip < dump.sql.gz | mysql -H127.0.0.1 -p3307 -uUSERNAME -pYOURPASSWORD db_name ``` - The [Metadata Service](../system-services-metadata) periodically (by default configuration every 60 seconds) checks - and adds missing tables and views to the [Metadata Database](../system-databases-metadata), the database dump + The [Metadata Service](./system-services-metadata) periodically (by default configuration every 60 seconds) checks + and adds missing tables and views to the [Metadata Database](./system-databases-metadata), the database dump will be visible afterwards. Currently, date formats for columns with time types (e.g. `DATE`, `TIMESTAMP`) are assumed to match the first date format found for the database image. This may need to be manually specified by the administrator. @@ -479,7 +479,7 @@ A user wants to import a database dump in `.sql` (or in `.sql.gz`) format into D !!! example "Specifying a custom date format" In case the pre-defined date formats are not matching the found date format in the database dump, the system - administrator needs to add it manually in the [Metadata Database](../system-databases-metadata). + administrator needs to add it manually in the [Metadata Database](./system-databases-metadata). ```sql INSERT INTO `mdb_images_date` (`iid`, `database_format`, `unix_format`, `example`, `has_time`) @@ -658,7 +658,7 @@ A user wants to create a subset and export it as csv file. ``` Afterwards, you can see the subset in the UI with subset id `@subsetId` and persist it there. Only the administrator - can persist the subset in the [Data Database](../system-databases-data) through JDBC by setting the `persisted` + can persist the subset in the [Data Database](./system-databases-data) through JDBC by setting the `persisted` column to `true` in the `qs_queries` table. === "Python" @@ -732,7 +732,7 @@ A user wants to assign a persistent identifier to a database owned by them. <figcaption>Figure 21: Open the get persisent identifier form.</figcaption> </figure> - First, provide information on the dataset creator(s). Since the [Metadata Service](../system-services-metadata) + First, provide information on the dataset creator(s). Since the [Metadata Service](./system-services-metadata) automatically resolves external PIDs, the easiest way is to provide the correct mandatory data is by filling the name identifier :material-numeric-1-circle-outline:. The creator type :material-numeric-2-circle-outline: denotes either a natural person or organization. Optionally fill out the given @@ -779,7 +779,7 @@ A user wants to assign a persistent identifier to a database owned by them. <figcaption>Figure 25: Related identifiers, license and language of the identifier.</figcaption> </figure> - Optionally add funding information, again the [Metadata Service](../system-services-metadata) + Optionally add funding information, again the [Metadata Service](./system-services-metadata) automatically resolves external PIDs, the easiest way is to provide the correct mandatory data is by filling the funder identifier :material-numeric-1-circle-outline: that attempts to get the funder name :material-numeric-2-circle-outline:. If you provide an award number :material-numeric-3-circle-outline: and/or @@ -817,11 +817,11 @@ A user wants to assign a persistent identifier to a database owned by them. !!! warning - Creating a PID directly in the [Metadata Database](../system-databases-metadata) is not recommended! It bypasses + Creating a PID directly in the [Metadata Database](./system-databases-metadata) is not recommended! It bypasses validation and creation of external PIDs (e.g. DOI) and may lead to inconstistent data locally compared to external systems (e.g. DataCite Fabrica). - Create a local PID directly in the [Metadata Database](../system-databases-metadata) by filling the tables in this + Create a local PID directly in the [Metadata Database](./system-databases-metadata) by filling the tables in this order (they have foreign key dependencies). 1. `mdb_identifiers` ... identifier core information @@ -928,7 +928,7 @@ A user wants a public database to be private and only give specific users access === "JDBC" To change the visibility of a database as administrator with direct JDBC access to - the [Metadata Database](../system-databases-metadata), change the visibility directly by executing the SQL-query + the [Metadata Database](./system-databases-metadata), change the visibility directly by executing the SQL-query in the `fda` schema: ```sql diff --git a/.docs/api/metadata-service.md b/.docs/api/metadata-service.md index 33f3db3bfe..362a9c36bc 100644 --- a/.docs/api/metadata-service.md +++ b/.docs/api/metadata-service.md @@ -14,7 +14,7 @@ author: Martin Weise - Readiness: `http://<hostname>:9099/actuator/health/readiness` - Liveness: `http://<hostname>:9099/actuator/health/liveness` * Prometheus: `http://<hostname>:9099/actuator/prometheus` - * Swagger UI: `http://<hostname>:9099/swagger-ui/index.html` <a href="../swagger/metadata" target="_blank">:fontawesome-solid-square-up-right: view online</a> + * Swagger UI: `http://<hostname>:9099/swagger-ui/index.html` <a href="./swagger/metadata" target="_blank">:fontawesome-solid-square-up-right: view online</a> ## Overview @@ -75,23 +75,23 @@ Executing SQL queries through the Query Endpoint must fulfill some restrictions: ### Semantics -The service provides metadata to the table columns in the [Metadata Database](../system-databases-metadata) from +The service provides metadata to the table columns in the [Metadata Database](./system-databases-metadata) from registered ontologies like Wikidata [`wd:`](https://wikidata.org), Ontology of Units of Measurement [`om2:`](https://www.ontology-of-units-of-measure.org/resource/om-2), Friend of a Friend [`foaf:`](http://xmlns.com/foaf/0.1/), the [`prov:`](http://www.w3.org/ns/prov#) namespace, etc. ### Tables -The service manages tables in the [Data Database](../system-databases-data) and manages the metadata of these tables -in the [Metadata Database](../system-databases-metadata). Any tables that are created outside of DBRepo (e.g. directly via the JDBC API) are +The service manages tables in the [Data Database](./system-databases-data) and manages the metadata of these tables +in the [Metadata Database](./system-databases-metadata). Any tables that are created outside of DBRepo (e.g. directly via the JDBC API) are periodically fetched by this service (based on the `OBTAIN_METADATA_RATE` environment variable, default interval is 60 seconds). ### Users -The service manages users in the [Data Database](../system-databases-data) -and [Metadata Database](../system-databases-metadata), as well as in the [Broker Service](../system-services-broker) -and the [Authentication Service](../system-services-authentication). +The service manages users in the [Data Database](./system-databases-data) +and [Metadata Database](./system-databases-metadata), as well as in the [Broker Service](./system-services-broker) +and the [Authentication Service](./system-services-authentication). The default configuration grants the users only very basic permissions on the databases: @@ -123,8 +123,8 @@ A list of all grants is available in the MariaDB documentation for [`GRANT`](htt ### Views -The service manages views in the [Data Database](../system-databases-data) -and [Metadata Database](../system-databases-metadata). Any views that are created outside of DBRepo (e.g. directly via +The service manages views in the [Data Database](./system-databases-data) +and [Metadata Database](./system-databases-metadata). Any views that are created outside of DBRepo (e.g. directly via the JDBC API) are periodically fetched by this service (based on the `OBTAIN_METADATA_RATE` environment variable, default interval is 60 seconds). @@ -136,7 +136,7 @@ default interval is 60 seconds). !!! question "Do you miss functionality? Do these limitations affect you?" We strongly encourage you to help us implement it as we are welcoming contributors to open-source software and get - in [contact](../contact) with us, we happily answer requests for collaboration with attached CV and your programming + in [contact](./contact) with us, we happily answer requests for collaboration with attached CV and your programming experience! ## Security diff --git a/.docs/api/open-api.md b/.docs/api/open-api.md index f06ec67a0d..72395271de 100644 --- a/.docs/api/open-api.md +++ b/.docs/api/open-api.md @@ -2,6 +2,12 @@ author: Martin Weise --- -All services are documented using the -[](https://www.openapis.org/){ tabindex=-1 } -documentation standard. + + +## tl;dr + +[:simple-swagger: View Swagger-UI](../../swagger/){ .md-button .md-button--primary tabindex=-1 } + +## Overview + +All services are documented using the [OpenAPI 3.1](https://www.openapis.org/) documentation standard. \ No newline at end of file diff --git a/.docs/api/storage-service.md b/.docs/api/storage-service.md index 2219c5fa57..bf40ca83c8 100644 --- a/.docs/api/storage-service.md +++ b/.docs/api/storage-service.md @@ -36,7 +36,7 @@ The default configuration creates two buckets `dbrepo-upload`, `dbrepo-download` !!! question "Do you miss functionality? Do these limitations affect you?" We strongly encourage you to help us implement it as we are welcoming contributors to open-source software and get - in [contact](../contact) with us, we happily answer requests for collaboration with attached CV and your programming + in [contact](./contact) with us, we happily answer requests for collaboration with attached CV and your programming experience! ## Security diff --git a/.docs/deployment-docker-compose.md b/.docs/deployment-docker-compose.md index 0d541cd174..7b6d992256 100644 --- a/.docs/deployment-docker-compose.md +++ b/.docs/deployment-docker-compose.md @@ -53,7 +53,7 @@ technologies. The conceptualized microservices operate the basic database operat ### Notes -Please note that we only save the state of the databases as well as the [Broker Service](../system-services-broker) +Please note that we only save the state of the databases as well as the [Broker Service](./system-services-broker) since RabbitMQ maintains state inside the container. ## Deployment @@ -147,8 +147,8 @@ Please be warned that the default configuration is not intended for public deplo running system within minutes to play around within the system and explore features. It is strongly advised to change the default `.env` environment variables. -Next, create a [user account](../usage-overview/#create-user-account) and -then [create a database](../usage-overview/#create-database) to [import a dataset](../usage-overview/#import-dataset). +Next, create a [user account](./usage-overview/#create-user-account) and +then [create a database](./usage-overview/#create-database) to [import a dataset](./usage-overview/#import-dataset). ## Security @@ -193,4 +193,4 @@ then [create a database](../usage-overview/#create-database) to [import a datase !!! info "Alternative Deployments" - Alternatively, you can also deploy DBRepo with [Helm](../deployment-helm/) in your virtual machine instead. + Alternatively, you can also deploy DBRepo with [Helm](./deployment-helm/) in your virtual machine instead. diff --git a/.docs/deployment-helm.md b/.docs/deployment-helm.md index 745ad87b94..5b0be43553 100644 --- a/.docs/deployment-helm.md +++ b/.docs/deployment-helm.md @@ -32,12 +32,12 @@ about values, etc. ## Limitations 1. MariaDB Galera does not (yet) support XA-transactions required by the authentication service (=Keycloak). Therefore - only a single MariaDB pod can be deployed at once for the [auth database](../system-databases-authentication). + only a single MariaDB pod can be deployed at once for the [auth database](./system-databases-authentication). 2. The entire Helm deployment is rootless (=`runAsNonRoot=true`) except for - the [Storage Service](../system-services-storage/) which still requires a root user. + the [Storage Service](./system-services-storage/) which still requires a root user. !!! question "Do you miss functionality? Do these limitations affect you?" We strongly encourage you to help us implement it as we are welcoming contributors to open-source software and get - in [contact](../contact) with us, we happily answer requests for collaboration with attached CV and your programming + in [contact](./contact) with us, we happily answer requests for collaboration with attached CV and your programming experience! diff --git a/.docs/docker/_header.md b/.docs/docker/_header.md index 081bf9697c..3a5b13338a 100644 --- a/.docs/docker/_header.md +++ b/.docs/docker/_header.md @@ -10,7 +10,7 @@ # Supported tags -* [`1.4.x`](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/blob/release-__APP_VERSION__/dbrepo-DIR/Dockerfile/) +* [`1.4.3`](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/blob/release-1.4.3/dbrepo-DIR/Dockerfile/) * [`latest`](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/blob/release-latest/dbrepo-DIR/Dockerfile/) # Non-supported tags @@ -29,8 +29,8 @@ * **Source of this description:** - [docs repo's `.docs/docker` directory](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/tree/release-__APP_VERSION__/.docs/docker) - ([history](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/commits/release-__APP_VERSION__/.docs/docker)) + [docs repo's `.docs/docker` directory](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/tree/release-1.4.3/.docs/docker) + ([history](https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/commits/release-1.4.3/.docs/docker)) # What is DBRepo? diff --git a/.docs/publications.md b/.docs/publications.md index cbaac17564..4b64d9a574 100644 --- a/.docs/publications.md +++ b/.docs/publications.md @@ -14,14 +14,14 @@ hide: Semantic Digital Repository for Relational Databases. *International Journal of Digital Curation*, 17(1), 11. DOI: [10.2218/ijdc.v17i1.825](https://doi.org/10.2218/ijdc.v17i1.825)<br /> - [[BibTeX](../papers/weise2022dbrepo.bib)] [[RIS](../papers/weise2022dbrepo.ris)] [[RDF](../papers/weise2022dbrepo.rdf)] [[EndNote](../papers/weise2022dbrepo.xml)] + [[BibTeX](./papers/weise2022dbrepo.bib)] [[RIS](./papers/weise2022dbrepo.ris)] [[RDF](./papers/weise2022dbrepo.rdf)] [[EndNote](./papers/weise2022dbrepo.xml)] ## Logos DBRepo logo in various formats: -* PNG: [bigger](../images/logo/logo.png) ([smaller](../images/logo/favicon.png)) -* SVG: [bigger](../images/logo/logo.svg) ([smaller](../images/logo/favicon.svg)) +* PNG: [bigger](./images/logo/logo.png) ([smaller](./images/logo/favicon.png)) +* SVG: [bigger](./images/logo/logo.svg) ([smaller](./images/logo/favicon.svg)) ## Refereed diff --git a/.docs/usage-storage.md b/.docs/usage-storage.md index 253fe8e960..fb409571f7 100644 --- a/.docs/usage-storage.md +++ b/.docs/usage-storage.md @@ -53,7 +53,7 @@ $ aws --endpoint-url http://localhost:9000 \ ## Other -Alternatively, you can use the middleware of the [User Interface](../system-other-ui/) to upload files. +Alternatively, you can use the middleware of the [User Interface](./system-other-ui/) to upload files. Alternatively, you can use a S3-compatible client: diff --git a/.docs/usage-upload.md b/.docs/usage-upload.md index f84f853eb6..1735e6cced 100644 --- a/.docs/usage-upload.md +++ b/.docs/usage-upload.md @@ -11,7 +11,7 @@ We recommend using a TUS-compatible client: * [tus-js-client](https://github.com/tus/tus-js-client) (JavaScript/Node.js) * [tusd](https://github.com/tus/tusd) (Go) -Upload a file to the `dbrepo-upload` bucket in the [Storage Service](../system-services-storage/) using the Node.js +Upload a file to the `dbrepo-upload` bucket in the [Storage Service](./system-services-storage/) using the Node.js middleware. === "Terminal" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 95892b1615..1c0328375d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,8 +5,8 @@ variables: DOCKER_HOST: "unix:///var/run/dind/docker.sock" TESTCONTAINERS_RYUK_DISABLED: "false" DOC_VERSIONS: "latest,1.4.3,1.4.2,1.4.1,1.4.0" - APP_VERSION: "1.4.3" - CHART_VERSION: "1.4.3" + APP_VERSION: "1.4.4" + CHART_VERSION: "1.4.4" image: debian:12-slim @@ -569,7 +569,6 @@ docs-registry: - "apt-get update && apt-get install -y sed" script: - pip install -r ./requirements.txt - - find .docs -type f -exec sed -i -e "s/__APP_VERSION__/${APP_VERSION}/g" {} \; - python3 .docs/docker/release.py release-images: @@ -588,7 +587,7 @@ release-images: - "ifconfig eth0 mtu 1450 up" - "apk add make bash" script: - - "make release" + - "make release-images" release-chart: stage: release @@ -596,13 +595,14 @@ release-chart: only: refs: - /^release-.*/ + except: + refs: + - release-latest before_script: - "echo ${CI_REGISTRY2_PASSWORD} | docker login --username ${CI_REGISTRY2_USER} --password-stdin $CI_REGISTRY2_URL" - - "echo ${CI_GPG_KEYRING} | base64 -d > ~/keyring.gpg" - "apk add sed helm curl" - "helm plugin install https://github.com/sigstore/helm-sigstore" script: - - "helm package --sign --key 'Martin Weise' ./helm/dbrepo --keyring ~/keyring.gpg --destination ./build" - "helm push ./build/dbrepo-${CHART_VERSION}.tgz oci://${CI_REGISTRY2_URL}/helm" - "helm sigstore upload ./build/dbrepo-${CHART_VERSION}.tgz" @@ -617,26 +617,29 @@ release-docs: before_script: - "wget https://github.com/mikefarah/yq/releases/download/v4.2.0/yq_linux_amd64 -O /usr/bin/yq" - "chmod +x /usr/bin/yq" - - "apt-get update && apt-get install -y git make sed wget ssh" + - "apk add --update alpine-sdk bash sed wget openssh" + - "pip install -r ./requirements.txt" - "mkdir -p ./final/${VERSION}/swagger" script: - - "make gen-swagger-doc gen-lib-doc gen-docs-doc" + - "make gen-lib-doc gen-docs-doc" - "cp -r ./lib/python/docs/build/html ./final/${VERSION}/sphinx" # sphinx - "cp .docs/.swagger/api.yaml ./final/${VERSION}/swagger/api.yaml" # swagger - "cp .docs/.swagger/swagger-ui.html ./final/${VERSION}/swagger/index.html" # swagger - "cp .docs/.swagger/custom.css ./final/${VERSION}/swagger/custom.css" # swagger - - "cp -r ./site ./final/${VERSION}" # mkdocs + - "cp -r ./site/* ./final/${VERSION}" # mkdocs - eval $(ssh-agent -s) + - "mkdir -p /root/.ssh" - echo "$CI_KEY_PRIVATE" > /root/.ssh/id_rsa && chmod 0600 /root/.ssh/id_rsa - echo "$CI_KEY_PUBLIC" > /root/.ssh/id_rsa.pub - echo "$CI_DOC_ID" > ~/.ssh/known_hosts - tar czf ./final.tar.gz ./final - "scp -oHostKeyAlgorithms=+ssh-rsa -oPubkeyAcceptedAlgorithms=+ssh-rsa final.tar.gz $CI_DOC_USER@$CI_DOC_IP:final.tar.gz" - - "ssh -oHostKeyAlgorithms=+ssh-rsa -oPubkeyAcceptedAlgorithms=+ssh-rsa $CI_DOC_USER@$CI_DOC_IP 'rm -rf /system/user/ifs/infrastructures/public_html/dbrepo/*; tar xzf ./final.tar.gz; rm -f ./final.tar.gz; cp -r ./final/* /system/user/ifs/infrastructures/public_html/dbrepo; rm -rf ./final'" + - "ssh -oHostKeyAlgorithms=+ssh-rsa -oPubkeyAcceptedAlgorithms=+ssh-rsa $CI_DOC_USER@$CI_DOC_IP 'rm -rf /system/user/ifs/infrastructures/public_html/dbrepo/*; tar xzf ./final.tar.gz; rm -f ./final.tar.gz; cp -r ./final/* /system/user/ifs/infrastructures/public_html/dbrepo/${VERSION}; rm -rf ./final'" release-libs: stage: release image: docker.io/python:3.11-alpine + when: manual only: refs: - /^release-.*/ diff --git a/Makefile b/Makefile index 3c178cfa9e..84d1f85e5d 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,7 @@ APP_VERSION ?= 1.4.3 CHART_VERSION ?= 1.4.3 -REPOSITORY_1_URL ?= docker.io/dbrepo -REPOSITORY_2_URL ?= s210.dl.hpc.tuwien.ac.at/dbrepo +REPOSITORY_URL ?= docker.io/dbrepo .PHONY: all all: help diff --git a/build-docs.sh b/build-docs.sh index a38121fceb..fba2afc766 100644 --- a/build-docs.sh +++ b/build-docs.sh @@ -88,6 +88,7 @@ done # finalization echo "===================================================" -echo "Moving default version $APP_VERSION docs to /" -cp -r ./final/${APP_VERSION}/* ./final/ +echo "Moving HTML redirect and JSON versions to /" +cp ./final/${APP_VERSION}/redirect.html ./final/index.html +cp ./final/${APP_VERSION}/versions.json ./final/versions.json echo "===================================================" diff --git a/dbrepo-analyse-service/Pipfile.lock b/dbrepo-analyse-service/Pipfile.lock index 8a14b2d86f..adf893c960 100644 --- a/dbrepo-analyse-service/Pipfile.lock +++ b/dbrepo-analyse-service/Pipfile.lock @@ -167,20 +167,20 @@ }, "boto3": { "hashes": [ - "sha256:4eb8019421cb664a6fcbbee6152aa95a28ce8bbc1c4ee263871c09cdd58bf8ee", - "sha256:e9edaf979fbe59737e158f2f0f3f0861ff1d61233f18f6be8ebb483905f24587" + "sha256:8f9c43c54b3dfaa36c4a0d7b42c417227a515bc7a2e163e62802780000a5a3e2", + "sha256:cea2365a25b2b83a97e77f24ac6f922ef62e20636b42f9f6ee9f97188f9c1c03" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.118" + "version": "==1.34.119" }, "botocore": { "hashes": [ - "sha256:0a3d1ec0186f8b516deb39474de3d226d531f77f92a0f56ad79b80219db3ae9e", - "sha256:e3f6c5636a4394768e81e33a16f5c6ae7f364f512415d423f9b9dc67fc638df4" + "sha256:4bdf7926a1290b2650d62899ceba65073dd2693e61c35f5cdeb3a286a0aaa27b", + "sha256:b253f15b24b87b070e176af48e8ef146516090429d30a7d8b136a4c079b28008" ], "markers": "python_version >= '3.8'", - "version": "==1.34.118" + "version": "==1.34.119" }, "certifi": { "hashes": [ @@ -354,45 +354,45 @@ }, "cryptography": { "hashes": [ - "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55", - "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785", - "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b", - "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886", - "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82", - "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1", - "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda", - "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f", - "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68", - "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60", - "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7", - "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd", - "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582", - "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc", - "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858", - "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b", - "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2", - "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678", - "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13", - "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4", - "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8", - "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604", - "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477", - "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e", - "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a", - "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9", - "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14", - "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda", - "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da", - "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562", - "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2", - "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9" + "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad", + "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", + "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", + "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", + "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", + "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648", + "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", + "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", + "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c", + "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", + "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", + "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", + "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", + "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", + "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", + "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", + "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", + "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", + "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", + "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", + "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", + "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", + "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", + "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", + "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", + "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", + "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", + "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842", + "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", + "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", + "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", + "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e" ], "markers": "python_version >= '3.7'", - "version": "==42.0.7" + "version": "==42.0.8" }, "dbrepo": { "hashes": [ - "sha256:110db9e4e70f5656a6351409d4b022656abf7de0bd72d5e061a25685f708d9a4" + "sha256:2bdb48c70b4c99b5044fbfc12aa653c1e9281ca8913a433cc08a1e14cb4bd2ef" ], "path": "./lib/dbrepo-1.4.4.tar.gz" }, @@ -1414,7 +1414,7 @@ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.8'", + "markers": "python_version >= '3.10'", "version": "==2.2.1" }, "werkzeug": { @@ -1940,12 +1940,12 @@ }, "pytest": { "hashes": [ - "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd", - "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1" + "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", + "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==8.2.1" + "version": "==8.2.2" }, "python-dateutil": { "hashes": [ @@ -2017,7 +2017,7 @@ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.8'", + "markers": "python_version >= '3.10'", "version": "==2.2.1" }, "wrapt": { diff --git a/dbrepo-analyse-service/app.py b/dbrepo-analyse-service/app.py index de1ca37a4d..0e8a10bf1d 100644 --- a/dbrepo-analyse-service/app.py +++ b/dbrepo-analyse-service/app.py @@ -19,7 +19,6 @@ from botocore.exceptions import ClientError from clients.keycloak_client import KeycloakClient, User from determine_dt import determine_datatypes from determine_pk import determine_pk -from determine_stats import determine_stats logging.addLevelName(level=logging.NOTSET, levelName='TRACE') logging.basicConfig(level=logging.DEBUG) @@ -58,7 +57,7 @@ basic_auth = HTTPBasicAuth() auth = MultiAuth(token_auth, basic_auth) metrics = PrometheusMetrics(app) -metrics.info("app_info", "Application info", version="__APPVERSION__") +metrics.info("app_info", "Application info", version="1.4.4") app.config["SWAGGER"] = {"openapi": "3.0.1", "title": "Swagger UI", "uiversion": 3} swagger_config = { @@ -79,6 +78,64 @@ swagger_config = { template = { "openapi": "3.0.0", "components": { + "schemas": { + "DataTypesDto": { + "properties": { + "columns": { + "$ref": "#/components/schemas/SuggestedColumnDto" + }, + "line_termination": { + "example": "\r\n", + "type": "string" + }, + "separator": { + "example": ",", + "type": "string" + } + }, + "type": "object" + }, + "ErrorDto": { + "properties": { + "message": { + "example": "Message", + "type": "string" + }, + "success": { + "example": False, + "type": "boolean" + } + }, + "type": "object" + }, + "KeysDto": { + "properties": { + "keys": { + "items": { + "properties": { + "column_name": { + "format": "int64", + "type": "integer" + } + } + }, + "type": "array" + } + }, + "required": [ + "keys" + ], + "type": "object" + }, + "SuggestedColumnDto": { + "properties": { + "column_name": { + "type": "string" + } + }, + "type": "object" + } + }, "securitySchemes": { "bearerAuth": { "type": "http", @@ -96,7 +153,7 @@ template = { "info": { "title": "Database Repository Analyse Service API", "description": "Service that analyses data structures", - "version": "__APPVERSION__", + "version": "1.4.4", "contact": { "name": "Prof. Andreas Rauber", "email": "andreas.rauber@tuwien.ac.at" @@ -180,7 +237,6 @@ def get_user_roles(user: User) -> List[str]: @app.route("/health", methods=["GET"], endpoint="analyse_health") -@swag_from("as-yml/health.yml") def get_health(): res = dumps({"status": "UP", "message": "Application is up and running"}) return Response(res, mimetype="application/json"), 200 @@ -231,25 +287,3 @@ def analyse_keys(): except OSError as e: logging.error(f"Failed to determine primary key: {e}") return ApiError(status='BAD_REQUEST', message=str(e), code='analyse.database.invalid'), 400 - - -@app.route("/api/analyse/database/<database_id>/table/<table_id>/statistics", methods=["GET"], - endpoint="analyse_analyse_table_stat") -@auth.login_required(role=['admin', 'export-query-data', 'export-table-data']) -@metrics.gauge(name='dbrepo_analyse_table_stat', description='Time needed to analyse table statistics') -@swag_from("as-yml/analyse_table_stat.yml") -def analyse_table_stat(database_id: int = None, table_id: int = None): - if database_id is None: - return ApiError(status='BAD_REQUEST', message="Missing path variable 'database_id'", - code='analyse.database.invalid'), 400 - if table_id is None: - return ApiError(status='BAD_REQUEST', message="Missing path variable 'table_id'", - code='analyse.table.invalid'), 400 - - try: - table_stats = determine_stats(database_id=database_id, table_id=table_id) - logging.info(f"Analysed table statistics") - return table_stats.model_dump(), 202 - except OSError: - return ApiError(status='NOT_FOUND', message='Database or table does not exist', - code='analyse.database.missing'), 404 diff --git a/dbrepo-analyse-service/as-yml/analyse_datatypes.yml b/dbrepo-analyse-service/as-yml/analyse_datatypes.yml index 5d30665da8..ae52198766 100644 --- a/dbrepo-analyse-service/as-yml/analyse_datatypes.yml +++ b/dbrepo-analyse-service/as-yml/analyse_datatypes.yml @@ -57,48 +57,6 @@ responses: application/json: schema: $ref: '#/components/schemas/ErrorDto' -components: - schemas: - DetermineDataTypesDto: - required: - - filename - - separator - type: object - properties: - enum: - type: boolean - example: false - enum_tol: - type: double - example: 0.01 - filename: - type: string - example: s3-key-from-seaweedfs - separator: - type: string - example: "," - DataTypesDto: - type: object - properties: - columns: - $ref: '#/components/schemas/SuggestedColumnDto' - line_termination: - type: string - example: "\r\n" - separator: - type: string - example: "," - SuggestedColumnDto: - type: object - properties: - column_name: - type: string - ErrorDto: - type: object - properties: - success: - type: boolean - example: false - message: - type: string - example: Message \ No newline at end of file +security: + - bearerAuth: [ ] + - basicAuth: [ ] diff --git a/dbrepo-analyse-service/as-yml/analyse_keys.yml b/dbrepo-analyse-service/as-yml/analyse_keys.yml index a01b396cec..da4f0bbca0 100644 --- a/dbrepo-analyse-service/as-yml/analyse_keys.yml +++ b/dbrepo-analyse-service/as-yml/analyse_keys.yml @@ -45,42 +45,6 @@ responses: application/json: schema: $ref: '#/components/schemas/ErrorDto' -components: - schemas: - KeysDto: - required: - - keys - type: object - properties: - keys: - type: array - items: - properties: - column_name: - type: integer - format: int64 - DataTypesDto: - type: object - properties: - columns: - $ref: '#/components/schemas/SuggestedColumnDto' - line_termination: - type: string - example: "\r\n" - separator: - type: string - example: "," - SuggestedColumnDto: - type: object - properties: - column_name: - type: string - ErrorDto: - type: object - properties: - success: - type: boolean - example: false - message: - type: string - example: Message \ No newline at end of file +security: + - bearerAuth: [ ] + - basicAuth: [ ] \ No newline at end of file diff --git a/dbrepo-analyse-service/as-yml/analyse_table_stat.yml b/dbrepo-analyse-service/as-yml/analyse_table_stat.yml index 6978daf229..8639d4dd92 100644 --- a/dbrepo-analyse-service/as-yml/analyse_table_stat.yml +++ b/dbrepo-analyse-service/as-yml/analyse_table_stat.yml @@ -39,50 +39,3 @@ responses: application/json: schema: $ref: '#/components/schemas/ErrorDto' -components: - schemas: - TableStats: - required: - - columns - type: object - properties: - columns: - type: object - properties: - column_name: - $ref: '#/components/schemas/Stats' - Stats: - type: object - properties: - val_min: - type: float - example: "0.0" - val_max: - type: float - example: "1.0" - mean: - type: float - example: "0.3" - median: - type: float - example: "0.45" - std_dev: - type: float - example: "0.12" - ErrorDto: - type: object - properties: - success: - type: boolean - example: false - message: - type: string - example: Message - securitySchemes: - basicAuth: - type: http - scheme: basic - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT \ No newline at end of file diff --git a/dbrepo-analyse-service/as-yml/health.yml b/dbrepo-analyse-service/as-yml/health.yml deleted file mode 100644 index a0f7ebcbac..0000000000 --- a/dbrepo-analyse-service/as-yml/health.yml +++ /dev/null @@ -1,18 +0,0 @@ -tags: - - health-endpoint -summary: "Check if application is running" -description: "This is a simple API which checks if the application is healthy" -consumes: - - "application/json" -produces: - - "application/json" -responses: - 200: - description: "OK" - schema: - type: "object" - properties: - status: - type: "string" - example: "UP" - \ No newline at end of file diff --git a/dbrepo-analyse-service/determine_stats.py b/dbrepo-analyse-service/determine_stats.py deleted file mode 100644 index d529ab8c28..0000000000 --- a/dbrepo-analyse-service/determine_stats.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from flask import current_app - -from pandas import DataFrame, isna -from dbrepo.RestClient import RestClient - -from api.dto import TableStat, ColumnStat - - -def determine_stats(database_id: int, table_id: int) -> TableStat: - client = RestClient(endpoint=current_app.config['GATEWAY_SERVICE_ENDPOINT'], - username=current_app.config['ADMIN_USERNAME'], password=current_app.config['ADMIN_PASSWORD']) - df: DataFrame = client.get_table_data(database_id=database_id, table_id=table_id, page=0, size=1000, df=True) - stats = TableStat(columns=dict()) - for name, dtype in df.dtypes.items(): - # Check if the column has a numeric data type - if dtype.kind in "fi": - val_min = None if isna(df[name].min()) else df[name].min() - val_max = None if isna(df[name].max()) else df[name].max() - mean = None if isna(df[name].mean()) else df[name].mean() - median = None if isna(df[name].median()) else df[name].median() - std_dev = None if isna(df[name].std()) else df[name].std() - stats.columns[str(name)] = ColumnStat(val_min=val_min, val_max=val_max, mean=mean, median=median, - std_dev=std_dev) - logging.debug(f"statistical props of the first 1000 rows: <min={val_min}, max={val_max}, mean={mean}, " - f"median={median}, std_dev={std_dev}>") - return stats diff --git a/dbrepo-analyse-service/lib/dbrepo-1.4.4-py3-none-any.whl b/dbrepo-analyse-service/lib/dbrepo-1.4.4-py3-none-any.whl index f58e17a58e747e35bbd37f43efe6b460ba31530f..694a6fc02560b3b5d858df0e5a0bd9acf45c8f20 100644 GIT binary patch delta 14746 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 delta 14271 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 Bc!B@` diff --git a/dbrepo-analyse-service/lib/dbrepo-1.4.4.tar.gz b/dbrepo-analyse-service/lib/dbrepo-1.4.4.tar.gz index 5463f6b170c24fff05d29a434562104553292fea..f344d01026b92476d80703cbfb7d884cb7822e05 100644 GIT binary patch literal 38911 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 literal 37662 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 b!VYJdM*{5#Cj0rv{~3Q@`|HRMz{&ssMI%h+ diff --git a/dbrepo-auth-service/dbrepo-realm.json b/dbrepo-auth-service/dbrepo-realm.json index f5ced37ff2..bd5a5464e7 100644 --- a/dbrepo-auth-service/dbrepo-realm.json +++ b/dbrepo-auth-service/dbrepo-realm.json @@ -126,7 +126,7 @@ "description" : "${default-table-handling}", "composite" : true, "composites" : { - "realm" : [ "modify-table-column-semantics", "list-tables", "find-table", "create-table", "delete-table" ] + "realm" : [ "modify-table-column-semantics", "list-tables", "update-table-statistic", "find-table", "create-table", "delete-table" ] }, "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", @@ -166,6 +166,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "0e12eedf-545d-4d32-ac4d-2821dcb118b8", + "name" : "update-table-statistic", + "description" : "${update-table-statistic}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "e63e61a2-d852-4ad3-bfb5-92d9ceafef6a", "name" : "escalated-user-handling", @@ -2110,7 +2118,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-address-mapper" ] } }, { "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", @@ -2119,7 +2127,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "saml-role-list-mapper", "saml-user-attribute-mapper" ] } } ], "org.keycloak.keys.KeyProvider" : [ { @@ -2171,7 +2179,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "fbce9485-c780-438c-bbe9-135352504aa7", + "id" : "8b55b559-905f-4f73-b050-0cd68f676a42", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -2193,7 +2201,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "dce6a1a5-7099-4366-a644-79b08fd399fd", + "id" : "293efab0-aa10-44e6-8f5a-dd63d6908d9e", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -2222,7 +2230,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ac79c3e5-8aa3-499c-ae23-1656ab67972c", + "id" : "f3c7659d-9c24-43e7-b94c-8bfb4811084f", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2244,7 +2252,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ecafaa7c-4a1f-4842-b8cd-6661fea1da33", + "id" : "1d83f267-0342-41c1-9a64-11cc9b8e62fc", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2266,7 +2274,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "55d29fb3-07a8-47b7-a051-9176e404ab55", + "id" : "bb881bf0-e8f5-418e-91ec-09624683ec66", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2288,7 +2296,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "23a6b3f9-d7dc-41f0-b9b0-917e64aa62ed", + "id" : "aea83d83-6c28-4df6-9543-2bf74cc4b78a", "alias" : "Handle Existing Account", "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId" : "basic-flow", @@ -2310,7 +2318,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ea03fe83-9a6c-47e0-85e9-94fb006bc504", + "id" : "78283326-7419-4cca-a5dd-cf510db7041c", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -2332,7 +2340,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "c2f4bb71-24a7-42a2-a870-d0485b6430f7", + "id" : "c88bb673-7092-4996-8c46-e9b08c94eb8c", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -2355,7 +2363,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "08a0c43e-434c-4f9d-97d8-efcc697c0bdb", + "id" : "6632c7a3-8a7f-4f94-a15d-bdce1563f419", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -2377,7 +2385,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "9df5a633-532d-4da0-99ad-b60bac9a984b", + "id" : "3a383f61-8ad4-4815-93a8-d04eefc48791", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2413,7 +2421,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "cac0d15d-bba8-4731-a184-5711bfdc38d8", + "id" : "fc65865d-d3a4-4769-a665-fd49b34d2687", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2449,7 +2457,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "5cb539c9-6124-4883-b058-e4b0062c8ed0", + "id" : "40077362-bb0b-41c7-a297-1d4c3625b17d", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2478,7 +2486,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "610d7baf-f915-44e0-8617-8ae6309b1098", + "id" : "5b2f7f25-f5dd-4013-800d-6030b79e257e", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2493,7 +2501,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "d613df19-1185-4796-b1e9-9716f4b241c9", + "id" : "e9da2536-e792-461d-aceb-085f18ca533c", "alias" : "first broker login", "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId" : "basic-flow", @@ -2516,7 +2524,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7a475371-0966-4dff-9296-28790d5aa227", + "id" : "4c17ae53-d99e-4f47-92ef-47accae912fd", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2538,7 +2546,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7d71011c-9828-495a-8fc6-82e88e308d26", + "id" : "da0ed32c-3259-4571-877b-914fa2aa30b3", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2560,7 +2568,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "0eb1b344-0a43-4ffa-8bf2-98560bd7471f", + "id" : "476d469b-5c54-42af-a41c-5dbe08412395", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2576,7 +2584,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "fd2328ca-38ca-4dc9-b9e5-09790a3512c5", + "id" : "714c4dc0-d7b3-4e12-93bd-59a7c4fbeef2", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2612,7 +2620,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "770cf19b-4bc2-4161-b156-161dc23eebaa", + "id" : "316122ff-d003-49f7-9a0d-a570489bec9d", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2648,7 +2656,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ab251c63-6c1b-4d93-9f26-2f5d18c81832", + "id" : "5c90488c-9d5c-460d-9deb-9740740c3a9e", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2664,13 +2672,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "709dcd0e-a60b-4ae5-ac66-d15374b1562c", + "id" : "874c7063-05d5-45fb-b919-840798663176", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "f80268b2-6944-4acd-b622-426369f8c44d", + "id" : "93cf220e-2830-4ccb-9054-b3b87ef75fd4", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/dbrepo-data-db/sidecar/app.py b/dbrepo-data-db/sidecar/app.py index f4342301df..c88966bb00 100644 --- a/dbrepo-data-db/sidecar/app.py +++ b/dbrepo-data-db/sidecar/app.py @@ -170,7 +170,6 @@ def get_user_roles(user: User) -> List[str]: @app.route("/health", methods=["GET"], endpoint="actuator_health") -@swag_from("ds-yml/health.yml") def health(): logging.debug("endpoint health, body=%s", request) res = dumps({"status": "UP", "message": "Application is up and running"}) diff --git a/dbrepo-data-db/sidecar/ds-yml/import.yml b/dbrepo-data-db/sidecar/ds-yml/import.yml index a129e86fa1..87c6777127 100644 --- a/dbrepo-data-db/sidecar/ds-yml/import.yml +++ b/dbrepo-data-db/sidecar/ds-yml/import.yml @@ -11,8 +11,8 @@ parameters: description: Name of the object file to import from the Storage Service required: true security: -- bearerAuth: [] -- basicAuth: [] + - bearerAuth: [ ] + - basicAuth: [ ] responses: 202: description: Imported the .csv diff --git a/dbrepo-data-service/Dockerfile b/dbrepo-data-service/Dockerfile index 69edbda315..d4016836d9 100644 --- a/dbrepo-data-service/Dockerfile +++ b/dbrepo-data-service/Dockerfile @@ -24,6 +24,8 @@ RUN mvn clean package -DskipTests FROM amazoncorretto:17-alpine3.19 as runtime MAINTAINER Martin Weise <martin.weise@tuwien.ac.at> +RUN apk add --no-cache curl bash jq + WORKDIR /app USER 65534 diff --git a/dbrepo-data-service/metrics.md b/dbrepo-data-service/metrics.md index 5e0773ad8c..425b58ad17 100644 --- a/dbrepo-data-service/metrics.md +++ b/dbrepo-data-service/metrics.md @@ -6,12 +6,14 @@ | `dbrepo_subset_find` | Find subset | | `dbrepo_subset_list` | Find subsets | | `dbrepo_subset_persist` | Persist subset | -| `dbrepo_table_data_create` | Create table data | +| `dbrepo_table_data_create` | Insert a raw data tuple | | `dbrepo_table_data_delete` | Delete table data | | `dbrepo_table_data_export` | Export table data | | `dbrepo_table_data_history` | Find table history | -| `dbrepo_table_data_import` | Import dataset | +| `dbrepo_table_data_import` | Import data from a dataset | | `dbrepo_table_data_list` | Retrieve table data | -| `dbrepo_table_data_update` | Update table data | +| `dbrepo_table_data_update` | Update a raw data tuple | +| `dbrepo_table_schema_list` | Find table schemas | +| `dbrepo_table_statistic` | Generate table statistic | | `dbrepo_view_data` | Retrieve view data | | `dbrepo_view_schema_list` | Find view schemas | diff --git a/dbrepo-data-service/pom.xml b/dbrepo-data-service/pom.xml index 98ad4d797c..3df58f676f 100644 --- a/dbrepo-data-service/pom.xml +++ b/dbrepo-data-service/pom.xml @@ -60,7 +60,7 @@ <jackson-datatype.version>2.15.0</jackson-datatype.version> <commons-io.version>2.15.0</commons-io.version> <commons-validator.version>1.8.0</commons-validator.version> - <jacoco.version>0.8.11</jacoco.version> + <jacoco.version>0.8.12</jacoco.version> <jwt.version>4.3.0</jwt.version> <opencsv.version>5.7.1</opencsv.version> <super-csv.version>2.4.0</super-csv.version> @@ -266,16 +266,6 @@ <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>${jacoco.version}</version> - <configuration> - <excludes> - <exclude>at/tuwien/mapper/**/*</exclude> - <exclude>at/tuwien/exception/**/*</exclude> - <exclude>at/tuwien/config/**/*</exclude> - <exclude>at/tuwien/auth/**/*</exclude> - <exclude>at/tuwien/handlers/**/*</exclude> - <exclude>**/DbrepoDataServiceApplication.class</exclude> - </excludes> - </configuration> <executions> <execution> <id>default-prepare-agent</id> diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java index ea7b1b3677..4b58c5de33 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/AccessEndpoint.java @@ -4,6 +4,7 @@ import at.tuwien.api.database.UpdateDatabaseAccessDto; import at.tuwien.api.database.internal.PrivilegedDatabaseDto; import at.tuwien.api.error.ApiErrorDto; import at.tuwien.api.user.PrivilegedUserDto; +import at.tuwien.api.user.UserDto; import at.tuwien.exception.*; import at.tuwien.gateway.MetadataServiceGateway; import at.tuwien.service.AccessService; @@ -42,7 +43,8 @@ public class AccessEndpoint { @PostMapping("/{userId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Give access to some database", security = {@SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Give access to some database", security = {@SecurityRequirement(name = "basicAuth")}, + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Granting access succeeded"), @@ -76,10 +78,10 @@ public class AccessEndpoint { @NotBlank @PathVariable("userId") UUID userId, @Valid @RequestBody UpdateDatabaseAccessDto data) throws NotAllowedException, QueryMalformedException, DatabaseNotFoundException, RemoteUnavailableException, - UserNotFoundException, DatabaseMalformedException { + UserNotFoundException, DatabaseMalformedException, ServiceException { log.debug("endpoint give access to database, databaseId={}, userId={}", databaseId, userId); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); - final PrivilegedUserDto user = metadataServiceGateway.getUserById(userId); + final PrivilegedUserDto user = metadataServiceGateway.getPrivilegedUserById(userId); if (database.getAccesses().stream().anyMatch(a -> a.getUser().getId().equals(userId))) { log.error("Failed to create access to user with id {}: already has access", userId); throw new NotAllowedException("Failed to create access to user with id " + userId + ": already has access"); @@ -95,7 +97,8 @@ public class AccessEndpoint { @PutMapping("/{userId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Update access to some database", security = {@SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update access to some database", security = {@SecurityRequirement(name = "basicAuth")}, + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Update access succeeded", @@ -130,11 +133,11 @@ public class AccessEndpoint { @NotBlank @PathVariable("userId") UUID userId, @Valid @RequestBody UpdateDatabaseAccessDto access) throws NotAllowedException, QueryMalformedException, DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, - DatabaseMalformedException { + DatabaseMalformedException, ServiceException { log.debug("endpoint modify access to database, databaseId={}, userId={}, access.type={}", databaseId, userId, access.getType()); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); - final PrivilegedUserDto user = metadataServiceGateway.getUserById(userId); + final UserDto user = metadataServiceGateway.getUserById(userId); if (database.getAccesses().stream().noneMatch(a -> a.getUser().getId().equals(userId))) { log.error("Failed to update access to user with id {}: no access", userId); throw new NotAllowedException("Failed to update access to user with id " + userId + ": no access"); @@ -150,7 +153,8 @@ public class AccessEndpoint { @DeleteMapping("/{userId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Revoke access to some database", security = {@SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Revoke access to some database", security = {@SecurityRequirement(name = "basicAuth")}, + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Revoked access successfully"), @@ -183,10 +187,10 @@ public class AccessEndpoint { public ResponseEntity<?> revoke(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("userId") UUID userId) throws NotAllowedException, QueryMalformedException, DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, - DatabaseMalformedException { + DatabaseMalformedException, ServiceException { log.debug("endpoint revoke access to database, databaseId={}, userId={}", databaseId, userId); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); - final PrivilegedUserDto user = metadataServiceGateway.getUserById(userId); + final UserDto user = metadataServiceGateway.getUserById(userId); if (database.getAccesses().stream().noneMatch(a -> a.getUser().getId().equals(userId))) { log.error("Failed to delete access to user with id {}: no access", userId); throw new NotAllowedException("Failed to delete access to user with id " + userId + ": no access"); diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java index f69019e717..bd32093068 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java @@ -54,7 +54,8 @@ public class DatabaseEndpoint { @PostMapping @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Create database", security = {@SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Create database", security = {@SecurityRequirement(name = "basicAuth")}, + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created a database", @@ -84,7 +85,7 @@ public class DatabaseEndpoint { }) public ResponseEntity<DatabaseDto> create(@Valid @RequestBody CreateDatabaseDto data) throws DatabaseUnavailableException, RemoteUnavailableException, ContainerNotFoundException, - DatabaseMalformedException, QueryStoreCreateException { + DatabaseMalformedException, QueryStoreCreateException, ServiceException { log.debug("endpoint create database, data.containerId={}, data.internalName={}, data.username={}", data.getContainerId(), data.getInternalName(), data.getUsername()); final PrivilegedContainerDto container = metadataServiceGateway.getContainerById(data.getContainerId()); @@ -107,7 +108,8 @@ public class DatabaseEndpoint { @PutMapping("/{databaseId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Update user password in database", security = {@SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Update user password in database", security = {@SecurityRequirement(name = "basicAuth")}, + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Updated user password in database"), @@ -130,7 +132,7 @@ public class DatabaseEndpoint { public ResponseEntity<Void> update(@NotBlank @PathVariable("databaseId") Long databaseId, @Valid @RequestBody UpdateUserPasswordDto data) throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, - DatabaseMalformedException { + DatabaseMalformedException, ServiceException { log.debug("endpoint update user password in database, databaseId={}, data.username={}", databaseId, data.getUsername()); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); 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 b9a9ae442d..0d4b53b92c 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 @@ -84,7 +84,7 @@ public class SubsetEndpoint { @RequestParam(name = "persisted", required = false) Boolean filterPersisted, Principal principal) throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, - QueryNotFoundException, NotAllowedException { + QueryNotFoundException, NotAllowedException, ServiceException { log.debug("endpoint find subsets in database, databaseId={}, filterPersisted={}, principal.name={}", databaseId, filterPersisted, principal != null ? principal.getName() : null); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); @@ -149,7 +149,8 @@ public class SubsetEndpoint { Principal principal) throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, QueryNotFoundException, FormatNotAvailableException, StorageUnavailableException, QueryMalformedException, - SidecarExportException, StorageNotFoundException, NotAllowedException, UserNotFoundException { + SidecarExportException, StorageNotFoundException, NotAllowedException, UserNotFoundException, + ServiceException { String accept = httpServletRequest.getHeader("Accept"); log.debug("endpoint find subset in database, databaseId={}, subsetId={}, accept={}, timestamp={}", databaseId, subsetId, accept, timestamp); @@ -251,7 +252,7 @@ public class SubsetEndpoint { throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, QueryNotFoundException, StorageUnavailableException, QueryMalformedException, SidecarExportException, StorageNotFoundException, QueryStoreInsertException, TableMalformedException, PaginationException, - QueryNotSupportedException, NotAllowedException, UserNotFoundException { + QueryNotSupportedException, NotAllowedException, UserNotFoundException, ServiceException { log.debug("endpoint create subset in database, databaseId={}, data.statement={}, principal.name={}, page={}, size={}, timestamp={}", databaseId, data.getStatement(), principal.getName(), page, size, timestamp); /* check */ @@ -323,7 +324,8 @@ public class SubsetEndpoint { @RequestParam(required = false) Long page, @RequestParam(required = false) Long size) throws PaginationException, DatabaseNotFoundException, RemoteUnavailableException, NotAllowedException, QueryNotFoundException, - DatabaseUnavailableException, TableMalformedException, QueryMalformedException, UserNotFoundException { + DatabaseUnavailableException, TableMalformedException, QueryMalformedException, UserNotFoundException, + ServiceException { log.debug("endpoint re-execute query, databaseId={}, subsetId={}, principal.name={} page={}, size={}", databaseId, subsetId, principal != null ? principal.getName() : null, page, size); endpointValidator.validateDataParams(page, size); @@ -408,7 +410,7 @@ public class SubsetEndpoint { @NotNull @Valid @RequestBody QueryPersistDto data, @NotNull Principal principal) throws NotAllowedException, RemoteUnavailableException, DatabaseNotFoundException, QueryStorePersistException, - DatabaseUnavailableException, QueryNotFoundException, UserNotFoundException { + DatabaseUnavailableException, QueryNotFoundException, UserNotFoundException, ServiceException { log.debug("endpoint persist query, databaseId={}, queryId={}, data.persist={}, principal.name={}", databaseId, queryId, data.getPersist(), principal.getName()); metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); diff --git a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java index b93ff32264..6856e634ab 100644 --- a/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java +++ b/dbrepo-data-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java @@ -12,7 +12,6 @@ import at.tuwien.api.database.table.internal.TableCreateDto; import at.tuwien.api.error.ApiErrorDto; import at.tuwien.exception.*; import at.tuwien.gateway.MetadataServiceGateway; -import at.tuwien.service.AnalyseService; import at.tuwien.service.TableService; import at.tuwien.utils.UserUtil; import at.tuwien.validation.EndpointValidator; @@ -47,22 +46,21 @@ import java.util.List; public class TableEndpoint { private final TableService tableService; - private final AnalyseService analyseService; private final EndpointValidator endpointValidator; private final MetadataServiceGateway metadataServiceGateway; @Autowired - public TableEndpoint(TableService tableService, AnalyseService analyseService, EndpointValidator endpointValidator, + public TableEndpoint(TableService tableService, EndpointValidator endpointValidator, MetadataServiceGateway metadataServiceGateway) { this.tableService = tableService; - this.analyseService = analyseService; this.endpointValidator = endpointValidator; this.metadataServiceGateway = metadataServiceGateway; } @PostMapping @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Create table", security = {@SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Create table", security = {@SecurityRequirement(name = "basicAuth")}, + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Created table", @@ -93,7 +91,7 @@ public class TableEndpoint { public ResponseEntity<TableDto> create(@NotNull @PathVariable("databaseId") Long databaseId, @Valid @RequestBody TableCreateDto data) throws DatabaseNotFoundException, RemoteUnavailableException, TableMalformedException, DatabaseUnavailableException, TableExistsException, - TableNotFoundException, QueryMalformedException { + TableNotFoundException, QueryMalformedException, ServiceException { log.debug("endpoint create table, databaseId={}, data.name={}", databaseId, data.getName()); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); try { @@ -107,7 +105,8 @@ public class TableEndpoint { @DeleteMapping("/{tableId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Delete table", security = {@SecurityRequirement(name = "basicAuth")}) + @Operation(summary = "Delete table", security = {@SecurityRequirement(name = "basicAuth")}, + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted table", @@ -133,7 +132,7 @@ public class TableEndpoint { public ResponseEntity<Void> delete(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("tableId") Long tableId) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - QueryMalformedException { + QueryMalformedException, ServiceException { log.debug("endpoint delete table, databaseId={}, tableId={}", databaseId, tableId); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); try { @@ -177,7 +176,7 @@ public class TableEndpoint { @RequestParam(required = false) Long page, @RequestParam(required = false) Long size) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - TableMalformedException, PaginationException, QueryMalformedException { + TableMalformedException, PaginationException, QueryMalformedException, ServiceException { log.debug("endpoint find table data, databaseId={}, tableId={}, timestamp={}, page={}, size={}", databaseId, tableId, timestamp, page, size); endpointValidator.validateDataParams(page, size); @@ -229,12 +228,12 @@ public class TableEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "404", - description = "Failed to find table in metadata database", + description = "Failed to find table in metadata database or blob in storage service", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "503", - description = "Failed to establish connection with the metadata service", + description = "Failed to establish connection with the metadata service or storage service", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @@ -244,15 +243,15 @@ public class TableEndpoint { @Valid @RequestBody TupleDto data, @NotNull Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - TableMalformedException, QueryMalformedException, NotAllowedException { + TableMalformedException, QueryMalformedException, NotAllowedException, StorageUnavailableException, + StorageNotFoundException, ServiceException { log.debug("endpoint insert raw table data, databaseId={}, tableId={}", databaseId, tableId); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); final DatabaseAccessDto access = metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); endpointValidator.validateOnlyWriteOwnOrWriteAllAccess(access.getType(), table.getOwner().getId(), UserUtil.getId(principal)); try { tableService.createTuple(table, data); - final TableStatisticDto statistics = analyseService.analyseTable(databaseId, tableId); - metadataServiceGateway.updateTableStatistics(databaseId, tableId, statistics); + metadataServiceGateway.updateTableStatistics(databaseId, tableId); return ResponseEntity.status(HttpStatus.CREATED) .build(); } catch (SQLException e) { @@ -296,7 +295,7 @@ public class TableEndpoint { @Valid @RequestBody TupleUpdateDto data, @NotNull Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - TableMalformedException, QueryMalformedException, NotAllowedException { + TableMalformedException, QueryMalformedException, NotAllowedException, ServiceException { log.debug("endpoint update raw table data, databaseId={}, tableId={}, data.keys={}", databaseId, tableId, data.getKeys()); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); @@ -304,8 +303,7 @@ public class TableEndpoint { endpointValidator.validateOnlyWriteOwnOrWriteAllAccess(access.getType(), table.getOwner().getId(), UserUtil.getId(principal)); try { tableService.updateTuple(table, data); - final TableStatisticDto statistics = analyseService.analyseTable(databaseId, tableId); - metadataServiceGateway.updateTableStatistics(databaseId, tableId, statistics); + metadataServiceGateway.updateTableStatistics(databaseId, tableId); return ResponseEntity.status(HttpStatus.ACCEPTED) .build(); } catch (SQLException e) { @@ -349,7 +347,7 @@ public class TableEndpoint { @Valid @RequestBody TupleDeleteDto data, @NotNull Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - TableMalformedException, QueryMalformedException, NotAllowedException { + TableMalformedException, QueryMalformedException, NotAllowedException, ServiceException { log.debug("endpoint delete raw table data, databaseId={}, tableId={}, data.keys={}", databaseId, tableId, data.getKeys()); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); @@ -357,8 +355,7 @@ public class TableEndpoint { endpointValidator.validateOnlyWriteOwnOrWriteAllAccess(access.getType(), table.getOwner().getId(), UserUtil.getId(principal)); try { tableService.deleteTuple(table, data); - final TableStatisticDto statistics = analyseService.analyseTable(databaseId, tableId); - metadataServiceGateway.updateTableStatistics(databaseId, tableId, statistics); + metadataServiceGateway.updateTableStatistics(databaseId, tableId); return ResponseEntity.status(HttpStatus.ACCEPTED) .build(); } catch (SQLException e) { @@ -369,13 +366,20 @@ public class TableEndpoint { @GetMapping("/{tableId}/history") @Observed(name = "dbrepo_table_data_history") - @Operation(summary = "Find table history", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) + @Operation(summary = "Find table history", + description = "Lists the insert/delete operations performed. Authentication is only required for tables in private databases", + security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found table history", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = TableHistoryDto[].class))}), + @ApiResponse(responseCode = "400", + description = "Invalid pagination request", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), @ApiResponse(responseCode = "403", description = "Find table history not allowed", content = {@Content( @@ -392,19 +396,30 @@ public class TableEndpoint { mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), }) - public ResponseEntity<List<TableHistoryDto>> getHistory(@NotBlank @PathVariable("databaseId") Long databaseId, - @NotBlank @PathVariable("tableId") Long tableId, + public ResponseEntity<List<TableHistoryDto>> getHistory(@NotNull @PathVariable("databaseId") Long databaseId, + @NotNull @PathVariable("tableId") Long tableId, + @RequestParam(value = "size", required = false) Long size, Principal principal) throws DatabaseUnavailableException, - RemoteUnavailableException, TableNotFoundException, NotAllowedException { + RemoteUnavailableException, TableNotFoundException, NotAllowedException, ServiceException, + PaginationException { log.debug("endpoint find table history, databaseId={}, tableId={}", databaseId, tableId); + if (size != null && size <= 0) { + log.error("Invalid size: must be > 0"); + throw new PaginationException("Invalid size: must be bigger than zero"); + } else if (size == null) { + log.debug("size not set: default to 100L"); + size = 100L; + } final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); - if (!table.getIsPublic() && principal == null) { - log.error("Failed to find table history: no authentication found"); - throw new NotAllowedException("Failed to find table history: no authentication found"); + if (!table.getIsPublic()) { + if (principal == null) { + log.error("Failed to find table history: no authentication found"); + throw new NotAllowedException("Failed to find table history: no authentication found"); + } + metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); } - metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); try { - final List<TableHistoryDto> dto = tableService.history(table); + final List<TableHistoryDto> dto = tableService.history(table, size); return ResponseEntity.status(HttpStatus.OK) .body(dto); } catch (SQLException e) { @@ -414,8 +429,9 @@ public class TableEndpoint { } @GetMapping - @PreAuthorize("isAuthenticated()") - @Operation(summary = "Find table schemas") + @PreAuthorize("hasAuthority('admin')") + @Observed(name = "dbrepo_table_schema_list") + @Operation(summary = "Find table schemas", hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Got table schemas", @@ -450,7 +466,7 @@ public class TableEndpoint { }) public ResponseEntity<List<TableDto>> getSchema(@NotBlank @PathVariable("databaseId") Long databaseId) throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, - DatabaseMalformedException, TableNotFoundException, QueryMalformedException { + DatabaseMalformedException, TableNotFoundException, QueryMalformedException, ServiceException { log.debug("endpoint inspect table schemas, databaseId={}", databaseId); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); try { @@ -497,7 +513,7 @@ public class TableEndpoint { Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, NotAllowedException, StorageUnavailableException, QueryMalformedException, SidecarExportException, - StorageNotFoundException { + StorageNotFoundException, ServiceException { log.debug("endpoint find table history, databaseId={}, tableId={}, timestamp={}", databaseId, tableId, timestamp); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); if (!table.getIsPublic()) { @@ -562,7 +578,8 @@ public class TableEndpoint { @Valid @RequestBody ImportCsvDto data, @NotNull Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, - QueryMalformedException, StorageNotFoundException, SidecarImportException, NotAllowedException { + QueryMalformedException, StorageNotFoundException, SidecarImportException, NotAllowedException, + ServiceException { log.debug("endpoint insert table data, databaseId={}, tableId={}, data.location={}", databaseId, tableId, data.getLocation()); final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); final DatabaseAccessDto access = metadataServiceGateway.getAccess(databaseId, UserUtil.getId(principal)); @@ -577,11 +594,49 @@ public class TableEndpoint { } try { tableService.importDataset(table, data); - final TableStatisticDto statistics = analyseService.analyseTable(databaseId, tableId); - metadataServiceGateway.updateTableStatistics(databaseId, tableId, statistics); + metadataServiceGateway.updateTableStatistics(databaseId, tableId); return ResponseEntity.accepted() .build(); + } catch (SQLException e) { + log.error("Failed to establish connection to database: {}", e.getMessage()); + throw new DatabaseUnavailableException("Failed to establish connection to database", e); + } + } + @GetMapping("/{tableId}/statistic") + @Observed(name = "dbrepo_table_statistic") + @Operation(summary = "Generate table statistic") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Generated table statistic", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = TableStatisticDto.class))}), + @ApiResponse(responseCode = "400", + description = "Failed to obtain column statistic", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "404", + description = "Failed to find table in metadata database", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), + @ApiResponse(responseCode = "503", + description = "Failed to establish connection with the metadata service", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiErrorDto.class))}), + }) + public ResponseEntity<TableStatisticDto> statistic(@NotBlank @PathVariable("databaseId") Long databaseId, + @NotBlank @PathVariable("tableId") Long tableId) + throws DatabaseUnavailableException, RemoteUnavailableException, TableNotFoundException, + ServiceException, TableMalformedException, QueryMalformedException { + log.debug("endpoint generate table statistic, databaseId={}, tableId={}", databaseId, tableId); + final PrivilegedTableDto table = metadataServiceGateway.getTableById(databaseId, tableId); + try { + final TableStatisticDto dto = tableService.getStatistics(table); + return ResponseEntity.ok(dto); } catch (SQLException e) { log.error("Failed to establish connection to database: {}", e.getMessage()); throw new DatabaseUnavailableException("Failed to establish connection to database", e); 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 91e5f7390d..64eea4ebd0 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 @@ -4,7 +4,6 @@ import at.tuwien.api.database.*; import at.tuwien.api.database.internal.PrivilegedDatabaseDto; import at.tuwien.api.database.internal.PrivilegedViewDto; import at.tuwien.api.database.query.QueryResultDto; -import at.tuwien.api.database.table.TableDto; import at.tuwien.api.error.ApiErrorDto; import at.tuwien.exception.*; import at.tuwien.gateway.MetadataServiceGateway; @@ -54,9 +53,9 @@ public class ViewEndpoint { } @GetMapping - @PreAuthorize("isAuthenticated()") + @PreAuthorize("hasAuthority('admin')") @Observed(name = "dbrepo_view_schema_list") - @Operation(summary = "Find view schemas") + @Operation(summary = "Find view schemas", hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Found view schemas", @@ -91,7 +90,8 @@ public class ViewEndpoint { }) public ResponseEntity<List<ViewDto>> getSchema(@NotBlank @PathVariable("databaseId") Long databaseId) throws DatabaseUnavailableException, DatabaseNotFoundException, RemoteUnavailableException, - ViewMalformedException, ViewNotFoundException, DatabaseMalformedException, ViewSchemaException { + ViewMalformedException, ViewNotFoundException, DatabaseMalformedException, ViewSchemaException, + ServiceException { log.debug("endpoint inspect view schemas, databaseId={}", databaseId); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); try { @@ -104,7 +104,8 @@ public class ViewEndpoint { @PostMapping @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Create view", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) + @Operation(summary = "Create view", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}, + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Created view", @@ -134,7 +135,7 @@ public class ViewEndpoint { }) public ResponseEntity<ViewDto> create(@NotNull @PathVariable("databaseId") Long databaseId, @Valid @RequestBody ViewCreateDto data) throws DatabaseUnavailableException, - DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException { + DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, ServiceException { log.debug("endpoint create view, databaseId={}, data.name={}", databaseId, data.getName()); final PrivilegedDatabaseDto database = metadataServiceGateway.getDatabaseById(databaseId); try { @@ -148,7 +149,8 @@ public class ViewEndpoint { @DeleteMapping("/{viewId}") @PreAuthorize("hasAuthority('admin')") - @Operation(summary = "Delete view", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}) + @Operation(summary = "Delete view", security = {@SecurityRequirement(name = "basicAuth"), @SecurityRequirement(name = "bearerAuth")}, + hidden = true) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Deleted view"), @@ -176,7 +178,7 @@ public class ViewEndpoint { public ResponseEntity<Void> delete(@NotBlank @PathVariable("databaseId") Long databaseId, @NotBlank @PathVariable("viewId") Long viewId) throws DatabaseUnavailableException, RemoteUnavailableException, ViewNotFoundException, - ViewMalformedException { + ViewMalformedException, ServiceException { log.debug("endpoint delete view, databaseId={}, viewId={}", databaseId, viewId); final PrivilegedViewDto view = metadataServiceGateway.getViewById(databaseId, viewId); try { @@ -232,7 +234,8 @@ public class ViewEndpoint { @NotNull HttpServletRequest request, Principal principal) throws DatabaseUnavailableException, RemoteUnavailableException, ViewNotFoundException, - QueryMalformedException, ViewMalformedException, PaginationException, NotAllowedException { + QueryMalformedException, ViewMalformedException, PaginationException, NotAllowedException, + ServiceException { log.debug("endpoint get view data, databaseId={}, viewId={}, page={}, size={}, timestamp={}", databaseId, viewId, page, size, timestamp); endpointValidator.validateDataParams(page, size); diff --git a/dbrepo-data-service/rest-service/src/main/resources/application-local.yml b/dbrepo-data-service/rest-service/src/main/resources/application-local.yml index fda4447d0b..c36b248b7e 100644 --- a/dbrepo-data-service/rest-service/src/main/resources/application-local.yml +++ b/dbrepo-data-service/rest-service/src/main/resources/application-local.yml @@ -49,9 +49,9 @@ logging: org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: debug dbrepo: endpoints: - gatewayService: http://localhost - storageService: http://localhost:9000 - authService: http://localhost:8080 + metadataService: http://localhost + storageService: http://localhost/api/storage + authService: http://localhost/api/auth s3: accessKeyId: seaweedfsadmin secretAccessKey: seaweedfsadmin diff --git a/dbrepo-data-service/rest-service/src/main/resources/application.yml b/dbrepo-data-service/rest-service/src/main/resources/application.yml index 44daf91e46..771f95d8d7 100644 --- a/dbrepo-data-service/rest-service/src/main/resources/application.yml +++ b/dbrepo-data-service/rest-service/src/main/resources/application.yml @@ -50,9 +50,9 @@ logging: org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: debug dbrepo: endpoints: - gatewayService: "${GATEWAY_SERVICE_ENDPOINT:http://gateway-service}" - storageService: "${S3_ENDPOINT:http://storage-service:9000}" - authService: "${AUTH_SERVICE_HOST:http://auth-service:8080}" + metadataService: "${METADATA_SERVICE_ENDPOINT:http://gateway-service}" + storageService: "${S3_ENDPOINT:http://gateway-service/api/storage}" + authService: "${AUTH_SERVICE_HOST:http://gateway-service/api/auth}" s3: accessKeyId: "${S3_ACCESS_KEY_ID:seaweedfsadmin}" secretAccessKey: "${S3_SECRET_ACCESS_KEY:seaweedfsadmin}" diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/config/MariaDbConfig.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/config/MariaDbConfig.java index 43d3b51507..54af799db3 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/config/MariaDbConfig.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/config/MariaDbConfig.java @@ -12,6 +12,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import java.io.IOException; import java.sql.*; import java.time.Instant; import java.util.*; @@ -77,6 +78,34 @@ public class MariaDbConfig { log.debug("created init database {}", database.getInternalName()); } + public static void grantReadAccess(PrivilegedDatabaseDto database, String username) { + final String jdbc = "jdbc:mariadb://" + database.getContainer().getHost() + ":" + database.getContainer().getPort() + "/" + database.getInternalName(); + log.trace("connect to database {}", jdbc); + try (Connection connection = DriverManager.getConnection(jdbc, database.getContainer().getUsername(), database.getContainer().getPassword())) { + connection.prepareStatement("GRANT SELECT ON *.* TO `" + username + "`@`%`;") + .executeUpdate(); + connection.prepareStatement("FLUSH PRIVILEGES;") + .executeUpdate(); + } catch (SQLException e) { + log.error("could not grant read access", e); + } + log.debug("granted read access to user {} in database {}", username, database.getInternalName()); + } + + public static void grantWriteAccess(PrivilegedDatabaseDto database, String username) { + final String jdbc = "jdbc:mariadb://" + database.getContainer().getHost() + ":" + database.getContainer().getPort() + "/" + database.getInternalName(); + log.trace("connect to database {}", jdbc); + try (Connection connection = DriverManager.getConnection(jdbc, database.getContainer().getUsername(), database.getContainer().getPassword())) { + connection.prepareStatement("GRANT SELECT, CREATE, CREATE VIEW, CREATE ROUTINE, CREATE TEMPORARY TABLES, LOCK TABLES, INDEX, TRIGGER, INSERT, UPDATE, DELETE ON *.* TO `" + username + "`@`%`;") + .executeUpdate(); + connection.prepareStatement("FLUSH PRIVILEGES;") + .executeUpdate(); + } catch (SQLException e) { + log.error("could not grant read access", e); + } + log.debug("granted read access to user {} in database {}", username, database.getInternalName()); + } + public static void dropAllDatabases(PrivilegedContainerDto container) { final String jdbc = "jdbc:mariadb://" + container.getHost() + ":" + container.getPort(); log.trace("connect to database {}", jdbc); @@ -136,31 +165,43 @@ public class MariaDbConfig { } } - public static String getPrivileges(String hostname, Integer port, String username, String password) - throws Exception { - return getPrivileges(hostname, port, null, username, password); - } - - public static String getPrivileges(String hostname, Integer port, String database, String username, String password) - throws Exception { - final String jdbc = "jdbc:mariadb://" + hostname + ":" + port + (database != null ? "/" + database : ""); + public static List<String> getPrivileges(PrivilegedDatabaseDto database, String username) throws SQLException { + final String jdbc = "jdbc:mariadb://" + database.getContainer().getHost() + ":" + database.getContainer().getPort() + "/" + database.getInternalName(); log.trace("connect to database {}", jdbc); - try (Connection connection = DriverManager.getConnection(jdbc, username, password)) { + try (Connection connection = DriverManager.getConnection(jdbc, database.getContainer().getUsername(), database.getContainer().getPassword())) { final String query = "SHOW GRANTS FOR `" + username + "`;"; log.trace("prepare statement '{}'", query); final PreparedStatement statement = connection.prepareStatement(query); final ResultSet set = statement.executeQuery(); statement.close(); if (set.next()) { - return set.getString(1); + final Matcher matcher = Pattern.compile("GRANT (.*) ON.*").matcher(set.getString(1)); + if (matcher.find()) { + final List<String> privileges = Arrays.asList(matcher.group(1).split(","));; + log.trace("found privileges: {}", privileges); + return privileges; + } } } - throw new Exception("Failed to get privileges"); + throw new SQLException("Failed to get privileges"); + } + + public static void dropTable(PrivilegedDatabaseDto database, String table) throws SQLException { + final String jdbc = "jdbc:mariadb://" + database.getContainer().getHost() + ":" + database.getContainer().getPort() + "/" + database.getInternalName(); + log.trace("connect to database {}", jdbc); + try (Connection connection = DriverManager.getConnection(jdbc, database.getContainer().getUsername(), database.getContainer().getPassword())) { + final String query = "DROP TABLE `" + table + "`;"; + log.trace("prepare statement '{}'", query); + final PreparedStatement statement = connection.prepareStatement(query); + statement.executeUpdate(); + statement.close(); + } + log.debug("dropped table {}", table); } - public static void mockQuery(String hostname, String query, String username, String password) + public static void mockQuery(String hostname, Integer port, String database, String query, String username, String password) throws SQLException { - final String jdbc = "jdbc:mariadb://" + hostname; + final String jdbc = "jdbc:mariadb://" + hostname + ":" + port + "/" + database; log.trace("connect to database {}", jdbc); try (Connection connection = DriverManager.getConnection(jdbc, username, password)) { final PreparedStatement statement = connection.prepareStatement(query); diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java index 4598a94b94..544f3f0d17 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/AccessEndpointUnitTest.java @@ -41,12 +41,12 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void create_succeeds() throws UserNotFoundException, NotAllowedException, QueryMalformedException, - DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException { + DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) .thenReturn(DATABASE_1_PRIVILEGED_DTO); - when(metadataServiceGateway.getUserById(USER_4_ID)) + when(metadataServiceGateway.getPrivilegedUserById(USER_4_ID)) .thenReturn(USER_4_PRIVILEGED_DTO); /* test */ @@ -56,12 +56,12 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void create_alreadyAccess_fails() throws UserNotFoundException, DatabaseNotFoundException, - RemoteUnavailableException { + RemoteUnavailableException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) .thenReturn(DATABASE_1_PRIVILEGED_DTO); - when(metadataServiceGateway.getUserById(USER_1_ID)) + when(metadataServiceGateway.getPrivilegedUserById(USER_1_ID)) .thenReturn(USER_1_PRIVILEGED_DTO); /* test */ @@ -72,7 +72,8 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void create_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException { + public void create_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, + ServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -88,18 +89,18 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void create_userNotFound_fails() throws UserNotFoundException, DatabaseNotFoundException, - RemoteUnavailableException { + RemoteUnavailableException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) .thenReturn(DATABASE_1_PRIVILEGED_DTO); doThrow(UserNotFoundException.class) .when(metadataServiceGateway) - .getUserById(USER_1_ID); + .getPrivilegedUserById(USER_4_ID); /* test */ assertThrows(UserNotFoundException.class, () -> { - accessEndpoint.create(DATABASE_1_ID, USER_1_ID, UPDATE_DATABASE_ACCESS_READ_DTO); + accessEndpoint.create(DATABASE_1_ID, USER_4_ID, UPDATE_DATABASE_ACCESS_READ_DTO); }); } @@ -116,12 +117,12 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void update_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, - NotAllowedException, QueryMalformedException, DatabaseMalformedException { + NotAllowedException, QueryMalformedException, DatabaseMalformedException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) .thenReturn(DATABASE_1_PRIVILEGED_DTO); - when(metadataServiceGateway.getUserById(USER_1_ID)) + when(metadataServiceGateway.getPrivilegedUserById(USER_1_ID)) .thenReturn(USER_1_PRIVILEGED_DTO); /* test */ @@ -140,7 +141,8 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void update_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException { + public void update_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, + ServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -156,7 +158,7 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void update_userNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - UserNotFoundException { + UserNotFoundException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -174,12 +176,12 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void revoke_succeeds() throws UserNotFoundException, NotAllowedException, QueryMalformedException, - DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException { + DatabaseNotFoundException, RemoteUnavailableException, DatabaseMalformedException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) .thenReturn(DATABASE_1_PRIVILEGED_DTO); - when(metadataServiceGateway.getUserById(USER_1_ID)) + when(metadataServiceGateway.getPrivilegedUserById(USER_1_ID)) .thenReturn(USER_1_PRIVILEGED_DTO); /* test */ @@ -198,7 +200,8 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void revoke_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException { + public void revoke_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, + ServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -214,7 +217,7 @@ public class AccessEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void revoke_userNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - UserNotFoundException { + UserNotFoundException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java index 8ab4d444f1..21769ff5eb 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java @@ -58,7 +58,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void create_succeeds() throws DatabaseUnavailableException, RemoteUnavailableException, - QueryStoreCreateException, ContainerNotFoundException, DatabaseMalformedException { + QueryStoreCreateException, ContainerNotFoundException, DatabaseMalformedException, ServiceException { /* test */ databaseEndpoint.create(DATABASE_1_CREATE_INTERNAL); @@ -67,7 +67,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME) public void create_noRole_fails() throws RemoteUnavailableException, ContainerNotFoundException, - SQLException, QueryStoreCreateException, DatabaseMalformedException { + SQLException, QueryStoreCreateException, DatabaseMalformedException, ServiceException { /* mock */ when(metadataServiceGateway.getContainerById(CONTAINER_1_ID)) @@ -89,7 +89,8 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void create_containerNotFound_fails() throws RemoteUnavailableException, ContainerNotFoundException { + public void create_containerNotFound_fails() throws RemoteUnavailableException, ContainerNotFoundException, + ServiceException { /* mock */ doThrow(ContainerNotFoundException.class) @@ -105,7 +106,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void create_queryStore_fails() throws RemoteUnavailableException, ContainerNotFoundException, SQLException, - DatabaseMalformedException, QueryStoreCreateException { + DatabaseMalformedException, QueryStoreCreateException, ServiceException { /* mock */ doThrow(ContainerNotFoundException.class) @@ -126,7 +127,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void update_succeeds() throws DatabaseUnavailableException, RemoteUnavailableException, - DatabaseMalformedException, DatabaseNotFoundException { + DatabaseMalformedException, DatabaseNotFoundException, ServiceException { /* test */ databaseEndpoint.update(DATABASE_1_ID, USER_1_UPDATE_PASSWORD_DTO); @@ -134,7 +135,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME) - public void update_noRole_fails() throws RemoteUnavailableException, DatabaseNotFoundException { + public void update_noRole_fails() throws RemoteUnavailableException, DatabaseNotFoundException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -148,7 +149,8 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void update_databaseNotFound_fails() throws RemoteUnavailableException, DatabaseNotFoundException { + public void update_databaseNotFound_fails() throws RemoteUnavailableException, DatabaseNotFoundException, + ServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -164,7 +166,7 @@ public class DatabaseEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void update_password_fails() throws RemoteUnavailableException, DatabaseNotFoundException, SQLException, - DatabaseMalformedException { + DatabaseMalformedException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/SubsetEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/SubsetEndpointUnitTest.java index d212bb3064..6cdb0c6753 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/SubsetEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/SubsetEndpointUnitTest.java @@ -66,7 +66,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void findAllById_succeeds() throws DatabaseUnavailableException, NotAllowedException, QueryNotFoundException, - DatabaseNotFoundException, RemoteUnavailableException, SQLException { + DatabaseNotFoundException, RemoteUnavailableException, SQLException, ServiceException { /* test */ final List<QueryDto> response = generic_findAllById(DATABASE_3_ID, DATABASE_3_PRIVILEGED_DTO, null); @@ -98,7 +98,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { public void findById_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseUnavailableException, StorageUnavailableException, NotAllowedException, QueryMalformedException, QueryNotFoundException, SidecarExportException, FormatNotAvailableException, StorageNotFoundException, - SQLException { + SQLException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_3_ID)) @@ -113,7 +113,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { public void findById_acceptCsv_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseUnavailableException, StorageUnavailableException, NotAllowedException, QueryMalformedException, QueryNotFoundException, SidecarExportException, FormatNotAvailableException, - StorageNotFoundException, SQLException { + StorageNotFoundException, SQLException, ServiceException { final ExportResourceDto mock = ExportResourceDto.builder() .filename("deadbeef") .resource(new InputStreamResource(InputStream.nullInputStream())) @@ -134,7 +134,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { public void findById_timestamp_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseUnavailableException, StorageUnavailableException, NotAllowedException, QueryMalformedException, QueryNotFoundException, SidecarExportException, FormatNotAvailableException, - StorageNotFoundException, SQLException { + StorageNotFoundException, SQLException, ServiceException { final ExportResourceDto mock = ExportResourceDto.builder() .filename("deadbeef") .resource(new InputStreamResource(InputStream.nullInputStream())) @@ -152,7 +152,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void findById_fails() throws DatabaseNotFoundException, RemoteUnavailableException { + public void findById_fails() throws DatabaseNotFoundException, RemoteUnavailableException, ServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -171,7 +171,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { NotAllowedException, SidecarExportException, QueryNotSupportedException, PaginationException, StorageNotFoundException, DatabaseUnavailableException, StorageUnavailableException, QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, RemoteUnavailableException, - FormatNotAvailableException, SQLException { + SQLException, ServiceException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement(QUERY_5_STATEMENT) .build(); @@ -207,7 +207,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { TableMalformedException, NotAllowedException, SidecarExportException, QueryNotSupportedException, PaginationException, StorageNotFoundException, DatabaseUnavailableException, StorageUnavailableException, QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, RemoteUnavailableException, - FormatNotAvailableException, SQLException { + SQLException, ServiceException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement(QUERY_5_STATEMENT) .build(); @@ -227,7 +227,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"execute-query"}) public void create_databaseNotFound_fails() throws NotAllowedException, RemoteUnavailableException, - DatabaseNotFoundException { + DatabaseNotFoundException, ServiceException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement(QUERY_5_STATEMENT) .build(); @@ -260,7 +260,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_4_USERNAME, authorities = {"execute-query"}) - public void create_noAccess_fails() throws NotAllowedException, RemoteUnavailableException { + public void create_noAccess_fails() throws NotAllowedException, RemoteUnavailableException, ServiceException { final ExecuteStatementDto request = ExecuteStatementDto.builder() .statement(QUERY_5_STATEMENT) .build(); @@ -279,7 +279,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test public void getData_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, NotAllowedException, SQLException, QueryNotFoundException, TableMalformedException, QueryMalformedException, - DatabaseUnavailableException, PaginationException { + DatabaseUnavailableException, PaginationException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_3_ID)) @@ -308,7 +308,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test public void getData_onlyHead_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, NotAllowedException, SQLException, QueryNotFoundException, TableMalformedException, QueryMalformedException, - DatabaseUnavailableException, PaginationException { + DatabaseUnavailableException, PaginationException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_3_ID)) @@ -332,7 +332,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME) public void getData_private_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseUnavailableException, NotAllowedException, TableMalformedException, - QueryMalformedException, QueryNotFoundException, PaginationException, SQLException { + QueryMalformedException, QueryNotFoundException, PaginationException, SQLException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -357,7 +357,8 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void getData_privateAnonymous_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException { + public void getData_privateAnonymous_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, + ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -372,7 +373,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME) public void getData_privateNoAccess_fails() throws DatabaseNotFoundException, RemoteUnavailableException, - NotAllowedException { + NotAllowedException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -391,7 +392,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME) public void getData_privateOnlyHead_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, UserNotFoundException, DatabaseUnavailableException, NotAllowedException, TableMalformedException, - QueryMalformedException, QueryNotFoundException, PaginationException, SQLException { + QueryMalformedException, QueryNotFoundException, PaginationException, SQLException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -415,7 +416,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_3_USERNAME, authorities = {"persist-query"}) public void persist_succeeds() throws NotAllowedException, RemoteUnavailableException, DatabaseNotFoundException, QueryStorePersistException, SQLException, UserNotFoundException, QueryNotFoundException, - DatabaseUnavailableException { + DatabaseUnavailableException, ServiceException { final QueryPersistDto request = QueryPersistDto.builder() .persist(true) .build(); @@ -450,7 +451,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"persist-query"}) - public void persist_noAccess_fails() throws NotAllowedException, RemoteUnavailableException { + public void persist_noAccess_fails() throws NotAllowedException, RemoteUnavailableException, ServiceException { final QueryPersistDto request = QueryPersistDto.builder() .persist(true) .build(); @@ -469,7 +470,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"persist-query"}) public void persist_databaseNotFound_fails() throws NotAllowedException, RemoteUnavailableException, - DatabaseNotFoundException { + DatabaseNotFoundException, ServiceException { final QueryPersistDto request = QueryPersistDto.builder() .persist(true) .build(); @@ -489,7 +490,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { protected List<QueryDto> generic_findAllById(Long databaseId, PrivilegedDatabaseDto database, Principal principal) throws DatabaseUnavailableException, NotAllowedException, QueryNotFoundException, DatabaseNotFoundException, - RemoteUnavailableException, SQLException { + RemoteUnavailableException, SQLException, ServiceException { /* mock */ if (database != null) { @@ -513,7 +514,7 @@ public class SubsetEndpointUnitTest extends AbstractUnitTest { Principal principal) throws UserNotFoundException, DatabaseUnavailableException, StorageUnavailableException, NotAllowedException, QueryMalformedException, QueryNotFoundException, DatabaseNotFoundException, SidecarExportException, RemoteUnavailableException, FormatNotAvailableException, - StorageNotFoundException, SQLException { + StorageNotFoundException, SQLException, ServiceException { /* mock */ when(queryService.findById(DATABASE_3_PRIVILEGED_DTO, subsetId)) diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java index 73760a5a2a..62375e2ab4 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointUnitTest.java @@ -7,7 +7,6 @@ import at.tuwien.api.database.table.*; import at.tuwien.endpoints.TableEndpoint; import at.tuwien.exception.*; import at.tuwien.gateway.MetadataServiceGateway; -import at.tuwien.service.AnalyseService; import at.tuwien.service.TableService; import at.tuwien.test.AbstractUnitTest; import lombok.extern.log4j.Log4j2; @@ -44,9 +43,6 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @MockBean private TableService tableService; - @MockBean - private AnalyseService analyseService; - @MockBean private MetadataServiceGateway metadataServiceGateway; @@ -59,7 +55,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void create_succeeds() throws DatabaseUnavailableException, TableMalformedException, DatabaseNotFoundException, TableExistsException, RemoteUnavailableException, SQLException, - TableNotFoundException, QueryMalformedException { + TableNotFoundException, QueryMalformedException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -84,7 +80,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void create_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException { + public void create_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, + ServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -100,7 +97,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void delete_succeeds() throws RemoteUnavailableException, DatabaseUnavailableException, - TableNotFoundException, QueryMalformedException, SQLException { + TableNotFoundException, QueryMalformedException, SQLException, ServiceException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID)) @@ -126,7 +123,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void delete_tableNotFound_fails() throws RemoteUnavailableException, TableNotFoundException { + public void delete_tableNotFound_fails() throws RemoteUnavailableException, TableNotFoundException, + ServiceException { /* mock */ doThrow(TableNotFoundException.class) @@ -142,7 +140,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void getData_succeeds() throws DatabaseUnavailableException, TableNotFoundException, TableMalformedException, - SQLException, QueryMalformedException, RemoteUnavailableException, PaginationException { + SQLException, QueryMalformedException, RemoteUnavailableException, PaginationException, ServiceException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_3_ID, TABLE_8_ID)) @@ -166,7 +164,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser - public void getData_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException { + public void getData_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, + ServiceException { /* mock */ doThrow(TableNotFoundException.class) @@ -182,7 +181,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void createTuple_succeeds() throws DatabaseUnavailableException, TableNotFoundException, - TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, SQLException { + TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, + SQLException, StorageUnavailableException, StorageNotFoundException, ServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -198,11 +198,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { doNothing() .when(tableService) .createTuple(TABLE_8_PRIVILEGED_DTO, request); - when(analyseService.analyseTable(DATABASE_3_ID, TABLE_8_ID)) - .thenReturn(TABLE_8_STATISTIC_DTO); doNothing() .when(metadataServiceGateway) - .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID, TABLE_8_STATISTIC_DTO); + .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID); /* test */ final ResponseEntity<Void> response = tableEndpoint.insertRawTuple(DATABASE_3_ID, TABLE_8_ID, request, USER_1_PRINCIPAL); @@ -227,7 +225,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) - public void createTuple_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException { + public void createTuple_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, + ServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -249,7 +248,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void createTuple_readAccess_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException { + NotAllowedException, ServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -272,7 +271,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void createTuple_writeOwnAccess_succeeds() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException { + NotAllowedException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException, + StorageUnavailableException, StorageNotFoundException, ServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -293,7 +293,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void createTuple_writeOwnAccessForeign_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException { + NotAllowedException, ServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -316,7 +316,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void createTuple_writeAllAccessForeign_succeeds() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException { + NotAllowedException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException, + StorageUnavailableException, StorageNotFoundException, ServiceException { final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 7L); @@ -337,7 +338,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_succeeds() throws DatabaseUnavailableException, TableNotFoundException, - TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, SQLException { + TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, + SQLException, ServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -356,11 +358,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { doNothing() .when(tableService) .updateTuple(TABLE_8_PRIVILEGED_DTO, request); - when(analyseService.analyseTable(DATABASE_3_ID, TABLE_8_ID)) - .thenReturn(TABLE_8_STATISTIC_DTO); doNothing() .when(metadataServiceGateway) - .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID, TABLE_8_STATISTIC_DTO); + .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID); /* test */ final ResponseEntity<Void> response = tableEndpoint.updateRawTuple(DATABASE_3_ID, TABLE_8_ID, request, USER_1_PRINCIPAL); @@ -388,7 +388,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) - public void updateTuple_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException { + public void updateTuple_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, + ServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -413,7 +414,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_readAccess_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException { + NotAllowedException, ServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -440,7 +441,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_writeOwnAccess_succeeds() throws DatabaseUnavailableException, TableNotFoundException, TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, - SQLException { + SQLException, ServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -459,11 +460,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { doNothing() .when(tableService) .updateTuple(TABLE_8_PRIVILEGED_DTO, request); - when(analyseService.analyseTable(DATABASE_3_ID, TABLE_8_ID)) - .thenReturn(TABLE_8_STATISTIC_DTO); doNothing() .when(metadataServiceGateway) - .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID, TABLE_8_STATISTIC_DTO); + .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID); /* test */ final ResponseEntity<Void> response = tableEndpoint.updateRawTuple(DATABASE_3_ID, TABLE_8_ID, request, USER_1_PRINCIPAL); @@ -473,7 +472,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_writeOwnAccessForeign_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException { + NotAllowedException, ServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -500,7 +499,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void updateTuple_writeAllAccessForeign_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException, - SQLException { + SQLException, ServiceException { final TupleUpdateDto request = TupleUpdateDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -519,11 +518,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { doNothing() .when(tableService) .updateTuple(TABLE_8_PRIVILEGED_DTO, request); - when(analyseService.analyseTable(DATABASE_3_ID, TABLE_8_ID)) - .thenReturn(TABLE_8_STATISTIC_DTO); doNothing() .when(metadataServiceGateway) - .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID, TABLE_8_STATISTIC_DTO); + .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID); /* test */ final ResponseEntity<Void> response = tableEndpoint.updateRawTuple(DATABASE_3_ID, TABLE_8_ID, request, USER_3_PRINCIPAL); @@ -533,7 +530,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_succeeds() throws DatabaseUnavailableException, TableNotFoundException, - TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, SQLException { + TableMalformedException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, + SQLException, ServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -548,11 +546,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { doNothing() .when(tableService) .deleteTuple(TABLE_8_PRIVILEGED_DTO, request); - when(analyseService.analyseTable(DATABASE_3_ID, TABLE_8_ID)) - .thenReturn(TABLE_8_STATISTIC_DTO); doNothing() .when(metadataServiceGateway) - .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID, TABLE_8_STATISTIC_DTO); + .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID); /* test */ final ResponseEntity<Void> response = tableEndpoint.deleteRawTuple(DATABASE_3_ID, TABLE_8_ID, request, USER_1_PRINCIPAL); @@ -576,7 +572,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"delete-table-data"}) - public void deleteTuple_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException { + public void deleteTuple_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, + ServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -597,7 +594,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_readAccess_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException { + NotAllowedException, ServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -620,7 +617,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_writeOwnAccess_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, TableMalformedException, SQLException, QueryMalformedException, - DatabaseUnavailableException { + DatabaseUnavailableException, ServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -635,11 +632,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { doNothing() .when(tableService) .deleteTuple(TABLE_8_PRIVILEGED_DTO, request); - when(analyseService.analyseTable(DATABASE_3_ID, TABLE_8_ID)) - .thenReturn(TABLE_8_STATISTIC_DTO); doNothing() .when(metadataServiceGateway) - .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID, TABLE_8_STATISTIC_DTO); + .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID); /* test */ final ResponseEntity<Void> response = tableEndpoint.deleteRawTuple(DATABASE_3_ID, TABLE_8_ID, request, USER_1_PRINCIPAL); @@ -649,7 +644,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_writeOwnAccessForeign_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException { + NotAllowedException, ServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -672,7 +667,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_3_USERNAME, authorities = {"delete-table-data"}) public void deleteTuple_writeAllAccessForeign_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, DatabaseUnavailableException, TableMalformedException, QueryMalformedException, - SQLException { + SQLException, ServiceException { final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ put(COLUMN_8_1_INTERNAL_NAME, 6L); @@ -687,11 +682,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { doNothing() .when(tableService) .deleteTuple(TABLE_8_PRIVILEGED_DTO, request); - when(analyseService.analyseTable(DATABASE_3_ID, TABLE_8_ID)) - .thenReturn(TABLE_8_STATISTIC_DTO); doNothing() .when(metadataServiceGateway) - .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID, TABLE_8_STATISTIC_DTO); + .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID); /* test */ final ResponseEntity<Void> response = tableEndpoint.deleteRawTuple(DATABASE_3_ID, TABLE_8_ID, request, USER_3_PRINCIPAL); @@ -701,22 +694,23 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithAnonymousUser public void getHistory_succeeds() throws DatabaseUnavailableException, TableNotFoundException, - RemoteUnavailableException, SQLException, NotAllowedException { + RemoteUnavailableException, SQLException, NotAllowedException, ServiceException, PaginationException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_3_ID, TABLE_8_ID)) .thenReturn(TABLE_8_PRIVILEGED_DTO); - when(tableService.history(TABLE_8_PRIVILEGED_DTO)) + when(tableService.history(TABLE_8_PRIVILEGED_DTO, null)) .thenReturn(List.of()); /* test */ - final ResponseEntity<List<TableHistoryDto>> response = tableEndpoint.getHistory(DATABASE_3_ID, TABLE_8_ID, null); + final ResponseEntity<List<TableHistoryDto>> response = tableEndpoint.getHistory(DATABASE_3_ID, TABLE_8_ID, null, null); assertEquals(HttpStatus.OK, response.getStatusCode()); } @Test @WithAnonymousUser - public void getHistory_privateNoRole_fails() throws TableNotFoundException, RemoteUnavailableException { + public void getHistory_privateNoRole_fails() throws TableNotFoundException, RemoteUnavailableException, + ServiceException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID)) @@ -724,14 +718,14 @@ public class TableEndpointUnitTest extends AbstractUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - tableEndpoint.getHistory(DATABASE_1_ID, TABLE_1_ID, null); + tableEndpoint.getHistory(DATABASE_1_ID, TABLE_1_ID, null, null); }); } @Test @WithMockUser(username = USER_4_USERNAME) public void getHistory_privateNoAccess_fails() throws NotAllowedException, RemoteUnavailableException, - TableNotFoundException { + TableNotFoundException, ServiceException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID)) @@ -742,13 +736,14 @@ public class TableEndpointUnitTest extends AbstractUnitTest { /* test */ assertThrows(NotAllowedException.class, () -> { - tableEndpoint.getHistory(DATABASE_1_ID, TABLE_1_ID, USER_4_PRINCIPAL); + tableEndpoint.getHistory(DATABASE_1_ID, TABLE_1_ID, null, USER_4_PRINCIPAL); }); } @Test @WithAnonymousUser - public void getHistory_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException { + public void getHistory_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, + ServiceException { /* mock */ doThrow(TableNotFoundException.class) @@ -757,7 +752,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { /* test */ assertThrows(TableNotFoundException.class, () -> { - tableEndpoint.getHistory(DATABASE_3_ID, TABLE_8_ID, null); + tableEndpoint.getHistory(DATABASE_3_ID, TABLE_8_ID, null, null); }); } @@ -765,7 +760,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithAnonymousUser public void exportData_succeeds() throws DatabaseUnavailableException, TableNotFoundException, NotAllowedException, StorageUnavailableException, QueryMalformedException, SidecarExportException, RemoteUnavailableException, - StorageNotFoundException, SQLException { + StorageNotFoundException, SQLException, ServiceException { final ExportResourceDto mock = ExportResourceDto.builder() .filename("deadbeef") .resource(new InputStreamResource(InputStream.nullInputStream())) @@ -786,7 +781,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_4_USERNAME) public void exportData_privateNoAccess_fails() throws TableNotFoundException, NotAllowedException, - RemoteUnavailableException { + RemoteUnavailableException, ServiceException { /* mock */ when(metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID)) @@ -806,7 +801,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void importData_succeeds() throws DatabaseUnavailableException, TableNotFoundException, SidecarImportException, NotAllowedException, QueryMalformedException, RemoteUnavailableException, - StorageNotFoundException, SQLException { + StorageNotFoundException, SQLException, ServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") @@ -821,11 +816,9 @@ public class TableEndpointUnitTest extends AbstractUnitTest { doNothing() .when(tableService) .importDataset(TABLE_8_PRIVILEGED_DTO, request); - when(analyseService.analyseTable(DATABASE_3_ID, TABLE_8_ID)) - .thenReturn(TABLE_8_STATISTIC_DTO); doNothing() .when(metadataServiceGateway) - .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID, TABLE_8_STATISTIC_DTO); + .updateTableStatistics(DATABASE_3_ID, TABLE_8_ID); /* test */ final ResponseEntity<Void> response = tableEndpoint.importDataset(DATABASE_3_ID, TABLE_8_ID, request, USER_1_PRINCIPAL); @@ -849,7 +842,8 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) - public void importData_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException { + public void importData_tableNotFound_fails() throws TableNotFoundException, RemoteUnavailableException, + ServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") @@ -870,7 +864,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void importData_readAccess_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException { + NotAllowedException, ServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") @@ -893,7 +887,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"insert-table-data"}) public void importData_writeOwnAccess_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, DatabaseUnavailableException, SidecarImportException, QueryMalformedException, - StorageNotFoundException { + StorageNotFoundException, ServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") @@ -913,7 +907,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void importData_writeOwnAccessForeign_fails() throws TableNotFoundException, RemoteUnavailableException, - NotAllowedException { + NotAllowedException, ServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") @@ -936,7 +930,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_3_USERNAME, authorities = {"insert-table-data"}) public void importData_writeAllAccessForeign_succeeds() throws TableNotFoundException, RemoteUnavailableException, NotAllowedException, DatabaseUnavailableException, SidecarImportException, QueryMalformedException, - StorageNotFoundException { + StorageNotFoundException, ServiceException { final ImportCsvDto request = ImportCsvDto.builder() .skipLines(1L) .lineTermination("\\n") 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 de98f501d0..af4767b049 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 @@ -52,7 +52,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void create_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, - SQLException, DatabaseUnavailableException { + SQLException, DatabaseUnavailableException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -68,7 +68,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME) public void create_noRole_fails() throws DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, - SQLException { + SQLException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -84,7 +84,8 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void create_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException { + public void create_databaseNotFound_fails() throws DatabaseNotFoundException, RemoteUnavailableException, + ServiceException { /* mock */ doThrow(DatabaseNotFoundException.class) @@ -100,7 +101,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) public void delete_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, - SQLException, DatabaseUnavailableException, ViewNotFoundException { + SQLException, DatabaseUnavailableException, ViewNotFoundException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -117,7 +118,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME) public void delete_noRole_fails() throws DatabaseNotFoundException, RemoteUnavailableException, ViewMalformedException, - SQLException { + SQLException, ServiceException { /* mock */ when(metadataServiceGateway.getDatabaseById(DATABASE_1_ID)) @@ -134,7 +135,8 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_LOCAL_ADMIN_USERNAME, authorities = {"admin"}) - public void delete_databaseNotFound_fails() throws RemoteUnavailableException, ViewNotFoundException { + public void delete_databaseNotFound_fails() throws RemoteUnavailableException, ViewNotFoundException, + ServiceException { /* mock */ doThrow(ViewNotFoundException.class) @@ -151,7 +153,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"view-database-view-data"}) public void getData_succeeds() throws RemoteUnavailableException, ViewNotFoundException, ViewMalformedException, SQLException, DatabaseUnavailableException, QueryMalformedException, PaginationException, - NotAllowedException { + NotAllowedException, ServiceException { /* mock */ when(metadataServiceGateway.getViewById(DATABASE_1_ID, VIEW_1_ID)) @@ -181,7 +183,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @WithMockUser(username = USER_1_USERNAME, authorities = {"view-database-view-data"}) public void getData_onlyHead_succeeds() throws RemoteUnavailableException, ViewNotFoundException, ViewMalformedException, SQLException, DatabaseUnavailableException, QueryMalformedException, - PaginationException, NotAllowedException { + PaginationException, NotAllowedException, ServiceException { /* mock */ when(metadataServiceGateway.getViewById(DATABASE_1_ID, VIEW_1_ID)) @@ -207,7 +209,8 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_1_USERNAME, authorities = {"view-database-view-data"}) - public void getData_viewNotFound_fails() throws RemoteUnavailableException, ViewNotFoundException { + public void getData_viewNotFound_fails() throws RemoteUnavailableException, ViewNotFoundException, + ServiceException { /* mock */ doThrow(ViewNotFoundException.class) @@ -223,7 +226,7 @@ public class ViewEndpointUnitTest extends AbstractUnitTest { @Test @WithMockUser(username = USER_3_USERNAME, authorities = {"view-database-view-data"}) public void getData_privateNoAccess_fails() throws RemoteUnavailableException, ViewNotFoundException, - NotAllowedException { + NotAllowedException, ServiceException { /* mock */ when(metadataServiceGateway.getViewById(DATABASE_1_ID, VIEW_3_ID)) diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/DataDatabaseGatewayUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/DataDatabaseGatewayUnitTest.java new file mode 100644 index 0000000000..30a4d31cfb --- /dev/null +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/DataDatabaseGatewayUnitTest.java @@ -0,0 +1,151 @@ +package at.tuwien.gateway; + +import at.tuwien.exception.*; +import at.tuwien.test.AbstractUnitTest; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.*; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class DataDatabaseGatewayUnitTest extends AbstractUnitTest { + + @MockBean + @Qualifier("restTemplate") + private RestTemplate restTemplate; + + @Autowired + private DataDatabaseSidecarGateway dataDatabaseSidecarGateway; + + @BeforeEach + public void beforeEach() { + genesis(); + } + + @Test + public void importFile_succeeds() throws RemoteUnavailableException, StorageNotFoundException, ServiceException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.accepted() + .build()); + + /* test */ + dataDatabaseSidecarGateway.importFile(CONTAINER_1_HOST, CONTAINER_1_PORT, "filename"); + } + + @Test + public void importFile_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + dataDatabaseSidecarGateway.importFile(CONTAINER_1_HOST, CONTAINER_1_PORT, "filename"); + }); + } + + @Test + public void importFile_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + dataDatabaseSidecarGateway.importFile(CONTAINER_1_HOST, CONTAINER_1_PORT, "filename"); + }); + } + + @Test + public void importFile_s3_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(StorageNotFoundException.class, () -> { + dataDatabaseSidecarGateway.importFile(CONTAINER_1_HOST, CONTAINER_1_PORT, "filename"); + }); + } + + @Test + public void exportFile_succeeds() throws RemoteUnavailableException, StorageNotFoundException, ServiceException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.accepted() + .build()); + + /* test */ + dataDatabaseSidecarGateway.exportFile(CONTAINER_1_HOST, CONTAINER_1_PORT, "filename"); + } + + @Test + public void exportFile_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + dataDatabaseSidecarGateway.exportFile(CONTAINER_1_HOST, CONTAINER_1_PORT, "filename"); + }); + } + + @Test + public void exportFile_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + dataDatabaseSidecarGateway.exportFile(CONTAINER_1_HOST, CONTAINER_1_PORT, "filename"); + }); + } + + @Test + public void exportFile_s3_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(StorageNotFoundException.class, () -> { + dataDatabaseSidecarGateway.exportFile(CONTAINER_1_HOST, CONTAINER_1_PORT, "filename"); + }); + } + +} diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/InterceptorUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/InterceptorUnitTest.java new file mode 100644 index 0000000000..0fd20a8025 --- /dev/null +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/InterceptorUnitTest.java @@ -0,0 +1,61 @@ +package at.tuwien.gateway; + +import at.tuwien.api.keycloak.TokenDto; +import at.tuwien.exception.RemoteUnavailableException; +import at.tuwien.exception.ServiceException; +import at.tuwien.exception.StorageNotFoundException; +import at.tuwien.test.AbstractUnitTest; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class InterceptorUnitTest extends AbstractUnitTest { + + @MockBean + @Qualifier("keycloakRestTemplate") + private RestTemplate restTemplate; + + @Autowired + private DataDatabaseSidecarGateway dataDatabaseSidecarGateway; + + @BeforeEach + public void beforeEach() { + genesis(); + } + + @Test + public void intercept_succeeds() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.ok() + .build()); + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.ok() + .build()); + + /* test */ + restTemplate.exchange("https://example.com", HttpMethod.GET, HttpEntity.EMPTY, Void.class); + } +} diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakSidecarGatewayUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakSidecarGatewayUnitTest.java new file mode 100644 index 0000000000..2a02e03466 --- /dev/null +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/KeycloakSidecarGatewayUnitTest.java @@ -0,0 +1,101 @@ +package at.tuwien.gateway; + +import at.tuwien.api.keycloak.TokenDto; +import at.tuwien.exception.RemoteUnavailableException; +import at.tuwien.exception.ServiceConnectionException; +import at.tuwien.exception.ServiceException; +import at.tuwien.test.AbstractUnitTest; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class KeycloakSidecarGatewayUnitTest extends AbstractUnitTest { + + @MockBean + @Qualifier("restTemplate") + private RestTemplate restTemplate; + + @Autowired + private KeycloakGateway keycloakGateway; + + @BeforeEach + public void beforeEach() { + genesis(); + } + + @Test + public void obtainUserToken_succeeds() throws ServiceException, RemoteUnavailableException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.ok() + .build()); + + /* test */ + final TokenDto response = keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); + } + + @Test + public void obtainUserToken_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); + }); + } + + @Test + public void obtainUserToken_badRequest_fails() { + + /* mock */ + doThrow(HttpClientErrorException.BadRequest.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class)); + + /* test */ + assertThrows(ServiceException.class, () -> { + keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); + }); + } + + @Test + public void obtainUserToken_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.POST), any(HttpEntity.class), eq(TokenDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + keycloakGateway.obtainUserToken(USER_1_USERNAME, USER_1_PASSWORD); + }); + } + +} diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/MetadataServiceGatewayUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/MetadataServiceGatewayUnitTest.java new file mode 100644 index 0000000000..1ba4978788 --- /dev/null +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/gateway/MetadataServiceGatewayUnitTest.java @@ -0,0 +1,933 @@ +package at.tuwien.gateway; + +import at.tuwien.api.container.ContainerDto; +import at.tuwien.api.container.internal.PrivilegedContainerDto; +import at.tuwien.api.database.DatabaseAccessDto; +import at.tuwien.api.database.ViewDto; +import at.tuwien.api.database.internal.PrivilegedDatabaseDto; +import at.tuwien.api.database.internal.PrivilegedViewDto; +import at.tuwien.api.database.table.TableDto; +import at.tuwien.api.database.table.internal.PrivilegedTableDto; +import at.tuwien.api.identifier.IdentifierDto; +import at.tuwien.api.user.PrivilegedUserDto; +import at.tuwien.api.user.UserDto; +import at.tuwien.exception.*; +import at.tuwien.test.AbstractUnitTest; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.*; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class MetadataServiceGatewayUnitTest extends AbstractUnitTest { + + @MockBean + @Qualifier("restTemplate") + private RestTemplate restTemplate; + + @Autowired + private MetadataServiceGateway metadataServiceGateway; + + @BeforeEach + public void beforeEach() { + genesis(); + } + + @Test + public void getTableById_succeeds() throws TableNotFoundException, RemoteUnavailableException, ServiceException { + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Type", IMAGE_1_JDBC); + headers.set("X-Host", CONTAINER_1_HOST); + headers.set("X-Port", "" + CONTAINER_1_PORT); + headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); + headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); + headers.set("X-Database", DATABASE_1_INTERNALNAME); + headers.set("X-Sidecar-Host", CONTAINER_1_SIDECAR_HOST); + headers.set("X-Sidecar-Port", "" + CONTAINER_1_SIDECAR_PORT); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .headers(headers) + .body(TABLE_1_DTO)); + + /* test */ + final PrivilegedTableDto response = metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID); + assertEquals(IMAGE_1_JDBC, response.getDatabase().getContainer().getImage().getJdbcMethod()); + assertEquals(CONTAINER_1_HOST, response.getDatabase().getContainer().getHost()); + assertEquals(CONTAINER_1_PORT, response.getDatabase().getContainer().getPort()); + assertEquals(CONTAINER_1_PRIVILEGED_USERNAME, response.getDatabase().getContainer().getUsername()); + assertEquals(CONTAINER_1_PRIVILEGED_PASSWORD, response.getDatabase().getContainer().getPassword()); + assertEquals(DATABASE_1_INTERNALNAME, response.getDatabase().getInternalName()); + assertEquals(CONTAINER_1_SIDECAR_HOST, response.getDatabase().getContainer().getSidecarHost()); + assertEquals(CONTAINER_1_SIDECAR_PORT, response.getDatabase().getContainer().getSidecarPort()); + } + + @Test + public void getTableById_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto.class)); + + /* test */ + assertThrows(TableNotFoundException.class, () -> { + metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID); + }); + } + + @Test + public void getTableById_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.ServiceUnavailable.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID); + }); + } + + @Test + public void getTableById_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .body(TABLE_1_DTO)); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID); + }); + } + + @Test + public void getTableById_headerMissing_fails() { + final List<String> customHeaders = List.of("X-Type", "X-Host", "X-Port", "X-Username", "X-Password", "X-Database", "X-Sidecar-Host", "X-Sidecar-Port"); + + for (int i = 0; i < customHeaders.size(); i++) { + final HttpHeaders headers = new HttpHeaders(); + for (int j = 0; j < i; j++) { + headers.add(customHeaders.get(j), ""); + } + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .headers(headers) + .body(TABLE_1_DTO)); + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID); + }); + } + } + + @Test + public void getTableById_emptyBody_fails() { + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Type", IMAGE_1_JDBC); + headers.set("X-Host", CONTAINER_1_HOST); + headers.set("X-Port", "" + CONTAINER_1_PORT); + headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); + headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); + headers.set("X-Database", DATABASE_1_INTERNALNAME); + headers.set("X-Sidecar-Host", CONTAINER_1_SIDECAR_HOST); + headers.set("X-Sidecar-Port", "" + CONTAINER_1_SIDECAR_PORT); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(TableDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .headers(headers) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getTableById(DATABASE_1_ID, TABLE_1_ID); + }); + } + + @Test + public void getDatabaseByInternalName_succeeds() throws DatabaseNotFoundException, RemoteUnavailableException, + ServiceException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto[].class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .body(new PrivilegedDatabaseDto[]{DATABASE_1_PRIVILEGED_DTO})); + + /* test */ + final PrivilegedDatabaseDto response = metadataServiceGateway.getDatabaseByInternalName(DATABASE_1_INTERNALNAME); + assertEquals(response.getId(), response.getId()); + } + + @Test + public void getDatabaseByInternalName_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.ServiceUnavailable.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto[].class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + metadataServiceGateway.getDatabaseByInternalName(DATABASE_1_INTERNALNAME); + }); + } + + @Test + public void getDatabaseByInternalName_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto[].class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .body(new PrivilegedDatabaseDto[]{DATABASE_1_PRIVILEGED_DTO})); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getDatabaseByInternalName(DATABASE_1_INTERNALNAME); + }); + } + + @Test + public void getDatabaseByInternalName_emptyBody_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto[].class))) + .thenReturn(ResponseEntity.ok() + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getDatabaseByInternalName(DATABASE_1_INTERNALNAME); + }); + } + + @Test + public void getDatabaseByInternalName_notFound_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto[].class))) + .thenReturn(ResponseEntity.ok() + .body(new PrivilegedDatabaseDto[]{})); + + /* test */ + assertThrows(DatabaseNotFoundException.class, () -> { + metadataServiceGateway.getDatabaseByInternalName(DATABASE_1_INTERNALNAME); + }); + } + + @Test + public void getDatabaseById_succeeds() throws RemoteUnavailableException, ServiceException, + DatabaseNotFoundException { + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); + headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto.class))) + .thenReturn(ResponseEntity.ok() + .headers(headers) + .body(DATABASE_1_PRIVILEGED_DTO)); + + /* test */ + final PrivilegedDatabaseDto response = metadataServiceGateway.getDatabaseById(DATABASE_1_ID); + assertEquals(DATABASE_1_ID, response.getId()); + } + + @Test + public void getDatabaseById_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + metadataServiceGateway.getDatabaseById(DATABASE_1_ID); + }); + } + + @Test + public void getDatabaseById_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto.class)); + + /* test */ + assertThrows(DatabaseNotFoundException.class, () -> { + metadataServiceGateway.getDatabaseById(DATABASE_1_ID); + }); + } + + @Test + public void getDatabaseById_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getDatabaseById(DATABASE_1_ID); + }); + } + + @Test + public void getDatabaseById_emptyBody_fails() { + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); + headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .headers(headers) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getDatabaseById(DATABASE_1_ID); + }); + } + + @Test + public void getDatabaseById_headerMissing_fails() { + final List<String> customHeaders = List.of("X-Username", "X-Password"); + + for (int i = 0; i < customHeaders.size(); i++) { + final HttpHeaders headers = new HttpHeaders(); + for (int j = 0; j < i; j++) { + headers.add(customHeaders.get(j), ""); + } + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(PrivilegedDatabaseDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .headers(headers) + .build()); + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getDatabaseById(DATABASE_1_ID); + }); + } + } + + @Test + public void getContainerById_succeeds() throws RemoteUnavailableException, ContainerNotFoundException, ServiceException { + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); + headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ContainerDto.class))) + .thenReturn(ResponseEntity.ok() + .headers(headers) + .body(CONTAINER_1_DTO)); + + /* test */ + final PrivilegedContainerDto response = metadataServiceGateway.getContainerById(CONTAINER_1_ID); + assertEquals(CONTAINER_1_ID, response.getId()); + } + + @Test + public void getContainerById_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ContainerDto.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + metadataServiceGateway.getContainerById(CONTAINER_1_ID); + }); + } + + @Test + public void getContainerById_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ContainerDto.class)); + + /* test */ + assertThrows(ContainerNotFoundException.class, () -> { + metadataServiceGateway.getContainerById(CONTAINER_1_ID); + }); + } + + @Test + public void getContainerById_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ContainerDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getContainerById(CONTAINER_1_ID); + }); + } + + @Test + public void getContainerById_headerMissing_fails() { + final List<String> customHeaders = List.of("X-Username", "X-Password"); + + for (int i = 0; i < customHeaders.size(); i++) { + final HttpHeaders headers = new HttpHeaders(); + for (int j = 0; j < i; j++) { + headers.add(customHeaders.get(j), ""); + } + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ContainerDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getContainerById(CONTAINER_1_ID); + }); + } + } + + @Test + public void getContainerById_emptyBody_fails() { + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); + headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ContainerDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .headers(headers) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getContainerById(CONTAINER_1_ID); + }); + } + + @Test + public void getViewById_succeeds() throws RemoteUnavailableException, ViewNotFoundException, ServiceException { + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Type", IMAGE_1_JDBC); + headers.set("X-Host", CONTAINER_1_HOST); + headers.set("X-Port", "" + CONTAINER_1_PORT); + headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); + headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); + headers.set("X-Database", DATABASE_1_INTERNALNAME); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto.class))) + .thenReturn(ResponseEntity.ok() + .headers(headers) + .body(VIEW_1_DTO)); + + /* test */ + final PrivilegedViewDto response = metadataServiceGateway.getViewById(CONTAINER_1_ID, VIEW_1_ID); + assertEquals(VIEW_1_ID, response.getId()); + } + + @Test + public void getViewById_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + metadataServiceGateway.getViewById(CONTAINER_1_ID, VIEW_1_ID); + }); + } + + @Test + public void getViewById_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto.class)); + + /* test */ + assertThrows(ViewNotFoundException.class, () -> { + metadataServiceGateway.getViewById(CONTAINER_1_ID, VIEW_1_ID); + }); + } + + @Test + public void getViewById_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getViewById(CONTAINER_1_ID, VIEW_1_ID); + }); + } + + @Test + public void getViewById_headerMissing_fails() { + final List<String> customHeaders = List.of("X-Type", "X-Host", "X-Port", "X-Username", "X-Password", "X-Database"); + + for (int i = 0; i < customHeaders.size(); i++) { + final HttpHeaders headers = new HttpHeaders(); + for (int j = 0; j < i; j++) { + headers.add(customHeaders.get(j), ""); + } + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getViewById(CONTAINER_1_ID, VIEW_1_ID); + }); + } + } + + @Test + public void getViewById_emptyBody_fails() { + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Type", IMAGE_1_JDBC); + headers.set("X-Host", CONTAINER_1_HOST); + headers.set("X-Port", "" + CONTAINER_1_PORT); + headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); + headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); + headers.set("X-Database", DATABASE_1_INTERNALNAME); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(ViewDto.class))) + .thenReturn(ResponseEntity.ok() + .headers(headers) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getViewById(CONTAINER_1_ID, VIEW_1_ID); + }); + } + + @Test + public void getUserById_succeeds() throws RemoteUnavailableException, UserNotFoundException, ServiceException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class))) + .thenReturn(ResponseEntity.ok() + .body(USER_1_DTO)); + + /* test */ + final UserDto response = metadataServiceGateway.getUserById(USER_1_ID); + assertEquals(USER_1_ID, response.getId()); + } + + @Test + public void getUserById_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + metadataServiceGateway.getUserById(USER_1_ID); + }); + } + + @Test + public void getUserById_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class)); + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + metadataServiceGateway.getUserById(USER_1_ID); + }); + } + + @Test + public void getUserById_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getUserById(USER_1_ID); + }); + } + + @Test + public void getUserById_emptyBody_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class))) + .thenReturn(ResponseEntity.ok() + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getUserById(USER_1_ID); + }); + } + + @Test + public void getPrivilegedUserById_succeeds() throws RemoteUnavailableException, UserNotFoundException, + ServiceException { + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); + headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class))) + .thenReturn(ResponseEntity.ok() + .headers(headers) + .body(USER_1_DTO)); + + /* test */ + final PrivilegedUserDto response = metadataServiceGateway.getPrivilegedUserById(USER_1_ID); + assertEquals(USER_1_ID, response.getId()); + assertEquals(CONTAINER_1_PRIVILEGED_USERNAME, response.getUsername()); + assertEquals(CONTAINER_1_PRIVILEGED_PASSWORD, response.getPassword()); + } + + @Test + public void getPrivilegedUserById_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + metadataServiceGateway.getPrivilegedUserById(USER_1_ID); + }); + } + + @Test + public void getPrivilegedUserById_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class)); + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + metadataServiceGateway.getPrivilegedUserById(USER_1_ID); + }); + } + + @Test + public void getPrivilegedUserById_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getPrivilegedUserById(USER_1_ID); + }); + } + + @Test + public void getPrivilegedUserById_headerMissing_fails() { + final List<String> customHeaders = List.of("X-Username", "X-Password"); + + for (int i = 0; i < customHeaders.size(); i++) { + final HttpHeaders headers = new HttpHeaders(); + for (int j = 0; j < i; j++) { + headers.add(customHeaders.get(j), ""); + } + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.OK) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getPrivilegedUserById(USER_1_ID); + }); + } + } + + @Test + public void getPrivilegedUserById_emptyBody_fails() { + final HttpHeaders headers = new HttpHeaders(); + headers.set("X-Username", CONTAINER_1_PRIVILEGED_USERNAME); + headers.set("X-Password", CONTAINER_1_PRIVILEGED_PASSWORD); + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(UserDto.class))) + .thenReturn(ResponseEntity.ok() + .headers(headers) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getPrivilegedUserById(USER_1_ID); + }); + } + + @Test + public void getAccess_succeeds() throws RemoteUnavailableException, NotAllowedException, ServiceException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(DatabaseAccessDto.class))) + .thenReturn(ResponseEntity.ok() + .body(DATABASE_1_USER_1_READ_ACCESS_DTO)); + + /* test */ + final DatabaseAccessDto response = metadataServiceGateway.getAccess(DATABASE_1_ID, USER_1_ID); + } + + @Test + public void getAccess_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(DatabaseAccessDto.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + metadataServiceGateway.getAccess(DATABASE_1_ID, USER_1_ID); + }); + } + + @Test + public void getAccess_forbidden_fails() { + + /* mock */ + doThrow(HttpClientErrorException.Forbidden.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(DatabaseAccessDto.class)); + + /* test */ + assertThrows(NotAllowedException.class, () -> { + metadataServiceGateway.getAccess(DATABASE_1_ID, USER_1_ID); + }); + } + + @Test + public void getAccess_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(DatabaseAccessDto.class)); + + /* test */ + assertThrows(NotAllowedException.class, () -> { + metadataServiceGateway.getAccess(DATABASE_1_ID, USER_1_ID); + }); + } + + @Test + public void getAccess_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(DatabaseAccessDto.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getAccess(DATABASE_1_ID, USER_1_ID); + }); + } + + @Test + public void getAccess_emptyBody_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(DatabaseAccessDto.class))) + .thenReturn(ResponseEntity.ok() + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getAccess(DATABASE_1_ID, USER_1_ID); + }); + } + + @Test + public void getIdentifiers_witSubset_succeeds() throws RemoteUnavailableException, DatabaseNotFoundException, ServiceException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(IdentifierDto[].class))) + .thenReturn(ResponseEntity.ok() + .body(new IdentifierDto[]{IDENTIFIER_1_DTO})); + + /* test */ + final List<IdentifierDto> response = metadataServiceGateway.getIdentifiers(DATABASE_1_ID, QUERY_1_ID); + assertEquals(1, response.size()); + } + + @Test + public void getIdentifiers_succeeds() throws RemoteUnavailableException, DatabaseNotFoundException, ServiceException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(IdentifierDto[].class))) + .thenReturn(ResponseEntity.ok() + .body(new IdentifierDto[]{IDENTIFIER_1_DTO})); + + /* test */ + final List<IdentifierDto> response = metadataServiceGateway.getIdentifiers(DATABASE_1_ID, null); + assertEquals(1, response.size()); + } + + @Test + public void getIdentifiers_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(IdentifierDto[].class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + metadataServiceGateway.getIdentifiers(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void getIdentifiers_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(IdentifierDto[].class)); + + /* test */ + assertThrows(DatabaseNotFoundException.class, () -> { + metadataServiceGateway.getIdentifiers(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void getIdentifiers_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(IdentifierDto[].class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getIdentifiers(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void getIdentifiers_emptyBody_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(HttpEntity.EMPTY), eq(IdentifierDto[].class))) + .thenReturn(ResponseEntity.ok() + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.getIdentifiers(DATABASE_1_ID, QUERY_1_ID); + }); + } + + @Test + public void updateTableStatistics_succeeds() throws RemoteUnavailableException, ServiceException, TableNotFoundException { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.accepted() + .build()); + + /* test */ + metadataServiceGateway.updateTableStatistics(DATABASE_1_ID, TABLE_1_ID); + } + + @Test + public void updateTableStatistics_unavailable_fails() { + + /* mock */ + doThrow(HttpServerErrorException.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(RemoteUnavailableException.class, () -> { + metadataServiceGateway.updateTableStatistics(DATABASE_1_ID, TABLE_1_ID); + }); + } + + @Test + public void updateTableStatistics_notFound_fails() { + + /* mock */ + doThrow(HttpClientErrorException.NotFound.class) + .when(restTemplate) + .exchange(anyString(), eq(HttpMethod.PUT), eq(HttpEntity.EMPTY), eq(Void.class)); + + /* test */ + assertThrows(TableNotFoundException.class, () -> { + metadataServiceGateway.updateTableStatistics(DATABASE_1_ID, TABLE_1_ID); + }); + } + + @Test + public void updateTableStatistics_statusCode_fails() { + + /* mock */ + when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), eq(HttpEntity.EMPTY), eq(Void.class))) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT) + .build()); + + /* test */ + assertThrows(ServiceException.class, () -> { + metadataServiceGateway.updateTableStatistics(DATABASE_1_ID, TABLE_1_ID); + }); + } + +} diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerIntegrationTest.java index 2994e7f098..dec1fcc028 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerIntegrationTest.java @@ -3,6 +3,7 @@ package at.tuwien.listener; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; import at.tuwien.exception.RemoteUnavailableException; +import at.tuwien.exception.ServiceException; import at.tuwien.exception.TableNotFoundException; import at.tuwien.gateway.MetadataServiceGateway; import at.tuwien.test.AbstractUnitTest; @@ -61,7 +62,8 @@ public class DefaultListenerIntegrationTest extends AbstractUnitTest { } @Test - public void onMessage_succeeds(CapturedOutput output) throws TableNotFoundException, RemoteUnavailableException { + public void onMessage_succeeds(CapturedOutput output) throws TableNotFoundException, RemoteUnavailableException, + ServiceException { final Message request = buildMessage("dbrepo." + DATABASE_1_ID + "." + TABLE_1_ID, "{\"id\":4,\"date\":\"2023-10-03\",\"mintemp\":15.0,\"rainfall\":0.2}", new HashMap<>()); /* mock */ @@ -75,7 +77,8 @@ public class DefaultListenerIntegrationTest extends AbstractUnitTest { @Test @Disabled - public void onMessage_tableNotFound_fails(CapturedOutput output) throws TableNotFoundException, RemoteUnavailableException { + public void onMessage_tableNotFound_fails(CapturedOutput output) throws TableNotFoundException, + RemoteUnavailableException, ServiceException { final Message request = buildMessage("dbrepo." + DATABASE_1_ID + "." + TABLE_1_ID, "{\"id\":4,\"date\":\"2023-10-03\",\"mintemp\":15.0,\"rainfall\":0.2}", new HashMap<>()); /* mock */ diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerUnitTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerUnitTest.java index 5c2f61d5b7..ab4a171c89 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerUnitTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/listener/DefaultListenerUnitTest.java @@ -3,6 +3,7 @@ package at.tuwien.listener; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; import at.tuwien.exception.RemoteUnavailableException; +import at.tuwien.exception.ServiceException; import at.tuwien.exception.TableNotFoundException; import at.tuwien.gateway.MetadataServiceGateway; import at.tuwien.test.AbstractUnitTest; @@ -75,7 +76,7 @@ public class DefaultListenerUnitTest extends AbstractUnitTest { @Test public void onMessage_messageMalformed_fails(CapturedOutput output) throws TableNotFoundException, - RemoteUnavailableException { + RemoteUnavailableException, ServiceException { final Message request = buildMessage("dbrepo.1.1", "{,}", new HashMap<>()); /* mock */ @@ -89,7 +90,7 @@ public class DefaultListenerUnitTest extends AbstractUnitTest { @Test public void onMessage_tableNotFound_fails(CapturedOutput output) throws TableNotFoundException, - RemoteUnavailableException { + RemoteUnavailableException, ServiceException { final Message request = buildMessage("dbrepo.1.1", "{\"id\":1}", new HashMap<>()); /* mock */ diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java index a49b264a03..803632c078 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java @@ -191,7 +191,7 @@ public class PrometheusEndpointMvcTest extends AbstractUnitTest { /* ignore */ } try { - tableEndpoint.getHistory(DATABASE_1_ID, TABLE_1_ID, USER_1_PRINCIPAL); + tableEndpoint.getHistory(DATABASE_1_ID, TABLE_1_ID, null, USER_1_PRINCIPAL); } catch (Exception e) { /* ignore */ } diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/AccessServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/AccessServiceIntegrationTest.java new file mode 100644 index 0000000000..b7b8e0e263 --- /dev/null +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/AccessServiceIntegrationTest.java @@ -0,0 +1,145 @@ +package at.tuwien.service; + +import at.tuwien.api.database.AccessTypeDto; +import at.tuwien.config.MariaDbConfig; +import at.tuwien.config.MariaDbContainerConfig; +import at.tuwien.exception.DatabaseMalformedException; +import at.tuwien.test.AbstractUnitTest; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.sql.SQLException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +@Testcontainers +public class AccessServiceIntegrationTest extends AbstractUnitTest { + + @Autowired + private AccessService accessService; + + @Value("${dbrepo.grant.default.read}") + private String grantDefaultRead; + + @Value("${dbrepo.grant.default.write}") + private String grantDefaultWrite; + + @Container + private static MariaDBContainer<?> mariaDBContainer = MariaDbContainerConfig.getContainer(); + + @BeforeEach + public void beforeEach() throws SQLException { + genesis(); + /* metadata database */ + MariaDbConfig.dropDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_1_INTERNALNAME); + MariaDbConfig.createInitDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_1_DTO); + } + + @Test + public void create_read_succeeds() throws SQLException, DatabaseMalformedException { + + /* test */ + accessService.create(DATABASE_1_PRIVILEGED_DTO, USER_1_PRIVILEGED_DTO, AccessTypeDto.READ); + final List<String> privileges = MariaDbConfig.getPrivileges(DATABASE_1_PRIVILEGED_DTO, USER_1_USERNAME); + for (String privilege : grantDefaultRead.split(",")) { + assertTrue(privileges.stream().anyMatch(p -> p.trim().equals(privilege.trim()))); + } + } + + @Test + public void create_writeOwn_succeeds() throws SQLException, DatabaseMalformedException { + + /* test */ + accessService.create(DATABASE_1_PRIVILEGED_DTO, USER_1_PRIVILEGED_DTO, AccessTypeDto.WRITE_OWN); + final List<String> privileges = MariaDbConfig.getPrivileges(DATABASE_1_PRIVILEGED_DTO, USER_1_USERNAME); + for (String privilege : grantDefaultWrite.split(",")) { + assertTrue(privileges.stream().anyMatch(p -> p.trim().equals(privilege.trim()))); + } + } + + @Test + public void create_writeAll_succeeds() throws SQLException, DatabaseMalformedException { + + /* test */ + accessService.create(DATABASE_1_PRIVILEGED_DTO, USER_1_PRIVILEGED_DTO, AccessTypeDto.WRITE_ALL); + final List<String> privileges = MariaDbConfig.getPrivileges(DATABASE_1_PRIVILEGED_DTO, USER_1_USERNAME); + for (String privilege : grantDefaultWrite.split(",")) { + assertTrue(privileges.stream().anyMatch(p -> p.trim().equals(privilege.trim()))); + } + } + + @Test + public void update_read_succeeds() throws SQLException, DatabaseMalformedException { + + /* test */ + accessService.update(DATABASE_1_PRIVILEGED_DTO, USER_1_DTO, AccessTypeDto.READ); + final List<String> privileges = MariaDbConfig.getPrivileges(DATABASE_1_PRIVILEGED_DTO, USER_1_USERNAME); + for (String privilege : grantDefaultRead.split(",")) { + assertTrue(privileges.stream().anyMatch(p -> p.trim().equals(privilege.trim()))); + } + } + + @Test + public void update_writeOwn_succeeds() throws SQLException, DatabaseMalformedException { + + /* test */ + accessService.update(DATABASE_1_PRIVILEGED_DTO, USER_1_DTO, AccessTypeDto.WRITE_OWN); + final List<String> privileges = MariaDbConfig.getPrivileges(DATABASE_1_PRIVILEGED_DTO, USER_1_USERNAME); + for (String privilege : grantDefaultWrite.split(",")) { + assertTrue(privileges.stream().anyMatch(p -> p.trim().equals(privilege.trim()))); + } + } + + @Test + public void update_writeAll_succeeds() throws SQLException, DatabaseMalformedException { + + /* test */ + accessService.update(DATABASE_1_PRIVILEGED_DTO, USER_1_DTO, AccessTypeDto.WRITE_ALL); + final List<String> privileges = MariaDbConfig.getPrivileges(DATABASE_1_PRIVILEGED_DTO, USER_1_USERNAME); + for (String privilege : grantDefaultWrite.split(",")) { + assertTrue(privileges.stream().anyMatch(p -> p.trim().equals(privilege.trim()))); + } + } + + @Test + public void update_notFound_fails() { + + /* test */ + assertThrows(DatabaseMalformedException.class, () -> { + accessService.update(DATABASE_1_PRIVILEGED_DTO, USER_5_DTO, AccessTypeDto.WRITE_ALL); + }); + } + + @Test + public void delete_succeeds() throws SQLException, DatabaseMalformedException { + + /* test */ + accessService.delete(DATABASE_1_PRIVILEGED_DTO, USER_1_DTO); + final List<String> privileges = MariaDbConfig.getPrivileges(DATABASE_1_PRIVILEGED_DTO, USER_1_USERNAME); + assertEquals(1, privileges.size()); + assertEquals("USAGE", privileges.get(0)); + } + + @Test + public void delete_notFound_fails() { + + /* test */ + assertThrows(DatabaseMalformedException.class, () -> { + accessService.delete(DATABASE_1_PRIVILEGED_DTO, USER_5_DTO); + }); + } + +} diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java new file mode 100644 index 0000000000..e53f6ad88f --- /dev/null +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/DatabaseServiceIntegrationTest.java @@ -0,0 +1,119 @@ +package at.tuwien.service; + +import at.tuwien.api.database.internal.PrivilegedDatabaseDto; +import at.tuwien.api.user.internal.UpdateUserPasswordDto; +import at.tuwien.config.MariaDbConfig; +import at.tuwien.config.MariaDbContainerConfig; +import at.tuwien.exception.*; +import at.tuwien.mapper.MariaDbMapper; +import at.tuwien.mapper.MariaDbMapperImpl; +import at.tuwien.test.AbstractUnitTest; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +@Testcontainers +public class DatabaseServiceIntegrationTest extends AbstractUnitTest { + + @Autowired + private DatabaseService databaseService; + + @Container + private static MariaDBContainer<?> mariaDBContainer = MariaDbContainerConfig.getContainer(); + + @Autowired + private MariaDbMapper mariaDbMapper; + + @BeforeEach + public void beforeEach() throws SQLException { + genesis(); + /* metadata database */ + MariaDbConfig.dropDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_1_INTERNALNAME); + } + + @Test + public void create_succeeds() throws SQLException, DatabaseMalformedException { + + /* test */ + final PrivilegedDatabaseDto response = databaseService.create(CONTAINER_1_PRIVILEGED_DTO, DATABASE_1_CREATE_INTERNAL); + assertNull(response.getName()); + assertEquals(DATABASE_1_INTERNALNAME, response.getInternalName()); + assertEquals(EXCHANGE_DBREPO_NAME, response.getExchangeName()); + assertNotNull(response.getCreator()); + assertEquals(USER_1_ID, response.getCreator().getId()); + assertNotNull(response.getOwner()); + assertEquals(USER_1_ID, response.getOwner().getId()); + assertNotNull(response.getContact()); + assertEquals(USER_1_ID, response.getContact().getId()); + assertNotNull(response.getContainer()); + assertEquals(CONTAINER_1_ID, response.getContainer().getId()); + } + + @Test + public void create_exists_fails() throws SQLException { + + /* mock */ + MariaDbConfig.createDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_1_INTERNALNAME); + + /* test */ + assertThrows(DatabaseMalformedException.class, () -> { + databaseService.create(CONTAINER_1_PRIVILEGED_DTO, DATABASE_1_CREATE_INTERNAL); + }); + } + + @Test + public void update_succeeds() throws SQLException, DatabaseMalformedException { + final UpdateUserPasswordDto request = UpdateUserPasswordDto.builder() + .username(USER_1_USERNAME) + .password(USER_2_PASSWORD) + .build(); + + /* mock */ + MariaDbConfig.createInitDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_1_DTO); + MariaDbConfig.grantWriteAccess(DATABASE_1_PRIVILEGED_DTO, USER_1_USERNAME); + + /* pre-condition */ + MariaDbConfig.mockQuery(CONTAINER_1_HOST, CONTAINER_1_PORT, DATABASE_1_INTERNALNAME, "CREATE SEQUENCE debug NOCACHE", USER_1_USERNAME, USER_1_PASSWORD); + try { + MariaDbConfig.mockQuery(CONTAINER_1_HOST, CONTAINER_1_PORT, DATABASE_1_INTERNALNAME, "CREATE SEQUENCE debug NOCACHE", USER_1_USERNAME, USER_2_PASSWORD); + fail(); + } catch (SQLException e) { + /* ignore */ + } + + /* test */ + databaseService.update(DATABASE_1_PRIVILEGED_DTO, request); + MariaDbConfig.mockQuery(CONTAINER_1_HOST, CONTAINER_1_PORT, DATABASE_1_INTERNALNAME, "CREATE SEQUENCE debug2 NOCACHE", USER_1_USERNAME, USER_2_PASSWORD); + } + + @Test + public void update_notExists_fails() throws SQLException { + final UpdateUserPasswordDto request = UpdateUserPasswordDto.builder() + .username("i_do_not_exist") + .password(USER_1_PASSWORD) + .build(); + + /* mock */ + MariaDbConfig.createInitDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_1_DTO); + + /* test */ + assertThrows(DatabaseMalformedException.class, () -> { + databaseService.update(DATABASE_1_PRIVILEGED_DTO, request); + }); + } + +} diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/QueueServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/QueueServiceIntegrationTest.java index 452c88932c..4bfa5b443a 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/QueueServiceIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/QueueServiceIntegrationTest.java @@ -4,6 +4,7 @@ import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; import at.tuwien.exception.ContainerNotFoundException; import at.tuwien.exception.RemoteUnavailableException; +import at.tuwien.exception.ServiceException; import at.tuwien.exception.TableNotFoundException; import at.tuwien.gateway.MetadataServiceGateway; import at.tuwien.service.impl.QueueServiceRabbitMqImpl; @@ -50,7 +51,8 @@ public class QueueServiceIntegrationTest extends AbstractUnitTest { } @Test - public void insert_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, ContainerNotFoundException, TableNotFoundException { + public void insert_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, + ContainerNotFoundException, TableNotFoundException, ServiceException { final Map<String, Object> request = new HashMap<>() {{ put("id", 4L); put("date", "2023-10-03"); @@ -73,7 +75,8 @@ public class QueueServiceIntegrationTest extends AbstractUnitTest { } @Test - public void insert_onlyMandatoryFields_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, TableNotFoundException { + public void insert_onlyMandatoryFields_succeeds() throws InterruptedException, SQLException, + RemoteUnavailableException, TableNotFoundException, ServiceException { final Map<String, Object> request = new HashMap<>() {{ put("id", 5L); put("date", "2023-10-04"); diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SchemaServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SchemaServiceIntegrationTest.java index c9efad23f0..25dcb0caea 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SchemaServiceIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/SchemaServiceIntegrationTest.java @@ -1,13 +1,19 @@ package at.tuwien.service; +import at.tuwien.api.container.image.ImageDateDto; import at.tuwien.api.database.ViewColumnDto; import at.tuwien.api.database.ViewDto; +import at.tuwien.api.database.table.TableBriefDto; import at.tuwien.api.database.table.TableDto; import at.tuwien.api.database.table.columns.ColumnDto; import at.tuwien.api.database.table.columns.ColumnTypeDto; import at.tuwien.api.database.table.constraints.ConstraintsDto; +import at.tuwien.api.database.table.constraints.foreign.ForeignKeyDto; +import at.tuwien.api.database.table.constraints.foreign.ForeignKeyReferenceDto; +import at.tuwien.api.database.table.constraints.foreign.ReferenceTypeDto; import at.tuwien.api.database.table.constraints.primary.PrimaryKeyDto; import at.tuwien.api.database.table.constraints.unique.UniqueDto; +import at.tuwien.api.identifier.IdentifierDto; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; import at.tuwien.exception.*; @@ -24,6 +30,7 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import java.sql.SQLException; +import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -62,34 +69,11 @@ public class SchemaServiceIntegrationTest extends AbstractUnitTest { final List<ColumnDto> columns = response.getColumns(); assertNotNull(columns); assertEquals(5, columns.size()); - final ColumnDto column0 = columns.get(0); - assertEquals("id", column0.getName()); - assertEquals("id", column0.getInternalName()); - assertEquals(ColumnTypeDto.BIGINT, column0.getColumnType()); - assertFalse(column0.getIsNullAllowed()); - final ColumnDto column1 = columns.get(1); - assertEquals("given_name", column1.getName()); - assertEquals("given_name", column1.getInternalName()); - assertEquals(ColumnTypeDto.VARCHAR, column1.getColumnType()); - assertEquals(255, column1.getSize()); - assertFalse(column1.getIsNullAllowed()); - final ColumnDto column2 = columns.get(2); - assertEquals("middle_name", column2.getName()); - assertEquals("middle_name", column2.getInternalName()); - assertEquals(ColumnTypeDto.VARCHAR, column2.getColumnType()); - assertEquals(255, column2.getSize()); - assertTrue(column2.getIsNullAllowed()); - final ColumnDto column3 = columns.get(3); - assertEquals("family_name", column3.getName()); - assertEquals("family_name", column3.getInternalName()); - assertEquals(ColumnTypeDto.VARCHAR, column3.getColumnType()); - assertEquals(255, column3.getSize()); - assertFalse(column3.getIsNullAllowed()); - final ColumnDto column4 = columns.get(4); - assertEquals("age", column4.getName()); - assertEquals("age", column4.getInternalName()); - assertEquals(ColumnTypeDto.INT, column4.getColumnType()); - assertFalse(column4.getIsNullAllowed()); + assertColumn(columns.get(0), null, null, DATABASE_1_ID, "id", "id", ColumnTypeDto.BIGINT, 19L, 0L, false, null, null); + assertColumn(columns.get(1), null, null, DATABASE_1_ID, "given_name", "given_name", ColumnTypeDto.VARCHAR, 255L, null, false, null, null); + assertColumn(columns.get(2), null, null, DATABASE_1_ID, "middle_name", "middle_name", ColumnTypeDto.VARCHAR, 255L, null, true, null, null); + assertColumn(columns.get(3), null, null, DATABASE_1_ID, "family_name", "family_name", ColumnTypeDto.VARCHAR, 255L, null, false, null, null); + assertColumn(columns.get(4), null, null, DATABASE_1_ID, "age", "age", ColumnTypeDto.INT, 10L, 0L, false, null, null); final ConstraintsDto constraints = response.getConstraints(); assertNotNull(constraints); final Set<PrimaryKeyDto> primaryKey = constraints.getPrimaryKey(); @@ -100,9 +84,210 @@ public class SchemaServiceIntegrationTest extends AbstractUnitTest { final List<UniqueDto> uniques = constraints.getUniques(); assertEquals(1, uniques.size()); assertEquals(2, uniques.get(0).getColumns().size()); + assertEquals("not_in_metadata_db", uniques.get(0).getTable().getName()); assertEquals("not_in_metadata_db", uniques.get(0).getTable().getInternalName()); assertEquals("given_name", uniques.get(0).getColumns().get(0).getInternalName()); assertEquals("family_name", uniques.get(0).getColumns().get(1).getInternalName()); + final List<ForeignKeyDto> foreignKeys = constraints.getForeignKeys(); + assertEquals(0, foreignKeys.size()); + } + + @Test + public void inspectTableFullConstraints_succeeds() throws TableNotFoundException, SQLException, QueryMalformedException { + + /* test */ + final TableDto response = schemaService.inspectTable(DATABASE_1_PRIVILEGED_DTO, "weather_aus"); + assertEquals("weather_aus", response.getInternalName()); + assertEquals("weather_aus", response.getName()); + assertEquals(DATABASE_1_ID, response.getTdbid()); + assertTrue(response.getIsVersioned()); + assertEquals(DATABASE_1_PUBLIC, response.getIsPublic()); + assertEquals(DATABASE_1_OWNER, response.getCreatedBy()); + assertNotNull(response.getCreator()); + assertEquals(DATABASE_1_OWNER, response.getCreator().getId()); + assertEquals(USER_1_NAME, response.getCreator().getName()); + assertEquals(USER_1_USERNAME, response.getCreator().getUsername()); + assertEquals(USER_1_FIRSTNAME, response.getCreator().getFirstname()); + assertEquals(USER_1_LASTNAME, response.getCreator().getLastname()); + assertEquals(USER_1_QUALIFIED_NAME, response.getCreator().getQualifiedName()); + assertNotNull(response.getCreator().getAttributes()); + assertEquals(USER_1_AFFILIATION, response.getCreator().getAttributes().getAffiliation()); + assertEquals(USER_1_THEME, response.getCreator().getAttributes().getTheme()); + assertEquals(USER_1_LANGUAGE, response.getCreator().getAttributes().getLanguage()); + assertEquals(USER_1_ORCID_UNCOMPRESSED, response.getCreator().getAttributes().getOrcid()); + assertNull(response.getCreator().getAttributes().getMariadbPassword()); + final List<IdentifierDto> identifiers = response.getIdentifiers(); + assertNotNull(identifiers); + assertEquals(0, identifiers.size()); + final List<ColumnDto> columns = response.getColumns(); + assertNotNull(columns); + assertEquals(5, columns.size()); + assertColumn(columns.get(0), null, null, DATABASE_1_ID, "id", "id", ColumnTypeDto.BIGINT, 19L, 0L, false, null, null); + assertColumn(columns.get(1), null, null, DATABASE_1_ID, "date", "date", ColumnTypeDto.DATE, null, null, false, IMAGE_DATE_1_ID, null); + assertColumn(columns.get(2), null, null, DATABASE_1_ID, "location", "location", ColumnTypeDto.VARCHAR, 255L, null, true, null, "Closest city"); + assertColumn(columns.get(3), null, null, DATABASE_1_ID, "mintemp", "mintemp", ColumnTypeDto.DOUBLE, 22L, null, true, null, null); + assertColumn(columns.get(4), null, null, DATABASE_1_ID, "rainfall", "rainfall", ColumnTypeDto.DOUBLE, 22L, null, true, null, null); + final ConstraintsDto constraints = response.getConstraints(); + final List<PrimaryKeyDto> primaryKey = new LinkedList<>(constraints.getPrimaryKey()); + assertEquals(1, primaryKey.size()); + final PrimaryKeyDto pk0 = primaryKey.get(0); + assertNull(pk0.getId()); + assertNotNull(pk0.getTable()); + assertNull(pk0.getTable().getId()); + assertEquals("weather_aus", pk0.getTable().getName()); + assertEquals("weather_aus", pk0.getTable().getInternalName()); + assertEquals("Weather in Australia", pk0.getTable().getDescription()); + assertNotNull(pk0.getColumn()); + assertNull(pk0.getColumn().getId()); + assertNull(pk0.getColumn().getTableId()); + assertEquals(DATABASE_1_ID, pk0.getColumn().getDatabaseId()); + assertNull(pk0.getColumn().getAlias()); + assertEquals("id", pk0.getColumn().getName()); + assertEquals("id", pk0.getColumn().getInternalName()); + assertEquals(ColumnTypeDto.BIGINT, pk0.getColumn().getColumnType()); + final List<UniqueDto> uniques = constraints.getUniques(); + assertEquals(1, uniques.size()); + final UniqueDto unique0 = uniques.get(0); + assertNotNull(unique0.getTable()); + assertNull(unique0.getTable().getId()); + assertEquals(TABLE_1_INTERNALNAME, unique0.getTable().getName()); + assertEquals(TABLE_1_INTERNALNAME, unique0.getTable().getInternalName()); + assertEquals(TABLE_1_DESCRIPTION, unique0.getTable().getDescription()); + assertTrue(unique0.getTable().getIsVersioned()); + assertNotNull(unique0.getColumns()); + assertEquals(1, unique0.getColumns().size()); + assertNull(unique0.getColumns().get(0).getId()); + assertNull(unique0.getColumns().get(0).getTableId()); + assertEquals("date", unique0.getColumns().get(0).getName()); + assertEquals("date", unique0.getColumns().get(0).getInternalName()); + final List<String> checks = new LinkedList<>(constraints.getChecks()); + assertEquals("`mintemp` > 0", checks.get(0)); + final List<ForeignKeyDto> foreignKeys = constraints.getForeignKeys(); + assertEquals(1, foreignKeys.size()); + final ForeignKeyDto fk0 = foreignKeys.get(0); + assertNotNull(fk0.getName()); + assertNotNull(fk0.getReferences()); + final ForeignKeyReferenceDto fk0ref0 = fk0.getReferences().get(0); + assertNull(fk0ref0.getId()); + assertNotNull(fk0ref0.getColumn()); + assertNotNull(fk0ref0.getReferencedColumn()); + assertNotNull(fk0ref0.getForeignKey()); + assertEquals(DATABASE_1_ID, fk0ref0.getColumn().getDatabaseId()); + assertNull(fk0ref0.getColumn().getId()); + assertNull(fk0ref0.getColumn().getTableId()); + assertEquals("location", fk0ref0.getColumn().getName()); + assertEquals("location", fk0ref0.getColumn().getInternalName()); + assertEquals(DATABASE_1_ID, fk0ref0.getReferencedColumn().getDatabaseId()); + assertNull(fk0ref0.getReferencedColumn().getId()); + assertNull(fk0ref0.getReferencedColumn().getTableId()); + assertEquals("location", fk0ref0.getReferencedColumn().getName()); + assertEquals("location", fk0ref0.getReferencedColumn().getInternalName()); + assertNotNull(fk0.getOnUpdate()); + assertEquals(ReferenceTypeDto.RESTRICT, fk0.getOnUpdate()); + assertNotNull(fk0.getOnDelete()); + assertEquals(ReferenceTypeDto.SET_NULL, fk0.getOnDelete()); + final TableBriefDto fk0table = fk0.getTable(); + assertNull(fk0table.getId()); + assertEquals(DATABASE_1_ID, fk0table.getDatabaseId()); + assertEquals(TABLE_1_INTERNALNAME, fk0table.getName()); + assertEquals(TABLE_1_INTERNALNAME, fk0table.getInternalName()); + assertNotNull(fk0.getOnDelete()); + assertNotNull(fk0.getOnUpdate()); + assertNotNull(fk0.getReferencedTable()); + assertEquals(TABLE_2_INTERNALNAME, fk0.getReferencedTable().getName()); + assertEquals(TABLE_2_INTERNALNAME, fk0.getReferencedTable().getInternalName()); + } + + @Test + public void inspectTable_multipleForeignKeyReferences_succeeds() throws TableNotFoundException, SQLException, QueryMalformedException { + + /* test */ + final TableDto response = schemaService.inspectTable(DATABASE_1_PRIVILEGED_DTO, "complex_foreign_keys"); + final ConstraintsDto constraints = response.getConstraints(); + final List<ForeignKeyDto> foreignKeys = constraints.getForeignKeys(); + assertEquals(1, foreignKeys.size()); + final ForeignKeyDto fk0 = foreignKeys.get(0); + assertNotNull(fk0.getName()); + assertNotNull(fk0.getReferences()); + final ForeignKeyReferenceDto fk0ref0 = fk0.getReferences().get(0); + assertNull(fk0ref0.getId()); + assertNotNull(fk0ref0.getColumn()); + assertNotNull(fk0ref0.getReferencedColumn()); + assertNotNull(fk0ref0.getForeignKey()); + assertEquals(DATABASE_1_ID, fk0ref0.getColumn().getDatabaseId()); + assertNull(fk0ref0.getColumn().getId()); + assertNull(fk0ref0.getColumn().getTableId()); + assertEquals("weather_id", fk0ref0.getColumn().getName()); + assertEquals("weather_id", fk0ref0.getColumn().getInternalName()); + assertEquals(DATABASE_1_ID, fk0ref0.getReferencedColumn().getDatabaseId()); + assertNull(fk0ref0.getReferencedColumn().getId()); + assertNull(fk0ref0.getReferencedColumn().getTableId()); + assertEquals("id", fk0ref0.getReferencedColumn().getName()); + assertEquals("id", fk0ref0.getReferencedColumn().getInternalName()); + final ForeignKeyReferenceDto fk0ref1 = fk0.getReferences().get(1); + assertNull(fk0ref1.getId()); + assertNotNull(fk0ref1.getColumn()); + assertNotNull(fk0ref1.getReferencedColumn()); + assertNotNull(fk0ref1.getForeignKey()); + assertEquals(DATABASE_1_ID, fk0ref1.getColumn().getDatabaseId()); + assertNull(fk0ref1.getColumn().getId()); + assertNull(fk0ref1.getColumn().getTableId()); + assertEquals("other_id", fk0ref1.getColumn().getName()); + assertEquals("other_id", fk0ref1.getColumn().getInternalName()); + assertEquals(DATABASE_1_ID, fk0ref1.getReferencedColumn().getDatabaseId()); + assertNull(fk0ref1.getReferencedColumn().getId()); + assertNull(fk0ref1.getReferencedColumn().getTableId()); + assertEquals("other_id", fk0ref1.getReferencedColumn().getName()); + assertEquals("other_id", fk0ref1.getReferencedColumn().getInternalName()); + final TableBriefDto fk0refT0 = fk0.getTable(); + assertNull(fk0refT0.getId()); + assertEquals(DATABASE_1_ID, fk0refT0.getDatabaseId()); + assertEquals("complex_foreign_keys", fk0refT0.getName()); + assertEquals("complex_foreign_keys", fk0refT0.getInternalName()); + assertNotNull(fk0.getReferencedTable()); + assertEquals("complex_primary_key", fk0.getReferencedTable().getName()); + assertEquals("complex_primary_key", fk0.getReferencedTable().getInternalName()); + assertNotNull(fk0.getOnDelete()); + assertNotNull(fk0.getOnUpdate()); + } + + @Test + public void inspectTable_multiplePrimaryKey_succeeds() throws TableNotFoundException, SQLException, QueryMalformedException { + + /* test */ + final TableDto response = schemaService.inspectTable(DATABASE_1_PRIVILEGED_DTO, "complex_primary_key"); + final ConstraintsDto constraints = response.getConstraints(); + final List<PrimaryKeyDto> primaryKey = new LinkedList<>(constraints.getPrimaryKey()); + assertEquals(2, primaryKey.size()); + final PrimaryKeyDto pk0 = primaryKey.get(0); + assertNull(pk0.getId()); + assertNotNull(pk0.getTable()); + assertNull(pk0.getTable().getId()); + assertEquals("complex_primary_key", pk0.getTable().getName()); + assertEquals("complex_primary_key", pk0.getTable().getInternalName()); + assertNotNull(pk0.getColumn()); + assertNull(pk0.getColumn().getId()); + assertNull(pk0.getColumn().getTableId()); + assertEquals(DATABASE_1_ID, pk0.getColumn().getDatabaseId()); + assertNull(pk0.getColumn().getAlias()); + assertEquals("id", pk0.getColumn().getName()); + assertEquals("id", pk0.getColumn().getInternalName()); + assertEquals(ColumnTypeDto.BIGINT, pk0.getColumn().getColumnType()); + final PrimaryKeyDto pk1 = primaryKey.get(1); + assertNull(pk1.getId()); + assertNotNull(pk1.getTable()); + assertNull(pk1.getTable().getId()); + assertEquals("complex_primary_key", pk1.getTable().getName()); + assertEquals("complex_primary_key", pk1.getTable().getInternalName()); + assertNotNull(pk1.getColumn()); + assertNull(pk1.getColumn().getId()); + assertNull(pk1.getColumn().getTableId()); + assertEquals(DATABASE_1_ID, pk1.getColumn().getDatabaseId()); + assertNull(pk1.getColumn().getAlias()); + assertEquals("other_id", pk1.getColumn().getName()); + assertEquals("other_id", pk1.getColumn().getInternalName()); + assertEquals(ColumnTypeDto.BIGINT, pk1.getColumn().getColumnType()); + } @Test @@ -140,4 +325,51 @@ public class SchemaServiceIntegrationTest extends AbstractUnitTest { assertEquals(DATABASE_1_ID, column3.getDatabaseId()); } + protected static void assertViewColumn(ViewColumnDto column, Long id, Long databaseId, String name, String internalName, + ColumnTypeDto type, Long size, Long d, Boolean nullAllowed, + ImageDateDto dateFormat, String description) { + log.trace("assert column: {}", internalName); + assertNotNull(column); + assertEquals(id, column.getId()); + assertEquals(databaseId, column.getDatabaseId()); + assertEquals(name, column.getName()); + assertEquals(internalName, column.getInternalName()); + assertEquals(type, column.getColumnType()); + assertEquals(size, column.getSize()); + assertEquals(d, column.getD()); + assertEquals(nullAllowed, column.getIsNullAllowed()); + assertEquals(description, column.getDescription()); + if (dateFormat != null) { + assertNotNull(column.getDateFormat()); + assertEquals(dateFormat.getId(), column.getDateFormat().getId()); + } else { + assertNull(column.getDateFormat()); + } + } + + protected static void assertColumn(ColumnDto column, Long id, Long tableId, Long databaseId, String name, + String internalName, ColumnTypeDto type, Long size, Long d, Boolean nullAllowed, + Long dfid, String description) { + log.trace("assert column: {}", internalName); + assertNotNull(column); + assertEquals(id, column.getId()); + assertEquals(tableId, column.getTableId()); + assertEquals(databaseId, column.getDatabaseId()); + assertNotNull(column.getTable()); + assertEquals(tableId, column.getTable().getId()); + assertEquals(name, column.getName()); + assertEquals(internalName, column.getInternalName()); + assertEquals(type, column.getColumnType()); + assertEquals(size, column.getSize()); + assertEquals(d, column.getD()); + assertEquals(nullAllowed, column.getIsNullAllowed()); + assertEquals(description, column.getDescription()); + if (dfid != null) { + assertNotNull(column.getDateFormat()); + assertEquals(dfid, column.getDateFormat().getId()); + } else { + assertNull(column.getDateFormat()); + } + } + } diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java new file mode 100644 index 0000000000..336a2072c5 --- /dev/null +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/StorageServiceIntegrationTest.java @@ -0,0 +1,171 @@ +package at.tuwien.service; + +import at.tuwien.ExportResourceDto; +import at.tuwien.api.database.AccessTypeDto; +import at.tuwien.config.MariaDbConfig; +import at.tuwien.config.MariaDbContainerConfig; +import at.tuwien.config.S3Config; +import at.tuwien.exception.DatabaseMalformedException; +import at.tuwien.exception.StorageNotFoundException; +import at.tuwien.exception.StorageUnavailableException; +import at.tuwien.test.AbstractUnitTest; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.containers.MinIOContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.File; +import java.io.InputStream; +import java.sql.SQLException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@SpringBootTest +@ExtendWith(SpringExtension.class) +@Testcontainers +public class StorageServiceIntegrationTest extends AbstractUnitTest { + + @Autowired + private StorageService storageService; + + @Autowired + private S3Client s3Client; + + @Autowired + private S3Config s3Config; + + @Container + private static final MinIOContainer minIOContainer = new MinIOContainer("minio/minio:RELEASE.2024-06-06T09-36-42Z"); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + registry.add("dbrepo.endpoints.storageService", minIOContainer::getS3URL); + } + + @BeforeEach + public void beforeEach() throws SQLException { + genesis(); + /* s3 */ + if (s3Client.listBuckets().buckets().stream().noneMatch(b -> b.name().equals(s3Config.getS3ImportBucket()))) { + s3Client.createBucket(CreateBucketRequest.builder() + .bucket(s3Config.getS3ImportBucket()) + .build()); + } + if (s3Client.listBuckets().buckets().stream().noneMatch(b -> b.name().equals(s3Config.getS3ExportBucket()))) { + s3Client.createBucket(CreateBucketRequest.builder() + .bucket(s3Config.getS3ExportBucket()) + .build()); + } + } + + @Test + public void getObject_succeeds() throws StorageUnavailableException, StorageNotFoundException { + + /* mock */ + s3Client.putObject(PutObjectRequest.builder() + .key("s3key") + .bucket(s3Config.getS3ImportBucket()) + .build(), RequestBody.fromFile(new File("src/test/resources/csv/weather_aus.csv"))); + + /* test */ + final InputStream response = storageService.getObject(s3Config.getS3ImportBucket(), "s3key"); + assertNotNull(response); + } + + @Test + public void getObject_notFound_fails() { + + /* test */ + assertThrows(StorageNotFoundException.class, () -> { + storageService.getObject(s3Config.getS3ImportBucket(), "i_do_not_exist"); + }); + } + + @Test + public void getObject_bucket_fails() { + + /* test */ + assertThrows(StorageUnavailableException.class, () -> { + storageService.getObject("i_do_not_exist", "s3key"); + }); + } + + @Test + public void getBytes_succeeds() throws StorageUnavailableException, StorageNotFoundException { + + /* mock */ + s3Client.putObject(PutObjectRequest.builder() + .key("s3key") + .bucket(s3Config.getS3ImportBucket()) + .build(), RequestBody.fromFile(new File("src/test/resources/csv/weather_aus.csv"))); + + /* test */ + final byte[] response = storageService.getBytes(s3Config.getS3ImportBucket(), "s3key"); + assertNotNull(response); + } + + @Test + public void getBytes_simple_succeeds() throws StorageUnavailableException, StorageNotFoundException { + + /* mock */ + s3Client.putObject(PutObjectRequest.builder() + .key("s3key") + .bucket(s3Config.getS3ImportBucket()) + .build(), RequestBody.fromFile(new File("src/test/resources/csv/weather_aus.csv"))); + + /* test */ + final byte[] response = storageService.getBytes("s3key"); + assertNotNull(response); + } + + @Test + public void getBytes_notFound_fails() { + + /* test */ + assertThrows(StorageNotFoundException.class, () -> { + storageService.getBytes(s3Config.getS3ImportBucket(), "i_do_not_exist"); + }); + } + + @Test + public void getResource_succeeds() throws StorageUnavailableException, StorageNotFoundException { + + /* mock */ + s3Client.putObject(PutObjectRequest.builder() + .key("s3key") + .bucket(s3Config.getS3ImportBucket()) + .build(), RequestBody.fromFile(new File("src/test/resources/csv/weather_aus.csv"))); + + /* test */ + final ExportResourceDto response = storageService.getResource(s3Config.getS3ImportBucket(), "s3key"); + assertEquals("s3key", response.getFilename()); + assertNotNull(response.getResource()); + } + + @Test + public void getResource_notFound_fails() { + + /* test */ + assertThrows(StorageNotFoundException.class, () -> { + storageService.getBytes(s3Config.getS3ImportBucket(), "i_do_not_exist"); + }); + } + +} 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 f041dc0e7c..aa30bc0580 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 @@ -57,13 +57,13 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void execute_succeeds() throws QueryStoreInsertException, TableMalformedException, SQLException, QueryNotFoundException, InterruptedException, UserNotFoundException, NotAllowedException, - RemoteUnavailableException { + RemoteUnavailableException, ServiceException, DatabaseNotFoundException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; /* mock */ - when(metadataServiceGateway.getUser(QUERY_1_CREATED_BY)) + when(metadataServiceGateway.getUserById(QUERY_1_CREATED_BY)) .thenReturn(QUERY_1_CREATOR); /* test */ @@ -98,7 +98,7 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void execute_oneResult_succeeds() throws QueryStoreInsertException, TableMalformedException, SQLException, QueryNotFoundException, InterruptedException, UserNotFoundException, NotAllowedException, - RemoteUnavailableException { + RemoteUnavailableException, ServiceException, DatabaseNotFoundException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; @@ -106,7 +106,7 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { /* mock */ when(metadataServiceGateway.getIdentifiers(DATABASE_1_ID, QUERY_1_ID)) .thenReturn(List.of(IDENTIFIER_2_DTO)); - when(metadataServiceGateway.getUser(QUERY_1_CREATED_BY)) + when(metadataServiceGateway.getUserById(QUERY_1_CREATED_BY)) .thenReturn(QUERY_1_CREATOR); /* test */ @@ -129,13 +129,13 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void execute_oneResultPagination_succeeds() throws QueryStoreInsertException, TableMalformedException, SQLException, QueryNotFoundException, InterruptedException, UserNotFoundException, NotAllowedException, - RemoteUnavailableException { + RemoteUnavailableException, ServiceException, DatabaseNotFoundException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; /* mock */ - when(metadataServiceGateway.getUser(USER_1_ID)) + when(metadataServiceGateway.getUserById(USER_1_ID)) .thenReturn(USER_1_DTO); when(metadataServiceGateway.getIdentifiers(eq(DATABASE_1_ID), anyLong())) .thenReturn(List.of()); @@ -159,7 +159,7 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void findAll_succeeds() throws SQLException, QueryNotFoundException, InterruptedException, - NotAllowedException, RemoteUnavailableException { + NotAllowedException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException { /* test */ final List<QueryDto> response = findAll_generic(null); @@ -170,7 +170,7 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void findAll_onlyPersisted_succeeds() throws SQLException, QueryNotFoundException, InterruptedException, - NotAllowedException, RemoteUnavailableException { + NotAllowedException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException { /* test */ final List<QueryDto> response = findAll_generic(true); @@ -180,7 +180,7 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void findAll_onlyNonPersisted_succeeds() throws SQLException, QueryNotFoundException, InterruptedException, - NotAllowedException, RemoteUnavailableException { + NotAllowedException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException { /* test */ final List<QueryDto> response = findAll_generic(false); @@ -190,14 +190,15 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void findById_succeeds() throws SQLException, QueryNotFoundException, InterruptedException, - UserNotFoundException, NotAllowedException, RemoteUnavailableException { + UserNotFoundException, NotAllowedException, RemoteUnavailableException, ServiceException, + DatabaseNotFoundException { /* test */ findById_generic(QUERY_1_ID); } @Test - public void findById_fails() { + public void findById_fails() { /* test */ assertThrows(QueryNotFoundException.class, () -> { @@ -207,10 +208,11 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void persist_succeeds() throws SQLException, InterruptedException, QueryStorePersistException, - QueryNotFoundException, UserNotFoundException, NotAllowedException, RemoteUnavailableException { + QueryNotFoundException, UserNotFoundException, NotAllowedException, RemoteUnavailableException, + ServiceException, DatabaseNotFoundException { /* mock */ - when(metadataServiceGateway.getUser(QUERY_2_CREATED_BY)) + when(metadataServiceGateway.getUserById(QUERY_2_CREATED_BY)) .thenReturn(QUERY_2_CREATOR); /* test */ @@ -222,10 +224,11 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { @Test public void persist_unPersist_succeeds() throws SQLException, InterruptedException, QueryStorePersistException, - QueryNotFoundException, UserNotFoundException, NotAllowedException, RemoteUnavailableException { + QueryNotFoundException, UserNotFoundException, NotAllowedException, RemoteUnavailableException, + ServiceException, DatabaseNotFoundException { /* mock */ - when(metadataServiceGateway.getUser(QUERY_1_CREATED_BY)) + when(metadataServiceGateway.getUserById(QUERY_1_CREATED_BY)) .thenReturn(QUERY_1_CREATOR); /* test */ @@ -235,8 +238,9 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { assertFalse(response.getIsPersisted()); } - protected void findById_generic(Long queryId) throws InterruptedException, NotAllowedException, RemoteUnavailableException, - SQLException, UserNotFoundException, QueryNotFoundException { + protected void findById_generic(Long queryId) throws InterruptedException, NotAllowedException, + RemoteUnavailableException, SQLException, UserNotFoundException, QueryNotFoundException, ServiceException, + DatabaseNotFoundException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; @@ -244,7 +248,7 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { /* mock */ when(metadataServiceGateway.getIdentifiers(DATABASE_1_ID, QUERY_1_ID)) .thenReturn(List.of(IDENTIFIER_2_DTO)); - when(metadataServiceGateway.getUser(QUERY_1_CREATED_BY)) + when(metadataServiceGateway.getUserById(QUERY_1_CREATED_BY)) .thenReturn(QUERY_1_CREATOR); MariaDbConfig.insertQueryStore(DATABASE_1_PRIVILEGED_DTO, QUERY_1_DTO, USER_1_ID); @@ -255,7 +259,8 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { } protected List<QueryDto> findAll_generic(Boolean filterPersisted) throws InterruptedException, SQLException, - QueryNotFoundException, NotAllowedException, RemoteUnavailableException { + QueryNotFoundException, NotAllowedException, RemoteUnavailableException, ServiceException, + DatabaseNotFoundException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; @@ -263,15 +268,16 @@ public class SubsetServiceIntegrationTest extends AbstractUnitTest { /* mock */ MariaDbConfig.insertQueryStore(DATABASE_1_PRIVILEGED_DTO, QUERY_1_DTO, USER_1_ID); MariaDbConfig.insertQueryStore(DATABASE_1_PRIVILEGED_DTO, QUERY_2_DTO, USER_1_ID); - when(metadataServiceGateway.getIdentifiers(DATABASE_1_ID)) + when(metadataServiceGateway.getIdentifiers(DATABASE_1_ID, null)) .thenReturn(List.of(IDENTIFIER_2_DTO, IDENTIFIER_5_DTO)); /* test */ return queryService.findAll(DATABASE_1_PRIVILEGED_DTO, filterPersisted); } - protected void persist_generic(Long queryId, List<IdentifierDto> identifiers, Boolean persist) throws InterruptedException, - NotAllowedException, RemoteUnavailableException, SQLException, QueryStorePersistException { + protected void persist_generic(Long queryId, List<IdentifierDto> identifiers, Boolean persist) + throws InterruptedException, RemoteUnavailableException, SQLException, QueryStorePersistException, + ServiceException, DatabaseNotFoundException { /* pre-condition */ Thread.sleep(1000) /* wait for test container some more */; diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationTest.java index 25144d827d..86a0442ef6 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/TableServiceIntegrationTest.java @@ -1,20 +1,29 @@ package at.tuwien.service; -import at.tuwien.api.database.table.TableDto; -import at.tuwien.api.database.table.TupleDeleteDto; -import at.tuwien.api.database.table.TupleDto; -import at.tuwien.api.database.table.TupleUpdateDto; +import at.tuwien.ExportResourceDto; +import at.tuwien.api.database.query.ImportCsvDto; +import at.tuwien.api.database.query.QueryResultDto; +import at.tuwien.api.database.table.*; +import at.tuwien.api.database.table.columns.ColumnCreateDto; import at.tuwien.api.database.table.columns.ColumnDto; +import at.tuwien.api.database.table.columns.ColumnStatisticDto; import at.tuwien.api.database.table.columns.ColumnTypeDto; +import at.tuwien.api.database.table.constraints.ConstraintsCreateDto; import at.tuwien.api.database.table.constraints.ConstraintsDto; +import at.tuwien.api.database.table.constraints.foreign.ForeignKeyCreateDto; +import at.tuwien.api.database.table.constraints.foreign.ForeignKeyDto; +import at.tuwien.api.database.table.constraints.foreign.ForeignKeyReferenceDto; import at.tuwien.api.database.table.constraints.primary.PrimaryKeyDto; import at.tuwien.api.database.table.constraints.unique.UniqueDto; +import at.tuwien.api.database.table.internal.TableCreateDto; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; import at.tuwien.exception.*; +import at.tuwien.gateway.DataDatabaseSidecarGateway; import at.tuwien.gateway.MetadataServiceGateway; import at.tuwien.test.AbstractUnitTest; import lombok.extern.log4j.Log4j2; +import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,19 +31,26 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.io.InputStreamResource; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.testcontainers.containers.MariaDBContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.BigInteger; import java.sql.SQLException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.time.Instant; +import java.util.*; -import static org.junit.Assert.assertEquals; +import static at.tuwien.service.SchemaServiceIntegrationTest.assertColumn; +import static at.tuwien.service.SchemaServiceIntegrationTest.assertViewColumn; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; @Log4j2 @@ -49,6 +65,12 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { @MockBean private MetadataServiceGateway metadataServiceGateway; + @MockBean + private DataDatabaseSidecarGateway dataDatabaseSidecarGateway; + + @MockBean + private StorageService storageService; + @Container private static MariaDBContainer<?> mariaDBContainer = MariaDbContainerConfig.getContainer(); @@ -57,12 +79,14 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { genesis(); /* metadata database */ MariaDbConfig.dropDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_1_INTERNALNAME); + MariaDbConfig.dropDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_2_INTERNALNAME); MariaDbConfig.createInitDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_1_DTO); } @Test public void updateTuple_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException { + ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, + ServiceException { /* modify row based on primary key */ final TupleUpdateDto request = TupleUpdateDto.builder() .data(new HashMap<>() {{ @@ -96,8 +120,9 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void updateTuple_modifyPrimaryKey_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException { + public void updateTuple_modifyPrimaryKey_succeeds() throws InterruptedException, SQLException, + RemoteUnavailableException, ContainerNotFoundException, TableNotFoundException, TableMalformedException, + QueryMalformedException, ServiceException { /* modify row primary key based on primary key */ final TupleUpdateDto request = TupleUpdateDto.builder() .data(new HashMap<>() {{ @@ -132,8 +157,9 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void updateTuple_missingPrimaryKey_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException { + public void updateTuple_missingPrimaryKey_succeeds() throws InterruptedException, SQLException, + RemoteUnavailableException, ContainerNotFoundException, TableNotFoundException, TableMalformedException, + QueryMalformedException, ServiceException { /* modify row based on non-primary key column */ final TupleUpdateDto request = TupleUpdateDto.builder() .data(new HashMap<>() {{ @@ -168,7 +194,8 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { @Test public void updateTuple_notInOrder_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException { + ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, + ServiceException { /* modify row based on non-primary key column */ final TupleUpdateDto request = TupleUpdateDto.builder() .data(new HashMap<>() {{ @@ -203,7 +230,8 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { @Test public void createTuple_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException { + ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, + StorageUnavailableException, StorageNotFoundException, ServiceException { /* add row with primary key */ final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ @@ -236,7 +264,8 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { @Test public void createTuple_notInOrder_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException { + ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, + StorageUnavailableException, StorageNotFoundException, ServiceException { /* add row with primary key */ final TupleDto request = TupleDto.builder() .data(new HashMap<>() {{ @@ -269,7 +298,8 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { @Test public void deleteTuple_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException { + ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException, + ServiceException { /* delete row based on primary key */ final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ @@ -293,8 +323,9 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { } @Test - public void deleteTuple_withoutPrimaryKey_succeeds() throws InterruptedException, SQLException, RemoteUnavailableException, - ContainerNotFoundException, TableNotFoundException, TableMalformedException, QueryMalformedException { + public void deleteTuple_withoutPrimaryKey_succeeds() throws InterruptedException, SQLException, + RemoteUnavailableException, ContainerNotFoundException, TableNotFoundException, TableMalformedException, + QueryMalformedException, ServiceException { /* remove row based on non-primary key */ final TupleDeleteDto request = TupleDeleteDto.builder() .keys(new HashMap<>() {{ @@ -324,56 +355,392 @@ public class TableServiceIntegrationTest extends AbstractUnitTest { /* test */ final List<TableDto> response = tableService.getSchemas(DATABASE_1_PRIVILEGED_DTO); + assertEquals(3, response.size()); final TableDto table0 = response.get(0); - Assertions.assertEquals("not_in_metadata_db", table0.getInternalName()); - Assertions.assertEquals("not_in_metadata_db", table0.getName()); + Assertions.assertEquals("complex_foreign_keys", table0.getInternalName()); + Assertions.assertEquals("complex_foreign_keys", table0.getName()); Assertions.assertEquals(DATABASE_1_ID, table0.getTdbid()); assertTrue(table0.getIsVersioned()); Assertions.assertEquals(DATABASE_1_PUBLIC, table0.getIsPublic()); - final List<ColumnDto> columns = table0.getColumns(); - assertNotNull(columns); - Assertions.assertEquals(5, columns.size()); - final ColumnDto column0 = columns.get(0); - Assertions.assertEquals("id", column0.getName()); - Assertions.assertEquals("id", column0.getInternalName()); - Assertions.assertEquals(ColumnTypeDto.BIGINT, column0.getColumnType()); - assertFalse(column0.getIsNullAllowed()); - final ColumnDto column1 = columns.get(1); - Assertions.assertEquals("given_name", column1.getName()); - Assertions.assertEquals("given_name", column1.getInternalName()); - Assertions.assertEquals(ColumnTypeDto.VARCHAR, column1.getColumnType()); - Assertions.assertEquals(255, column1.getSize()); - assertFalse(column1.getIsNullAllowed()); - final ColumnDto column2 = columns.get(2); - Assertions.assertEquals("middle_name", column2.getName()); - Assertions.assertEquals("middle_name", column2.getInternalName()); - Assertions.assertEquals(ColumnTypeDto.VARCHAR, column2.getColumnType()); - Assertions.assertEquals(255, column2.getSize()); - assertTrue(column2.getIsNullAllowed()); - final ColumnDto column3 = columns.get(3); - Assertions.assertEquals("family_name", column3.getName()); - Assertions.assertEquals("family_name", column3.getInternalName()); - Assertions.assertEquals(ColumnTypeDto.VARCHAR, column3.getColumnType()); - Assertions.assertEquals(255, column3.getSize()); - assertFalse(column3.getIsNullAllowed()); - final ColumnDto column4 = columns.get(4); - Assertions.assertEquals("age", column4.getName()); - Assertions.assertEquals("age", column4.getInternalName()); - Assertions.assertEquals(ColumnTypeDto.INT, column4.getColumnType()); - assertFalse(column4.getIsNullAllowed()); - final ConstraintsDto constraints = table0.getConstraints(); - assertNotNull(constraints); - final Set<PrimaryKeyDto> primaryKey = constraints.getPrimaryKey(); - Assertions.assertEquals(1, primaryKey.size()); - final Set<String> checks = constraints.getChecks(); - Assertions.assertEquals(1, checks.size()); - Assertions.assertEquals(Set.of("`age` > 0 and `age` < 120"), checks); - final List<UniqueDto> uniques = constraints.getUniques(); - Assertions.assertEquals(1, uniques.size()); - Assertions.assertEquals(2, uniques.get(0).getColumns().size()); - Assertions.assertEquals("not_in_metadata_db", uniques.get(0).getTable().getInternalName()); - Assertions.assertEquals("given_name", uniques.get(0).getColumns().get(0).getInternalName()); - Assertions.assertEquals("family_name", uniques.get(0).getColumns().get(1).getInternalName()); + final List<ColumnDto> columns0 = table0.getColumns(); + assertNotNull(columns0); + Assertions.assertEquals(3, columns0.size()); + assertColumn(columns0.get(0), null, null, DATABASE_1_ID, "id", "id", ColumnTypeDto.BIGINT, 19L, 0L, false, null, null); + assertColumn(columns0.get(1), null, null, DATABASE_1_ID, "weather_id", "weather_id", ColumnTypeDto.BIGINT, 19L, 0L, false, null, null); + assertColumn(columns0.get(2), null, null, DATABASE_1_ID, "other_id", "other_id", ColumnTypeDto.BIGINT, 19L, 0L, false, null, null); + final ConstraintsDto constraints0 = table0.getConstraints(); + assertNotNull(constraints0); + assertEquals(1, constraints0.getPrimaryKey().size()); + final PrimaryKeyDto pk0 = new LinkedList<>(constraints0.getPrimaryKey()).get(0); + assertNull(pk0.getId()); + assertNull(pk0.getColumn().getId()); + assertEquals("id", pk0.getColumn().getName()); + assertEquals("id", pk0.getColumn().getInternalName()); + assertEquals(1, constraints0.getForeignKeys().size()); + final ForeignKeyDto fk0 = constraints0.getForeignKeys().get(0); + assertNotNull(fk0.getName()); + assertNull(fk0.getTable().getId()); + assertEquals("complex_foreign_keys", fk0.getTable().getName()); + assertEquals("complex_foreign_keys", fk0.getTable().getInternalName()); + assertNull(fk0.getReferencedTable().getId()); + assertEquals("complex_primary_key", fk0.getReferencedTable().getName()); + assertEquals("complex_primary_key", fk0.getReferencedTable().getInternalName()); + assertEquals(2, fk0.getReferences().size()); + final ForeignKeyReferenceDto fk0r0 = fk0.getReferences().get(0); + assertEquals("weather_id", fk0r0.getColumn().getName()); + assertEquals("weather_id", fk0r0.getColumn().getInternalName()); + assertNotNull(fk0r0.getColumn().getName()); + assertNotNull(fk0r0.getForeignKey()); + assertEquals("id", fk0r0.getReferencedColumn().getName()); + assertEquals("id", fk0r0.getReferencedColumn().getInternalName()); + final ForeignKeyReferenceDto fk0r1 = fk0.getReferences().get(1); + assertEquals("other_id", fk0r1.getColumn().getName()); + assertEquals("other_id", fk0r1.getColumn().getInternalName()); + assertNotNull(fk0r1.getColumn().getName()); + assertNotNull(fk0r1.getForeignKey()); + assertEquals("other_id", fk0r1.getReferencedColumn().getName()); + assertEquals("other_id", fk0r1.getReferencedColumn().getInternalName()); + assertEquals(0, constraints0.getChecks().size()); + assertEquals(0, constraints0.getUniques().size()); + /* table 1 */ + final TableDto table1 = response.get(1); + Assertions.assertEquals("complex_primary_key", table1.getInternalName()); + Assertions.assertEquals("complex_primary_key", table1.getName()); + Assertions.assertEquals(DATABASE_1_ID, table1.getTdbid()); + assertTrue(table1.getIsVersioned()); + Assertions.assertEquals(DATABASE_1_PUBLIC, table1.getIsPublic()); + final List<ColumnDto> columns1 = table1.getColumns(); + assertNotNull(columns1); + Assertions.assertEquals(2, columns1.size()); + assertColumn(columns1.get(0), null, null, DATABASE_1_ID, "id", "id", ColumnTypeDto.BIGINT, 19L, 0L, false, null, null); + assertColumn(columns1.get(1), null, null, DATABASE_1_ID, "other_id", "other_id", ColumnTypeDto.BIGINT, 19L, 0L, false, null, null); + final ConstraintsDto constraints1 = table1.getConstraints(); + assertNotNull(constraints1); + assertEquals(2, constraints1.getPrimaryKey().size()); + final PrimaryKeyDto pk10 = new LinkedList<>(constraints1.getPrimaryKey()).get(0); + assertNull(pk10.getId()); + assertNull(pk10.getColumn().getId()); + assertEquals("id", pk10.getColumn().getName()); + assertEquals("id", pk10.getColumn().getInternalName()); + final PrimaryKeyDto pk11 = new LinkedList<>(constraints1.getPrimaryKey()).get(1); + assertNull(pk11.getId()); + assertNull(pk11.getColumn().getId()); + assertEquals("other_id", pk11.getColumn().getName()); + assertEquals("other_id", pk11.getColumn().getInternalName()); + assertEquals(0, constraints1.getForeignKeys().size()); + assertEquals(0, constraints1.getChecks().size()); + assertEquals(0, constraints1.getUniques().size()); + /* table 2 */ + final TableDto table2 = response.get(2); + Assertions.assertEquals("not_in_metadata_db", table2.getInternalName()); + Assertions.assertEquals("not_in_metadata_db", table2.getName()); + Assertions.assertEquals(DATABASE_1_ID, table2.getTdbid()); + assertTrue(table2.getIsVersioned()); + Assertions.assertEquals(DATABASE_1_PUBLIC, table2.getIsPublic()); + final List<ColumnDto> columns2 = table2.getColumns(); + assertNotNull(columns2); + Assertions.assertEquals(5, columns2.size()); + assertColumn(columns2.get(0), null, null, DATABASE_1_ID, "id", "id", ColumnTypeDto.BIGINT, 19L, 0L, false, null, null); + assertColumn(columns2.get(1), null, null, DATABASE_1_ID, "given_name", "given_name", ColumnTypeDto.VARCHAR, 255L, null, false, null, null); + assertColumn(columns2.get(2), null, null, DATABASE_1_ID, "middle_name", "middle_name", ColumnTypeDto.VARCHAR, 255L, null, true, null, null); + assertColumn(columns2.get(3), null, null, DATABASE_1_ID, "family_name", "family_name", ColumnTypeDto.VARCHAR, 255L, null, false, null, null); + assertColumn(columns2.get(4), null, null, DATABASE_1_ID, "age", "age", ColumnTypeDto.INT, 10L, 0L, false, null, null); + final ConstraintsDto constraints2 = table2.getConstraints(); + assertNotNull(constraints2); + final Set<PrimaryKeyDto> primaryKey2 = constraints2.getPrimaryKey(); + Assertions.assertEquals(1, primaryKey2.size()); + final Set<String> checks2 = constraints2.getChecks(); + Assertions.assertEquals(1, checks2.size()); + Assertions.assertEquals(Set.of("`age` > 0 and `age` < 120"), checks2); + final List<UniqueDto> uniques2 = constraints2.getUniques(); + Assertions.assertEquals(1, uniques2.size()); + Assertions.assertEquals(2, uniques2.get(0).getColumns().size()); + Assertions.assertEquals("not_in_metadata_db", uniques2.get(0).getTable().getInternalName()); + Assertions.assertEquals("given_name", uniques2.get(0).getColumns().get(0).getInternalName()); + Assertions.assertEquals("family_name", uniques2.get(0).getColumns().get(1).getInternalName()); + } + + @Test + public void create_succeeds() throws TableNotFoundException, TableMalformedException, SQLException, + QueryMalformedException, TableExistsException { + + /* test */ + final TableDto response = tableService.createTable(DATABASE_1_PRIVILEGED_DTO, TABLE_4_CREATE_INTERNAL_DTO); + assertEquals(TABLE_4_NAME, response.getName()); + assertEquals(TABLE_4_INTERNALNAME, response.getInternalName()); + assertEquals(TABLE_4_COLUMNS.size(), response.getColumns().size()); + } + + @Test + public void getStatistics_succeeds() throws TableMalformedException, SQLException, QueryMalformedException { + + /* test */ + final TableStatisticDto response = tableService.getStatistics(TABLE_1_PRIVILEGED_DTO); + assertEquals(TABLE_1_COLUMNS.size(), response.getColumns().size()); + assertEquals(3L, response.getRows()); + assertEquals(Set.of("id", "date", "location", "mintemp", "rainfall"), response.getColumns().keySet()); + final ColumnStatisticDto column0 = response.getColumns().get("id"); + assertEquals(BigDecimal.valueOf(1L), column0.getMin()); + assertEquals(BigDecimal.valueOf(3L), column0.getMax()); + assertNotNull(column0.getMean()); + assertNotNull(column0.getMedian()); + assertNotNull(column0.getStdDev()); + final ColumnStatisticDto column1 = response.getColumns().get("date"); + assertNull(column1.getMin()); + assertNull(column1.getMax()); + assertNull(column1.getMean()); + assertNull(column1.getMedian()); + assertNull(column1.getStdDev()); + final ColumnStatisticDto column2 = response.getColumns().get("location"); + assertNull(column2.getMin()); + assertNull(column2.getMax()); + assertNull(column2.getMean()); + assertNull(column2.getMedian()); + assertNull(column2.getStdDev()); + final ColumnStatisticDto column3 = response.getColumns().get("mintemp"); + assertEquals(BigDecimal.valueOf(7.4), column3.getMin()); + assertEquals(BigDecimal.valueOf(13.4), column3.getMax()); + assertNotNull(column3.getMean()); + assertNotNull(column3.getMedian()); + assertNotNull(column3.getStdDev()); + final ColumnStatisticDto column4 = response.getColumns().get("rainfall"); + assertEquals(BigDecimal.valueOf(0L), column4.getMin()); + assertEquals(BigDecimal.valueOf(0.6), column4.getMax()); + assertNotNull(column4.getMean()); + assertNotNull(column4.getMedian()); + assertNotNull(column4.getStdDev()); + } + + @Test + public void create_malformed_fails() { + final at.tuwien.api.database.table.internal.TableCreateDto request = TableCreateDto.builder() + .needSequence(false) + .name("missing_foreign_key") + .columns(List.of(ColumnCreateDto.builder() + .name("id") + .type(ColumnTypeDto.BIGINT) + .nullAllowed(false) + .build())) + .constraints(ConstraintsCreateDto.builder() + .foreignKeys(List.of(ForeignKeyCreateDto.builder() + .columns(List.of("i_do_not_exist")) + .referencedTable("neither_do_i") + .referencedColumns(List.of("behold")) + .build())) + .build()) + .build(); + + /* test */ + assertThrows(TableMalformedException.class, () -> { + tableService.createTable(DATABASE_1_PRIVILEGED_DTO, request); + }); + } + + @Test + public void create_needSequence_succeeds() throws TableNotFoundException, TableMalformedException, SQLException, + QueryMalformedException, TableExistsException { + + /* mock */ + MariaDbConfig.dropTable(DATABASE_1_PRIVILEGED_DTO, TABLE_1_INTERNALNAME); + + /* test */ + final TableDto response = tableService.createTable(DATABASE_1_PRIVILEGED_DTO, TABLE_1_CREATE_INTERNAL_DTO); + assertEquals(TABLE_1_NAME, response.getName()); + assertEquals(TABLE_1_INTERNALNAME, response.getInternalName()); + assertEquals(TABLE_1_COLUMNS.size(), response.getColumns().size()); + } + + @Test + public void delete_succeeds() throws SQLException, QueryMalformedException { + + /* test */ + tableService.delete(TABLE_1_PRIVILEGED_DTO); + } + + @Test + public void delete_notFound_fails() throws SQLException { + + /* mock */ + MariaDbConfig.createDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_2_INTERNALNAME); + + /* test */ + assertThrows(QueryMalformedException.class, () -> { + tableService.delete(TABLE_5_PRIVILEGED_DTO); + }); + } + + @Test + public void getCount_succeeds() throws SQLException, QueryMalformedException { + + /* test */ + final Long response = tableService.getCount(TABLE_1_PRIVILEGED_DTO, null); + assertEquals(3, response); + } + + @Test + public void getCount_timestamp_succeeds() throws SQLException, QueryMalformedException { + + /* test */ + final Long response = tableService.getCount(TABLE_1_PRIVILEGED_DTO, Instant.ofEpochSecond(0)); + assertEquals(0, response); + } + + @Test + public void getCount_notFound_fails() throws SQLException { + + /* mock */ + MariaDbConfig.createDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_2_INTERNALNAME); + + /* test */ + assertThrows(QueryMalformedException.class, () -> { + tableService.getCount(TABLE_5_PRIVILEGED_DTO, null); + }); + } + + @Test + public void getData_succeeds() throws SQLException, TableMalformedException { + + /* test */ + final QueryResultDto response = tableService.getData(TABLE_1_PRIVILEGED_DTO, null, 0L, 10L); + assertEquals(TABLE_1_ID, response.getId()); + final List<Map<String, Integer>> headers = response.getHeaders(); + assertEquals(5, headers.size()); + assertEquals(0, headers.get(0).get("id")); + assertEquals(1, headers.get(1).get("date")); + assertEquals(2, headers.get(2).get("location")); + assertEquals(3, headers.get(3).get("mintemp")); + assertEquals(4, headers.get(4).get("rainfall")); + final List<Map<String, Object>> result = response.getResult(); + assertEquals(Instant.ofEpochSecond(1228089600), result.get(0).get("date")); + assertEquals(0.6, result.get(0).get("rainfall")); + assertEquals("Albury", result.get(0).get("location")); + assertEquals(BigInteger.valueOf(1L), result.get(0).get("id")); + assertEquals(13.4, result.get(0).get("mintemp")); + assertEquals(Instant.ofEpochSecond(1228176000), result.get(1).get("date")); + assertEquals(0.0, result.get(1).get("rainfall")); + assertEquals("Albury", result.get(1).get("location")); + assertEquals(BigInteger.valueOf(2L), result.get(1).get("id")); + assertEquals(7.4, result.get(1).get("mintemp")); + assertEquals(Instant.ofEpochSecond(1228262400), result.get(2).get("date")); + assertEquals(0.0, result.get(2).get("rainfall")); + assertEquals("Albury", result.get(2).get("location")); + assertEquals(BigInteger.valueOf(3L), result.get(2).get("id")); + assertEquals(12.9, result.get(2).get("mintemp")); + } + + @Test + public void getData_notFound_fails() throws SQLException { + + /* mock */ + MariaDbConfig.createDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_2_INTERNALNAME); + + /* test */ + assertThrows(TableMalformedException.class, () -> { + tableService.getData(TABLE_5_PRIVILEGED_DTO, null, 0L, 10L); + }); + } + + @Test + public void history_succeeds() throws SQLException, TableNotFoundException { + + /* test */ + final List<TableHistoryDto> response = tableService.history(TABLE_1_PRIVILEGED_DTO, 1000L); + assertEquals(1, response.size()); + final TableHistoryDto history0 = response.get(0); + assertNotNull(history0.getTimestamp()); + assertEquals("INSERT", history0.getEvent()); + assertEquals(3, history0.getTotal()); + } + + @Test + public void history_notFound_fails() throws SQLException { + + /* mock */ + MariaDbConfig.createDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_2_INTERNALNAME); + + /* test */ + assertThrows(TableNotFoundException.class, () -> { + tableService.history(TABLE_5_PRIVILEGED_DTO, null); + }); + } + + @Test + public void importDataset_withSeparatorAndQuoteAndNullElement_succeeds() throws SidecarImportException, ServiceException, SQLException, + QueryMalformedException, RemoteUnavailableException, StorageNotFoundException, IOException { + final ImportCsvDto request = ImportCsvDto.builder() + .location("weather_aus.csv") + .separator(';') + .quote('"') + .nullElement("NA") + .build(); + + /* mock */ + final File source = new File("src/test/resources/csv/weather_aus.csv"); + final File target = new File("/tmp/weather_aus.csv"); + log.trace("copy dataset from {} to {}", source.toPath().toAbsolutePath(), target.toPath().toAbsolutePath()); + FileUtils.copyFile(source, target); + doNothing() + .when(dataDatabaseSidecarGateway) + .importFile(anyString(), anyInt(), eq("weather_aus.csv")); + + /* test */ + tableService.importDataset(TABLE_1_PRIVILEGED_DTO, request); + } + + @Test + public void importDataset_malformedData_fails() throws ServiceException, RemoteUnavailableException, StorageNotFoundException, + IOException { + final ImportCsvDto request = ImportCsvDto.builder() + .location("weather_aus.csv") + .separator(';') + .quote('"') + .build(); + + /* mock */ + final File source = new File("src/test/resources/csv/weather_aus.csv"); + final File target = new File("/tmp/weather_aus.csv"); + log.trace("copy dataset from {} to {}", source.toPath().toAbsolutePath(), target.toPath().toAbsolutePath()); + FileUtils.copyFile(source, target); + doNothing() + .when(dataDatabaseSidecarGateway) + .importFile(anyString(), anyInt(), eq("weather_aus.csv")); + + /* test */ + assertThrows(QueryMalformedException.class, () -> { + tableService.importDataset(TABLE_1_PRIVILEGED_DTO, request); + }); + } + + @Test + public void exportDataset_succeeds() throws ServiceException, SQLException, + QueryMalformedException, RemoteUnavailableException, StorageNotFoundException, StorageUnavailableException, + SidecarExportException { + final ExportResourceDto mock = ExportResourceDto.builder() + .filename("weather_aus.csv") + .resource(new InputStreamResource(InputStream.nullInputStream())) + .build(); + + /* mock */ + doNothing() + .when(dataDatabaseSidecarGateway) + .exportFile(anyString(), anyInt(), eq("weather_aus.csv")); + when(storageService.getResource("weather_aus.csv")) + .thenReturn(mock); + + /* test */ + final ExportResourceDto response = tableService.exportDataset(TABLE_1_PRIVILEGED_DTO, null); + } + + @Test + public void exportDataset_malformedData_fails() throws SQLException { + + /* mock */ + MariaDbConfig.createDatabase(CONTAINER_1_PRIVILEGED_DTO, DATABASE_2_INTERNALNAME); + + /* test */ + assertThrows(QueryMalformedException.class, () -> { + tableService.exportDataset(TABLE_5_PRIVILEGED_DTO, null); + }); } } diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/ViewServiceIntegrationTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/ViewServiceIntegrationTest.java index ba846f37cc..54df39999b 100644 --- a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/ViewServiceIntegrationTest.java +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/service/ViewServiceIntegrationTest.java @@ -3,7 +3,6 @@ package at.tuwien.service; import at.tuwien.api.database.ViewColumnDto; import at.tuwien.api.database.ViewDto; import at.tuwien.api.database.query.QueryResultDto; -import at.tuwien.api.database.table.columns.ColumnDto; import at.tuwien.config.MariaDbConfig; import at.tuwien.config.MariaDbContainerConfig; import at.tuwien.exception.*; @@ -24,6 +23,7 @@ import java.time.Instant; import java.util.List; import java.util.Map; +import static at.tuwien.service.SchemaServiceIntegrationTest.assertViewColumn; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -58,7 +58,21 @@ public class ViewServiceIntegrationTest extends AbstractUnitTest { public void create_succeeds() throws SQLException, ViewMalformedException { /* test */ - viewService.create(DATABASE_1_PRIVILEGED_DTO, VIEW_1_CREATE_DTO); + final ViewDto response = viewService.create(DATABASE_1_PRIVILEGED_DTO, VIEW_1_CREATE_DTO); + assertEquals(VIEW_1_NAME, response.getName()); + assertEquals(VIEW_1_INTERNAL_NAME, response.getInternalName()); + assertEquals(VIEW_1_QUERY, response.getQuery()); + assertNotNull(response.getQueryHash()); + assertEquals(DATABASE_1_PUBLIC, response.getIsPublic()); + final List<ViewColumnDto> columns = response.getColumns(); + assertEquals(VIEW_1_COLUMNS.size(), columns.size()); + ViewColumnDto ref = VIEW_1_COLUMNS_DTO.get(0); + SchemaServiceIntegrationTest.assertViewColumn(columns.get(0), null, ref.getDatabaseId(), ref.getName(), ref.getInternalName(), ref.getColumnType(), ref.getSize(), ref.getD(), ref.getIsNullAllowed(), ref.getDateFormat(), ref.getDescription()); + ref = VIEW_1_COLUMNS_DTO.get(1); + SchemaServiceIntegrationTest.assertViewColumn(columns.get(1), null, ref.getDatabaseId(), ref.getName(), ref.getInternalName(), ref.getColumnType(), ref.getSize(), ref.getD(), ref.getIsNullAllowed(), ref.getDateFormat(), ref.getDescription()); + ref = VIEW_1_COLUMNS_DTO.get(2); + SchemaServiceIntegrationTest.assertViewColumn(columns.get(2), null, ref.getDatabaseId(), ref.getName(), ref.getInternalName(), ref.getColumnType(), ref.getSize(), ref.getD(), ref.getIsNullAllowed(), ref.getDateFormat(), ref.getDescription()); + } @Test diff --git a/dbrepo-data-service/rest-service/src/test/java/at/tuwien/utils/MariaDbUtilTest.java b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/utils/MariaDbUtilTest.java new file mode 100644 index 0000000000..6ed73e8a01 --- /dev/null +++ b/dbrepo-data-service/rest-service/src/test/java/at/tuwien/utils/MariaDbUtilTest.java @@ -0,0 +1,42 @@ +package at.tuwien.utils; + +import at.tuwien.api.database.table.columns.ColumnTypeDto; +import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MariaDbUtilTest { + + @Test + public void needValueQuotes_succeeds() { + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.TINYBLOB)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.MEDIUMBLOB)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.LONGBLOB)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.BLOB)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.CHAR)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.VARCHAR)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.ENUM)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.SET)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.TINYTEXT)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.MEDIUMTEXT)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.LONGTEXT)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.TEXT)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.BINARY)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.VARBINARY)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.DATETIME)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.DATE)); + assertTrue(MariaDbUtil.needValueQuotes(ColumnTypeDto.TIMESTAMP)); + assertFalse(MariaDbUtil.needValueQuotes(ColumnTypeDto.INT)); + assertFalse(MariaDbUtil.needValueQuotes(ColumnTypeDto.TINYINT)); + assertFalse(MariaDbUtil.needValueQuotes(ColumnTypeDto.MEDIUMINT)); + assertFalse(MariaDbUtil.needValueQuotes(ColumnTypeDto.DOUBLE)); + assertFalse(MariaDbUtil.needValueQuotes(ColumnTypeDto.DECIMAL)); + assertFalse(MariaDbUtil.needValueQuotes(ColumnTypeDto.BOOL)); + } +} diff --git a/dbrepo-data-service/rest-service/src/test/resources/application.properties b/dbrepo-data-service/rest-service/src/test/resources/application.properties index ed58329c18..07eb7f642b 100644 --- a/dbrepo-data-service/rest-service/src/test/resources/application.properties +++ b/dbrepo-data-service/rest-service/src/test/resources/application.properties @@ -25,4 +25,8 @@ logging.level.at.tuwien.=trace spring.rabbitmq.host=localhost spring.rabbitmq.virtual-host=dbrepo spring.rabbitmq.username=guest -spring.rabbitmq.password=guest \ No newline at end of file +spring.rabbitmq.password=guest + +# s3 +dbrepo.s3.accessKeyId=minioadmin +dbrepo.s3.secretAccessKey=minioadmin \ No newline at end of file diff --git a/dbrepo-data-service/rest-service/src/test/resources/init/weather.sql b/dbrepo-data-service/rest-service/src/test/resources/init/weather.sql index 2eef31b475..052f23adf8 100644 --- a/dbrepo-data-service/rest-service/src/test/resources/init/weather.sql +++ b/dbrepo-data-service/rest-service/src/test/resources/init/weather.sql @@ -13,12 +13,27 @@ CREATE TABLE weather_aus ( id BIGINT NOT NULL PRIMARY KEY, `date` DATE NOT NULL, - location VARCHAR(255) NULL, + location VARCHAR(255) NULL COMMENT 'Closest city', mintemp DOUBLE PRECISION NULL, rainfall DOUBLE PRECISION NULL, - FOREIGN KEY (location) REFERENCES weather_location (location), + FOREIGN KEY (location) REFERENCES weather_location (location) ON DELETE SET NULL, UNIQUE (`date`), CHECK (`mintemp` > 0) +) WITH SYSTEM VERSIONING COMMENT 'Weather in Australia'; + +CREATE TABLE complex_primary_key +( + id BIGINT NOT NULL, + other_id BIGINT NOT NULL, + PRIMARY KEY (id, other_id) +) WITH SYSTEM VERSIONING; + +CREATE TABLE complex_foreign_keys +( + id BIGINT NOT NULL PRIMARY KEY, + weather_id BIGINT NOT NULL, + other_id BIGINT NOT NULL, + FOREIGN KEY (weather_id, other_id) REFERENCES complex_primary_key (id, `other_id`) ) WITH SYSTEM VERSIONING; CREATE TABLE sensor diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java b/dbrepo-data-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java index 6cd55e9ef7..805035d421 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/auth/BasicAuthenticationProvider.java @@ -3,6 +3,7 @@ package at.tuwien.auth; import at.tuwien.api.keycloak.TokenDto; import at.tuwien.api.user.UserDetailsDto; import at.tuwien.config.GatewayConfig; +import at.tuwien.exception.RemoteUnavailableException; import at.tuwien.exception.ServiceConnectionException; import at.tuwien.exception.ServiceException; import at.tuwien.gateway.KeycloakGateway; @@ -53,7 +54,7 @@ public class BasicAuthenticationProvider implements AuthenticationManager { final TokenDto tokenDto = keycloakGateway.obtainUserToken(auth.getName(), auth.getCredentials().toString()); final UserDetails userDetails = authTokenFilter.verifyJwt(tokenDto.getAccessToken()); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - } catch (ServletException | ServiceConnectionException | ServiceException e) { + } catch (ServletException | RemoteUnavailableException | ServiceException e) { throw new BadCredentialsException("Failed to authenticate with authentication service", e); } } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/dbrepo-data-service/services/src/main/java/at/tuwien/config/GatewayConfig.java index 57df3af3a6..b04aff18ce 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/config/GatewayConfig.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/config/GatewayConfig.java @@ -19,8 +19,8 @@ import java.util.List; @Configuration public class GatewayConfig { - @Value("${dbrepo.endpoints.gatewayService}") - private String gatewayEndpoint; + @Value("${dbrepo.endpoints.metadataService}") + private String metadataEndpoint; @Value("${dbrepo.admin.username}") private String adminUsername; @@ -31,8 +31,8 @@ public class GatewayConfig { @Bean public RestTemplate restTemplate() { final RestTemplate restTemplate = new RestTemplate(); - restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(gatewayEndpoint)); - log.debug("add basic authentication for internal gateway: username={}, password=(hidden)", adminUsername); + restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(metadataEndpoint)); + log.debug("add basic authentication for metadata service: username={}, password=(hidden)", adminUsername); restTemplate.getInterceptors() .addAll(List.of(new BasicAuthenticationInterceptor(adminUsername, adminPassword), clientHttpRequestInterceptor())); diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/AnalyseServiceGateway.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/AnalyseServiceGateway.java deleted file mode 100644 index b10f386cd3..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/AnalyseServiceGateway.java +++ /dev/null @@ -1,10 +0,0 @@ -package at.tuwien.gateway; - -import at.tuwien.api.database.table.TableStatisticDto; -import at.tuwien.exception.NotAllowedException; -import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.TableNotFoundException; - -public interface AnalyseServiceGateway { - TableStatisticDto analyseTable(Long databaseId, Long tableId) throws RemoteUnavailableException, NotAllowedException, TableNotFoundException; -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/DataDatabaseSidecarGateway.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/DataDatabaseSidecarGateway.java index 417fe77d7a..ecac6865f6 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/DataDatabaseSidecarGateway.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/DataDatabaseSidecarGateway.java @@ -1,13 +1,11 @@ package at.tuwien.gateway; -import at.tuwien.exception.SidecarExportException; -import at.tuwien.exception.SidecarImportException; -import at.tuwien.exception.StorageNotFoundException; +import at.tuwien.exception.*; public interface DataDatabaseSidecarGateway { - void importFile(String hostname, Integer port, String filename) throws SidecarImportException, - StorageNotFoundException; + void importFile(String hostname, Integer port, String filename) throws StorageNotFoundException, + RemoteUnavailableException, ServiceException; void exportFile(String hostname, Integer port, String filename) throws StorageNotFoundException, - SidecarExportException; + ServiceException, RemoteUnavailableException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java index a05a75a6ff..1058119a25 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/KeycloakGateway.java @@ -1,11 +1,14 @@ package at.tuwien.gateway; import at.tuwien.api.keycloak.TokenDto; +import at.tuwien.exception.RemoteUnavailableException; import at.tuwien.exception.ServiceConnectionException; import at.tuwien.exception.ServiceException; +import javax.naming.ServiceUnavailableException; + public interface KeycloakGateway { - TokenDto obtainUserToken(String username, String password) throws ServiceConnectionException, ServiceException; + TokenDto obtainUserToken(String username, String password) throws RemoteUnavailableException, ServiceException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/MetadataServiceGateway.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/MetadataServiceGateway.java index ad1cb75693..4c01a40a44 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/MetadataServiceGateway.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/MetadataServiceGateway.java @@ -10,6 +10,7 @@ import at.tuwien.api.identifier.IdentifierDto; import at.tuwien.api.user.PrivilegedUserDto; import at.tuwien.api.user.UserDto; import at.tuwien.exception.*; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.UUID; @@ -21,21 +22,12 @@ public interface MetadataServiceGateway { * * @param containerId The container id * @return The container with privileged connection information, if successful. - * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. - * @throws ContainerNotFoundException The container was not found in the metadata service. + * @throws ContainerNotFoundException The table was not found in the metadata service. + * @throws RemoteUnavailableException The remote service is not available. + * @throws ServiceException The remote service returned invalid data. */ - PrivilegedContainerDto getContainerById(Long containerId) throws RemoteUnavailableException, ContainerNotFoundException; - - /** - * Get all databases from the metadata service. - * - * @return List of databases, if successful. - * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. - */ - List<PrivilegedDatabaseDto> getDatabases() throws RemoteUnavailableException; - - void updateTableStatistics(Long databaseId, Long tableId, TableStatisticDto data) - throws RemoteUnavailableException; + PrivilegedContainerDto getContainerById(Long containerId) throws RemoteUnavailableException, + ContainerNotFoundException, ServiceException; /** * Get a database with given id from the metadata service. @@ -43,9 +35,11 @@ public interface MetadataServiceGateway { * @param id The database id. * @return The database, if successful. * @throws DatabaseNotFoundException The database was not found in the metadata service. - * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. + * @throws RemoteUnavailableException The remote service is not available. + * @throws ServiceException The remote service returned invalid data. */ - PrivilegedDatabaseDto getDatabaseById(Long id) throws DatabaseNotFoundException, RemoteUnavailableException; + PrivilegedDatabaseDto getDatabaseById(Long id) throws DatabaseNotFoundException, RemoteUnavailableException, + ServiceException; /** * Get a database with given internal name from the metadata service. @@ -53,9 +47,11 @@ public interface MetadataServiceGateway { * @param internalName The internal name. * @return The database, if successful. * @throws DatabaseNotFoundException The database was not found in the metadata service. - * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. + * @throws RemoteUnavailableException The remote service is not available. + * @throws ServiceException The remote service returned invalid data. */ - PrivilegedDatabaseDto getDatabaseByInternalName(String internalName) throws DatabaseNotFoundException, RemoteUnavailableException; + PrivilegedDatabaseDto getDatabaseByInternalName(String internalName) throws DatabaseNotFoundException, + RemoteUnavailableException, ServiceException; /** * Get a table with given database id and table id from the metadata service. @@ -64,11 +60,23 @@ public interface MetadataServiceGateway { * @param id The table id. * @return The table, if successful. * @throws TableNotFoundException The table was not found in the metadata service. - * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. + * @throws RemoteUnavailableException The remote service is not available. + * @throws ServiceException The remote service returned invalid data. */ - PrivilegedTableDto getTableById(Long databaseId, Long id) throws TableNotFoundException, RemoteUnavailableException; + PrivilegedTableDto getTableById(Long databaseId, Long id) throws TableNotFoundException, RemoteUnavailableException, + ServiceException; - PrivilegedViewDto getViewById(Long databaseId, Long id) throws RemoteUnavailableException, ViewNotFoundException; + /** + * Get a view with given database id and view id from the metadata service. + * @param databaseId The database id. + * @param id The view id. + * @return The view, if successful. + * @throws ViewNotFoundException The view was not found in the metadata service. + * @throws RemoteUnavailableException The remote service is not available. + * @throws ServiceException The remote service returned invalid data. + */ + PrivilegedViewDto getViewById(Long databaseId, Long id) throws RemoteUnavailableException, ViewNotFoundException, + ServiceException; /** * Get a user with given user id from the metadata service. @@ -77,16 +85,53 @@ public interface MetadataServiceGateway { * @return The user, if successful. * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. * @throws UserNotFoundException The user was not found in the metadata service. + * @throws ServiceException The remote service returned invalid data. */ - PrivilegedUserDto getUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException; + UserDto getUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException, ServiceException; - DatabaseAccessDto getAccess(Long databaseId, UUID userId) throws RemoteUnavailableException, NotAllowedException; + /** + * Get a user with given user id from the metadata service. + * + * @param userId The user id. + * @return The user, if successful. + * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. + * @throws UserNotFoundException The user was not found in the metadata service. + * @throws ServiceException The remote service returned invalid data. + */ + PrivilegedUserDto getPrivilegedUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException, + ServiceException; - List<IdentifierDto> getIdentifiers(Long databaseId, Long subsetId) throws RemoteUnavailableException, - NotAllowedException; + /** + * Get database access for a given user and database id from the metadata service. + * @param databaseId The database id. + * @param userId The user id. + * @return The database access, if successful. + * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. + * @throws NotAllowedException The access to this database is denied for the given user. + * @throws ServiceException The remote service returned invalid data. + */ + DatabaseAccessDto getAccess(Long databaseId, UUID userId) throws RemoteUnavailableException, NotAllowedException, + ServiceException; - List<IdentifierDto> getIdentifiers(Long databaseId) throws RemoteUnavailableException, - NotAllowedException; + /** + * Get a list of identifiers for a given database id and optional subset id. + * @param databaseId The database id. + * @param subsetId The subset id. Optional. + * @return The list of identifiers. + * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. + * @throws DatabaseNotFoundException The database was not found. + * @throws ServiceException The remote service returned invalid data. + */ + List<IdentifierDto> getIdentifiers(@NotNull Long databaseId, Long subsetId) throws ServiceException, + RemoteUnavailableException, DatabaseNotFoundException; - UserDto getUser(UUID userId) throws RemoteUnavailableException, NotAllowedException, UserNotFoundException; + /** + * Update the table statistics in the metadata service. + * @param databaseId The database id. + * @param tableId The table id. + * @throws RemoteUnavailableException The remote service is not available and invalid data was returned. + * @throws TableNotFoundException The table was not found. + * @throws ServiceException The remote service returned invalid data. + */ + void updateTableStatistics(Long databaseId, Long tableId) throws TableNotFoundException, ServiceException, RemoteUnavailableException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/AnalyseServiceGatewayImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/AnalyseServiceGatewayImpl.java deleted file mode 100644 index ff4f769a08..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/AnalyseServiceGatewayImpl.java +++ /dev/null @@ -1,50 +0,0 @@ -package at.tuwien.gateway.impl; - -import at.tuwien.api.database.table.TableStatisticDto; -import at.tuwien.exception.*; -import at.tuwien.gateway.AnalyseServiceGateway; -import lombok.extern.log4j.Log4j2; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.ResourceAccessException; -import org.springframework.web.client.RestTemplate; - -@Log4j2 -@Service -public class AnalyseServiceGatewayImpl implements AnalyseServiceGateway { - - private final RestTemplate restTemplate; - - @Autowired - public AnalyseServiceGatewayImpl(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - @Override - public TableStatisticDto analyseTable(Long databaseId, Long tableId) throws RemoteUnavailableException, - NotAllowedException, TableNotFoundException { - final ResponseEntity<TableStatisticDto> response; - final String url = "/api/analyse/database/" + databaseId + "/table/" + tableId + "/statistics"; - log.trace("mapped url: {}", url); - try { - response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null), TableStatisticDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to analyse table with id {}: {}", tableId, e.getMessage()); - throw new RemoteUnavailableException("Failed to analyse table", e); - } catch (HttpClientErrorException.NotFound e) { - log.error("Failed to analyse table with id {}: not found: {}", tableId, e.getMessage()); - throw new TableNotFoundException("Failed to analyse table: not found", e); - } - if (response.getBody() == null) { - log.error("Failed to analyse table: body is null"); - throw new NotAllowedException("Failed to analyse table: body is null"); - } - return response.getBody(); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/DataDatabaseSidecarGatewayImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/DataDatabaseSidecarGatewayImpl.java index 0c1a74dbcf..b3e7c3bd41 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/DataDatabaseSidecarGatewayImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/DataDatabaseSidecarGatewayImpl.java @@ -1,13 +1,12 @@ package at.tuwien.gateway.impl; -import at.tuwien.exception.SidecarExportException; -import at.tuwien.exception.SidecarImportException; -import at.tuwien.exception.StorageNotFoundException; +import at.tuwien.exception.*; import at.tuwien.gateway.DataDatabaseSidecarGateway; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.*; import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; @@ -24,38 +23,44 @@ public class DataDatabaseSidecarGatewayImpl implements DataDatabaseSidecarGatewa } @Override - public void importFile(String hostname, Integer port, String filename) throws SidecarImportException, - StorageNotFoundException { + public void importFile(String hostname, Integer port, String filename) throws StorageNotFoundException, + RemoteUnavailableException, ServiceException { final ResponseEntity<Void> response; final String url = "http://" + hostname + ":" + port + "/sidecar/import/" + filename; log.debug("import file into data database sidecar"); try { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(null), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to import .csv in data-db sidecar: {}", e.getMessage()); - throw new StorageNotFoundException("Failed to import .csv in data-db sidecar: " + e.getMessage(), e); + } catch (ResourceAccessException | HttpServerErrorException e) { + log.error("Failed to import dataset with filename: {}: {}", filename, e.getMessage()); + throw new RemoteUnavailableException("Failed to import dataset: " + e.getMessage(), e); + } catch (HttpClientErrorException.BadRequest e) { + log.error("Failed to import dataset with filename: {}: not found: {}", filename, e.getMessage()); + throw new StorageNotFoundException("Failed to import dataset: not found: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { - log.error("Failed to import .csv in data-db sidecar"); - throw new SidecarImportException("Failed to import .csv in data-db sidecar"); + log.error("Failed to import dataset with filename: {}: service responded unsuccessful: {}", filename, response.getStatusCode()); + throw new ServiceException("Failed to import dataset: service responded unsuccessful: " + response.getStatusCode()); } } @Override public void exportFile(String hostname, Integer port, String filename) throws StorageNotFoundException, - SidecarExportException { + RemoteUnavailableException, ServiceException { final ResponseEntity<Void> response; final String url = "http://" + hostname + ":" + port + "/sidecar/export/" + filename; - log.debug("export file into data database sidecar: {}", url); + log.debug("export file from data database sidecar: {}", url); try { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(null), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to export .csv in data-db sidecar: {}", e.getMessage()); - throw new StorageNotFoundException("Failed to export .csv in data-db sidecar: " + e.getMessage(), e); + } catch (ResourceAccessException | HttpServerErrorException e) { + log.error("Failed to export dataset with filename: {}: {}", filename, e.getMessage()); + throw new RemoteUnavailableException("Failed to export dataset: " + e.getMessage(), e); + } catch (HttpClientErrorException.BadRequest e) { + log.error("Failed to export dataset with filename: {}: not found: {}", filename, e.getMessage()); + throw new StorageNotFoundException("Failed to export dataset: not found: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { - log.error("Failed to export .csv in data-db sidecar"); - throw new SidecarExportException("Failed to export .csv in data-db sidecar"); + log.error("Failed to export dataset with filename: {}: service responded unsuccessful: {}", filename, response.getStatusCode()); + throw new ServiceException("Failed to export dataset: service responded unsuccessful: " + response.getStatusCode()); } } } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java index 76f3e83cef..545e259097 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/KeycloakGatewayImpl.java @@ -2,10 +2,12 @@ package at.tuwien.gateway.impl; import at.tuwien.api.keycloak.TokenDto; import at.tuwien.config.KeycloakConfig; +import at.tuwien.exception.RemoteUnavailableException; import at.tuwien.exception.ServiceConnectionException; import at.tuwien.exception.ServiceException; import at.tuwien.gateway.KeycloakGateway; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.*; import org.springframework.stereotype.Service; @@ -22,37 +24,14 @@ public class KeycloakGatewayImpl implements KeycloakGateway { private final RestTemplate restTemplate; private final KeycloakConfig keycloakConfig; - public KeycloakGatewayImpl(@Qualifier("keycloakRestTemplate") RestTemplate restTemplate, - KeycloakConfig keycloakConfig) { + @Autowired + public KeycloakGatewayImpl(RestTemplate restTemplate, KeycloakConfig keycloakConfig) { this.restTemplate = restTemplate; this.keycloakConfig = keycloakConfig; } - public TokenDto obtainToken() throws ServiceConnectionException, ServiceException { - final HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - final MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(); - payload.add("username", keycloakConfig.getKeycloakUsername()); - payload.add("password", keycloakConfig.getKeycloakPassword()); - payload.add("grant_type", "password"); - payload.add("client_id", "admin-cli"); - final String url = keycloakConfig.getKeycloakEndpoint() + "/realms/master/protocol/openid-connect/token"; - log.debug("request admin token from url {}", url); - final ResponseEntity<TokenDto> response; - try { - response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to obtain admin token: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to obtain admin token: " + e.getMessage(), e); - } catch (Exception e) { - log.error("Failed to obtain admin token: remote host answered unexpected: {}", e.getMessage(), e); - throw new ServiceException("Failed to obtain admin token: remote host answered unexpected: " + e.getMessage(), e); - } - return response.getBody(); - } - @Override - public TokenDto obtainUserToken(String username, String password) throws ServiceConnectionException, ServiceException { + public TokenDto obtainUserToken(String username, String password) throws RemoteUnavailableException, ServiceException { final HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); final MultiValueMap<String, String> payload = new LinkedMultiValueMap<>(); @@ -66,15 +45,18 @@ public class KeycloakGatewayImpl implements KeycloakGateway { log.debug("request user token from url {}", url); final ResponseEntity<TokenDto> response; try { - response = new RestTemplate() - .exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(payload, headers), TokenDto.class); + } catch (HttpServerErrorException e) { log.error("Failed to obtain user token: {}", e.getMessage()); - throw new ServiceConnectionException("Failed to obtain user token: " + e.getMessage(), e); + throw new RemoteUnavailableException("Failed to obtain user token: " + e.getMessage(), e); } catch (Exception e) { log.error("Failed to obtain user token: unexpected response: {}", e.getMessage(), e); throw new ServiceException("Failed to obtain user token: unexpected response: " + e.getMessage(), e); } + if (!response.getStatusCode().equals(HttpStatus.OK)) { + log.error("Failed to obtain user token: service responded unsuccessful: {}", response.getStatusCode()); + throw new ServiceException("obtain user token: service responded unsuccessful: " + response.getStatusCode()); + } return response.getBody(); } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/MetadataServiceGatewayImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/MetadataServiceGatewayImpl.java index cb3c57b332..1fcf3e50ee 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/MetadataServiceGatewayImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/gateway/impl/MetadataServiceGatewayImpl.java @@ -15,6 +15,7 @@ import at.tuwien.api.user.UserDto; import at.tuwien.exception.*; import at.tuwien.gateway.MetadataServiceGateway; import at.tuwien.mapper.MetadataMapper; +import jakarta.validation.constraints.NotNull; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; @@ -45,17 +46,29 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { @Override public PrivilegedContainerDto getContainerById(Long containerId) throws RemoteUnavailableException, - ContainerNotFoundException { + ContainerNotFoundException, ServiceException { final ResponseEntity<ContainerDto> response; try { - response = restTemplate.exchange("/api/container/" + containerId, HttpMethod.GET, new HttpEntity<>(null), + response = restTemplate.exchange("/api/container/" + containerId, HttpMethod.GET, HttpEntity.EMPTY, ContainerDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to find container: {}", e.getMessage()); + } catch (ResourceAccessException | HttpServerErrorException e) { + log.error("Failed to find container with id {}: {}", containerId, e.getMessage()); throw new RemoteUnavailableException("Failed to find container: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { - log.error("Failed to find container: body is null"); - throw new ContainerNotFoundException("Failed to find container: body is null"); + log.error("Failed to find container with id {}: {}", containerId, e.getMessage()); + throw new ContainerNotFoundException("Failed to find container: " + e.getMessage()); + } + if (response.getStatusCode() != HttpStatus.OK) { + log.error("Failed to find container with id {}: service responded unsuccessful: {}", containerId, response.getStatusCode()); + throw new ServiceException("Failed to find container: service responded unsuccessful: " + response.getStatusCode()); + } + if (!response.getHeaders().keySet().containsAll(List.of("X-Username", "X-Password"))) { + log.error("Failed to find all privileged container headers"); + throw new ServiceException("Failed to find all privileged container headers"); + } + if (response.getBody() == null) { + log.error("Failed to find container with id {}: body is empty", containerId); + throw new ServiceException("Failed to find container with id " + containerId + ": body is empty"); } final PrivilegedContainerDto container = metadataMapper.containerDtoToPrivilegedContainerDto(response.getBody()); container.setUsername(response.getHeaders().get("X-Username").get(0)); @@ -64,89 +77,83 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } @Override - public List<PrivilegedDatabaseDto> getDatabases() throws RemoteUnavailableException { - final ResponseEntity<PrivilegedDatabaseDto[]> response; - try { - response = restTemplate.exchange("/api/database", HttpMethod.GET, new HttpEntity<>(null), - PrivilegedDatabaseDto[].class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to find databases: {}", e.getMessage()); - throw new RemoteUnavailableException("Failed to find databases: " + e.getMessage(), e); - } - if (response.getBody() == null) { - log.error("Failed to find databases: body is null"); - throw new RemoteUnavailableException("Failed to find databases: body is null"); - } - return List.of(response.getBody()); - } - - @Override - public void updateTableStatistics(Long databaseId, Long tableId, TableStatisticDto data) - throws RemoteUnavailableException { - final ResponseEntity<Void> response; - try { - response = restTemplate.exchange("/api/database/" + databaseId + "/table/" + tableId, HttpMethod.PUT, - new HttpEntity<>(data), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to update table statistics: {}", e.getMessage()); - throw new RemoteUnavailableException("Failed to update table statistics: " + e.getMessage(), e); - } - if (response.getStatusCode() != HttpStatus.ACCEPTED) { - log.error("Failed to update table statistics: unexpected status code"); - throw new RemoteUnavailableException("Failed to update table statistics: unexpected status code"); - } - } - - @Override - public PrivilegedDatabaseDto getDatabaseById(Long id) throws DatabaseNotFoundException, RemoteUnavailableException { + public PrivilegedDatabaseDto getDatabaseById(Long id) throws DatabaseNotFoundException, RemoteUnavailableException, + ServiceException { final ResponseEntity<PrivilegedDatabaseDto> response; try { - response = restTemplate.exchange("/api/database/" + id, HttpMethod.GET, new HttpEntity<>(null), + response = restTemplate.exchange("/api/database/" + id, HttpMethod.GET, HttpEntity.EMPTY, PrivilegedDatabaseDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + } catch (ResourceAccessException | HttpServerErrorException e) { log.error("Failed to find database with id {}: {}", id, e.getMessage()); - throw new RemoteUnavailableException("Failed to find database with id " + id + ": " + e.getMessage(), e); + throw new RemoteUnavailableException("Failed to find database: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { log.error("Failed to find database with id {}: body is null", id); - throw new DatabaseNotFoundException("Failed to find database id " + id + ": body is null", e); + throw new DatabaseNotFoundException("Failed to find database: body is null", e); + } + if (response.getStatusCode() != HttpStatus.OK) { + log.error("Failed to find database with id {}: service responded unsuccessful: {}", id, response.getStatusCode()); + throw new ServiceException("Failed to find database: service responded unsuccessful: " + response.getStatusCode()); + } + if (!response.getHeaders().keySet().containsAll(List.of("X-Username", "X-Password"))) { + log.error("Failed to find all privileged database headers"); + throw new ServiceException("Failed to find all privileged database headers"); + } + if (response.getBody() == null) { + log.error("Failed to find database with id {}: body is empty", id); + throw new ServiceException("Failed to find database with id " + id + ": body is empty"); } final PrivilegedDatabaseDto database = response.getBody(); database.getContainer().setUsername(response.getHeaders().get("X-Username").get(0)); database.getContainer().setPassword(response.getHeaders().get("X-Password").get(0)); - log.debug("found privileged database username={}, password={}", database.getContainer().getUsername(), - database.getContainer().getPassword().isEmpty() ? "(empty)" : "(hidden)"); + log.debug("found privileged database username={}", database.getContainer().getUsername()); return database; } @Override public PrivilegedDatabaseDto getDatabaseByInternalName(String internalName) throws DatabaseNotFoundException, - RemoteUnavailableException { + RemoteUnavailableException, ServiceException { final ResponseEntity<PrivilegedDatabaseDto[]> response; try { - response = restTemplate.exchange("/api/database/", HttpMethod.GET, new HttpEntity<>(null), PrivilegedDatabaseDto[].class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + response = restTemplate.exchange("/api/database/", HttpMethod.GET, HttpEntity.EMPTY, PrivilegedDatabaseDto[].class); + } catch (ResourceAccessException | HttpServerErrorException e) { log.error("Failed to find database with internal name {}: {}", internalName, e.getMessage()); - throw new RemoteUnavailableException("Failed to find database with internal name " + internalName + ": " + e.getMessage(), e); + throw new RemoteUnavailableException("Failed to find database: " + e.getMessage(), e); } - if (response.getBody() == null || response.getBody().length != 1) { - log.error("Failed to find database with internal name {}: body is null", internalName); - throw new DatabaseNotFoundException("Failed to find database with internal name " + internalName + ": body is null"); + if (!response.getStatusCode().equals(HttpStatus.OK) || response.getBody() == null) { + log.error("Failed to find database with internal name {}: service responded unsuccessful: {}", internalName, response.getStatusCode()); + throw new ServiceException("Failed to find database: service responded unsuccessful: " + response.getStatusCode()); + } + if (response.getBody().length != 1) { + log.error("Failed to find database with internal name {}: body is empty", internalName); + throw new DatabaseNotFoundException("Failed to find database: body is empty"); } return response.getBody()[0]; } @Override - public PrivilegedTableDto getTableById(Long databaseId, Long id) throws TableNotFoundException, RemoteUnavailableException { + public PrivilegedTableDto getTableById(Long databaseId, Long id) throws TableNotFoundException, + RemoteUnavailableException, ServiceException { final ResponseEntity<TableDto> response; try { - response = restTemplate.exchange("/api/database/" + databaseId + "/table/" + id, HttpMethod.GET, new HttpEntity<>(null), TableDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + response = restTemplate.exchange("/api/database/" + databaseId + "/table/" + id, HttpMethod.GET, HttpEntity.EMPTY, TableDto.class); + } catch (ResourceAccessException | HttpServerErrorException e) { log.error("Failed to find table with id {}: {}", id, e.getMessage()); - throw new RemoteUnavailableException("Failed to find table with id " + id + ": " + e.getMessage(), e); + throw new RemoteUnavailableException("Failed to find table: " + e.getMessage(), e); + } catch (HttpClientErrorException.NotFound e) { + log.error("Failed to find table with id {}: not found: {}", id, e.getMessage()); + throw new TableNotFoundException("Failed to find table: " + e.getMessage()); + } + if (!response.getStatusCode().equals(HttpStatus.OK)) { + log.error("Failed to find table with id {}: service responded unsuccessful: {}", id, response.getStatusCode()); + throw new ServiceException("Failed to find table: service responded unsuccessful: " + response.getStatusCode()); + } + if (!response.getHeaders().keySet().containsAll(List.of("X-Type", "X-Host", "X-Port", "X-Username", "X-Password", "X-Database", "X-Sidecar-Host", "X-Sidecar-Port"))) { + log.error("Failed to find all privileged table headers"); + throw new ServiceException("Failed to find all privileged table headers"); } if (response.getBody() == null) { - log.error("Failed to find table with id {}: body is null", id); - throw new TableNotFoundException("Failed to find table with id " + id + ": body is null"); + log.error("Failed to find table with id {}: body is empty", id); + throw new ServiceException("Failed to find table with id " + id + ": body is empty"); } final PrivilegedTableDto table = metadataMapper.tableDtoToPrivilegedTableDto(response.getBody()); table.getDatabase().getContainer().getImage().setJdbcMethod(response.getHeaders().get("X-Type").get(0)); @@ -157,24 +164,34 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { table.getDatabase().setInternalName(response.getHeaders().get("X-Database").get(0)); table.getDatabase().getContainer().setSidecarHost(response.getHeaders().get("X-Sidecar-Host").get(0)); table.getDatabase().getContainer().setSidecarPort(Integer.parseInt(response.getHeaders().get("X-Sidecar-Port").get(0))); - log.debug("found privileged database username={}, password={}", - table.getDatabase().getContainer().getUsername(), - table.getDatabase().getContainer().getPassword().isEmpty() ? "(empty)" : "(hidden)"); + log.debug("found privileged database username={}", table.getDatabase().getContainer().getUsername()); return table; } @Override - public PrivilegedViewDto getViewById(Long databaseId, Long id) throws RemoteUnavailableException, ViewNotFoundException { + public PrivilegedViewDto getViewById(Long databaseId, Long id) throws RemoteUnavailableException, + ViewNotFoundException, ServiceException { final ResponseEntity<ViewDto> response; try { - response = restTemplate.exchange("/api/database/" + databaseId + "/view/" + id, HttpMethod.GET, new HttpEntity<>(null), ViewDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + response = restTemplate.exchange("/api/database/" + databaseId + "/view/" + id, HttpMethod.GET, HttpEntity.EMPTY, ViewDto.class); + } catch (ResourceAccessException | HttpServerErrorException e) { log.error("Failed to find view with id {}: {}", id, e.getMessage()); - throw new RemoteUnavailableException("Failed to find view with id " + id + ": " + e.getMessage(), e); + throw new RemoteUnavailableException("Failed to find view: " + e.getMessage(), e); + } catch (HttpClientErrorException.NotFound e) { + log.error("Failed to find view with id {}: not found: {}", id, e.getMessage()); + throw new ViewNotFoundException("Failed to find view: " + e.getMessage()); + } + if (!response.getStatusCode().equals(HttpStatus.OK)) { + log.error("Failed to find view with id {}: service responded unsuccessful: {}", id, response.getStatusCode()); + throw new ServiceException("Failed to find view: service responded unsuccessful: " + response.getStatusCode()); + } + if (!response.getHeaders().keySet().containsAll(List.of("X-Type", "X-Host", "X-Port", "X-Username", "X-Password", "X-Database"))) { + log.error("Failed to find all privileged view headers"); + throw new ServiceException("Failed to find all privileged view headers"); } if (response.getBody() == null) { - log.error("Failed to find view with id {}: body is null", id); - throw new ViewNotFoundException("Failed to find view with id " + id + ": body is null"); + log.error("Failed to find view with id {}: body is empty", id); + throw new ServiceException("Failed to find view with id " + id + ": body is empty"); } final PrivilegedViewDto table = metadataMapper.viewDtoToPrivilegedViewDto(response.getBody()); table.getDatabase().getContainer().getImage().setJdbcMethod(response.getHeaders().get("X-Type").get(0)); @@ -187,101 +204,128 @@ public class MetadataServiceGatewayImpl implements MetadataServiceGateway { } @Override - public PrivilegedUserDto getUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException { - final ResponseEntity<PrivilegedUserDto> response; + public UserDto getUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException, + ServiceException { + final ResponseEntity<UserDto> response; try { - response = restTemplate.exchange("/api/user/" + userId, HttpMethod.GET, new HttpEntity<>(null), PrivilegedUserDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { + response = restTemplate.exchange("/api/user/" + userId, HttpMethod.GET, HttpEntity.EMPTY, UserDto.class); + } catch (ResourceAccessException | HttpServerErrorException e) { log.error("Failed to find user with id {}: {}", userId, e.getMessage()); - throw new RemoteUnavailableException("Failed to find user with id " + userId + ": " + e.getMessage(), e); + throw new RemoteUnavailableException("Failed to find user: " + e.getMessage(), e); + } catch (HttpClientErrorException.NotFound e) { + log.error("Failed to find user with id {}: not found: {}", userId, e.getMessage()); + throw new UserNotFoundException("Failed to find user: " + e.getMessage()); + } + if (!response.getStatusCode().equals(HttpStatus.OK)) { + log.error("Failed to find user with id {}: service responded unsuccessful: {}", userId, response.getStatusCode()); + throw new ServiceException("Failed to find user: service responded unsuccessful: " + response.getStatusCode()); } if (response.getBody() == null) { - log.error("Failed to find user: body is null"); - throw new UserNotFoundException("Failed to find user: body is null"); + log.error("Failed to find user with id {}: body is empty", userId); + throw new ServiceException("Failed to find user with id " + userId + ": body is empty"); } return response.getBody(); } @Override - public DatabaseAccessDto getAccess(Long databaseId, UUID userId) throws RemoteUnavailableException, - NotAllowedException { - final ResponseEntity<DatabaseAccessDto> response; + public PrivilegedUserDto getPrivilegedUserById(UUID userId) throws RemoteUnavailableException, UserNotFoundException, + ServiceException { + final ResponseEntity<UserDto> response; try { - response = restTemplate.exchange("/api/database/" + databaseId + "/access/" + userId, HttpMethod.GET, new HttpEntity<>(null), DatabaseAccessDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to find database access for user with id {}: {}", userId, e.getMessage()); - throw new RemoteUnavailableException("Failed to find database access", e); - } catch (HttpClientErrorException.Forbidden e) { - log.error("Failed to find database access for user with id {}: foreign user: {}", userId, e.getMessage()); - throw new NotAllowedException("Failed to find database access: foreign user", e); + response = restTemplate.exchange("/api/user/" + userId, HttpMethod.GET, HttpEntity.EMPTY, UserDto.class); + } catch (ResourceAccessException | HttpServerErrorException e) { + log.error("Failed to find user with id {}: {}", userId, e.getMessage()); + throw new RemoteUnavailableException("Failed to find user: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { - log.error("Failed to find database access for user with id {}: missing access: {}", userId, e.getMessage()); - throw new NotAllowedException("Failed to find database access: missing access", e); + log.error("Failed to find user with id {}: not found: {}", userId, e.getMessage()); + throw new UserNotFoundException("Failed to find user: " + e.getMessage()); + } + if (!response.getStatusCode().equals(HttpStatus.OK)) { + log.error("Failed to find user with id {}: service responded unsuccessful: {}", userId, response.getStatusCode()); + throw new ServiceException("Failed to find user: service responded unsuccessful: " + response.getStatusCode()); + } + if (!response.getHeaders().keySet().containsAll(List.of("X-Username", "X-Password"))) { + log.error("Failed to find all privileged user headers"); + throw new ServiceException("Failed to find all privileged user headers"); } if (response.getBody() == null) { - log.error("Failed to find database access: body is null"); - throw new NotAllowedException("Failed to find database access: body is null"); + log.error("Failed to find user with id {}: body is empty", userId); + throw new ServiceException("Failed to find user with id " + userId + ": body is empty"); } - return response.getBody(); + final PrivilegedUserDto user = metadataMapper.userDtoToPrivilegedUserDto(response.getBody()); + user.setUsername(response.getHeaders().get("X-Username").get(0)); + user.setPassword(response.getHeaders().get("X-Password").get(0)); + return user; } @Override - public List<IdentifierDto> getIdentifiers(Long databaseId, Long subsetId) throws RemoteUnavailableException, - NotAllowedException { - final ResponseEntity<IdentifierDto[]> response; - final String url = "/api/identifier?dbid=" + databaseId + "&qid=" + subsetId; - log.trace("mapped url: {}", url); + public DatabaseAccessDto getAccess(Long databaseId, UUID userId) throws RemoteUnavailableException, + NotAllowedException, ServiceException { + final ResponseEntity<DatabaseAccessDto> response; try { - response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null), IdentifierDto[].class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to find identifiers for database with id {} and subset with id {}: {}", databaseId, subsetId, e.getMessage()); - throw new RemoteUnavailableException("Failed to find identifiers", e); + response = restTemplate.exchange("/api/database/" + databaseId + "/access/" + userId, HttpMethod.GET, HttpEntity.EMPTY, DatabaseAccessDto.class); + } catch (ResourceAccessException | HttpServerErrorException e) { + log.error("Failed to find database access for user with id {}: {}", userId, e.getMessage()); + throw new RemoteUnavailableException("Failed to find database access: " + e.getMessage(), e); + } catch (HttpClientErrorException.Forbidden | HttpClientErrorException.NotFound e) { + log.error("Failed to find database access for user with id {}: foreign user: {}", userId, e.getMessage()); + throw new NotAllowedException("Failed to find database access: foreign user: " + e.getMessage(), e); + } + if (!response.getStatusCode().equals(HttpStatus.OK)) { + log.error("Failed to find database access for user with id {}: service responded unsuccessful: {}", userId, response.getStatusCode()); + throw new ServiceException("Failed to find database access: service responded unsuccessful: " + response.getStatusCode()); } if (response.getBody() == null) { - log.error("Failed to find identifiers: body is null"); - throw new NotAllowedException("Failed to find identifiers: body is null"); + log.error("Failed to find database access: body is empty"); + throw new ServiceException("Failed to find database access: body is empty"); } - return List.of(response.getBody()); + return response.getBody(); } @Override - public List<IdentifierDto> getIdentifiers(Long databaseId) throws RemoteUnavailableException, - NotAllowedException { + public List<IdentifierDto> getIdentifiers(@NotNull Long databaseId, Long subsetId) throws ServiceException, + RemoteUnavailableException, DatabaseNotFoundException { final ResponseEntity<IdentifierDto[]> response; - final String url = "/api/identifier?dbid=" + databaseId; + final String url = "/api/identifier?dbid=" + databaseId + (subsetId != null ? ("&qid=" + subsetId) : ""); log.trace("mapped url: {}", url); try { - response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null), IdentifierDto[].class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to find identifiers for database with id {}: {}", databaseId, e.getMessage()); - throw new RemoteUnavailableException("Failed to find identifiers", e); + response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, IdentifierDto[].class); + } catch (ResourceAccessException | HttpServerErrorException e) { + log.error("Failed to find identifiers for database with id {} and subset with id {}: {}", databaseId, subsetId, e.getMessage()); + throw new RemoteUnavailableException("Failed to find identifiers: " + e.getMessage(), e); + } catch (HttpClientErrorException.NotFound e) { + log.error("Failed to find identifiers for database with id {} and subset with id {}: foreign user: {}", databaseId, subsetId, e.getMessage()); + throw new DatabaseNotFoundException("Failed to find identifiers: foreign user: " + e.getMessage(), e); + } + if (!response.getStatusCode().equals(HttpStatus.OK)) { + log.error("Failed to find identifiers for database with id {} and subset with id {}: service responded unsuccessful: {}", databaseId, subsetId, response.getStatusCode()); + throw new ServiceException("Failed to find identifiers for database: service responded unsuccessful: " + response.getStatusCode()); } if (response.getBody() == null) { log.error("Failed to find identifiers: body is null"); - throw new NotAllowedException("Failed to find identifiers: body is null"); + throw new ServiceException("Failed to find identifiers: body is null"); } return List.of(response.getBody()); } @Override - public UserDto getUser(UUID userId) throws RemoteUnavailableException, NotAllowedException, UserNotFoundException { - final ResponseEntity<UserDto> response; - final String url = "/api/user/" + userId; - log.trace("mapped url: {}", url); + public void updateTableStatistics(Long databaseId, Long tableId) throws TableNotFoundException, ServiceException, + RemoteUnavailableException { + final ResponseEntity<Void> response; + final String url = "/api/database/" + databaseId + "/table/" + tableId; try { - response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null), UserDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable e) { - log.error("Failed to find user with id {}: {}", userId, e.getMessage()); - throw new RemoteUnavailableException("Failed to find user", e); + response = restTemplate.exchange(url, HttpMethod.PUT, HttpEntity.EMPTY, Void.class); + } catch (ResourceAccessException | HttpServerErrorException e) { + log.error("Failed to update table statistic for table with id {}: {}", tableId, e.getMessage()); + throw new RemoteUnavailableException("Failed to update table statistic: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { - log.error("Failed to find user with id {}: not found: {}", userId, e.getMessage()); - throw new UserNotFoundException("Failed to find user: not found", e); + log.error("Failed to update table statistic for table with id {}: foreign user: {}", tableId, e.getMessage()); + throw new TableNotFoundException("Failed to update table statistic: foreign user: " + e.getMessage(), e); } - if (response.getBody() == null) { - log.error("Failed to find identifiers: body is null"); - throw new NotAllowedException("Failed to find identifiers: body is null"); + if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { + log.error("Failed to update table statistic for table with id {}: service responded unsuccessful: {}", tableId, response.getStatusCode()); + throw new ServiceException("Failed to update table statistic for database: service responded unsuccessful: " + response.getStatusCode()); } - return response.getBody(); } } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/listener/DefaultListener.java b/dbrepo-data-service/services/src/main/java/at/tuwien/listener/DefaultListener.java index c9e5bda270..89b18b3275 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/listener/DefaultListener.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/listener/DefaultListener.java @@ -66,7 +66,7 @@ public class DefaultListener implements MessageListener { log.error("Failed to read object: {}", e.getMessage()); } catch (SQLException | RemoteUnavailableException e) { log.error("Failed to insert tuple: {}", e.getMessage()); - } catch (TableNotFoundException e) { + } catch (TableNotFoundException | ServiceException e) { log.error("Failed to find table: {}", e.getMessage()); } } 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 e28d05929b..28204fbe2f 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 @@ -8,16 +8,18 @@ import at.tuwien.api.database.query.ImportCsvDto; import at.tuwien.api.database.query.QueryDto; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.*; -import at.tuwien.api.database.table.columns.ColumnBriefDto; -import at.tuwien.api.database.table.columns.ColumnCreateDto; -import at.tuwien.api.database.table.columns.ColumnDto; -import at.tuwien.api.database.table.columns.ColumnTypeDto; +import at.tuwien.api.database.table.columns.*; import at.tuwien.api.database.table.constraints.ConstraintsDto; +import at.tuwien.api.database.table.constraints.foreign.ForeignKeyBriefDto; +import at.tuwien.api.database.table.constraints.foreign.ForeignKeyDto; +import at.tuwien.api.database.table.constraints.foreign.ForeignKeyReferenceDto; +import at.tuwien.api.database.table.constraints.foreign.ReferenceTypeDto; import at.tuwien.api.database.table.constraints.primary.PrimaryKeyDto; import at.tuwien.api.database.table.constraints.unique.UniqueDto; import at.tuwien.api.database.table.internal.PrivilegedTableDto; import at.tuwien.config.QueryConfig; import at.tuwien.exception.*; +import at.tuwien.utils.MariaDbUtil; import com.github.dockerjava.zerodep.shaded.org.apache.commons.codec.binary.Hex; import com.google.common.hash.Hashing; import net.sf.jsqlparser.JSQLParserException; @@ -26,6 +28,8 @@ import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.select.*; import org.jetbrains.annotations.NotNull; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; import org.mapstruct.Named; import javax.swing.table.TableColumn; @@ -65,6 +69,45 @@ public interface MariaDbMapper { return slug.toLowerCase(Locale.ENGLISH); } + default String databaseSetPasswordQuery(String username, String password) { + final StringBuilder statement = new StringBuilder("ALTER USER `") + .append(username) + .append("`@`%` IDENTIFIED BY '") + .append(password) + .append("';"); + log.trace("mapped set password statement: {}", statement); + return statement.toString(); + } + + default String databaseCreateUserQuery(String username, String password) { + final StringBuilder statement = new StringBuilder("CREATE USER IF NOT EXISTS `") + .append(username) + .append("`@`%` IDENTIFIED BY PASSWORD '") + .append(password) + .append("';"); + log.trace("mapped create user statement: {}", statement); + return statement.toString(); + } + + default String databaseGrantPrivilegesQuery(String username, String grants) { + final StringBuilder statement = new StringBuilder("GRANT ") + .append(grants) + .append(" ON *.* TO `") + .append(username) + .append("`@`%`;"); + log.trace("mapped grant privileges statement: {}", statement); + return statement.toString(); + } + + @Named("createDatabase") + default String databaseCreateDatabaseQuery(String database) { + final StringBuilder statement = new StringBuilder("CREATE DATABASE `") + .append(database) + .append("`"); + log.trace("mapped create database statement: {}", statement); + return statement.toString(); + } + default QueryResultDto resultListToQueryResultDto(List<ColumnDto> columns, ResultSet result) throws SQLException { log.trace("mapping result list to query result, columns.size={}", columns.size()); final List<Map<String, Object>> resultList = new LinkedList<>(); @@ -109,13 +152,13 @@ public interface MariaDbMapper { } default String databaseTablesSelectRawQuery() { - final String statement = "SELECT DISTINCT t.`TABLE_NAME` FROM information_schema.TABLES t WHERE t.`TABLE_SCHEMA` = ? AND t.`TABLE_TYPE` = 'SYSTEM VERSIONED' AND t.`TABLE_NAME` != 'qs_queries'"; + final String statement = "SELECT DISTINCT t.`TABLE_NAME` FROM information_schema.TABLES t WHERE t.`TABLE_SCHEMA` = ? AND t.`TABLE_TYPE` = 'SYSTEM VERSIONED' AND t.`TABLE_NAME` != 'qs_queries' ORDER BY t.`TABLE_NAME` ASC"; log.trace("mapped select tables statement: {}", statement); return statement; } default String databaseTableSelectRawQuery() { - final String statement = "SELECT t.`TABLE_NAME`, t.`TABLE_TYPE`, t.`TABLE_ROWS`, t.`AVG_ROW_LENGTH`, t.`DATA_LENGTH`, t.`MAX_DATA_LENGTH`, COALESCE(t.`CREATE_TIME`, NOW()) as `CREATE_TIME`, t.`UPDATE_TIME`, v.`VIEW_DEFINITION` FROM information_schema.TABLES t LEFT JOIN information_schema.VIEWS v ON t.`TABLE_NAME` = v.`TABLE_NAME` WHERE t.`TABLE_SCHEMA` = ? AND t.`TABLE_TYPE` = 'SYSTEM VERSIONED' AND t.`TABLE_NAME` != 'qs_queries' AND t.`TABLE_NAME` = ?"; + final String statement = "SELECT t.`TABLE_NAME`, t.`TABLE_TYPE`, t.`TABLE_ROWS`, t.`AVG_ROW_LENGTH`, t.`DATA_LENGTH`, t.`MAX_DATA_LENGTH`, COALESCE(t.`CREATE_TIME`, NOW()) as `CREATE_TIME`, t.`UPDATE_TIME`, v.`VIEW_DEFINITION`, t.`TABLE_COMMENT` FROM information_schema.TABLES t LEFT JOIN information_schema.VIEWS v ON t.`TABLE_NAME` = v.`TABLE_NAME` WHERE t.`TABLE_SCHEMA` = ? AND t.`TABLE_TYPE` = 'SYSTEM VERSIONED' AND t.`TABLE_NAME` != 'qs_queries' AND t.`TABLE_NAME` = ?"; log.trace("mapped select table statement: {}", statement); return statement; } @@ -133,13 +176,13 @@ public interface MariaDbMapper { } default String databaseTableColumnsSelectRawQuery() { - final String statement = "SELECT `ORDINAL_POSITION`, `COLUMN_DEFAULT`, `IS_NULLABLE`, `DATA_TYPE`, `CHARACTER_MAXIMUM_LENGTH`, `NUMERIC_PRECISION`, `NUMERIC_SCALE`, `COLUMN_TYPE`, `COLUMN_KEY`, `COLUMN_NAME`, `COLUMN_COMMENT` FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?;"; + final String statement = "SELECT `ORDINAL_POSITION`, `COLUMN_DEFAULT`, `IS_NULLABLE`, `DATA_TYPE`, `CHARACTER_MAXIMUM_LENGTH`, `NUMERIC_PRECISION`, `NUMERIC_SCALE`, `COLUMN_TYPE`, `COLUMN_KEY`, `COLUMN_NAME`, IF(`COLUMN_COMMENT`='',NULL,`COLUMN_COMMENT`) AS `COLUMN_COMMENT` FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?;"; log.trace("mapped select columns statement: {}", statement); return statement; } default String databaseTableConstraintsSelectRawQuery() { - final String statement = "SELECT k.`ORDINAL_POSITION`, c.`CONSTRAINT_TYPE`, k.`CONSTRAINT_NAME`, k.`COLUMN_NAME` FROM information_schema.TABLE_CONSTRAINTS c JOIN information_schema.KEY_COLUMN_USAGE k ON c.`TABLE_NAME` = k.`TABLE_NAME` AND c.`CONSTRAINT_NAME` = k.`CONSTRAINT_NAME`WHERE c.`CONSTRAINT_TYPE` = 'UNIQUE' AND LOWER(k.`COLUMN_NAME`) != 'row_end' AND c.`TABLE_SCHEMA` = ? AND c.`TABLE_NAME` = ? ORDER BY k.`ORDINAL_POSITION` ASC;"; + final String statement = "SELECT k.`ORDINAL_POSITION`, c.`CONSTRAINT_TYPE`, k.`CONSTRAINT_NAME`, k.`COLUMN_NAME`, k.`REFERENCED_TABLE_NAME`, k.`REFERENCED_COLUMN_NAME`, r.`DELETE_RULE`, r.`UPDATE_RULE` FROM information_schema.TABLE_CONSTRAINTS c JOIN information_schema.KEY_COLUMN_USAGE k ON c.`TABLE_NAME` = k.`TABLE_NAME` AND c.`CONSTRAINT_NAME` = k.`CONSTRAINT_NAME` LEFT JOIN information_schema.REFERENTIAL_CONSTRAINTS r ON r.`CONSTRAINT_NAME` = k.`CONSTRAINT_NAME` WHERE LOWER(k.`COLUMN_NAME`) != 'row_end' AND c.`TABLE_SCHEMA` = ? AND c.`TABLE_NAME` = ? ORDER BY k.`ORDINAL_POSITION` ASC;"; log.trace("mapped select table constraints statement: {}", statement); return statement; } @@ -157,7 +200,9 @@ public interface MariaDbMapper { } default String tableCreateDtoToCreateSequenceRawQuery(at.tuwien.api.database.table.internal.TableCreateDto data) { - return "CREATE SEQUENCE IF NOT EXISTS `" + tableCreateDtoToSequenceName(data) + "` NOCACHE"; + final String statement = "CREATE SEQUENCE IF NOT EXISTS `" + tableCreateDtoToSequenceName(data) + "` NOCACHE"; + log.trace("mapped create sequence statement: {}", statement); + return statement; } default String filterToGetQueriesRawQuery(Boolean filterPersisted) { @@ -212,6 +257,31 @@ public interface MariaDbMapper { return ""; } + default String tableColumnStatisticsSelectRawQuery(List<ColumnDto> data, String table) { + final StringBuilder statement = new StringBuilder(); + final int[] idx = new int[]{0}; + data.stream() + .filter(column -> MariaDbUtil.numericDataTypes.contains(column.getColumnType())) + .forEach(column -> statement.append(idx[0]++ > 0 ? " UNION " : "") + .append("SELECT '") + .append(column.getInternalName()) + .append("' as name, MIN(`") + .append(column.getInternalName()) + .append("`) as min, MAX(`") + .append(column.getInternalName()) + .append("`) as max, MEDIAN(`") + .append(column.getInternalName()) + .append("`) OVER () as median, AVG(`") + .append(column.getInternalName()) + .append("`) as mean, STDDEV(`") + .append(column.getInternalName()) + .append("`) as std_dev FROM ") + .append(table)); + statement.append(";"); + log.trace("mapped select column statistic statement: {}", statement); + return statement.toString(); + } + default String tableCreateDtoToCreateTableRawQuery(at.tuwien.api.database.table.internal.TableCreateDto data) { final StringBuilder stringBuilder = new StringBuilder("CREATE TABLE `") .append(nameToInternalName(data.getName())) @@ -238,22 +308,25 @@ public interface MariaDbMapper { } /* create primary key index */ - stringBuilder.append(", PRIMARY KEY (") - .append(String.join(",", data.getConstraints() - .getPrimaryKey() - .stream() - .map(c -> { - final Optional<ColumnCreateDto> optional = data.getColumns() - .stream() - .filter(cc -> cc.getName().equals(c)) - .findFirst(); - log.trace("lookup {} in columns: {}", c, data.getColumns().stream().map(ColumnCreateDto::getName).toList()); - return "`" + nameToInternalName(c) + "`" + columnCreateDtoToPrimaryKeyLengthSpecification(optional.get()); - }) - .toArray(String[]::new))) - .append(")"); if (data.getConstraints() != null) { log.trace("constraints are {}", data.getConstraints()); + if (data.getConstraints().getPrimaryKey() != null && !data.getConstraints().getPrimaryKey().isEmpty()) { + /* create primary key index */ + stringBuilder.append(", PRIMARY KEY (") + .append(String.join(",", data.getConstraints() + .getPrimaryKey() + .stream() + .map(c -> { + final Optional<ColumnCreateDto> optional = data.getColumns() + .stream() + .filter(cc -> cc.getName().equals(c)) + .findFirst(); + log.trace("lookup {} in columns: {}", c, data.getColumns().stream().map(ColumnCreateDto::getName).toList()); + return "`" + nameToInternalName(c) + "`" + columnCreateDtoToPrimaryKeyLengthSpecification(optional.get()); + }) + .toArray(String[]::new))) + .append(")"); + } if (data.getConstraints().getUniques() != null) { /* create unique indices */ data.getConstraints().getUniques() @@ -289,15 +362,16 @@ public interface MariaDbMapper { .append(ck) .append(")")); } - if (data.getDescription() != null && !data.getDescription().isBlank()) { - /* create table comments */ - stringBuilder.append(" COMMENT \"") - .append(data.getDescription()) - .append("\""); - } } - stringBuilder.append(") WITH SYSTEM VERSIONING;"); - log.trace("mapped create table query: {}", stringBuilder); + stringBuilder.append(") WITH SYSTEM VERSIONING"); + if (data.getDescription() != null && !data.getDescription().isBlank()) { + /* create table comments */ + stringBuilder.append(" COMMENT \"") + .append(data.getDescription()) + .append("\""); + } + stringBuilder.append(";"); + log.trace("mapped create table statement: {}", stringBuilder); return stringBuilder.toString(); } @@ -331,6 +405,23 @@ public interface MariaDbMapper { return data.getLong(1); } + default TableStatisticDto resultSetToTableStatistic(ResultSet data) throws SQLException { + final TableStatisticDto statistic = TableStatisticDto.builder() + .columns(new LinkedHashMap<>()) + .build(); + while (data.next()) { + final ColumnStatisticDto columnStatistic = ColumnStatisticDto.builder() + .min(data.getBigDecimal(2)) + .max(data.getBigDecimal(3)) + .median(data.getBigDecimal(4)) + .mean(data.getBigDecimal(5)) + .stdDev(data.getBigDecimal(6)) + .build(); + statistic.getColumns().put(data.getString(1), columnStatistic); + } + return statistic; + } + /** * Selects the dataset page from a table/view. * @@ -388,8 +479,9 @@ public interface MariaDbMapper { return statement.toString(); } + @Named("dropTableQuery") default String dropTableRawQuery(String tableName) { - return "DROP TABLE IF EXISTS `" + tableName + "`;"; + return "DROP TABLE `" + tableName + "`;"; } default String tupleToRawInsertQuery(PrivilegedTableDto table, TupleDto data) throws TableMalformedException { @@ -401,18 +493,17 @@ public interface MariaDbMapper { final StringBuilder statement = new StringBuilder("INSERT INTO `") .append(table.getInternalName()) .append("` (") - .append(table.getColumns() + .append(data.getData() + .keySet() .stream() - .filter(column -> !column.getAutoGenerated()) - .map(column -> "`" + column.getInternalName() + "`") + .map(o -> "`" + o + "`") .collect(Collectors.joining(","))) - .append(") VALUES ("); - final int[] idx = new int[]{1, 0}; - table.getColumns() - .stream() - .filter(c -> !c.getAutoGenerated()) - .forEach(c -> statement.append(idx[1]++ > 0 ? "," : "") - .append("?")); + .append(") VALUES (") + .append(data.getData() + .keySet() + .stream() + .map(o -> "?") + .collect(Collectors.joining(","))); statement.append(");"); for (int i = 0; i < table.getColumns().size(); i++) { final ColumnDto column = table.getColumns() @@ -532,6 +623,7 @@ public interface MariaDbMapper { .tdbid(database.getId()) .queueName("dbrepo") .routingKey("dbrepo") + .description(resultSet.getString(10)) .columns(new LinkedList<>()) .identifiers(new LinkedList<>()) .creator(database.getOwner()) @@ -552,10 +644,16 @@ public interface MariaDbMapper { return table; } + ForeignKeyBriefDto foreignKeyDtoToForeignKeyBriefDto(ForeignKeyDto data); + default TableDto resultSetToConstraint(ResultSet resultSet, TableDto table) throws SQLException { final String type = resultSet.getString(2); final String name = resultSet.getString(3); final String columnName = resultSet.getString(4); + final String referencedTable = resultSet.getString(5); + final String referencedColumnName = resultSet.getString(6); + final ReferenceTypeDto deleteRule = resultSet.getString(7) != null ? ReferenceTypeDto.fromType(resultSet.getString(7)) : null; + final ReferenceTypeDto updateRule = resultSet.getString(8) != null ? ReferenceTypeDto.fromType(resultSet.getString(8)) : null; final Optional<ColumnDto> optional = table.getColumns().stream() .filter(c -> c.getInternalName().equals(columnName)) .findFirst(); @@ -564,7 +662,7 @@ public interface MariaDbMapper { throw new IllegalArgumentException("Failed to find table column"); } final ColumnDto column = optional.get(); - if (type.equals("UNIQUE")) { + if (type.equals("FOREIGN KEY") || type.equals("UNIQUE")) { final Optional<UniqueDto> optional2 = table.getConstraints().getUniques().stream().filter(u -> u.getName().equals(name)).findFirst(); if (optional2.isPresent()) { optional2.get() @@ -572,17 +670,66 @@ public interface MariaDbMapper { .add(column); return table; } + if (type.equals("UNIQUE")) { + table.getConstraints() + .getUniques() + .add(UniqueDto.builder() + .name(name) + .columns(new LinkedList<>(List.of(column))) + .build()); + return table; + } + final Optional<ForeignKeyDto> optional1 = table.getConstraints() + .getForeignKeys() + .stream() + .filter(fk -> fk.getName().equals(name)) + .findFirst(); + final ForeignKeyReferenceDto foreignKeyReference = ForeignKeyReferenceDto.builder() + .column(ColumnBriefDto.builder() + .name(columnName) + .internalName(columnName) + .databaseId(table.getTdbid()) + .build()) + .referencedColumn(ColumnBriefDto.builder() + .name(referencedColumnName) + .internalName(referencedColumnName) + .databaseId(table.getTdbid()) + .build()) + .build(); + if (optional1.isPresent()) { + foreignKeyReference.setForeignKey(foreignKeyDtoToForeignKeyBriefDto(optional1.get())); + optional1.get() + .getReferences() + .add(foreignKeyReference); + log.debug("found foreign key: create part ({}) referencing table {} ({})", columnName, referencedTable, referencedColumnName); + return table; + } + final ForeignKeyDto foreignKey = ForeignKeyDto.builder() + .name(name) + .table(tableDtoToTableBriefDto(table)) + .referencedTable(TableBriefDto.builder() + .name(referencedTable) + .internalName(referencedTable) + .databaseId(table.getTdbid()) + .build()) + .references(new LinkedList<>(List.of(foreignKeyReference))) + .onDelete(deleteRule) + .onUpdate(updateRule) + .build(); + foreignKey.getReferences() + .forEach(ref -> ref.setForeignKey(foreignKeyDtoToForeignKeyBriefDto(foreignKey))); table.getConstraints() - .getUniques() - .add(UniqueDto.builder() - .name(name) - .columns(new LinkedList<>(List.of(column))) - .build()); + .getForeignKeys() + .add(foreignKey); + log.debug("create foreign key: add part ({}) referencing table {} ({})", columnName, referencedTable, referencedColumnName); return table; } return table; } + @Mappings({ + @Mapping(target = "databaseId", source = "tdbid") + }) TableBriefDto tableDtoToTableBriefDto(TableDto data); ColumnBriefDto columnDtoToColumnBriefDto(ColumnDto data); @@ -668,6 +815,7 @@ public interface MariaDbMapper { } view.getColumns() .add(column); + log.trace("parsed view {}.{} column: {}", view.getDatabase().getInternalName(), view.getInternalName(), column.getInternalName()); return view; } @@ -720,15 +868,12 @@ public interface MariaDbMapper { statement.append("@") .append(column.getInternalName()); if (column.getDateFormat() != null) { - log.trace("import column has date format, need to format it differently"); /* reformat dates */ columnToDateSet(data, column, set); } else if (column.getColumnType().equals(ColumnTypeDto.BOOL)) { - log.trace("import column has boolean format, need to format it differently"); /* reformat booleans */ columnToBoolSet(data, column, set); } else { - log.trace("import column has text format"); /* reformat others */ columnToTextSet(data, column, set); } @@ -737,6 +882,7 @@ public interface MariaDbMapper { statement.append(")") .append(set.length() != 0 ? (" SET " + set) : "") .append(";"); + log.trace("mapped insert statement: {}", statement); return statement.toString(); } @@ -821,7 +967,7 @@ public interface MariaDbMapper { log.error("Failed to find table column {}", key); throw new IllegalArgumentException("Failed to find table column"); } - if (optional.get().getAutoGenerated() || value == null) { + if (optional.get().getAutoGenerated()) { return; } statement.append(idx[0]++ == 0 ? "" : ", ") @@ -840,7 +986,7 @@ public interface MariaDbMapper { log.error("Failed to find table column {}", key); throw new IllegalArgumentException("Failed to find table column"); } - if (optional.get().getAutoGenerated() || value == null) { + if (optional.get().getAutoGenerated()) { return; } statement.append(jdx[0]++ == 0 ? "" : ", ") @@ -852,13 +998,12 @@ public interface MariaDbMapper { } default void columnToDateSet(ImportCsvDto data, ColumnDto column, StringBuilder set) { - log.trace("mapping column to date set"); + log.trace("import column has date format, need to format it: {}", column.getDateFormat().getUnixFormat()); set.append(!set.isEmpty() ? ", " : "") .append("`") .append(column.getInternalName()) .append("` = STR_TO_DATE("); if (data.getNullElement() != null) { - log.trace("import has null element present"); set.append("IF(STRCMP(@") .append(column.getInternalName()) .append(",'") @@ -882,13 +1027,11 @@ public interface MariaDbMapper { } default void columnToBoolSet(ImportCsvDto data, ColumnDto column, StringBuilder set) { - log.trace("mapping column to bool set, data={}, column={}, set=(generated)", data, column); set.append(!set.isEmpty() ? ", " : "") .append("`") .append(column.getInternalName()) .append("` = "); if (data.getNullElement() != null) { - log.trace("import has null element present"); set.append("IF(!STRCMP(@") .append(column.getInternalName()) .append(",'") @@ -902,9 +1045,7 @@ public interface MariaDbMapper { } default void columnToBoolSet2(ImportCsvDto data, ColumnDto column, StringBuilder set) { - log.trace("mapping column to inner bool set, data={}, column={}, set=(generated)", data, column); if (data.getTrueElement() != null) { - log.trace("import has true element present"); set.append("IF(!STRCMP(@") .append(column.getInternalName()) .append(",'") @@ -929,7 +1070,6 @@ public interface MariaDbMapper { return; } if (data.getFalseElement() != null) { - log.trace("import has false element present"); set.append("IF(!STRCMP(@") .append(column.getInternalName()) .append(",'") @@ -958,13 +1098,11 @@ public interface MariaDbMapper { } default void columnToTextSet(ImportCsvDto data, ColumnDto column, StringBuilder set) { - log.trace("mapping column to text set"); set.append(!set.isEmpty() ? ", " : "") .append("`") .append(column.getInternalName()) .append("` = "); if (data.getNullElement() != null) { - log.trace("import has null element present"); set.append("IF(STRCMP(@") .append(column.getInternalName()) .append(",'") @@ -979,11 +1117,11 @@ public interface MariaDbMapper { } default void prepareStatementWithColumnTypeObject(PreparedStatement statement, ColumnTypeDto columnType, int idx, - Object value) throws SQLException { + String columnName, Object value) throws SQLException { switch (columnType) { case BLOB, TINYBLOB, MEDIUMBLOB, LONGBLOB: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.BLOB); break; } @@ -992,26 +1130,26 @@ public interface MariaDbMapper { try (ObjectOutputStream ois = new ObjectOutputStream(boas)) { ois.writeObject(value); statement.setBlob(idx, new ByteArrayInputStream(boas.toByteArray())); - log.trace("prepare statement idx {} blob", idx); + log.trace("prepare statement idx {} = {} blob", idx, columnName); } } catch (IOException e) { - log.error("Failed to set blob: {}", e.getMessage()); + log.error("Failed to set blob/tinyblob/mediumblob/longblob: {}", e.getMessage()); throw new SQLException("Failed to set blob: " + e.getMessage(), e); } break; case TEXT, CHAR, VARCHAR, TINYTEXT, MEDIUMTEXT, LONGTEXT, ENUM, SET: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.VARCHAR); break; } - log.trace("prepare statement idx {} string: {}", idx, value); + log.trace("prepare statement idx {} = {} text/char/varchar/tinytext/mediumtext/longtext/enum/set: {}", idx, columnName, value); statement.setString(idx, String.valueOf(value)); break; case DATE: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.DATE); break; } @@ -1020,113 +1158,114 @@ public interface MariaDbMapper { break; case BIGINT: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.BIGINT); break; } - log.trace("prepare statement idx {} long: {}", idx, value); + log.trace("prepare statement idx {} bigint: {}", idx, value); statement.setLong(idx, Long.parseLong(String.valueOf(value))); break; case INT, MEDIUMINT: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.INTEGER); break; } - log.trace("prepare statement idx {} long: {}", idx, value); + log.trace("prepare statement idx {} = {} int/mediumint: {}", idx, columnName, value); statement.setLong(idx, Long.parseLong(String.valueOf(value))); break; case TINYINT: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.TINYINT); break; } - log.trace("prepare statement idx {} long: {}", idx, value); + log.trace("prepare statement idx {} = {} tinyint: {}", idx, columnName, value); statement.setLong(idx, Long.parseLong(String.valueOf(value))); break; case SMALLINT: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.SMALLINT); break; } - log.trace("prepare statement idx {} long: {}", idx, value); + log.trace("prepare statement idx {} = {} smallint: {}", idx, columnName, value); statement.setLong(idx, Long.parseLong(String.valueOf(value))); break; case DECIMAL: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.DECIMAL); break; } + log.trace("prepare statement idx {} = {} decimal: {}", idx, columnName, value); statement.setDouble(idx, Double.parseDouble(String.valueOf(value))); break; case FLOAT: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.FLOAT); break; } - log.trace("prepare statement idx {} double: {}", idx, value); + log.trace("prepare statement idx {} = {} float: {}", idx, columnName, value); statement.setDouble(idx, Double.parseDouble(String.valueOf(value))); break; case DOUBLE: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.DOUBLE); break; } - log.trace("prepare statement idx {} double: {}", idx, value); + log.trace("prepare statement idx {} = {} double: {}", idx, columnName, value); statement.setDouble(idx, Double.parseDouble(String.valueOf(value))); break; case BINARY, VARBINARY, BIT: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.DECIMAL); break; } + log.trace("prepare statement idx {} = {} binary/varbinary/bit", idx, columnName); statement.setBinaryStream(idx, (InputStream) value); - log.trace("prepare statement idx {} binary stream", idx); break; case BOOL: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.BOOLEAN); break; } - log.trace("prepare statement idx {} bool: {}", idx, value); + log.trace("prepare statement idx {} = {} bool: {}", idx, columnName, value); statement.setBoolean(idx, Boolean.parseBoolean(String.valueOf(value))); break; case TIMESTAMP, DATETIME: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.TIMESTAMP); break; } + log.trace("prepare statement idx {} timestamp/datetime: {}", idx, value); statement.setTimestamp(idx, Timestamp.valueOf(String.valueOf(value))); - log.trace("prepare statement idx {} timestamp: {}", idx, value); break; case TIME: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.TIME); break; } + log.trace("prepare statement idx {} = {} time: {}", idx, columnName, value); statement.setTime(idx, Time.valueOf(String.valueOf(value))); - log.trace("prepare statement idx {} time: {}", idx, value); break; case YEAR: if (value == null) { - log.trace("idx {} is null, prepare with null value", idx); + log.trace("idx {} = {} is null, prepare with null value", idx, columnName); statement.setNull(idx, Types.TIME); break; } - log.trace("prepare statement idx {} string: {}", idx, value); + log.trace("prepare statement idx {} = {} year: {}", idx, columnName, value); statement.setString(idx, String.valueOf(value)); break; default: - log.error("Failed to map column type {} at index {} for value {}", columnType, idx, value); + log.error("Failed to map column type {} at idx {} = {} for value {}", columnType, idx, columnName, value); throw new IllegalArgumentException("Failed to map column type " + columnType); } } @@ -1313,7 +1452,7 @@ public interface MariaDbMapper { } default boolean columnMatches(ColumnDto column, String tableOrView) { - if (column.getTable().getInternalName().equals(tableOrView)) { + if (column.getTable() != null && column.getTable().getInternalName().equals(tableOrView)) { log.trace("table '{}' found in column table", tableOrView); return true; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MetadataMapper.java b/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MetadataMapper.java index 67633ba175..4cde78c7d9 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MetadataMapper.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MetadataMapper.java @@ -12,6 +12,8 @@ import at.tuwien.api.database.table.TableBriefDto; import at.tuwien.api.database.table.TableDto; import at.tuwien.api.database.table.columns.ColumnDto; import at.tuwien.api.database.table.internal.PrivilegedTableDto; +import at.tuwien.api.user.PrivilegedUserDto; +import at.tuwien.api.user.UserDto; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; @@ -43,4 +45,6 @@ public interface MetadataMapper { ContainerDto privilegedContainerDtoToContainerDto(PrivilegedContainerDto data); + PrivilegedUserDto userDtoToPrivilegedUserDto(UserDto data); + } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/AccessService.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/AccessService.java index ac86984f39..a6d57dc8b6 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/AccessService.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/AccessService.java @@ -3,17 +3,42 @@ package at.tuwien.service; import at.tuwien.api.database.AccessTypeDto; import at.tuwien.api.database.internal.PrivilegedDatabaseDto; import at.tuwien.api.user.PrivilegedUserDto; +import at.tuwien.api.user.UserDto; import at.tuwien.exception.*; import java.sql.SQLException; public interface AccessService { + + /** + * Create a user with access to a given database. + * @param database The database. + * @param user The user. + * @param access The access type. + * @throws SQLException The connection to the database could not be established. + * @throws DatabaseMalformedException The database schema is malformed. + */ void create(PrivilegedDatabaseDto database, PrivilegedUserDto user, AccessTypeDto access) throws SQLException, DatabaseMalformedException; - void update(PrivilegedDatabaseDto database, PrivilegedUserDto user, AccessTypeDto access) throws SQLException, + /** + * Update access to a given database for a given user. + * @param database The database. + * @param user The user. + * @param access The access type. + * @throws SQLException The connection to the database could not be established. + * @throws DatabaseMalformedException The database schema is malformed. + */ + void update(PrivilegedDatabaseDto database, UserDto user, AccessTypeDto access) throws SQLException, DatabaseMalformedException; - void delete(PrivilegedDatabaseDto database, PrivilegedUserDto user) throws SQLException, + /** + * Revoke access to a given database for a given user. + * @param database The database. + * @param user The user. + * @throws SQLException The connection to the database could not be established. + * @throws DatabaseMalformedException The database schema is malformed. + */ + void delete(PrivilegedDatabaseDto database, UserDto user) throws SQLException, DatabaseMalformedException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/AnalyseService.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/AnalyseService.java deleted file mode 100644 index eb1c047b05..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/AnalyseService.java +++ /dev/null @@ -1,11 +0,0 @@ -package at.tuwien.service; - -import at.tuwien.api.database.table.TableStatisticDto; -import at.tuwien.exception.NotAllowedException; -import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.TableNotFoundException; - -public interface AnalyseService { - TableStatisticDto analyseTable(Long databaseId, Long tableId) throws TableNotFoundException, - NotAllowedException, RemoteUnavailableException; -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/DatabaseService.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/DatabaseService.java index 6c99910e67..271b2abb82 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/DatabaseService.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/DatabaseService.java @@ -11,9 +11,24 @@ import java.sql.SQLException; public interface DatabaseService { + /** + * Creates a database in the given container. + * @param container The container. + * @param data The database metadata. + * @return The created database, if successful. + * @throws SQLException The connection to the database could not be established. + * @throws DatabaseMalformedException The database schema is malformed. + */ PrivilegedDatabaseDto create(PrivilegedContainerDto container, CreateDatabaseDto data) throws SQLException, DatabaseMalformedException; + /** + * Updates a user's password in a given database. + * @param database The database. + * @param data The user-password tuple. + * @throws SQLException The connection to the database could not be established. + * @throws DatabaseMalformedException The database schema is malformed. + */ void update(PrivilegedDatabaseDto database, UpdateUserPasswordDto data) throws SQLException, DatabaseMalformedException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/SubsetService.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/SubsetService.java index 9c9bc25a71..7c2575b9cc 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/SubsetService.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/SubsetService.java @@ -28,7 +28,7 @@ public interface SubsetService { QueryResultDto execute(PrivilegedDatabaseDto database, String statement, Instant timestamp, UUID userId, Long page, Long size, SortTypeDto sortDirection, String sortColumn) - throws QueryStoreInsertException, SQLException, QueryNotFoundException, TableMalformedException, UserNotFoundException, NotAllowedException, RemoteUnavailableException; + throws QueryStoreInsertException, SQLException, QueryNotFoundException, TableMalformedException, UserNotFoundException, NotAllowedException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException; QueryResultDto reExecute(PrivilegedDatabaseDto database, QueryDto query, Long page, Long size, SortTypeDto sortDirection, String sortColumn) throws TableMalformedException, @@ -45,11 +45,11 @@ public interface SubsetService { * @return The list of queries. */ List<QueryDto> findAll(PrivilegedDatabaseDto database, Boolean filterPersisted) throws SQLException, - QueryNotFoundException, NotAllowedException, RemoteUnavailableException; + QueryNotFoundException, NotAllowedException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException; ExportResourceDto export(PrivilegedDatabaseDto database, QueryDto query, Instant timestamp, String filename) throws SQLException, QueryMalformedException, SidecarExportException, StorageNotFoundException, - StorageUnavailableException; + StorageUnavailableException, ServiceException, RemoteUnavailableException; Long executeCountNonPersistent(PrivilegedDatabaseDto database, String statement, Instant timestamp) throws SQLException, QueryMalformedException, TableMalformedException; @@ -62,7 +62,7 @@ public interface SubsetService { * @return The query. * @throws QueryNotFoundException The query store did not return a query */ - QueryDto findById(PrivilegedDatabaseDto database, Long queryId) throws QueryNotFoundException, SQLException, NotAllowedException, RemoteUnavailableException, UserNotFoundException; + QueryDto findById(PrivilegedDatabaseDto database, Long queryId) throws QueryNotFoundException, SQLException, NotAllowedException, RemoteUnavailableException, UserNotFoundException, ServiceException, DatabaseNotFoundException; /** * Inserts a query and metadata to the query store of a given database id. diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/TableService.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/TableService.java index b8d2f39390..fb045b4a19 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/TableService.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/TableService.java @@ -15,40 +15,110 @@ import java.util.List; public interface TableService { - List<TableDto> getSchemas(PrivilegedDatabaseDto database) throws SQLException, TableNotFoundException, QueryMalformedException, DatabaseMalformedException; + /** + * Get table schemas from the information_schema in the data database. + * @param database The data database privileged object. + * @return List of tables, if successful. + * @throws SQLException Failed to parse SQL query, contains invalid syntax. + * @throws TableNotFoundException The table could not be inspected in the data database. + * @throws QueryMalformedException The inspection query is malformed. + * @throws DatabaseMalformedException The database inspection was unsuccessful, likely due to a bug in the mapping. + */ + List<TableDto> getSchemas(PrivilegedDatabaseDto database) throws SQLException, TableNotFoundException, + QueryMalformedException, DatabaseMalformedException; + + /** + * Generate table statistic for a given table. Only numerical columns are calculated. + * @param table The table. + * @return The table statistic, if successful. + * @throws SQLException Failed to parse SQL query, contains invalid syntax. + * @throws TableMalformedException The table statistic generation was unsuccessful, likely due to a bug in the mapping. + * @throws QueryMalformedException The inspection query is malformed. + */ + TableStatisticDto getStatistics(PrivilegedTableDto table) throws SQLException, TableMalformedException, + QueryMalformedException; - TableDto find(PrivilegedDatabaseDto database, String tableName) throws TableNotFoundException, SQLException, QueryMalformedException; + /** + * Finds a table with given data database and table name. + * @param database The data database. + * @param tableName The table name. + * @return The table, if successful. + * @throws TableNotFoundException The table could not be inspected in the data database. + * @throws SQLException Failed to parse SQL query, contains invalid syntax. + * @throws QueryMalformedException The inspection query is malformed. + */ + TableDto find(PrivilegedDatabaseDto database, String tableName) throws TableNotFoundException, SQLException, + QueryMalformedException; + /** + * Creates a table in given data database with table definition. + * @param database The data database privileged object. + * @param data The table definition. + * @return The created table, if successful. + * @throws SQLException Failed to parse SQL query, contains invalid syntax. + * @throws TableNotFoundException The table could not be inspected in the data database. + * @throws TableExistsException The table name already exists in the information_schema. + * @throws TableNotFoundException The table could not be inspected in the data database. + * @throws QueryMalformedException The create/inspection query is malformed. + */ TableDto createTable(PrivilegedDatabaseDto database, TableCreateDto data) throws SQLException, TableMalformedException, TableExistsException, TableNotFoundException, QueryMalformedException; + /** + * Drops a table in given table object. + * @param table The table object. + * @throws SQLException Failed to parse SQL query, contains invalid syntax. + * @throws QueryMalformedException The drop table query is malformed. + */ void delete(PrivilegedTableDto table) throws SQLException, QueryMalformedException; - QueryResultDto getData(PrivilegedTableDto table, Instant timestamp, Long page, - Long size) throws SQLException, TableMalformedException; - - List<TableHistoryDto> history(PrivilegedTableDto table) throws SQLException, - TableNotFoundException; - + /** + * Obtains data from a table with given table object at timestamp, loaded as page number and length size. + * @param table The table object. + * @param timestamp The timestamp. + * @param page The page number. + * @param size The page size/length. + * @return The data. + * @throws SQLException Failed to parse SQL query, contains invalid syntax. + * @throws TableMalformedException The table schema is malformed, likely due to a bug in the application. + */ + QueryResultDto getData(PrivilegedTableDto table, Instant timestamp, Long page, Long size) throws SQLException, + TableMalformedException; + + /** + * Obtains the table history for a given table object. + * @param table The table object. + * @param size The maximum size. + * @return The table history. + * @throws SQLException Failed to parse SQL query, contains invalid syntax. + * @throws TableNotFoundException The table could not be found in the data database. + */ + List<TableHistoryDto> history(PrivilegedTableDto table, Long size) throws SQLException, TableNotFoundException; + + /** + * Obtains the table data tuples count at time. + * @param table The table object. + * @param timestamp The timestamp. + * @return Number of tuples, if successful. + * @throws SQLException Failed to parse SQL query, contains invalid syntax. + * @throws QueryMalformedException The count query is malformed, likely due to a bug in the application. + */ Long getCount(PrivilegedTableDto table, Instant timestamp) throws SQLException, QueryMalformedException; - void importTuple(PrivilegedTableDto table, TupleDto data) - throws TableMalformedException, StorageUnavailableException, StorageNotFoundException, SQLException, QueryMalformedException; - - void importDataset(PrivilegedTableDto table, ImportCsvDto data) - throws SidecarImportException, StorageNotFoundException, SQLException, QueryMalformedException; + void importDataset(PrivilegedTableDto table, ImportCsvDto data) throws SidecarImportException, + StorageNotFoundException, SQLException, QueryMalformedException, ServiceException, RemoteUnavailableException; void deleteTuple(PrivilegedTableDto table, TupleDeleteDto data) throws SQLException, TableMalformedException, QueryMalformedException; void createTuple(PrivilegedTableDto table, TupleDto data) throws SQLException, - QueryMalformedException, TableMalformedException; + QueryMalformedException, TableMalformedException, StorageUnavailableException, StorageNotFoundException; void updateTuple(PrivilegedTableDto table, TupleUpdateDto data) throws SQLException, QueryMalformedException, TableMalformedException; ExportResourceDto exportDataset(PrivilegedTableDto table, Instant timestamp) throws SQLException, SidecarExportException, StorageNotFoundException, StorageUnavailableException, - QueryMalformedException; + QueryMalformedException, ServiceException, RemoteUnavailableException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/ViewService.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/ViewService.java index 4dcd96ed53..3455c320cd 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/ViewService.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/ViewService.java @@ -58,5 +58,5 @@ public interface ViewService { ExportResourceDto exportDataset(PrivilegedDatabaseDto database, ViewDto view, Instant timestamp) throws SQLException, QueryMalformedException, SidecarExportException, StorageNotFoundException, - StorageUnavailableException; + StorageUnavailableException, ServiceException, RemoteUnavailableException; } diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/AccessServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/AccessServiceMariaDbImpl.java index 96ded2b074..8c52e02010 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/AccessServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/AccessServiceMariaDbImpl.java @@ -3,10 +3,13 @@ package at.tuwien.service.impl; import at.tuwien.api.database.AccessTypeDto; import at.tuwien.api.database.internal.PrivilegedDatabaseDto; import at.tuwien.api.user.PrivilegedUserDto; +import at.tuwien.api.user.UserDto; import at.tuwien.exception.*; +import at.tuwien.mapper.MariaDbMapper; import at.tuwien.service.AccessService; import com.mchange.v2.c3p0.ComboPooledDataSource; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -23,6 +26,13 @@ public class AccessServiceMariaDbImpl extends HibernateConnector implements Acce @Value("${dbrepo.grant.default.write}") private String grantDefaultWrite; + private MariaDbMapper mariaDbMapper; + + @Autowired + public AccessServiceMariaDbImpl(MariaDbMapper mariaDbMapper) { + this.mariaDbMapper = mariaDbMapper; + } + @Override public void create(PrivilegedDatabaseDto database, PrivilegedUserDto user, AccessTypeDto access) throws SQLException, DatabaseMalformedException { @@ -30,11 +40,11 @@ public class AccessServiceMariaDbImpl extends HibernateConnector implements Acce final Connection connection = dataSource.getConnection(); try { /* create user if not exists */ - connection.prepareStatement("CREATE USER IF NOT EXISTS `" + user.getUsername() + "`@`%` IDENTIFIED BY PASSWORD '" + user.getPassword() + "';") + connection.prepareStatement(mariaDbMapper.databaseCreateUserQuery(user.getUsername(), user.getPassword())) .execute(); /* grant access */ final String grants = access != AccessTypeDto.READ ? grantDefaultWrite : grantDefaultRead; - connection.prepareStatement("GRANT " + grants + " ON *.* TO `" + user.getUsername() + "`@`%`;") + connection.prepareStatement(mariaDbMapper.databaseGrantPrivilegesQuery(user.getUsername(), grants)) .execute(); /* grant query store */ connection.prepareStatement("GRANT EXECUTE ON PROCEDURE `store_query` TO `" + user.getUsername() + "`@`%`;") @@ -54,15 +64,14 @@ public class AccessServiceMariaDbImpl extends HibernateConnector implements Acce } @Override - public void update(PrivilegedDatabaseDto database, PrivilegedUserDto user, AccessTypeDto access) + public void update(PrivilegedDatabaseDto database, UserDto user, AccessTypeDto access) throws DatabaseMalformedException, SQLException { final ComboPooledDataSource dataSource = getPrivilegedDataSource(database); final Connection connection = dataSource.getConnection(); try { /* grant access */ - connection.prepareStatement("GRANT SELECT" + - (access != AccessTypeDto.READ ? "CREATE, CREATE VIEW, CREATE ROUTINE, CREATE TEMPORARY TABLES, LOCK TABLES, INDEX, TRIGGER, INSERT, UPDATE, DELETE" : "") + - " ON *.* TO `" + user.getUsername() + "`@`%`;") + final String grants = access != AccessTypeDto.READ ? grantDefaultWrite : grantDefaultRead; + connection.prepareStatement(mariaDbMapper.databaseGrantPrivilegesQuery(user.getUsername(), grants)) .execute(); /* apply access rights */ connection.prepareStatement("FLUSH PRIVILEGES;"); @@ -78,7 +87,7 @@ public class AccessServiceMariaDbImpl extends HibernateConnector implements Acce } @Override - public void delete(PrivilegedDatabaseDto database, PrivilegedUserDto user) throws DatabaseMalformedException, + public void delete(PrivilegedDatabaseDto database, UserDto user) throws DatabaseMalformedException, SQLException { final ComboPooledDataSource dataSource = getPrivilegedDataSource(database); final Connection connection = dataSource.getConnection(); diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/AnalyseServiceImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/AnalyseServiceImpl.java deleted file mode 100644 index 7b722597c5..0000000000 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/AnalyseServiceImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package at.tuwien.service.impl; - -import at.tuwien.api.database.table.TableStatisticDto; -import at.tuwien.exception.NotAllowedException; -import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.TableNotFoundException; -import at.tuwien.gateway.AnalyseServiceGateway; -import at.tuwien.service.AnalyseService; -import lombok.extern.log4j.Log4j2; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Log4j2 -@Service -public class AnalyseServiceImpl implements AnalyseService { - - private final AnalyseServiceGateway analyseServiceGateway; - - @Autowired - public AnalyseServiceImpl(AnalyseServiceGateway analyseServiceGateway) { - this.analyseServiceGateway = analyseServiceGateway; - } - - @Override - public TableStatisticDto analyseTable(Long databaseId, Long tableId) throws TableNotFoundException, - NotAllowedException, RemoteUnavailableException { - return analyseServiceGateway.analyseTable(databaseId, tableId); - } - -} diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceMariaDbImpl.java index 7bd233371e..2d91dcb8f4 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceMariaDbImpl.java @@ -1,27 +1,20 @@ package at.tuwien.service.impl; import at.tuwien.api.container.internal.PrivilegedContainerDto; -import at.tuwien.api.database.DatabaseDto; -import at.tuwien.api.database.ViewDto; import at.tuwien.api.database.internal.CreateDatabaseDto; import at.tuwien.api.database.internal.PrivilegedDatabaseDto; -import at.tuwien.api.database.table.TableDto; import at.tuwien.api.user.UserDto; import at.tuwien.api.user.internal.UpdateUserPasswordDto; import at.tuwien.config.RabbitConfig; import at.tuwien.exception.*; import at.tuwien.mapper.MariaDbMapper; -import at.tuwien.mapper.MetadataMapper; import at.tuwien.service.DatabaseService; -import at.tuwien.service.SchemaService; import com.mchange.v2.c3p0.ComboPooledDataSource; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; @Log4j2 @@ -29,10 +22,12 @@ import java.sql.SQLException; public class DatabaseServiceMariaDbImpl extends HibernateConnector implements DatabaseService { private final RabbitConfig rabbitConfig; + private final MariaDbMapper mariaDbMapper; @Autowired - public DatabaseServiceMariaDbImpl(RabbitConfig rabbitConfig) { + public DatabaseServiceMariaDbImpl(RabbitConfig rabbitConfig, MariaDbMapper mariaDbMapper) { this.rabbitConfig = rabbitConfig; + this.mariaDbMapper = mariaDbMapper; } @Override @@ -42,7 +37,7 @@ public class DatabaseServiceMariaDbImpl extends HibernateConnector implements Da final Connection connection = dataSource.getConnection(); try { /* create database if not exists */ - connection.prepareStatement("CREATE DATABASE IF NOT EXISTS `" + data.getInternalName() + "`;") + connection.prepareStatement(mariaDbMapper.databaseCreateDatabaseQuery(data.getInternalName())) .execute(); connection.commit(); } catch (SQLException e) { @@ -76,7 +71,7 @@ public class DatabaseServiceMariaDbImpl extends HibernateConnector implements Da final Connection connection = dataSource.getConnection(); try { /* update user password */ - connection.prepareStatement("SET PASSWORD FOR `" + data.getUsername() + "`@`%` = '" + data.getPassword() + "';") + connection.prepareStatement(mariaDbMapper.databaseSetPasswordQuery(data.getUsername(), data.getPassword())) .execute(); connection.commit(); } catch (SQLException e) { diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SchemaServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SchemaServiceMariaDbImpl.java index c2c53c7830..537c4878a4 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SchemaServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/SchemaServiceMariaDbImpl.java @@ -100,6 +100,7 @@ public class SchemaServiceMariaDbImpl extends HibernateConnector implements Sche } } table.setTdbid(database.getId()); + database.getCreator().getAttributes().setMariadbPassword(null); table.setCreator(database.getCreator()); table.setCreatedBy(database.getCreator().getId()); final TableDto tmpTable = table; 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 57d7472dde..d298f2fada 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 @@ -84,7 +84,8 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs public QueryResultDto execute(PrivilegedDatabaseDto database, String statement, Instant timestamp, UUID userId, Long page, Long size, SortTypeDto sortDirection, String sortColumn) throws QueryStoreInsertException, SQLException, QueryNotFoundException, TableMalformedException, - UserNotFoundException, NotAllowedException, RemoteUnavailableException { + UserNotFoundException, NotAllowedException, RemoteUnavailableException, ServiceException, + DatabaseNotFoundException { final Long queryId = storeQuery(database, statement, timestamp, userId); final QueryDto query = findById(database, queryId); return reExecute(database, query, page, size, sortDirection, sortColumn); @@ -115,8 +116,8 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs @Override public List<QueryDto> findAll(PrivilegedDatabaseDto database, Boolean filterPersisted) throws SQLException, - QueryNotFoundException, NotAllowedException, RemoteUnavailableException { - final List<IdentifierDto> identifiers = metadataServiceGateway.getIdentifiers(database.getId()); + QueryNotFoundException, RemoteUnavailableException, ServiceException, DatabaseNotFoundException { + final List<IdentifierDto> identifiers = metadataServiceGateway.getIdentifiers(database.getId(), null); final ComboPooledDataSource dataSource = getPrivilegedDataSource(database); final Connection connection = dataSource.getConnection(); try { @@ -148,7 +149,7 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs @Override public ExportResourceDto export(PrivilegedDatabaseDto database, QueryDto query, Instant timestamp, String filename) throws SQLException, QueryMalformedException, SidecarExportException, StorageNotFoundException, - StorageUnavailableException { + StorageUnavailableException, ServiceException, RemoteUnavailableException { final String filePath = s3Config.getS3FilePath() + "/" + filename; final ComboPooledDataSource dataSource = getPrivilegedDataSource(database); final Connection connection = dataSource.getConnection(); @@ -203,7 +204,7 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs @Override public QueryDto findById(PrivilegedDatabaseDto database, Long queryId) throws QueryNotFoundException, SQLException, - NotAllowedException, RemoteUnavailableException, UserNotFoundException { + RemoteUnavailableException, UserNotFoundException, ServiceException, DatabaseNotFoundException { final ComboPooledDataSource dataSource = getPrivilegedDataSource(database); final Connection connection = dataSource.getConnection(); try { @@ -215,7 +216,7 @@ public class SubsetServiceMariaDbImpl extends HibernateConnector implements Subs } final QueryDto query = mariaDbMapper.resultSetToQueryDto(resultSet); query.setIdentifiers(metadataServiceGateway.getIdentifiers(database.getId(), queryId)); - final UserDto creator = metadataServiceGateway.getUser(query.getCreatedBy()); + final UserDto creator = metadataServiceGateway.getUserById(query.getCreatedBy()); log.debug("retrieved creator from metadata service: creator.id={}, creator.username={}", creator.getId(), creator.getUsername()); query.setCreator(creator); query.setDatabaseId(database.getId()); diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java index 92e369e0af..55e96c5161 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java @@ -6,6 +6,7 @@ import at.tuwien.api.database.query.ImportCsvDto; import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.*; import at.tuwien.api.database.table.columns.ColumnDto; +import at.tuwien.api.database.table.columns.ColumnStatisticDto; import at.tuwien.api.database.table.columns.ColumnTypeDto; import at.tuwien.api.database.table.internal.PrivilegedTableDto; import at.tuwien.api.database.table.internal.TableCreateDto; @@ -16,6 +17,7 @@ import at.tuwien.mapper.MariaDbMapper; import at.tuwien.service.SchemaService; import at.tuwien.service.StorageService; import at.tuwien.service.TableService; +import at.tuwien.utils.MariaDbUtil; import com.mchange.v2.c3p0.ComboPooledDataSource; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.RandomStringUtils; @@ -79,6 +81,33 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table return tables; } + @Override + public TableStatisticDto getStatistics(PrivilegedTableDto table) throws SQLException, TableMalformedException, + QueryMalformedException { + final ComboPooledDataSource dataSource = getPrivilegedDataSource(table.getDatabase()); + final Connection connection = dataSource.getConnection(); + final TableStatisticDto statistic; + try { + /* obtain statistic */ + final ResultSet resultSet = connection.prepareStatement(mariaDbMapper.tableColumnStatisticsSelectRawQuery(table.getColumns(), table.getInternalName())) + .executeQuery(); + statistic = mariaDbMapper.resultSetToTableStatistic(resultSet); + statistic.setRows(getCount(table, null)); + } catch (SQLException e) { + connection.rollback(); + log.error("Failed to obtain column statistics: {}", e.getMessage()); + throw new TableMalformedException("Failed to obtain column statistics: " + e.getMessage(), e); + } finally { + dataSource.close(); + } + table.getColumns() + .stream() + .filter(column -> !MariaDbUtil.numericDataTypes.contains(column.getColumnType())) + .forEach(column -> statistic.getColumns().put(column.getInternalName(), new ColumnStatisticDto())); + log.info("Obtained column statistics for table: {}", table.getInternalName()); + return statistic; + } + @Override public TableDto find(PrivilegedDatabaseDto database, String tableName) throws TableNotFoundException, SQLException, QueryMalformedException { @@ -114,7 +143,9 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table dataSource.close(); } log.info("Created table with name {}", tableName); - return find(database, tableName); + final TableDto table = find(database, tableName); + table.setName(data.getName()); + return table; } @Override @@ -129,12 +160,12 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table connection.commit(); } catch (SQLException e) { connection.rollback(); - log.error("Failed to delete table and history view: {}", e.getMessage()); - throw new QueryMalformedException("Failed to delete table and history view: " + e.getMessage(), e); + log.error("Failed to delete table: {}", e.getMessage()); + throw new QueryMalformedException("Failed to delete table: " + e.getMessage(), e); } finally { dataSource.close(); } - log.info("Deleted table and history view with name {}", tableName); + log.info("Deleted table with name {}", tableName); } @Override @@ -145,9 +176,9 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table final QueryResultDto queryResult; try { /* find table data */ - final ResultSet resultSet = connection.prepareStatement( - mariaDbMapper.selectDatasetRawQuery(table.getDatabase().getInternalName(), table.getInternalName(), - table.getColumns(), timestamp, size, page)) + final ResultSet resultSet = connection.prepareStatement(mariaDbMapper.selectDatasetRawQuery( + table.getDatabase().getInternalName(), table.getInternalName(), table.getColumns(), + timestamp, size, page)) .executeQuery(); connection.commit(); queryResult = mariaDbMapper.resultListToQueryResultDto(table.getColumns(), resultSet); @@ -159,11 +190,12 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table dataSource.close(); } log.info("Find data from table {}.{}", table.getDatabase().getInternalName(), table.getInternalName()); + queryResult.setId(table.getId()); return queryResult; } @Override - public List<TableHistoryDto> history(PrivilegedTableDto table) throws SQLException, + public List<TableHistoryDto> history(PrivilegedTableDto table, Long size) throws SQLException, TableNotFoundException { final ComboPooledDataSource dataSource = getPrivilegedDataSource(table.getDatabase()); final Connection connection = dataSource.getConnection(); @@ -171,7 +203,7 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table try { /* find table data */ final ResultSet resultSet = connection.prepareStatement(mariaDbMapper.selectHistoryRawQuery( - table.getDatabase().getInternalName(), table.getInternalName(), 100L)) + table.getDatabase().getInternalName(), table.getInternalName(), size)) .executeQuery(); history = mariaDbMapper.resultSetToTableHistory(resultSet); connection.commit(); @@ -210,48 +242,9 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table return queryResult; } - @Override - public void importTuple(PrivilegedTableDto table, TupleDto data) - throws TableMalformedException, StorageUnavailableException, StorageNotFoundException, SQLException, QueryMalformedException { - /* for each LOB-like data-column, retrieve the bytes and replace the value */ - for (String key : data.getData().keySet()) { - final boolean found = table.getColumns() - .stream() - .filter(c -> List.of(ColumnTypeDto.BLOB, ColumnTypeDto.LONGBLOB, ColumnTypeDto.TINYBLOB, ColumnTypeDto.MEDIUMBLOB).contains(c.getColumnType())) - .anyMatch(c -> c.getInternalName().equals(key)); - if (!found || data.getData().get(key) == null) { - continue; - } - final byte[] blob = storageService.getBytes(String.valueOf(data.getData().get(key))); - log.debug("replaced S3 storage key {} with blob", key); - data.getData().replace(key, blob); - } - final ComboPooledDataSource dataSource = getPrivilegedDataSource(table.getDatabase()); - final Connection connection = dataSource.getConnection(); - try { - /* import tuple */ - final int[] idx = new int[]{1}; - final PreparedStatement statement = connection.prepareStatement(mariaDbMapper.tupleToRawInsertQuery(table, data)); - for (String column : data.getData().keySet()) { - mariaDbMapper.prepareStatementWithColumnTypeObject(statement, - getColumnType(table.getColumns(), column), idx[0], data.getData().get(column)); - idx[0]++; - } - statement.execute(); - connection.commit(); - } catch (SQLException e) { - connection.rollback(); - log.error("Failed to import tuple: {}", e.getMessage()); - throw new QueryMalformedException("Failed to import tuple: " + e.getMessage(), e); - } finally { - dataSource.close(); - } - log.info("Imported tuple into table: {}.{}", table.getDatabase().getInternalName(), table.getInternalName()); - } - @Override public void importDataset(PrivilegedTableDto table, ImportCsvDto data) - throws SidecarImportException, StorageNotFoundException, SQLException, QueryMalformedException { + throws StorageNotFoundException, SQLException, QueryMalformedException, ServiceException, RemoteUnavailableException { /* import .csv from blob storage to sidecar */ dataDatabaseSidecarGateway.importFile(table.getDatabase().getContainer().getSidecarHost(), table.getDatabase().getContainer().getSidecarPort(), data.getLocation()); /* import .csv from sidecar to database */ @@ -286,7 +279,7 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table final PreparedStatement statement = connection.prepareStatement(mariaDbMapper.tupleToRawDeleteQuery(table, data)); for (String column : data.getKeys().keySet()) { mariaDbMapper.prepareStatementWithColumnTypeObject(statement, - getColumnType(table.getColumns(), column), idx[0], data.getKeys().get(column)); + getColumnType(table.getColumns(), column), idx[0], column, data.getKeys().get(column)); idx[0]++; } statement.executeUpdate(); @@ -303,8 +296,22 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table @Override public void createTuple(PrivilegedTableDto table, TupleDto data) throws SQLException, - QueryMalformedException, TableMalformedException { + QueryMalformedException, TableMalformedException, StorageUnavailableException, StorageNotFoundException { log.trace("create tuple: {}", data); + /* for each LOB-like data-column, retrieve the bytes and replace the value */ + for (String key : data.getData().keySet()) { + final boolean found = table.getColumns() + .stream() + .filter(c -> List.of(ColumnTypeDto.BLOB, ColumnTypeDto.LONGBLOB, ColumnTypeDto.TINYBLOB, ColumnTypeDto.MEDIUMBLOB).contains(c.getColumnType())) + .anyMatch(c -> c.getInternalName().equals(key)); + if (!found || data.getData().get(key) == null) { + continue; + } + final byte[] blob = storageService.getBytes(String.valueOf(data.getData().get(key))); + log.debug("replaced S3 storage key {} with blob", key); + data.getData() + .replace(key, blob); + } /* prepare the statement */ final ComboPooledDataSource dataSource = getPrivilegedDataSource(table.getDatabase()); final Connection connection = dataSource.getConnection(); @@ -314,7 +321,7 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table final PreparedStatement statement = connection.prepareStatement(mariaDbMapper.tupleToRawCreateQuery(table, data)); for (Map.Entry<String, Object> entry : data.getData().entrySet()) { mariaDbMapper.prepareStatementWithColumnTypeObject(statement, - getColumnType(table.getColumns(), entry.getKey()), idx[0], entry.getValue()); + getColumnType(table.getColumns(), entry.getKey()), idx[0], entry.getKey(), entry.getValue()); idx[0]++; } statement.executeUpdate(); @@ -342,13 +349,13 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table /* set data */ for (Map.Entry<String, Object> entry : data.getData().entrySet()) { mariaDbMapper.prepareStatementWithColumnTypeObject(statement, - getColumnType(table.getColumns(), entry.getKey()), idx[0], entry.getValue()); + getColumnType(table.getColumns(), entry.getKey()), idx[0], entry.getKey(), entry.getValue()); idx[0]++; } /* set key(s) */ for (Map.Entry<String, Object> entry : data.getKeys().entrySet()) { mariaDbMapper.prepareStatementWithColumnTypeObject(statement, - getColumnType(table.getColumns(), entry.getKey()), idx[0], entry.getValue()); + getColumnType(table.getColumns(), entry.getKey()), idx[0], entry.getKey(), entry.getValue()); idx[0]++; } statement.executeUpdate(); @@ -375,9 +382,9 @@ public class TableServiceMariaDbImpl extends HibernateConnector implements Table } @Override - public ExportResourceDto exportDataset(PrivilegedTableDto table, Instant timestamp) - throws SQLException, SidecarExportException, StorageNotFoundException, StorageUnavailableException, - QueryMalformedException { + public ExportResourceDto exportDataset(PrivilegedTableDto table, Instant timestamp) throws SQLException, + StorageNotFoundException, StorageUnavailableException, QueryMalformedException, ServiceException, + RemoteUnavailableException { final String fileName = RandomStringUtils.randomAlphabetic(40) + ".csv"; final String filePath = s3Config.getS3FilePath() + "/" + fileName; final ComboPooledDataSource dataSource = getPrivilegedDataSource(table.getDatabase()); diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/ViewServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/ViewServiceMariaDbImpl.java index b28e2a1dc4..c85f5bfbdb 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/ViewServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/ViewServiceMariaDbImpl.java @@ -16,12 +16,14 @@ import at.tuwien.mapper.MetadataMapper; import at.tuwien.service.SchemaService; import at.tuwien.service.StorageService; import at.tuwien.service.ViewService; +import com.google.common.hash.Hashing; import com.mchange.v2.c3p0.ComboPooledDataSource; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -96,7 +98,11 @@ public class ViewServiceMariaDbImpl extends HibernateConnector implements ViewSe final Connection connection = dataSource.getConnection(); ViewDto view = ViewDto.builder() .name(data.getName()) - .internalName(data.getName()) + .internalName(mariaDbMapper.nameToInternalName(data.getName())) + .query(data.getQuery()) + .queryHash(Hashing.sha256() + .hashString(data.getQuery(), StandardCharsets.UTF_8) + .toString()) .isPublic(database.getIsPublic()) .creator(database.getOwner()) .createdBy(database.getOwner().getId()) @@ -108,12 +114,12 @@ public class ViewServiceMariaDbImpl extends HibernateConnector implements ViewSe .build(); try { /* create view if not exists */ - connection.prepareStatement(mariaDbMapper.viewCreateRawQuery(data.getName(), data.getQuery())) + connection.prepareStatement(mariaDbMapper.viewCreateRawQuery(view.getInternalName(), data.getQuery())) .execute(); /* select view columns */ final PreparedStatement statement2 = connection.prepareStatement(mariaDbMapper.databaseTableColumnsSelectRawQuery()); statement2.setString(1, database.getInternalName()); - statement2.setString(2, data.getName()); + statement2.setString(2, view.getInternalName()); final ResultSet resultSet2 = statement2.executeQuery(); while (resultSet2.next()) { view = mariaDbMapper.resultSetToTable(resultSet2, view, queryConfig); @@ -205,8 +211,8 @@ public class ViewServiceMariaDbImpl extends HibernateConnector implements ViewSe @Override public ExportResourceDto exportDataset(PrivilegedDatabaseDto database, ViewDto view, Instant timestamp) - throws SQLException, QueryMalformedException, SidecarExportException, StorageNotFoundException, - StorageUnavailableException { + throws SQLException, QueryMalformedException, StorageNotFoundException, + StorageUnavailableException, ServiceException, RemoteUnavailableException { final String fileName = RandomStringUtils.randomAlphabetic(40) + ".csv"; final String filePath = s3Config.getS3FilePath() + "/" + fileName; final ComboPooledDataSource dataSource = getPrivilegedDataSource(database); diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/utils/MariaDbUtil.java b/dbrepo-data-service/services/src/main/java/at/tuwien/utils/MariaDbUtil.java index 17847c15c6..a917be6d46 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/utils/MariaDbUtil.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/utils/MariaDbUtil.java @@ -9,16 +9,36 @@ public class MariaDbUtil { /** * https://mariadb.com/kb/en/string-data-types/ */ - final static List<ColumnTypeDto> stringDataTypes = List.of(ColumnTypeDto.BINARY, + final static List<ColumnTypeDto> stringDataTypes = List.of( + ColumnTypeDto.BINARY, + ColumnTypeDto.VARBINARY, + ColumnTypeDto.TINYBLOB, + ColumnTypeDto.MEDIUMBLOB, + ColumnTypeDto.LONGBLOB, ColumnTypeDto.BLOB, ColumnTypeDto.CHAR, + ColumnTypeDto.VARCHAR, ColumnTypeDto.ENUM, - ColumnTypeDto.MEDIUMBLOB, - ColumnTypeDto.LONGBLOB, - ColumnTypeDto.LONGTEXT, - ColumnTypeDto.TEXT, + ColumnTypeDto.SET, ColumnTypeDto.TINYTEXT, - ColumnTypeDto.SET); + ColumnTypeDto.MEDIUMTEXT, + ColumnTypeDto.LONGTEXT, + ColumnTypeDto.TEXT); + + /** + * https://mariadb.com/kb/en/numeric-data-type-overview/ + */ + final public static List<ColumnTypeDto> numericDataTypes = List.of( + ColumnTypeDto.TINYINT, + ColumnTypeDto.BOOL, + ColumnTypeDto.SMALLINT, + ColumnTypeDto.MEDIUMINT, + ColumnTypeDto.INT, + ColumnTypeDto.BIGINT, + ColumnTypeDto.DECIMAL, + ColumnTypeDto.FLOAT, + ColumnTypeDto.DOUBLE, + ColumnTypeDto.BIT); /** * https://mariadb.com/kb/en/date-and-time-data-types/ diff --git a/dbrepo-gateway-service/dbrepo.conf b/dbrepo-gateway-service/dbrepo.conf index 4ea19528f1..f9c0001ceb 100644 --- a/dbrepo-gateway-service/dbrepo.conf +++ b/dbrepo-gateway-service/dbrepo.conf @@ -101,7 +101,7 @@ server { proxy_read_timeout 90; } - location ~ /api/database/([0-9]+)/table/([0-9]+)/(data|history|export) { + location ~ /api/database/([0-9]+)/table/([0-9]+)/(data|history|export|statistic) { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/dbrepo-metadata-service/Dockerfile b/dbrepo-metadata-service/Dockerfile index 5fd1138f24..75fe485c16 100644 --- a/dbrepo-metadata-service/Dockerfile +++ b/dbrepo-metadata-service/Dockerfile @@ -30,6 +30,8 @@ RUN mvn clean install -DskipTests FROM amazoncorretto:17-alpine3.19 as runtime MAINTAINER Martin Weise <martin.weise@tuwien.ac.at> +RUN apk add --no-cache curl bash jq + WORKDIR /app USER 65534 diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/ViewColumnDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/ViewColumnDto.java index 35edfc6d84..337a61a637 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/ViewColumnDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/ViewColumnDto.java @@ -76,6 +76,10 @@ public class ViewColumnDto { private UnitDto unit; + @Size(max = 2048) + @Schema(example = "Column comment") + private String description; + @NotNull @JsonProperty("is_public") @Schema(example = "true") diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableBriefDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableBriefDto.java index aa566a495c..11f99f48eb 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableBriefDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableBriefDto.java @@ -33,7 +33,6 @@ public class TableBriefDto { @Schema(example = "Air Quality") private String name; - @NotBlank @Schema(example = "Air Quality in Austria") private String description; @@ -49,9 +48,4 @@ public class TableBriefDto { @NotNull private UserBriefDto owner; - - @ToString.Exclude - @JsonIgnore - @NotNull - private List<ColumnBriefDto> columns; } diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableStatisticDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableStatisticDto.java index bcf744c0b3..8d41bcc7ff 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableStatisticDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/TableStatisticDto.java @@ -1,6 +1,7 @@ package at.tuwien.api.database.table; import at.tuwien.api.database.table.columns.ColumnStatisticDto; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotNull; import lombok.*; import lombok.extern.jackson.Jacksonized; @@ -16,6 +17,10 @@ import java.util.Map; @ToString public class TableStatisticDto { + @NotNull + @JsonProperty("rows") + private Long rows; + @NotNull private Map<String, ColumnStatisticDto> columns; } diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/columns/ColumnCreateDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/columns/ColumnCreateDto.java index 78fb7fb84e..37aa493020 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/columns/ColumnCreateDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/columns/ColumnCreateDto.java @@ -46,6 +46,12 @@ public class ColumnCreateDto { @Schema(example = "true") private Boolean nullAllowed; + @JsonProperty("concept_uri") + private String conceptUri; + + @JsonProperty("unit_uri") + private String unitUri; + @Schema(description = "date format id") private Long dfid; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/columns/ColumnDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/columns/ColumnDto.java index 740b2184e7..026d5d89b7 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/columns/ColumnDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/columns/ColumnDto.java @@ -117,7 +117,7 @@ public class ColumnDto { private UnitDto unit; @Size(max = 2048) - @Schema(example = "Formatted as YYYY-MM-dd") + @Schema(example = "Column comment") private String description; @ToString.Exclude diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyBriefDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyBriefDto.java new file mode 100644 index 0000000000..58a4d5b245 --- /dev/null +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyBriefDto.java @@ -0,0 +1,16 @@ +package at.tuwien.api.database.table.constraints.foreign; + +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class ForeignKeyBriefDto { + + private Long id; +} diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyDto.java index 9fe7c68382..1644c95cdc 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyDto.java @@ -1,5 +1,6 @@ package at.tuwien.api.database.table.constraints.foreign; +import at.tuwien.api.database.table.TableBriefDto; import at.tuwien.api.database.table.TableDto; import at.tuwien.api.database.table.columns.ColumnDto; import com.fasterxml.jackson.annotation.JsonProperty; @@ -28,12 +29,12 @@ public class ForeignKeyDto { @NotNull @ToString.Exclude - private TableDto table; + private TableBriefDto table; @NotNull @ToString.Exclude @JsonProperty("referenced_table") - private TableDto referencedTable; + private TableBriefDto referencedTable; @JsonProperty("on_update") private ReferenceTypeDto onUpdate; diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyReferenceDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyReferenceDto.java index fb978671bc..111903d926 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyReferenceDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ForeignKeyReferenceDto.java @@ -1,6 +1,6 @@ package at.tuwien.api.database.table.constraints.foreign; -import at.tuwien.api.database.table.columns.ColumnDto; +import at.tuwien.api.database.table.columns.ColumnBriefDto; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotNull; import lombok.*; @@ -19,14 +19,14 @@ public class ForeignKeyReferenceDto { @NotNull @JsonProperty("foreign_key") - private ForeignKeyDto foreignKey; + private ForeignKeyBriefDto foreignKey; @NotNull @ToString.Exclude - private ColumnDto column; + private ColumnBriefDto column; @NotNull @ToString.Exclude @JsonProperty("referenced_column") - private ColumnDto referencedColumn; + private ColumnBriefDto referencedColumn; } diff --git a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ReferenceTypeDto.java b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ReferenceTypeDto.java index ebd2d56887..239b95e7e9 100644 --- a/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ReferenceTypeDto.java +++ b/dbrepo-metadata-service/api/src/main/java/at/tuwien/api/database/table/constraints/foreign/ReferenceTypeDto.java @@ -27,6 +27,17 @@ public enum ReferenceTypeDto { this.type = type; } + public static ReferenceTypeDto fromType(String type) { + return switch (type) { + case "RESTRICT" -> ReferenceTypeDto.RESTRICT; + case "CASCADE" -> ReferenceTypeDto.CASCADE; + case "SET NULL" -> ReferenceTypeDto.SET_NULL; + case "NO ACTION" -> ReferenceTypeDto.NO_ACTION; + case "SET DEFAULT" -> ReferenceTypeDto.SET_DEFAULT; + default -> null; + }; + } + @Override public String toString() { return this.type; diff --git a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/container/image/ContainerImage.java b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/container/image/ContainerImage.java index b315c7e81d..568e16e474 100644 --- a/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/container/image/ContainerImage.java +++ b/dbrepo-metadata-service/entities/src/main/java/at/tuwien/entities/container/image/ContainerImage.java @@ -23,6 +23,9 @@ import java.util.List; @EntityListeners(AuditingEntityListener.class) @EqualsAndHashCode(onlyExplicitlyIncluded = true) @Table(name = "mdb_images", uniqueConstraints = @UniqueConstraint(columnNames = {"name", "version"})) +@NamedQueries({ + @NamedQuery(name = "ContainerImage.findAll", query = "select i from ContainerImage i order by i.id asc") +}) public class ContainerImage { @Id diff --git a/dbrepo-metadata-service/pom.xml b/dbrepo-metadata-service/pom.xml index 464cf28ed9..5633736450 100644 --- a/dbrepo-metadata-service/pom.xml +++ b/dbrepo-metadata-service/pom.xml @@ -45,7 +45,7 @@ <commons-io.version>2.15.0</commons-io.version> <commons-validator.version>1.8.0</commons-validator.version> <guava.version>33.0.0-jre</guava.version> - <jacoco.version>0.8.11</jacoco.version> + <jacoco.version>0.8.12</jacoco.version> <jwt.version>4.3.0</jwt.version> <c3p0.version>0.9.5.5</c3p0.version> <c3p0-hibernate.version>6.2.2.Final</c3p0-hibernate.version> @@ -279,23 +279,6 @@ <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>${jacoco.version}</version> - <configuration> - <excludes> - <exclude>at/tuwien/utils/**/*</exclude> - <exclude>at/tuwien/seeder/**/*</exclude> - <exclude>at/tuwien/mapper/**/*</exclude> - <exclude>at/tuwien/handlers/**/*</exclude> - <exclude>at/tuwien/exception/**/*</exclude> - <exclude>at/tuwien/converters/**/*</exclude> - <exclude>at/tuwien/utils/**/*</exclude> - <exclude>at/tuwien/config/**/*</exclude> - <exclude>at/tuwien/auth/**/*</exclude> - <exclude>at/tuwien/gateway/impl/ApiTemplateInterceptorImpl.class</exclude> - <exclude>**/testcontainers.properties</exclude> - <exclude>**/HibernateConnector.class</exclude> - <exclude>**/DbrepoMetadataServiceApplication.class</exclude> - </excludes> - </configuration> <executions> <execution> <id>default-prepare-agent</id> diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java index 1a41226977..1cb8f6d394 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/mapper/MetadataMapper.java @@ -19,8 +19,10 @@ import at.tuwien.api.database.table.columns.concepts.UnitDto; import at.tuwien.api.database.table.columns.concepts.UnitSaveDto; import at.tuwien.api.database.table.constraints.ConstraintsCreateDto; import at.tuwien.api.database.table.constraints.ConstraintsDto; +import at.tuwien.api.database.table.constraints.foreign.ForeignKeyBriefDto; import at.tuwien.api.database.table.constraints.foreign.ForeignKeyDto; import at.tuwien.api.database.table.constraints.foreign.ForeignKeyReferenceDto; +import at.tuwien.api.database.table.constraints.foreign.ReferenceTypeDto; import at.tuwien.api.database.table.constraints.primary.PrimaryKeyDto; import at.tuwien.api.database.table.constraints.unique.UniqueDto; import at.tuwien.api.datacite.doi.*; @@ -88,6 +90,8 @@ public interface MetadataMapper { BannerMessageBriefDto bannerMessageToBannerMessageBriefDto(BannerMessage data); + ViewColumn viewColumnDtoToViewColumn(ViewColumnDto data); + BannerMessage bannerMessageCreateDtoToBannerMessage(BannerMessageCreateDto data); BannerMessageType bannerMessageTypeDtoToBannerMessageType(BannerMessageTypeDto data); @@ -458,9 +462,7 @@ public interface MetadataMapper { TableColumnConcept conceptSaveDtoToTableColumnConcept(ConceptSaveDto data); @Mappings({ - @Mapping(source = "id", target = "id"), - @Mapping(target = "name", expression = "java(data.getName())"), - @Mapping(target = "internalName", expression = "java(data.getInternalName())") + @Mapping(target = "databaseId", source = "tdbid"), }) TableBriefDto tableToTableBriefDto(Table data); @@ -480,14 +482,12 @@ public interface MetadataMapper { @Mappings({ @Mapping(target = "table.owner", ignore = true), - @Mapping(target = "table.creator", ignore = true), - @Mapping(target = "table.constraints", ignore = true), @Mapping(target = "referencedTable.owner", ignore = true), - @Mapping(target = "referencedTable.creator", ignore = true), - @Mapping(target = "referencedTable.constraints", ignore = true), }) ForeignKeyDto foreignKeyToForeignKeyDto(ForeignKey data); + ForeignKeyBriefDto foreignKeyDtoToForeignKeyBriefDto(ForeignKeyDto data); + default ConstraintsDto constraintsToConstraintsDto(Constraints data) { if (data == null) { return null; @@ -539,6 +539,20 @@ public interface MetadataMapper { pk.getColumn().setTableId(data.getId()); pk.getColumn().setDatabaseId(data.getDatabase().getId()); }); + for (ForeignKeyDto fk : table.getConstraints().getForeignKeys()) { + for (ForeignKeyReferenceDto ref : fk.getReferences()) { + ref.setForeignKey(foreignKeyDtoToForeignKeyBriefDto(fk)); + ref.getColumn().setTableId(table.getId()); + ref.getColumn().setDatabaseId(table.getTdbid()); + final Optional<TableColumn> optional = data.getDatabase().getTables().stream().map(Table::getColumns).flatMap(List::stream).filter(c -> c.getId().equals(ref.getReferencedColumn().getId())).findFirst(); + if (optional.isEmpty()) { + log.error("Failed to find foreign key referenced column {}.{} in columns: {}", table.getInternalName(), ref.getReferencedColumn().getInternalName(), data.getDatabase().getTables().stream().map(Table::getColumns).flatMap(List::stream).toList()); + throw new IllegalArgumentException("Failed to find foreign key referenced column"); + } + ref.getReferencedColumn().setTableId(optional.get().getTable().getId()); + ref.getReferencedColumn().setDatabaseId(optional.get().getTable().getTdbid()); + } + } table.getConstraints() .getUniques() .forEach(uk -> { @@ -585,11 +599,12 @@ public interface MetadataMapper { Table tableDtoToTable(TableDto data); @Mappings({ - @Mapping(target = "table.owner", ignore = true), - @Mapping(target = "table.columns", ignore = true) + @Mapping(target = "table.owner", ignore = true) }) PrimaryKeyDto primaryKeyToPrimaryKeyDto(PrimaryKey data); + ReferenceType referenceTypeDtoToReferenceType(ReferenceTypeDto data); + /* keep */ default Constraints constraintsCreateDtoToConstraints(ConstraintsCreateDto data, Database database, Table table) { final int[] idx = new int[]{0, 0}; @@ -617,40 +632,39 @@ public interface MetadataMapper { log.error("Failed to find foreign key referenced table {} in tables: {}", fk.getReferencedTable(), database.getTables().stream().map(Table::getInternalName).toList()); throw new IllegalArgumentException("Failed to find foreign key referenced table"); } + final List<ForeignKeyReference> references = new LinkedList<>(); + for (int i = 0; i < fk.getColumns().size(); i++) { + final int k = i; + final Optional<TableColumn> column = table.getColumns() + .stream() + .filter(cc -> cc.getInternalName().equals(fk.getColumns().get(k))) + .findFirst(); + if (column.isEmpty()) { + log.error("Failed to find foreign key column {}.{} in columns: {}", table.getInternalName(), fk.getColumns().get(k), optional.get().getColumns().stream().map(TableColumn::getInternalName).toList()); + throw new IllegalArgumentException("Failed to find foreign key column"); + } + final Optional<TableColumn> referencedColumn = optional.get() + .getColumns() + .stream() + .filter(cc -> cc.getInternalName().equals(fk.getReferencedColumns().get(k))) + .findFirst(); + if (referencedColumn.isEmpty()) { + log.error("Failed to find foreign key referenced column {} in referenced columns: {}", fk.getReferencedColumns().get(k), database.getTables().stream().filter(t -> t.getInternalName().equals(fk.getReferencedTable())).map(Table::getColumns).flatMap(List::stream).map(TableColumn::getInternalName).toList()); + throw new IllegalArgumentException("Failed to find foreign key referenced column"); + } + references.add(ForeignKeyReference.builder() + .column(column.get()) + .referencedColumn(referencedColumn.get()) + .foreignKey(null) // set at the end + .build()); + } return ForeignKey.builder() .name("fk_" + table.getInternalName() + "_" + idx[1]++) + .table(table) .referencedTable(optional.get()) - .references(fk.getReferencedColumns() - .stream() - .map(c -> { - final Optional<TableColumn> column = table.getColumns() - .stream() - .filter(cc -> cc.getInternalName().equals(c)) - .findFirst(); - if (column.isEmpty()) { - log.error("Failed to find foreign key column {} in columns: {}", c, table.getColumns().stream().map(TableColumn::getInternalName).toList()); - throw new IllegalArgumentException("Failed to find foreign key column"); - } - final Optional<TableColumn> referencedColumn = database.getTables() - .stream() - .filter(t -> t.getInternalName().equals(fk.getReferencedTable())) - .map(Table::getColumns) - .flatMap(List::stream) - .filter(cc -> cc.getInternalName().equals(c)) - .findFirst(); - if (referencedColumn.isEmpty()) { - log.error("Failed to find foreign key referenced column {} in referenced columns: {}", c, database.getTables().stream().filter(t -> t.getInternalName().equals(fk.getReferencedTable())).map(Table::getColumns).flatMap(List::stream).map(TableColumn::getInternalName).toList()); - throw new IllegalArgumentException("Failed to find foreign key referenced column"); - } - return ForeignKeyReference.builder() - .column(column.get()) - .referencedColumn(referencedColumn.get()) - .foreignKey(null) // set later - .build(); - }) - .toList()) - .onDelete(ReferenceType.CASCADE) - .onUpdate(ReferenceType.CASCADE) + .references(references) + .onDelete(referenceTypeDtoToReferenceType(fk.getOnDelete())) + .onUpdate(referenceTypeDtoToReferenceType(fk.getOnUpdate())) .build(); }) .toList()) @@ -791,6 +805,12 @@ public interface MetadataMapper { }) ViewDto viewToViewDto(View data); + @Mappings({ + @Mapping(target = "databaseId", source = "view.vdbid"), + @Mapping(target = "isPublic", source = "view.isPublic") + }) + ViewColumnDto viewColumnToViewColumnDto(ViewColumn data); + ViewBriefDto viewToViewBriefDto(View data); @Mappings({ diff --git a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/repository/ImageRepository.java b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/repository/ImageRepository.java index d2262f37d6..593a472718 100644 --- a/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/repository/ImageRepository.java +++ b/dbrepo-metadata-service/repositories/src/main/java/at/tuwien/repository/ImageRepository.java @@ -4,11 +4,14 @@ import at.tuwien.entities.container.image.ContainerImage; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface ImageRepository extends JpaRepository<ContainerImage, Long> { + List<ContainerImage> findAll(); + Optional<ContainerImage> findByNameAndVersion(String name, String version); } 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 97f7207f0b..c87b4039c1 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 @@ -4,7 +4,6 @@ import at.tuwien.api.amqp.QueueDto; import at.tuwien.api.database.table.TableBriefDto; import at.tuwien.api.database.table.TableCreateDto; import at.tuwien.api.database.table.TableDto; -import at.tuwien.api.database.table.TableStatisticDto; import at.tuwien.api.database.table.columns.ColumnCreateDto; import at.tuwien.api.database.table.columns.ColumnDto; import at.tuwien.api.database.table.columns.ColumnTypeDto; @@ -62,7 +61,7 @@ public class TableEndpoint { private final EndpointValidator endpointValidator; @Autowired - public TableEndpoint(UserService userService, TableService tableService, RabbitConfig rabbitMqConfig, + public TableEndpoint(UserService userService, TableService tableService, RabbitConfig rabbitMqConfig, EntityService entityService, BrokerService messageQueueService, MetadataMapper metadataMapper, DatabaseService databaseService, EndpointValidator endpointValidator) { this.userService = userService; @@ -156,19 +155,19 @@ public class TableEndpoint { @PutMapping("/{tableId}") @Transactional - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("hasAuthority('update-table-statistic') or hasAuthority('admin')") @Observed(name = "dbrepo_statistic_table_update") @Operation(summary = "Update table statistics", security = {@SecurityRequirement(name = "bearerAuth"), @SecurityRequirement(name = "basicAuth")}) @ApiResponses(value = { @ApiResponse(responseCode = "202", description = "Updated table statistics successfully"), - @ApiResponse(responseCode = "400", - description = "Payload malformed", + @ApiResponse(responseCode = "404", + description = "Failed to find database/table in metadata database", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), - @ApiResponse(responseCode = "404", - description = "Failed to find database/table in metadata database", + @ApiResponse(responseCode = "400", + description = "Failed to map column statistic to known columns", content = {@Content( mediaType = "application/json", schema = @Schema(implementation = ApiErrorDto.class))}), @@ -184,14 +183,12 @@ public class TableEndpoint { schema = @Schema(implementation = ApiErrorDto.class))}), }) public ResponseEntity<Void> updateStatistic(@NotNull @PathVariable("databaseId") Long databaseId, - @NotNull @PathVariable("tableId") Long tableId, - @NotNull @Valid @RequestBody TableStatisticDto data) - throws MalformedException, TableNotFoundException, DatabaseNotFoundException, SearchServiceException, - SearchServiceConnectionException { - log.debug("endpoint update table statistics, databaseId={}, tableId={}, data.columns.size={}", databaseId, - tableId, data.getColumns().size()); + @NotNull @PathVariable("tableId") Long tableId) + throws TableNotFoundException, DatabaseNotFoundException, SearchServiceException, + SearchServiceConnectionException, MalformedException, ServiceException, ServiceConnectionException { + log.debug("endpoint update table statistics, databaseId={}, tableId={}", databaseId, tableId); final Table table = tableService.findById(databaseId, tableId); - tableService.updateStatistics(table, data); + tableService.updateStatistics(table); return ResponseEntity.accepted() .build(); } @@ -344,7 +341,7 @@ public class TableEndpoint { @NotNull Principal principal) throws NotAllowedException, MalformedException, ServiceException, ServiceConnectionException, DatabaseNotFoundException, UserNotFoundException, AccessNotFoundException, TableNotFoundException, TableExistsException, SearchServiceException, - SearchServiceConnectionException { + SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { log.debug("endpoint create table, databaseId={}, data.name={}", databaseId, data.getName()); final Database database = databaseService.findById(databaseId); endpointValidator.validateOnlyAccess(database, principal, true); diff --git a/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml b/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml index 6f69d7737c..3312af7c5a 100644 --- a/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml +++ b/dbrepo-metadata-service/rest-service/src/main/resources/application-local.yml @@ -62,11 +62,12 @@ dbrepo: username: admin password: admin endpoints: - searchService: http://localhost:4000 + searchService: http://localhost + analyseService: http://localhost dataService: http://localhost:9093 brokerService: http://localhost/admin/broker - authService: http://localhost:8080 - storageService: http://storage-service:9000 + authService: http://localhost/api/auth + storageService: http://localhost/api/storage pid: base: http://localhost/pid/ jwt: diff --git a/dbrepo-metadata-service/rest-service/src/main/resources/application.yml b/dbrepo-metadata-service/rest-service/src/main/resources/application.yml index 326e628b2c..ca7cec2ea5 100644 --- a/dbrepo-metadata-service/rest-service/src/main/resources/application.yml +++ b/dbrepo-metadata-service/rest-service/src/main/resources/application.yml @@ -51,7 +51,7 @@ logging: dbrepo: repository-name: "${REPOSITORY_NAME:Database Repository}" base-url: "${BASE_URL:http://localhost}" - admin-email: "${ADMIN_MAIL:noreply@example.com}" + admin-email: "${ADMIN_EMAIL:noreply@example.com}" deleted-record: "${DELETED_RECORD:persistent}" granularity: "${GRANULARITY:YYYY-MM-DDThh:mm:ssZ}" exchangeName: "${BROKER_EXCHANGE_NAME:dbrepo}" @@ -66,11 +66,12 @@ dbrepo: username: "${ADMIN_USERNAME:admin}" password: "${ADMIN_PASSWORD:admin}" endpoints: - searchService: "${SEARCH_SERVICE_ENDPOINT:http://search-service:8080}" + searchService: "${SEARCH_SERVICE_ENDPOINT:http://gateway-service}" + analyseService: "${ANALYSE_SERVICE_ENDPOINT:http://gateway-service}" dataService: "${DATA_SERVICE_ENDPOINT:http://data-service:8080}" brokerService: "${BROKER_SERVICE_ENDPOINT:http://gateway-service/admin/broker}" - authService: "${AUTH_SERVICE_ENDPOINT:http://auth-service:8080}" - storageService: "${S3_ENDPOINT:http://storage-service:9000}" + authService: "${AUTH_SERVICE_ENDPOINT:http://gateway-service/api/auth}" + storageService: "${S3_ENDPOINT:http://gateway-service/api/storage}" pid: base: "${PID_BASE:http://localhost/pid/}" jwt: diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java index 6bc0b98692..e5e5097ede 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/endpoints/TableEndpointUnitTest.java @@ -897,7 +897,7 @@ public class TableEndpointUnitTest extends AbstractUnitTest { Principal principal, User user, DatabaseAccess access) throws MalformedException, NotAllowedException, ServiceException, ServiceConnectionException, UserNotFoundException, DatabaseNotFoundException, AccessNotFoundException, TableNotFoundException, - TableExistsException, SearchServiceException, SearchServiceConnectionException { + TableExistsException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { /* mock */ if (principal != null) { diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mapper/MetadataMapperUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mapper/MetadataMapperUnitTest.java index e253d4f764..28ab4ccb41 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mapper/MetadataMapperUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mapper/MetadataMapperUnitTest.java @@ -152,7 +152,7 @@ public class MetadataMapperUnitTest extends AbstractUnitTest { } @Test - public void databaseToDatabaseDto_succeeds() { + public void customDatabaseToDatabaseDto_succeeds() { /* test */ final DatabaseDto response = metadataMapper.customDatabaseToDatabaseDto(DATABASE_1); @@ -289,6 +289,11 @@ public class MetadataMapperUnitTest extends AbstractUnitTest { final ForeignKeyReferenceDto table1fkr = table1fk.getReferences().get(0); assertEquals(1L, table1fkr.getId()); assertEquals(TABLE_2_COLUMNS_DTO.get(2).getId(), table1fkr.getColumn().getId()); + assertEquals(TABLE_2_COLUMNS_DTO.get(2).getTable().getId(), table1fkr.getColumn().getTableId()); + assertEquals(TABLE_2_COLUMNS_DTO.get(2).getDatabaseId(), table1fkr.getColumn().getDatabaseId()); + assertEquals(TABLE_1_COLUMNS_DTO.get(0).getDatabaseId(), table1fkr.getReferencedColumn().getId()); + assertEquals(TABLE_1_COLUMNS_DTO.get(0).getDatabaseId(), table1fkr.getReferencedColumn().getTableId()); + assertEquals(TABLE_1_COLUMNS_DTO.get(0).getDatabaseId(), table1fkr.getReferencedColumn().getDatabaseId()); assertEquals(1, table1.getConstraints().getPrimaryKey().size()); final PrimaryKeyDto table1pk = new ArrayList<>(table1.getConstraints().getPrimaryKey()).get(0); assertEquals(2L, table1pk.getId()); diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java index 76678a1fe3..44b924f396 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/mvc/PrometheusEndpointMvcTest.java @@ -538,7 +538,7 @@ public class PrometheusEndpointMvcTest extends AbstractUnitTest { /* ignore */ } try { - tableEndpoint.updateStatistic(DATABASE_1_ID, TABLE_1_ID, TableStatisticDto.builder().build()); + tableEndpoint.updateStatistic(DATABASE_1_ID, TABLE_1_ID); } catch (Exception e) { /* ignore */ } diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServicePersistenceTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServicePersistenceTest.java index 91b64ef93c..24536a9ca5 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServicePersistenceTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServicePersistenceTest.java @@ -37,7 +37,6 @@ import static org.mockito.Mockito.*; @Log4j2 @SpringBootTest -@Disabled("CI/CD") @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @ExtendWith(SpringExtension.class) public class TableServicePersistenceTest extends AbstractUnitTest { @@ -79,7 +78,7 @@ public class TableServicePersistenceTest extends AbstractUnitTest { @Test @Transactional public void create_succeeds() throws MalformedException, ServiceException, ServiceConnectionException, - UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, TableExistsException, SearchServiceException, SearchServiceConnectionException { + UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, TableExistsException, SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, SemanticEntityNotFoundException { final TableCreateDto request = TableCreateDto.builder() .name("New Table") .description("A wonderful table") diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceUnitTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceUnitTest.java index 6dfcb5187e..c16a4191f9 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceUnitTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/TableServiceUnitTest.java @@ -119,7 +119,8 @@ public class TableServiceUnitTest extends AbstractUnitTest { @Test public void createTable_succeeds() throws ServiceException, ServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, TableExistsException, SearchServiceException, - SearchServiceConnectionException, MalformedException { + SearchServiceConnectionException, MalformedException, OntologyNotFoundException, + SemanticEntityNotFoundException { /* mock */ when(userService.findByUsername(USER_1_USERNAME)) @@ -140,7 +141,8 @@ public class TableServiceUnitTest extends AbstractUnitTest { @Test public void createTable_nonStandardColumnNames_succeeds() throws ServiceException, ServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, TableExistsException, - SearchServiceException, SearchServiceConnectionException, MalformedException { + SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, + SemanticEntityNotFoundException { final TableCreateDto request = TableCreateDto.builder() .name("New Table") .description("A wonderful table") @@ -240,7 +242,8 @@ public class TableServiceUnitTest extends AbstractUnitTest { @Test public void create_succeeds() throws MalformedException, ServiceException, ServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, TableExistsException, - SearchServiceException, SearchServiceConnectionException { + SearchServiceException, SearchServiceConnectionException, OntologyNotFoundException, + SemanticEntityNotFoundException { /* mock */ when(userService.findByUsername(USER_1_USERNAME)) diff --git a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceTest.java b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceTest.java index f686a1c23a..fc83d3a650 100644 --- a/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceTest.java +++ b/dbrepo-metadata-service/rest-service/src/test/java/at/tuwien/service/ViewServicePersistenceTest.java @@ -1,7 +1,10 @@ package at.tuwien.service; +import at.tuwien.entities.database.Database; import at.tuwien.entities.database.View; import at.tuwien.exception.*; +import at.tuwien.gateway.DataServiceGateway; +import at.tuwien.gateway.SearchServiceGateway; import at.tuwien.repository.ContainerRepository; import at.tuwien.repository.DatabaseRepository; import at.tuwien.repository.LicenseRepository; @@ -14,12 +17,16 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; @Log4j2 @SpringBootTest @@ -43,6 +50,12 @@ public class ViewServicePersistenceTest extends AbstractUnitTest { @Autowired private ViewService viewService; + @MockBean + private DataServiceGateway dataServiceGateway; + + @MockBean + private SearchServiceGateway searchServiceGateway; + @BeforeEach public void beforeEach() { genesis(); @@ -65,4 +78,19 @@ public class ViewServicePersistenceTest extends AbstractUnitTest { assertEquals(VIEW_1_COLUMNS.size(), response.getColumns().size()); } + @Test + public void delete_succeeds() throws SearchServiceException, ServiceException, ServiceConnectionException, + DatabaseNotFoundException, SearchServiceConnectionException, ViewNotFoundException { + + /* mock */ + doNothing() + .when(dataServiceGateway) + .deleteView(DATABASE_1_ID, VIEW_1_ID); + when(searchServiceGateway.update(any(Database.class))) + .thenReturn(DATABASE_1_DTO); + + /* test */ + viewService.delete(VIEW_1); + } + } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/GatewayConfig.java index d0029e9458..c64fc52282 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/GatewayConfig.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/GatewayConfig.java @@ -30,6 +30,9 @@ public class GatewayConfig { @Value("${dbrepo.endpoints.dataService}") private String dataEndpoint; + @Value("${dbrepo.endpoints.analyseService}") + private String analyseEndpoint; + @Value("${dbrepo.endpoints.searchService}") private String searchEndpoint; @@ -54,6 +57,7 @@ public class GatewayConfig { public RestTemplate brokerRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(brokerEndpoint)); + log.debug("add basic authentication for broker service: username={}, password=(hidden)", brokerUsername); restTemplate.getInterceptors() .addAll(List.of(new BasicAuthenticationInterceptor(brokerUsername, brokerPassword), clientHttpRequestInterceptor())); @@ -64,7 +68,18 @@ public class GatewayConfig { public RestTemplate dataServiceRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(dataEndpoint)); - log.debug("add basic authentication for internal data service: username={}, password=(hidden)", adminUsername); + log.debug("add basic authentication for data service: username={}, password=(hidden)", adminUsername); + restTemplate.getInterceptors() + .addAll(List.of(new BasicAuthenticationInterceptor(adminUsername, adminPassword), + clientHttpRequestInterceptor())); + return restTemplate; + } + + @Bean("analyseServiceRestTemplate") + public RestTemplate analyseServiceRestTemplate() { + final RestTemplate restTemplate = new RestTemplate(); + restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(analyseEndpoint)); + log.debug("add basic authentication for analyse service: username={}, password=(hidden)", adminUsername); restTemplate.getInterceptors() .addAll(List.of(new BasicAuthenticationInterceptor(adminUsername, adminPassword), clientHttpRequestInterceptor())); @@ -75,7 +90,7 @@ public class GatewayConfig { public RestTemplate searchServiceRestTemplate() { final RestTemplate restTemplate = new RestTemplate(); restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(searchEndpoint)); - log.debug("add basic authentication for internal search service: username={}, password=(hidden)", adminUsername); + log.debug("add basic authentication for search service: username={}, password=(hidden)", adminUsername); restTemplate.getInterceptors() .addAll(List.of(new BasicAuthenticationInterceptor(adminUsername, adminPassword), clientHttpRequestInterceptor())); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataServiceGateway.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataServiceGateway.java index d8ba8a490f..9edb9f388d 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataServiceGateway.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/DataServiceGateway.java @@ -9,6 +9,7 @@ import at.tuwien.api.database.internal.CreateDatabaseDto; import at.tuwien.api.database.query.QueryDto; import at.tuwien.api.database.table.TableCreateDto; import at.tuwien.api.database.table.TableDto; +import at.tuwien.api.database.table.TableStatisticDto; import at.tuwien.api.user.internal.UpdateUserPasswordDto; import at.tuwien.exception.*; @@ -41,4 +42,6 @@ public interface DataServiceGateway { List<TableDto> getTableSchemas(Long databaseId) throws ServiceConnectionException, ServiceException, QueryNotFoundException; List<ViewDto> getViewSchemas(Long databaseId) throws ServiceConnectionException, ServiceException, QueryNotFoundException; + + TableStatisticDto getTableStatistics(Long databaseId, Long tableId) throws ServiceConnectionException, ServiceException, TableNotFoundException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/BrokerServiceGatewayImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/BrokerServiceGatewayImpl.java index 8ab7c8a730..b86780f5c4 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/BrokerServiceGatewayImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/BrokerServiceGatewayImpl.java @@ -2,7 +2,6 @@ package at.tuwien.gateway.impl; import at.tuwien.api.amqp.*; import at.tuwien.api.user.ExchangeUpdatePermissionsDto; -import at.tuwien.config.GatewayConfig; import at.tuwien.config.RabbitConfig; import at.tuwien.exception.*; import at.tuwien.gateway.BrokerServiceGateway; @@ -21,15 +20,12 @@ import org.springframework.web.client.RestTemplate; public class BrokerServiceGatewayImpl implements BrokerServiceGateway { private final RestTemplate restTemplate; - private final GatewayConfig gatewayConfig; private final RabbitConfig rabbitConfig; @Autowired - public BrokerServiceGatewayImpl(GatewayConfig gatewayConfig, - @Qualifier("brokerRestTemplate") RestTemplate restTemplate, + public BrokerServiceGatewayImpl(@Qualifier("brokerRestTemplate") RestTemplate restTemplate, RabbitConfig rabbitMqConfig) { this.restTemplate = restTemplate; - this.gatewayConfig = gatewayConfig; this.rabbitConfig = rabbitMqConfig; } @@ -37,7 +33,6 @@ public class BrokerServiceGatewayImpl implements BrokerServiceGateway { public void grantTopicPermission(String username, ExchangeUpdatePermissionsDto data) throws ServiceConnectionException, ServiceException { final String url = "/api/topic-permissions/" + rabbitConfig.getVirtualHost() + "/" + username; - log.debug("grant topic permission in url {}{}", gatewayConfig.getBrokerEndpoint(), url); final ResponseEntity<Void> response; try { response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(data), Void.class); @@ -57,7 +52,6 @@ public class BrokerServiceGatewayImpl implements BrokerServiceGateway { @Override public void grantVirtualHostPermission(String username, GrantVirtualHostPermissionsDto data) throws ServiceConnectionException, ServiceException { final String url = "/api/permissions/" + rabbitConfig.getVirtualHost() + "/" + username; - log.debug("grant virtual host permissions in url {}{}", gatewayConfig.getBrokerEndpoint(), url); final ResponseEntity<Void> response; try { response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(data), Void.class); @@ -77,7 +71,6 @@ public class BrokerServiceGatewayImpl implements BrokerServiceGateway { @Override public void grantExchangePermission(String username, GrantExchangePermissionsDto data) throws ServiceConnectionException, ServiceException { final String url = "/api/topic-permissions/" + rabbitConfig.getVirtualHost() + "/" + username; - log.debug("grant topic permissions in url {}{}", gatewayConfig.getBrokerEndpoint(), url); final ResponseEntity<Void> response; try { response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(data), Void.class); @@ -99,8 +92,6 @@ public class BrokerServiceGatewayImpl implements BrokerServiceGateway { final String url = "/api/queues/" + rabbitConfig.getVirtualHost() + "/" + name; final HttpHeaders headers = new HttpHeaders(); headers.set("Accept", "application/json"); - log.trace("gateway broker find queue, virtual host={}, queue={}", rabbitConfig.getVirtualHost(), name); - log.debug("find queue from url {}{}", gatewayConfig.getBrokerEndpoint(), url); final ResponseEntity<QueueDto> response; try { response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, headers), QueueDto.class); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataServiceGatewayImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataServiceGatewayImpl.java index a278fc60b9..6c09d6d500 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataServiceGatewayImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/DataServiceGatewayImpl.java @@ -6,6 +6,7 @@ import at.tuwien.api.database.internal.CreateDatabaseDto; import at.tuwien.api.database.query.QueryDto; import at.tuwien.api.database.table.TableCreateDto; import at.tuwien.api.database.table.TableDto; +import at.tuwien.api.database.table.TableStatisticDto; import at.tuwien.api.user.internal.UpdateUserPasswordDto; import at.tuwien.exception.*; import at.tuwien.gateway.DataServiceGateway; @@ -15,7 +16,6 @@ import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import java.util.Arrays; @@ -37,12 +37,10 @@ public class DataServiceGatewayImpl implements DataServiceGateway { throws ServiceConnectionException, ServiceException, DatabaseNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/access/" + userId; - log.debug("create access in data service"); try { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(UpdateDatabaseAccessDto.builder().type(access).build()), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + } catch (HttpServerErrorException e) { log.error("Failed to create access: {}", e.getMessage()); throw new ServiceConnectionException("Failed to create access: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { @@ -63,12 +61,10 @@ public class DataServiceGatewayImpl implements DataServiceGateway { throws ServiceConnectionException, ServiceException, AccessNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/access/" + userId; - log.debug("update access in data service"); try { response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(UpdateDatabaseAccessDto.builder().type(access).build()), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + } catch (HttpServerErrorException e) { log.error("Failed to update access: {}", e.getMessage()); throw new ServiceConnectionException("Failed to update access: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { @@ -89,11 +85,9 @@ public class DataServiceGatewayImpl implements DataServiceGateway { AccessNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/access/" + userId; - log.debug("delete access in data service"); try { - response = restTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(null), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + response = restTemplate.exchange(url, HttpMethod.DELETE, HttpEntity.EMPTY, Void.class); + } catch (HttpServerErrorException e) { log.error("Failed to delete access: {}", e.getMessage()); throw new ServiceConnectionException("Failed to delete access: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { @@ -113,11 +107,9 @@ public class DataServiceGatewayImpl implements DataServiceGateway { public DatabaseDto createDatabase(CreateDatabaseDto data) throws ServiceConnectionException, ServiceException { final ResponseEntity<DatabaseDto> response; final String url = "/api/database"; - log.debug("create database in data service"); try { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(data), DatabaseDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + } catch (HttpServerErrorException e) { log.error("Failed to create database: {}", e.getMessage()); throw new ServiceConnectionException("Failed to create database: " + e.getMessage(), e); } catch (HttpClientErrorException.BadRequest | HttpClientErrorException.Unauthorized e) { @@ -136,11 +128,9 @@ public class DataServiceGatewayImpl implements DataServiceGateway { ServiceException, DatabaseNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId; - log.debug("update database in data service"); try { response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(data), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + } catch (HttpServerErrorException e) { log.error("Failed to update user password in database: {}", e.getMessage()); throw new ServiceConnectionException("Failed to update user password in database: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { @@ -161,11 +151,9 @@ public class DataServiceGatewayImpl implements DataServiceGateway { DatabaseNotFoundException, TableExistsException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/table"; - log.debug("create table in data service"); try { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(data), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + } catch (HttpServerErrorException e) { log.error("Failed to create table: {}", e.getMessage()); throw new ServiceConnectionException("Failed to create table: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { @@ -189,11 +177,9 @@ public class DataServiceGatewayImpl implements DataServiceGateway { TableNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/table/" + tableId; - log.debug("delete table in data service"); try { - response = restTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(null), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + response = restTemplate.exchange(url, HttpMethod.DELETE, HttpEntity.EMPTY, Void.class); + } catch (HttpServerErrorException e) { log.error("Failed to delete table: {}", e.getMessage()); throw new ServiceConnectionException("Failed to delete table: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { @@ -213,11 +199,9 @@ public class DataServiceGatewayImpl implements DataServiceGateway { public ViewDto createView(Long databaseId, ViewCreateDto data) throws ServiceConnectionException, ServiceException { final ResponseEntity<ViewDto> response; final String url = "/api/database/" + databaseId + "/view"; - log.debug("create view in data service"); try { response = restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(data), ViewDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + } catch (HttpServerErrorException e) { log.error("Failed to create view: {}", e.getMessage()); throw new ServiceConnectionException("Failed to create view: " + e.getMessage(), e); } catch (HttpClientErrorException.BadRequest | HttpClientErrorException.Unauthorized e) { @@ -240,11 +224,9 @@ public class DataServiceGatewayImpl implements DataServiceGateway { ViewNotFoundException { final ResponseEntity<Void> response; final String url = "/api/database/" + databaseId + "/view/" + viewId; - log.debug("delete view in data service"); try { - response = restTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(null), Void.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + response = restTemplate.exchange(url, HttpMethod.DELETE, HttpEntity.EMPTY, Void.class); + } catch (HttpServerErrorException e) { log.error("Failed to delete view: {}", e.getMessage()); throw new ServiceConnectionException("Failed to delete view: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { @@ -265,11 +247,9 @@ public class DataServiceGatewayImpl implements DataServiceGateway { QueryNotFoundException { final ResponseEntity<QueryDto> response; final String url = "/api/database/" + databaseId + "/subset/" + queryId; - log.debug("get query in data service"); try { - response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null), QueryDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, QueryDto.class); + } catch (HttpServerErrorException e) { log.error("Failed to find query: {}", e.getMessage()); throw new ServiceConnectionException("Failed to find query", e); } catch (HttpClientErrorException.NotFound e) { @@ -294,11 +274,9 @@ public class DataServiceGatewayImpl implements DataServiceGateway { ServiceException, QueryNotFoundException { final ResponseEntity<ExportResourceDto> response; final String url = "/api/database/" + databaseId + "/subset/" + queryId; - log.debug("export query in data service"); try { - response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null), ExportResourceDto.class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, ExportResourceDto.class); + } catch (HttpServerErrorException e) { log.error("Failed to export query: {}", e.getMessage()); throw new ServiceConnectionException("Failed to export query: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { @@ -319,11 +297,9 @@ public class DataServiceGatewayImpl implements DataServiceGateway { public List<TableDto> getTableSchemas(Long databaseId) throws ServiceConnectionException, ServiceException, QueryNotFoundException { final ResponseEntity<TableDto[]> response; final String url = "/api/database/" + databaseId + "/table"; - log.debug("retrieve table schema metadata in data service"); try { - response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null), TableDto[].class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, TableDto[].class); + } catch (HttpServerErrorException e) { log.error("Failed to get table schemas: {}", e.getMessage()); throw new ServiceConnectionException("Failed to get table schemas: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { @@ -351,9 +327,8 @@ public class DataServiceGatewayImpl implements DataServiceGateway { final ResponseEntity<ViewDto[]> response; final String url = "/api/database/" + databaseId + "/view"; try { - response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null), ViewDto[].class); - } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | - HttpServerErrorException.InternalServerError e) { + response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, ViewDto[].class); + } catch (HttpServerErrorException e) { log.error("Failed to get view schemas: {}", e.getMessage()); throw new ServiceConnectionException("Failed to get view schemas: " + e.getMessage(), e); } catch (HttpClientErrorException.NotFound e) { @@ -376,4 +351,31 @@ public class DataServiceGatewayImpl implements DataServiceGateway { return views; } + @Override + public TableStatisticDto getTableStatistics(Long databaseId, Long tableId) throws ServiceConnectionException, ServiceException, TableNotFoundException { + final ResponseEntity<TableStatisticDto> response; + final String url = "/api/database/" + databaseId + "/table/" + tableId + "/statistic"; + try { + response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, TableStatisticDto.class); + } catch (HttpServerErrorException e) { + log.error("Failed to analyse table statistic: {}", e.getMessage()); + throw new ServiceConnectionException("Failed to analyse table statistic: " + e.getMessage(), e); + } catch (HttpClientErrorException.NotFound e) { + log.error("Failed to analyse table statistic: not found: {}", e.getMessage()); + throw new TableNotFoundException("Failed to analyse table statistic: not found: " + e.getMessage(), e); + } catch (HttpClientErrorException.Unauthorized e) { + log.error("Failed to analyse table statistic: {}", e.getMessage()); + throw new ServiceException("Failed to analyse table statistic: " + e.getMessage(), e); + } + if (!response.getStatusCode().equals(HttpStatus.OK)) { + log.error("Failed to analyse table statistic: wrong http code: {}", response.getStatusCode()); + throw new ServiceException("Failed to analyse table statistic: wrong http code: " + response.getStatusCode()); + } + if (response.getBody() == null) { + log.error("Failed to analyse table statistic: empty body: {}", response.getStatusCode()); + throw new ServiceException("Failed to analyse table statistic: empty body: " + response.getStatusCode()); + } + return response.getBody(); + } + } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/SearchServiceGatewayImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/SearchServiceGatewayImpl.java index 49a2a1423c..deba8360f2 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/SearchServiceGatewayImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/gateway/impl/SearchServiceGatewayImpl.java @@ -1,7 +1,6 @@ package at.tuwien.gateway.impl; import at.tuwien.api.database.DatabaseDto; -import at.tuwien.api.database.ViewDto; import at.tuwien.entities.database.Database; import at.tuwien.exception.*; import at.tuwien.gateway.SearchServiceGateway; @@ -16,9 +15,6 @@ import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; -import java.util.LinkedList; -import java.util.List; - @Log4j2 @Service public class SearchServiceGatewayImpl implements SearchServiceGateway { @@ -40,7 +36,6 @@ public class SearchServiceGatewayImpl implements SearchServiceGateway { headers.set("Accept", "application/json"); headers.set("Content-Type", "application/json"); final String url = "/api/search/database/" + database.getId(); - log.debug("update database in search service"); try { response = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>( metadataMapper.customDatabaseToDatabaseDto(database), headers), DatabaseDto.class); @@ -53,7 +48,7 @@ public class SearchServiceGatewayImpl implements SearchServiceGateway { throw new DatabaseNotFoundException("Failed to update database: not found", e); } catch (HttpClientErrorException.BadRequest | HttpClientErrorException.Unauthorized e) { log.error("Failed to update database: malformed payload: {}", e.getMessage()); - throw new SearchServiceException("Failed to update database: malformed payload", e); + throw new SearchServiceException("Failed to update database: malformed payload: " + e.getMessage(), e); } if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { log.error("Failed to update database: response code is not 202"); @@ -66,7 +61,6 @@ public class SearchServiceGatewayImpl implements SearchServiceGateway { public void delete(Long databaseId) throws SearchServiceConnectionException, SearchServiceException, DatabaseNotFoundException { final ResponseEntity<Void> response; final String url = "/api/search/database/" + databaseId; - log.trace("delete to url {}", url); try { response = restTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(null), Void.class); } catch (ResourceAccessException | HttpServerErrorException.ServiceUnavailable | diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ConceptService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ConceptService.java index 88e90908f8..94fa1e0c89 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ConceptService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ConceptService.java @@ -7,6 +7,8 @@ import java.util.List; public interface ConceptService { + TableColumnConcept create(TableColumnConcept concept); + /** * Finds all table column concepts in the metadata database. * diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java index 22d0f1781b..0eb228ccd5 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/TableService.java @@ -44,7 +44,7 @@ public interface TableService { */ Table createTable(Database database, TableCreateDto createDto, Principal principal) throws TableNotFoundException, ServiceException, ServiceConnectionException, UserNotFoundException, - DatabaseNotFoundException, TableExistsException, SearchServiceException, SearchServiceConnectionException, MalformedException; + DatabaseNotFoundException, TableExistsException, SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, SemanticEntityNotFoundException; /** * Deletes a table from the database in the metadata database and data database. @@ -58,8 +58,5 @@ public interface TableService { TableColumn findColumnById(Table table, Long columnId) throws MalformedException; - TableColumn findColumnByName(Table table, String name) throws MalformedException; - - @Transactional - void updateStatistics(Table table, TableStatisticDto data) throws MalformedException, SearchServiceException, DatabaseNotFoundException, SearchServiceConnectionException; + void updateStatistics(Table table) throws SearchServiceException, DatabaseNotFoundException, SearchServiceConnectionException, MalformedException, TableNotFoundException, ServiceException, ServiceConnectionException; } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/UnitService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/UnitService.java index c45d78c48c..93824eeb62 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/UnitService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/UnitService.java @@ -2,11 +2,14 @@ package at.tuwien.service; import at.tuwien.entities.database.table.columns.TableColumnUnit; import at.tuwien.exception.UnitNotFoundException; +import org.springframework.transaction.annotation.Transactional; import java.util.List; public interface UnitService { + TableColumnUnit create(TableColumnUnit unit); + /** * Finds all table column units in the metadata database. * diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ViewService.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ViewService.java index f2346ec340..d19a3be73b 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ViewService.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/ViewService.java @@ -33,7 +33,8 @@ public interface ViewService { * * @param view The view. */ - void delete(View view) throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, ViewNotFoundException, SearchServiceException, SearchServiceConnectionException; + void delete(View view) throws ServiceException, ServiceConnectionException, DatabaseNotFoundException, + ViewNotFoundException, SearchServiceException, SearchServiceConnectionException; /** * Creates a view in the container with given id and database with id with the given query. diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ConceptServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ConceptServiceImpl.java index 8dd0b76a84..647d4fe198 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ConceptServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ConceptServiceImpl.java @@ -23,6 +23,12 @@ public class ConceptServiceImpl implements ConceptService { this.conceptRepository = conceptRepository; } + @Override + @Transactional + public TableColumnConcept create(TableColumnConcept concept) { + return conceptRepository.save(concept); + } + @Override @Transactional(readOnly = true) public List<TableColumnConcept> findAll() { diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java index c4a8187b52..16d23d7af1 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/DatabaseServiceImpl.java @@ -6,6 +6,7 @@ import at.tuwien.api.database.DatabaseModifyVisibilityDto; import at.tuwien.api.database.ViewDto; import at.tuwien.api.database.internal.CreateDatabaseDto; import at.tuwien.api.database.table.TableDto; +import at.tuwien.api.database.table.columns.ColumnDto; import at.tuwien.api.database.table.constraints.primary.PrimaryKeyDto; import at.tuwien.api.user.internal.UpdateUserPasswordDto; import at.tuwien.entities.container.Container; @@ -40,7 +41,7 @@ public class DatabaseServiceImpl implements DatabaseService { @Autowired public DatabaseServiceImpl(MetadataMapper metadataMapper, ContainerService containerService, - DatabaseRepository databaseRepository, DataServiceGateway dataServiceGateway, + DatabaseRepository databaseRepository, DataServiceGateway dataServiceGateway, SearchServiceGateway searchServiceGateway) { this.metadataMapper = metadataMapper; this.containerService = containerService; @@ -222,7 +223,7 @@ public class DatabaseServiceImpl implements DatabaseService { fk.setTable(tableEntity); }); /* map primary key constraint */ - for (PrimaryKeyDto key : table.getConstraints().getPrimaryKey()) { + for (PrimaryKey key : tableEntity.getConstraints().getPrimaryKey()) { final Optional<TableColumn> optional = tableEntity.getColumns() .stream() .filter(c -> c.getInternalName().equals(key.getColumn().getInternalName())) @@ -231,12 +232,8 @@ public class DatabaseServiceImpl implements DatabaseService { log.error("Failed to find primary key column {} in table {}.{}", key.getColumn().getInternalName(), database.getInternalName(), table.getInternalName()); throw new MalformedException("Failed to find primary key column: " + key.getColumn().getInternalName()); } - tableEntity.getConstraints() - .getPrimaryKey() - .add(PrimaryKey.builder() - .table(tableEntity) - .column(optional.get()) - .build()); + key.setTable(tableEntity); + key.setColumn(optional.get()); } database.getTables() .add(tableEntity); diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java index bf5ca0a98f..e8fecdf300 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java @@ -36,7 +36,6 @@ public class TableServiceImpl implements TableService { private final UserService userService; private final UnitService unitService; - private final SparqlMapper ontologyMapper; private final RabbitConfig rabbitConfig; private final EntityService entityService; private final ConceptService conceptService; @@ -47,14 +46,12 @@ public class TableServiceImpl implements TableService { private final SearchServiceGateway searchServiceGateway; @Autowired - public TableServiceImpl(UserService userService, UnitService unitService, SparqlMapper ontologyMapper, - RabbitConfig rabbitConfig, EntityService entityService, ConceptService conceptService, - MetadataMapper metadataMapper, DatabaseService databaseService, - DataServiceGateway dataServiceGateway, DatabaseRepository databaseRepository, - SearchServiceGateway searchServiceGateway) { + public TableServiceImpl(UserService userService, UnitService unitService, RabbitConfig rabbitConfig, + EntityService entityService, ConceptService conceptService, MetadataMapper metadataMapper, + DatabaseService databaseService, DataServiceGateway dataServiceGateway, + DatabaseRepository databaseRepository, SearchServiceGateway searchServiceGateway) { this.userService = userService; this.unitService = unitService; - this.ontologyMapper = ontologyMapper; this.rabbitConfig = rabbitConfig; this.entityService = entityService; this.conceptService = conceptService; @@ -101,7 +98,7 @@ public class TableServiceImpl implements TableService { @Transactional public Table createTable(Database database, TableCreateDto data, Principal principal) throws ServiceException, ServiceConnectionException, UserNotFoundException, TableNotFoundException, DatabaseNotFoundException, - TableExistsException, SearchServiceException, SearchServiceConnectionException, MalformedException { + TableExistsException, SearchServiceException, SearchServiceConnectionException, MalformedException, OntologyNotFoundException, SemanticEntityNotFoundException { final User owner = userService.findByUsername(principal.getName()); /* check */ if (data.getConstraints().getPrimaryKey().isEmpty()) { @@ -134,36 +131,59 @@ public class TableServiceImpl implements TableService { .creator(owner) .ownedBy(owner.getId()) .owner(owner) + .numRows(0L) + .dataLength(0L) .identifiers(new LinkedList<>()) .columns(new LinkedList<>()) .build(); try { /* set the ordinal position for the columns */ - table.getColumns() - .addAll(data.getColumns() + final int[] idx = new int[]{0}; + for (int i = 0; i < data.getColumns().size(); i++) { + final ColumnCreateDto c = data.getColumns().get(i); + final TableColumn column = metadataMapper.columnCreateDtoToTableColumn(c, database.getContainer().getImage()); + column.setOrdinalPosition(idx[0]++); + column.setTable(table); + if (data.isNeedSequence() && column.getName().equals("id")) { + column.setAutoGenerated(true); + } + if (c.getUnitUri() != null) { + log.trace("column {} has assigned unit uri: {}", column.getInternalName(), c.getUnitUri()); + TableColumnUnit unit; + try { + unit = unitService.find(c.getUnitUri()); + } catch (UnitNotFoundException e) { + unit = unitService.create(metadataMapper.entityDtoToTableColumnUnit(entityService.findOneByUri(c.getUnitUri()))); + } + column.setUnit(unit); + } + if (c.getConceptUri() != null) { + log.trace("column {} has assigned concept uri: {}", column.getInternalName(), c.getConceptUri()); + TableColumnConcept concept; + try { + concept = conceptService.find(c.getConceptUri()); + } catch (ConceptNotFoundException e) { + concept = conceptService.create(metadataMapper.entityDtoToTableColumnConcept(entityService.findOneByUri(c.getConceptUri()))); + } + column.setConcept(concept); + } + if (List.of(TableColumnType.TIME, TableColumnType.TIMESTAMP, TableColumnType.DATE, TableColumnType.DATETIME).contains(column.getColumnType())) { + final Optional<ContainerImageDate> optional = database.getContainer() + .getImage() + .getDateFormats() .stream() - .map(c -> { - final TableColumn column = metadataMapper.columnCreateDtoToTableColumn(c, database.getContainer().getImage()); - if (data.isNeedSequence() && column.getName().equals("id")) { - column.setAutoGenerated(true); - } - if (List.of(TableColumnType.TIME, TableColumnType.TIMESTAMP, TableColumnType.DATE, TableColumnType.DATETIME).contains(column.getColumnType())) { - final Optional<ContainerImageDate> optional = database.getContainer() - .getImage() - .getDateFormats() - .stream() - .filter(df -> df.getId().equals(c.getDfid())) - .findFirst(); - if (optional.isEmpty()) { - log.error("Failed to find date format with id {} in metadata database", c.getDfid()); - throw new IllegalArgumentException("Failed to find date format in metadata database"); - } - column.setDateFormat(optional.get()); - log.debug("column is of temporal type: added date format with id {}", column.getDateFormat().getId()); - } - return column; - }) - .toList()); + .filter(df -> df.getId().equals(c.getDfid())) + .findFirst(); + if (optional.isEmpty()) { + log.error("Failed to find date format with id {} in metadata database", c.getDfid()); + throw new IllegalArgumentException("Failed to find date format in metadata database"); + } + column.setDateFormat(optional.get()); + log.debug("column is of temporal type: added date format with id {}", column.getDateFormat().getId()); + } + table.getColumns() + .add(column); + } /* set constraints */ table.setConstraints(metadataMapper.constraintsCreateDtoToConstraints(data.getConstraints(), database, table)); } catch (IllegalArgumentException e) { @@ -176,13 +196,8 @@ public class TableServiceImpl implements TableService { throw new MalformedException("Failed to create table: some unique constraint(s) reference non-existing table columns"); } } - int[] idx = {0}; - table.getColumns() - .forEach(column -> { - column.setTable(table); - column.setOrdinalPosition(idx[0]++); - }); - database.getTables().add(table); + database.getTables() + .add(table); /* create in data service */ dataServiceGateway.createTable(database.getId(), data); /* update in metadata database */ @@ -270,51 +285,35 @@ public class TableServiceImpl implements TableService { return optional.get(); } - @Override - @Transactional(readOnly = true) - public TableColumn findColumnByName(Table table, String name) throws MalformedException { - final Optional<TableColumn> optional = table.getColumns() - .stream() - .filter(c -> c.getInternalName().equals(name)) - .findFirst(); - if (optional.isEmpty()) { - log.error("Failed to find column with name {} in table with name {}", name, table.getInternalName()); - throw new MalformedException("Failed to find column in metadata database"); - } - return optional.get(); - } - @Override @Transactional - public void updateStatistics(Table table, TableStatisticDto data) throws MalformedException, SearchServiceException, - DatabaseNotFoundException, SearchServiceConnectionException { - final List<String> notFound = data.getColumns() - .keySet() - .stream() - .filter(key -> table.getColumns().stream().noneMatch(c -> c.getInternalName().equals(key))) - .toList(); - if (!notFound.isEmpty()) { - log.error("Failed to update statistics: column(s) not found: {}", notFound); - throw new MalformedException("Failed to update statistics: column(s) not found"); + public void updateStatistics(Table table) throws SearchServiceException, + DatabaseNotFoundException, SearchServiceConnectionException, MalformedException, TableNotFoundException, + ServiceException, ServiceConnectionException { + final TableStatisticDto statistic = dataServiceGateway.getTableStatistics(table.getTdbid(), table.getId()); + table.setNumRows(statistic.getRows()); + for (Map.Entry<String, ColumnStatisticDto> entry : statistic.getColumns().entrySet()) { + final Optional<TableColumn> optional = table.getColumns().stream().filter(c -> c.getInternalName().equals(entry.getKey())).findFirst(); + if (optional.isEmpty()) { + log.error("Failed to assign table column statistic: column {} does not exist in table {}.{}", entry.getKey(), table.getDatabase().getInternalName(), table.getInternalName()); + throw new MalformedException("Failed to assign table column statistic: column does not exist"); + } + final TableColumn column = optional.get(); + final ColumnStatisticDto columnStatistic = statistic.getColumns().get(entry.getKey()); + column.setMean(columnStatistic.getMean()); + column.setMedian(columnStatistic.getMedian()); + column.setMin(columnStatistic.getMin()); + column.setMax(columnStatistic.getMax()); + column.setStdDev(columnStatistic.getStdDev()); } - table.getColumns() - .forEach(column -> { - if (!data.getColumns().containsKey(column.getInternalName())) { - return; - } - final ColumnStatisticDto statistic = data.getColumns().get(column.getInternalName()); - column.setMean(statistic.getMean()); - column.setMedian(statistic.getMedian()); - column.setMin(statistic.getMin()); - column.setMax(statistic.getMax()); - }); /* update in metadata database */ final Database database = table.getDatabase(); database.getTables() .set(database.getTables().indexOf(table), table); + databaseRepository.save(database); /* update in open search service */ searchServiceGateway.update(database); - log.info("Updated table statistics"); + log.info("Updated statistics of table with id: {}", table.getId()); } } diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UnitServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UnitServiceImpl.java index c0bcf19f28..03270abcd5 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UnitServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/UnitServiceImpl.java @@ -23,6 +23,12 @@ public class UnitServiceImpl implements UnitService { this.unitRepository = unitRepository; } + @Override + @Transactional + public TableColumnUnit create(TableColumnUnit unit) { + return unitRepository.save(unit); + } + @Override @Transactional(readOnly = true) public List<TableColumnUnit> findAll() { diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ViewServiceImpl.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ViewServiceImpl.java index f9d888b5a0..a91e032844 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ViewServiceImpl.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/service/impl/ViewServiceImpl.java @@ -105,7 +105,13 @@ public class ViewServiceImpl implements ViewService { .build(); /* create in data service */ data.setName(view.getInternalName()); - final ViewDto dto = dataServiceGateway.createView(database.getId(), data); + final ViewDto rawView = dataServiceGateway.createView(database.getId(), data); + view.setColumns(rawView.getColumns() + .stream() + .map(metadataMapper::viewColumnDtoToViewColumn) + .toList()); + view.getColumns() + .forEach(column -> column.setView(view)); database.getViews() .add(view); database = databaseRepository.save(database); diff --git a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/AbstractUnitTest.java b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/AbstractUnitTest.java index 3ba5fc9e36..cf32bf4f00 100644 --- a/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/AbstractUnitTest.java +++ b/dbrepo-metadata-service/test/src/main/java/at/tuwien/test/AbstractUnitTest.java @@ -28,6 +28,7 @@ public abstract class AbstractUnitTest extends BaseTest { TABLE_1.setConstraints(TABLE_1_CONSTRAINTS); TABLE_1_PRIVILEGED_DTO.setColumns(new LinkedList<>(TABLE_1_COLUMNS_DTO)); TABLE_1_PRIVILEGED_DTO.setDatabase(DATABASE_1_PRIVILEGED_DTO); + VIEW_1_DTO.setDatabase(DATABASE_1_DTO); DATABASE_1.setIdentifiers(new LinkedList<>(List.of(IDENTIFIER_1, IDENTIFIER_2, IDENTIFIER_3, IDENTIFIER_4))); DATABASE_1.setTables(new LinkedList<>(List.of(TABLE_1, TABLE_2, TABLE_3, TABLE_4))); DATABASE_1.setViews(new LinkedList<>(List.of(VIEW_1, VIEW_2, VIEW_3))); @@ -41,6 +42,7 @@ public abstract class AbstractUnitTest extends BaseTest { TABLE_2_CONSTRAINTS.getForeignKeys().get(0).getReferences().get(0).setForeignKey(TABLE_2_CONSTRAINTS.getForeignKeys().get(0)); TABLE_2.setConstraints(TABLE_2_CONSTRAINTS); TABLE_2_PRIVILEGED_DTO.setColumns(new LinkedList<>(TABLE_2_COLUMNS_DTO)); + TABLE_2_PRIVILEGED_DTO.setDatabase(DATABASE_1_PRIVILEGED_DTO); TABLE_2_DTO.setColumns(TABLE_2_COLUMNS_DTO); TABLE_2_DTO.setConstraints(TABLE_2_CONSTRAINTS_DTO); TABLE_3.setDatabase(DATABASE_1); @@ -76,6 +78,8 @@ public abstract class AbstractUnitTest extends BaseTest { TABLE_5.setDatabase(DATABASE_2); TABLE_5.setColumns(new LinkedList<>(TABLE_5_COLUMNS)); TABLE_5.setConstraints(TABLE_5_CONSTRAINTS); + TABLE_5_PRIVILEGED_DTO.setColumns(new LinkedList<>(TABLE_5_COLUMNS_DTO)); + TABLE_5_PRIVILEGED_DTO.setDatabase(DATABASE_2_PRIVILEGED_DTO); TABLE_5_DTO.setColumns(TABLE_5_COLUMNS_DTO); TABLE_5_DTO.setConstraints(TABLE_5_CONSTRAINTS_DTO); TABLE_6.setDatabase(DATABASE_2); @@ -88,8 +92,8 @@ public abstract class AbstractUnitTest extends BaseTest { TABLE_7_CONSTRAINTS.getForeignKeys().get(1).getReferences().get(0).setForeignKey(TABLE_7_CONSTRAINTS.getForeignKeys().get(1)); TABLE_7_DTO.setColumns(TABLE_7_COLUMNS_DTO); TABLE_7_DTO.setConstraints(TABLE_7_CONSTRAINTS_DTO); - TABLE_7_CONSTRAINTS_DTO.getForeignKeys().get(0).getReferences().get(0).setForeignKey(TABLE_7_CONSTRAINTS_DTO.getForeignKeys().get(0)); - TABLE_7_CONSTRAINTS_DTO.getForeignKeys().get(1).getReferences().get(0).setForeignKey(TABLE_7_CONSTRAINTS_DTO.getForeignKeys().get(1)); + TABLE_7_CONSTRAINTS_DTO.getForeignKeys().get(0).getReferences().get(0).setForeignKey(TABLE_7_CONSTRAINTS_FOREIGN_KEY_BRIEF_0_DTO); + TABLE_7_CONSTRAINTS_DTO.getForeignKeys().get(1).getReferences().get(0).setForeignKey(TABLE_7_CONSTRAINTS_FOREIGN_KEY_BRIEF_1_DTO); VIEW_4.setDatabase(DATABASE_2); IDENTIFIER_5.setDatabase(DATABASE_2); /* DATABASE 3 */ 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 b786195756..fa47be8af8 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 @@ -22,10 +22,7 @@ import at.tuwien.api.database.table.columns.*; import at.tuwien.api.database.table.columns.concepts.*; import at.tuwien.api.database.table.constraints.ConstraintsCreateDto; import at.tuwien.api.database.table.constraints.ConstraintsDto; -import at.tuwien.api.database.table.constraints.foreign.ForeignKeyCreateDto; -import at.tuwien.api.database.table.constraints.foreign.ForeignKeyDto; -import at.tuwien.api.database.table.constraints.foreign.ForeignKeyReferenceDto; -import at.tuwien.api.database.table.constraints.foreign.ReferenceTypeDto; +import at.tuwien.api.database.table.constraints.foreign.*; import at.tuwien.api.database.table.constraints.primary.PrimaryKeyDto; import at.tuwien.api.database.table.constraints.unique.UniqueDto; import at.tuwien.api.database.table.internal.PrivilegedTableDto; @@ -189,7 +186,8 @@ public abstract class BaseTest { public final static String[] ESCALATED_QUERY_HANDLING = new String[]{"escalated-query-handling"}; public final static String[] DEFAULT_TABLE_HANDLING = new String[]{"default-table-handling", - "list-tables", "create-table", "modify-table-column-semantics", "find-table", "delete-table"}; + "list-tables", "create-table", "modify-table-column-semantics", "find-table", "delete-table", + "update-table-statistic"}; public final static String[] ESCALATED_TABLE_HANDLING = new String[]{"escalated-table-handling", "delete-foreign-table"}; @@ -1243,6 +1241,7 @@ public abstract class BaseTest { .containerId(CONTAINER_1_ID) .username(USER_1_USERNAME) .password(USER_1_PASSWORD) + .userId(USER_1_ID) .privilegedUsername(CONTAINER_1_PRIVILEGED_USERNAME) .privilegedPassword(CONTAINER_1_PRIVILEGED_PASSWORD) .build(); @@ -1510,7 +1509,7 @@ public abstract class BaseTest { public final static String TABLE_1_INTERNALNAME = "weather_aus"; public final static Boolean TABLE_1_VERSIONED = true; public final static Boolean TABLE_1_PROCESSED_CONSTRAINTS = true; - public final static String TABLE_1_DESCRIPTION = "Weather in the world"; + public final static String TABLE_1_DESCRIPTION = "Weather in Australia"; public final static String TABLE_1_QUEUE_NAME = TABLE_1_INTERNALNAME; public final static String TABLE_1_ROUTING_KEY = "dbrepo\\." + DATABASE_1_ID + "\\." + TABLE_1_ID; public final static Long TABLE_1_DATABASE_ID = DATABASE_1_ID; @@ -1592,6 +1591,8 @@ public abstract class BaseTest { public final static List<ColumnDto> TABLE_1_COLUMNS_DTO = List.of(ColumnDto.builder() .id(1L) .table(TABLE_1_DTO) + .tableId(TABLE_1_ID) + .databaseId(DATABASE_1_ID) .name("id") .internalName("id") .ordinalPosition(0) @@ -1604,6 +1605,8 @@ public abstract class BaseTest { ColumnDto.builder() .id(2L) .table(TABLE_1_DTO) + .tableId(TABLE_1_ID) + .databaseId(DATABASE_1_ID) .name("Date") .internalName("date") .ordinalPosition(1) @@ -1617,6 +1620,8 @@ public abstract class BaseTest { ColumnDto.builder() .id(3L) .table(TABLE_1_DTO) + .tableId(TABLE_1_ID) + .databaseId(DATABASE_1_ID) .name("Location") .internalName("location") .ordinalPosition(2) @@ -1630,6 +1635,8 @@ public abstract class BaseTest { ColumnDto.builder() .id(4L) .table(TABLE_1_DTO) + .tableId(TABLE_1_ID) + .databaseId(DATABASE_1_ID) .name("MinTemp") .internalName("mintemp") .ordinalPosition(3) @@ -1644,6 +1651,8 @@ public abstract class BaseTest { ColumnDto.builder() .id(5L) .table(TABLE_1_DTO) + .tableId(TABLE_1_ID) + .databaseId(DATABASE_1_ID) .name("Rainfall") .internalName("rainfall") .ordinalPosition(4) @@ -1664,7 +1673,6 @@ public abstract class BaseTest { .isVersioned(TABLE_1_VERSIONED) .description(TABLE_1_DESCRIPTION) .name(TABLE_1_NAME) - .columns(new LinkedList<>() /* TABLE_1_COLUMNS */) .owner(USER_1_BRIEF_DTO) .build(); @@ -1756,7 +1764,6 @@ public abstract class BaseTest { .isVersioned(TABLE_2_VERSIONED) .description(TABLE_2_DESCRIPTION) .name(TABLE_2_NAME) - .columns(new LinkedList<>() /* TABLE_2_COLUMNS */) .owner(USER_2_BRIEF_DTO) .build(); @@ -1825,7 +1832,6 @@ public abstract class BaseTest { .isVersioned(TABLE_3_VERSIONED) .description(TABLE_3_DESCRIPTION) .name(TABLE_3_NAME) - .columns(new LinkedList<>() /* TABLE_3_COLUMNS */) .owner(USER_3_BRIEF_DTO) .build(); @@ -1868,6 +1874,10 @@ public abstract class BaseTest { public final static String TABLE_5_ROUTING_KEY = "dbrepo\\." + DATABASE_2_ID + "\\." + TABLE_5_ID; public final static Instant TABLE_5_CREATED = Instant.ofEpochSecond(1677400067L) /* 2023-02-26 08:27:47 (UTC) */; public final static Instant TABLE_5_LAST_MODIFIED = Instant.ofEpochSecond(1677400067L) /* 2023-02-26 08:27:47 (UTC) */; + public final static Long TABLE_5_AVG_ROW_LENGTH = 1080L; + public final static Long TABLE_5_NUM_ROWS = 101L; + public final static Long TABLE_5_DATA_LENGTH = 15200L; + public final static Long TABLE_5_MAX_DATA_LENGTH = Long.MAX_VALUE; public final static Table TABLE_5 = Table.builder() .id(TABLE_5_ID) @@ -1902,13 +1912,35 @@ public abstract class BaseTest { .owner(USER_1_DTO) .build(); - public final static TableBriefDto TABLE_5_BRIEF_DTO = TableBriefDto.builder() + public final static PrivilegedTableDto TABLE_5_PRIVILEGED_DTO = PrivilegedTableDto.builder() .id(TABLE_5_ID) + .tdbid(DATABASE_2_ID) + .database(null) /* DATABASE_2_PRIVILEGED_DTO */ + .created(TABLE_5_CREATED) .internalName(TABLE_5_INTERNALNAME) .isVersioned(TABLE_5_VERSIONED) .description(TABLE_5_DESCRIPTION) .name(TABLE_5_NAME) + .queueName(TABLE_5_QUEUE_NAME) + .routingKey(TABLE_5_ROUTING_KEY) + .identifiers(new LinkedList<>()) .columns(new LinkedList<>() /* TABLE_5_COLUMNS_DTO */) + .constraints(null) /* TABLE_5_CONSTRAINTS_DTO */ + .createdBy(USER_5_ID) + .owner(USER_5_DTO) + .isPublic(DATABASE_2_PUBLIC) + .avgRowLength(TABLE_5_AVG_ROW_LENGTH) + .numRows(TABLE_5_NUM_ROWS) + .dataLength(TABLE_5_DATA_LENGTH) + .maxDataLength(TABLE_5_MAX_DATA_LENGTH) + .build(); + + public final static TableBriefDto TABLE_5_BRIEF_DTO = TableBriefDto.builder() + .id(TABLE_5_ID) + .internalName(TABLE_5_INTERNALNAME) + .isVersioned(TABLE_5_VERSIONED) + .description(TABLE_5_DESCRIPTION) + .name(TABLE_5_NAME) .owner(USER_1_BRIEF_DTO) .build(); @@ -1964,7 +1996,6 @@ public abstract class BaseTest { .isVersioned(TABLE_6_VERSIONED) .description(TABLE_6_DESCRIPTION) .name(TABLE_6_NAME) - .columns(new LinkedList<>()) /* TABLE_6_COLUMNS_DTO */ .owner(USER_1_BRIEF_DTO) .build(); @@ -2020,7 +2051,6 @@ public abstract class BaseTest { .isVersioned(TABLE_7_VERSIONED) .description(TABLE_7_DESCRIPTION) .name(TABLE_7_NAME) - .columns(new LinkedList<>()) /* TABLE_7_COLUMNS_DTO */ .owner(USER_1_BRIEF_DTO) .build(); @@ -2088,7 +2118,6 @@ public abstract class BaseTest { .internalName(TABLE_4_INTERNALNAME) .description(TABLE_4_DESCRIPTION) .name(TABLE_4_NAME) - .columns(new LinkedList<>() /* TABLE_4_COLUMNS */) .isVersioned(TABLE_4_VERSIONED) .owner(USER_1_BRIEF_DTO) .build(); @@ -2232,7 +2261,6 @@ public abstract class BaseTest { .description(TABLE_8_DESCRIPTION) .isVersioned(TABLE_8_VERSIONED) .name(TABLE_8_NAME) - .columns(new LinkedList<>()) /* TABLE_8_COLUMNS_DTO */ .owner(USER_1_BRIEF_DTO) .build(); @@ -2748,8 +2776,6 @@ public abstract class BaseTest { .columnType(TableColumnType.BIGINT) .isNullAllowed(false) .autoGenerated(false) - .enums(null) - .sets(null) .build(), TableColumn.builder() .id(2L) @@ -2761,8 +2787,6 @@ public abstract class BaseTest { .dateFormat(IMAGE_DATE_1) .isNullAllowed(true) .autoGenerated(false) - .enums(null) - .sets(null) .build(), TableColumn.builder() .id(3L) @@ -2774,8 +2798,6 @@ public abstract class BaseTest { .size(255L) .isNullAllowed(true) .autoGenerated(false) - .enums(null) - .sets(null) .build(), TableColumn.builder() .id(4L) @@ -2788,8 +2810,6 @@ public abstract class BaseTest { .d(0L) .isNullAllowed(true) .autoGenerated(false) - .enums(null) - .sets(null) .build(), TableColumn.builder() .id(5L) @@ -2804,10 +2824,69 @@ public abstract class BaseTest { .unit(UNIT_1) .isNullAllowed(true) .autoGenerated(false) + .build()); + + public final static List<ColumnCreateDto> TABLE_1_COLUMNS_CREATE_DTO = List.of(ColumnCreateDto.builder() + .name("id") + .type(ColumnTypeDto.BIGINT) + .nullAllowed(false) .enums(null) .sets(null) + .build(), + ColumnCreateDto.builder() + .name("Date") + .type(ColumnTypeDto.DATE) + .nullAllowed(true) + .dfid(IMAGE_DATE_1_ID) + .build(), + ColumnCreateDto.builder() + .name("Location") + .type(ColumnTypeDto.VARCHAR) + .size(255L) + .nullAllowed(true) + .dfid(IMAGE_DATE_1_ID) + .build(), + ColumnCreateDto.builder() + .name("MinTemp") + .type(ColumnTypeDto.DECIMAL) + .size(10L) + .d(0L) + .nullAllowed(true) + .dfid(IMAGE_DATE_1_ID) + .build(), + ColumnCreateDto.builder() + .name("Rainfall") + .type(ColumnTypeDto.DECIMAL) + .size(10L) + .d(0L) + .nullAllowed(true) + .dfid(IMAGE_DATE_1_ID) + .conceptUri(CONCEPT_1_URI) + .unitUri(UNIT_1_URI) .build()); + public final static ConstraintsCreateDto TABLE_1_CONSTRAINTS_CREATE_DTO = ConstraintsCreateDto.builder() + .checks(new LinkedHashSet<>()) + .primaryKey(new LinkedHashSet<>(List.of("id"))) + .foreignKeys(new LinkedList<>()) + .uniques(List.of(List.of("date"))) + .build(); + + public final static TableCreateDto TABLE_1_CREATE_DTO = TableCreateDto.builder() + .name(TABLE_1_NAME) + .description(TABLE_1_DESCRIPTION) + .columns(TABLE_1_COLUMNS_CREATE_DTO) + .constraints(TABLE_1_CONSTRAINTS_CREATE_DTO) + .build(); + + public final static at.tuwien.api.database.table.internal.TableCreateDto TABLE_1_CREATE_INTERNAL_DTO = at.tuwien.api.database.table.internal.TableCreateDto.builder() + .name(TABLE_1_NAME) + .description(TABLE_1_DESCRIPTION) + .columns(TABLE_1_COLUMNS_CREATE_DTO) + .constraints(TABLE_1_CONSTRAINTS_CREATE_DTO) + .needSequence(true) + .build(); + public final static List<TableColumn> TABLE_2_COLUMNS = List.of(TableColumn.builder() .id(6L) .ordinalPosition(0) @@ -2860,9 +2939,18 @@ public abstract class BaseTest { .columnType(ColumnTypeDto.VARCHAR) .build(); + public final static ColumnBriefDto TABLE_2_COLUMNS_BRIEF_2_DTO = ColumnBriefDto.builder() + .id(8L) + .name("lng") + .internalName("lng") + .columnType(ColumnTypeDto.DECIMAL) + .build(); + public final static List<ColumnDto> TABLE_2_COLUMNS_DTO = List.of(ColumnDto.builder() .id(6L) .table(TABLE_2_DTO) + .tableId(TABLE_2_ID) + .databaseId(DATABASE_1_ID) .name("location") .internalName("location") .ordinalPosition(0) @@ -2876,12 +2964,13 @@ public abstract class BaseTest { ColumnDto.builder() .id(7L) .table(TABLE_2_DTO) + .tableId(TABLE_2_ID) + .databaseId(DATABASE_1_ID) .name("lat") .internalName("lat") .ordinalPosition(1) - .columnType(ColumnTypeDto.DECIMAL) - .size(10L) - .d(0L) + .columnType(ColumnTypeDto.DOUBLE) + .size(22L) .isNullAllowed(true) .autoGenerated(false) .enums(null) @@ -2890,12 +2979,13 @@ public abstract class BaseTest { ColumnDto.builder() .id(8L) .table(TABLE_2_DTO) + .tableId(TABLE_2_ID) + .databaseId(DATABASE_1_ID) .name("lng") .internalName("lng") .ordinalPosition(2) - .columnType(ColumnTypeDto.DECIMAL) - .size(10L) - .d(0L) + .columnType(ColumnTypeDto.DOUBLE) + .size(22L) .isNullAllowed(true) .autoGenerated(false) .enums(null) @@ -3365,6 +3455,7 @@ public abstract class BaseTest { public final static List<ColumnDto> TABLE_3_COLUMNS_DTO = List.of(ColumnDto.builder() .id(9L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(true) .columnType(ColumnTypeDto.BIGINT) @@ -3378,6 +3469,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(10L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3391,6 +3483,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(11L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3404,6 +3497,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(12L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.DATE) @@ -3417,6 +3511,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(13L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3430,6 +3525,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(14L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3443,6 +3539,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(15L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3456,6 +3553,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(16L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3469,6 +3567,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(17L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3482,6 +3581,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(18L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3495,6 +3595,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(19L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.DATE) @@ -3508,6 +3609,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(20L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3521,6 +3623,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(21L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3534,6 +3637,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(22L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3547,6 +3651,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(23L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3560,6 +3665,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(24L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3573,6 +3679,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(25L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3586,6 +3693,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(26L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3599,6 +3707,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(27L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3612,6 +3721,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(28L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.DATE) @@ -3625,6 +3735,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(29L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3638,6 +3749,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(30L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3651,6 +3763,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(31L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3664,6 +3777,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(32L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3677,6 +3791,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(33L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3690,6 +3805,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(34L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3703,6 +3819,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(35L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3716,6 +3833,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(36L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3729,6 +3847,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(37L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3742,6 +3861,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(38L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3755,6 +3875,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(39L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3768,6 +3889,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(40L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3781,6 +3903,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(41L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3794,6 +3917,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(42L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -3807,6 +3931,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(43L) .tableId(TABLE_3_ID) + .table(TABLE_3_DTO) .databaseId(DATABASE_1_ID) .autoGenerated(false) .columnType(ColumnTypeDto.INT) @@ -4039,6 +4164,7 @@ public abstract class BaseTest { public final static List<ColumnDto> TABLE_5_COLUMNS_DTO = List.of(ColumnDto.builder() .id(45L) .ordinalPosition(0) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("id") .internalName("id") @@ -4049,6 +4175,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(46L) .ordinalPosition(1) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Animal Name") .internalName("animal_name") @@ -4059,6 +4186,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(47L) .ordinalPosition(2) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Hair") .internalName("hair") @@ -4069,6 +4197,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(48L) .ordinalPosition(3) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Feathers") .internalName("feathers") @@ -4079,6 +4208,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(49L) .ordinalPosition(4) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Bread") .internalName("bread") @@ -4089,6 +4219,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(50L) .ordinalPosition(5) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Eggs") .internalName("eggs") @@ -4099,6 +4230,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(51L) .ordinalPosition(6) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Milk") .internalName("milk") @@ -4109,6 +4241,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(52L) .ordinalPosition(7) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Water") .internalName("water") @@ -4119,6 +4252,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(53L) .ordinalPosition(8) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Airborne") .internalName("airborne") @@ -4129,6 +4263,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(54L) .ordinalPosition(9) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Waterborne") .internalName("waterborne") @@ -4139,6 +4274,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(55L) .ordinalPosition(10) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Aquantic") .internalName("aquantic") @@ -4149,6 +4285,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(56L) .ordinalPosition(11) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Predator") .internalName("predator") @@ -4159,6 +4296,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(57L) .ordinalPosition(12) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Backbone") .internalName("backbone") @@ -4169,6 +4307,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(58L) .ordinalPosition(13) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Breathes") .internalName("breathes") @@ -4179,6 +4318,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(59L) .ordinalPosition(14) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Venomous") .internalName("venomous") @@ -4189,6 +4329,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(60L) .ordinalPosition(15) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Fin") .internalName("fin") @@ -4199,6 +4340,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(61L) .ordinalPosition(16) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Legs") .internalName("legs") @@ -4209,6 +4351,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(62L) .ordinalPosition(17) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Tail") .internalName("tail") @@ -4219,6 +4362,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(63L) .ordinalPosition(18) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Domestic") .internalName("domestic") @@ -4229,6 +4373,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(64L) .ordinalPosition(19) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Catsize") .internalName("catsize") @@ -4239,6 +4384,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(64L) .ordinalPosition(20) + .tableId(TABLE_5_ID) .table(TABLE_5_DTO) .name("Class Type") .internalName("class_type") @@ -4456,6 +4602,7 @@ public abstract class BaseTest { public final static List<ColumnDto> TABLE_6_COLUMNS_DTO = List.of(ColumnDto.builder() .id(66L) .ordinalPosition(0) + .tableId(TABLE_6_ID) .table(TABLE_6_DTO) .name("id") .internalName("id") @@ -4466,6 +4613,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(67L) .ordinalPosition(1) + .tableId(TABLE_6_ID) .table(TABLE_6_DTO) .name("firstname") .internalName("firstname") @@ -4476,6 +4624,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(68L) .ordinalPosition(2) + .tableId(TABLE_6_ID) .table(TABLE_6_DTO) .name("lastname") .internalName("lastname") @@ -4486,6 +4635,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(69L) .ordinalPosition(3) + .tableId(TABLE_6_ID) .table(TABLE_6_DTO) .name("birth") .internalName("birth") @@ -4496,6 +4646,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(70L) .ordinalPosition(4) + .tableId(TABLE_6_ID) .table(TABLE_6_DTO) .name("reminder") .internalName("reminder") @@ -4507,6 +4658,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(71L) .ordinalPosition(5) + .tableId(TABLE_6_ID) .table(TABLE_6_DTO) .name("ref_id") .internalName("ref_id") @@ -4560,6 +4712,13 @@ public abstract class BaseTest { .columnType(ColumnTypeDto.BIGINT) .build(); + public final static ColumnBriefDto TABLE_7_COLUMNS_BRIEF_1_DTO = ColumnBriefDto.builder() + .id(27L) + .name("zoo_id") + .internalName("zoo_id") + .columnType(ColumnTypeDto.BIGINT) + .build(); + public final static List<TableColumn> TABLE_7_COLUMNS = List.of(TableColumn.builder() .id(26L) .ordinalPosition(0) @@ -4584,6 +4743,7 @@ public abstract class BaseTest { public final static List<ColumnDto> TABLE_7_COLUMNS_DTO = List.of(ColumnDto.builder() .id(26L) .ordinalPosition(0) + .tableId(TABLE_7_ID) .table(TABLE_7_DTO) .name("name_id") .internalName("name_id") @@ -4594,6 +4754,7 @@ public abstract class BaseTest { ColumnDto.builder() .id(27L) .ordinalPosition(1) + .tableId(TABLE_7_ID) .table(TABLE_7_DTO) .name("zoo_id") .internalName("zoo_id") @@ -4615,6 +4776,7 @@ public abstract class BaseTest { public final static List<ViewColumnDto> VIEW_1_COLUMNS_DTO = List.of( ViewColumnDto.builder() .id(1L) + .databaseId(DATABASE_1_ID) .name("location") .internalName("location") .ordinalPosition(0) @@ -4625,23 +4787,23 @@ public abstract class BaseTest { .build(), ViewColumnDto.builder() .id(2L) + .databaseId(DATABASE_1_ID) .name("lat") .internalName("lat") .ordinalPosition(1) - .columnType(ColumnTypeDto.DECIMAL) - .size(10L) - .d(0L) + .columnType(ColumnTypeDto.DOUBLE) + .size(22L) .isNullAllowed(true) .autoGenerated(false) .build(), ViewColumnDto.builder() .id(3L) + .databaseId(DATABASE_1_ID) .name("lng") .internalName("lng") .ordinalPosition(2) - .columnType(ColumnTypeDto.DECIMAL) - .size(10L) - .d(0L) + .columnType(ColumnTypeDto.DOUBLE) + .size(22L) .isNullAllowed(true) .autoGenerated(false) .build() @@ -4738,6 +4900,7 @@ public abstract class BaseTest { .query(VIEW_1_QUERY) .queryHash(VIEW_1_QUERY_HASH) .columns(VIEW_1_COLUMNS_DTO) + .database(null) .build(); public final static PrivilegedViewDto VIEW_1_PRIVILEGED_DTO = PrivilegedViewDto.builder() @@ -7412,16 +7575,17 @@ public abstract class BaseTest { public final static ConstraintsDto TABLE_2_CONSTRAINTS_DTO = ConstraintsDto.builder() .checks(new LinkedHashSet<>(List.of("`mintemp` > 0"))) .foreignKeys(new LinkedList<>(List.of(ForeignKeyDto.builder() + .id(1L) .name("fk_location") .onDelete(ReferenceTypeDto.NO_ACTION) .references(new LinkedList<>(List.of(ForeignKeyReferenceDto.builder() .id(1L) - .column(TABLE_2_COLUMNS_DTO.get(2)) - .referencedColumn(TABLE_1_COLUMNS_DTO.get(0)) + .column(TABLE_2_COLUMNS_BRIEF_2_DTO) + .referencedColumn(TABLE_1_COLUMNS_BRIEF_0_DTO) .foreignKey(null) // set later .build()))) - .table(TABLE_1_DTO) - .referencedTable(TABLE_2_DTO) + .table(TABLE_1_BRIEF_DTO) + .referencedTable(TABLE_2_BRIEF_DTO) .onUpdate(ReferenceTypeDto.NO_ACTION) .build()))) .uniques(new LinkedList<>(List.of(UniqueDto.builder() @@ -7561,34 +7725,48 @@ public abstract class BaseTest { .build()))) .build(); + public final static ForeignKeyDto TABLE_7_CONSTRAINTS_FOREIGN_KEY_0_DTO = ForeignKeyDto.builder() + .id(2L) + .name("fk_name_id") + .onDelete(ReferenceTypeDto.NO_ACTION) + .references(new LinkedList<>(List.of(ForeignKeyReferenceDto.builder() + .id(2L) + .column(TABLE_6_COLUMNS_BRIEF_0_DTO) + .referencedColumn(TABLE_7_COLUMNS_BRIEF_0_DTO) + .foreignKey(null) // set later + .build()))) + .table(TABLE_7_BRIEF_DTO) + .referencedTable(TABLE_6_BRIEF_DTO) + .onUpdate(ReferenceTypeDto.NO_ACTION) + .build(); + + public final static ForeignKeyBriefDto TABLE_7_CONSTRAINTS_FOREIGN_KEY_BRIEF_0_DTO = ForeignKeyBriefDto.builder() + .id(2L) + .build(); + + public final static ForeignKeyDto TABLE_7_CONSTRAINTS_FOREIGN_KEY_1_DTO = ForeignKeyDto.builder() + .id(3L) + .name("fk_zoo_id") + .onDelete(ReferenceTypeDto.NO_ACTION) + .references(new LinkedList<>(List.of(ForeignKeyReferenceDto.builder() + .id(3L) + .column(TABLE_5_COLUMNS_BRIEF_0_DTO) + .referencedColumn(TABLE_7_COLUMNS_BRIEF_1_DTO) + .foreignKey(null) // set later + .build()))) + .table(TABLE_7_BRIEF_DTO) + .referencedTable(TABLE_5_BRIEF_DTO) + .onUpdate(ReferenceTypeDto.NO_ACTION) + .build(); + + public final static ForeignKeyBriefDto TABLE_7_CONSTRAINTS_FOREIGN_KEY_BRIEF_1_DTO = ForeignKeyBriefDto.builder() + .id(3L) + .build(); + public final static ConstraintsDto TABLE_7_CONSTRAINTS_DTO = ConstraintsDto.builder() .checks(new LinkedHashSet<>()) - .foreignKeys(new LinkedList<>(List.of(ForeignKeyDto.builder() - .name("fk_name_id") - .onDelete(ReferenceTypeDto.NO_ACTION) - .references(new LinkedList<>(List.of(ForeignKeyReferenceDto.builder() - .id(2L) - .column(TABLE_6_COLUMNS_DTO.get(0)) - .referencedColumn(TABLE_7_COLUMNS_DTO.get(0)) - .foreignKey(null) // set later - .build()))) - .table(TABLE_7_DTO) - .referencedTable(TABLE_6_DTO) - .onUpdate(ReferenceTypeDto.NO_ACTION) - .build(), - ForeignKeyDto.builder() - .name("fk_zoo_id") - .onDelete(ReferenceTypeDto.NO_ACTION) - .references(new LinkedList<>(List.of(ForeignKeyReferenceDto.builder() - .id(3L) - .column(TABLE_5_COLUMNS_DTO.get(0)) - .referencedColumn(TABLE_7_COLUMNS_DTO.get(1)) - .foreignKey(null) // set later - .build()))) - .table(TABLE_7_DTO) - .referencedTable(TABLE_5_DTO) - .onUpdate(ReferenceTypeDto.NO_ACTION) - .build()))) + .foreignKeys(new LinkedList<>(List.of(TABLE_7_CONSTRAINTS_FOREIGN_KEY_0_DTO, + TABLE_7_CONSTRAINTS_FOREIGN_KEY_1_DTO))) .uniques(new LinkedList<>()) .primaryKey(new LinkedHashSet<>(Set.of(PrimaryKeyDto.builder() .table(TABLE_7_BRIEF_DTO) diff --git a/dbrepo-search-service/Dockerfile b/dbrepo-search-service/Dockerfile index dfa23dfe8c..875a9f28bd 100644 --- a/dbrepo-search-service/Dockerfile +++ b/dbrepo-search-service/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.11-alpine MAINTAINER Martin Weise <martin.weise@tuwien.ac.at> -RUN apk add bash curl +RUN apk add --no-cache curl bash jq WORKDIR /home/alpine diff --git a/dbrepo-search-service/Pipfile.lock b/dbrepo-search-service/Pipfile.lock index c85c4145f4..a0b551b34a 100644 --- a/dbrepo-search-service/Pipfile.lock +++ b/dbrepo-search-service/Pipfile.lock @@ -302,45 +302,45 @@ }, "cryptography": { "hashes": [ - "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55", - "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785", - "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b", - "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886", - "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82", - "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1", - "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda", - "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f", - "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68", - "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60", - "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7", - "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd", - "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582", - "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc", - "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858", - "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b", - "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2", - "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678", - "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13", - "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4", - "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8", - "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604", - "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477", - "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e", - "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a", - "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9", - "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14", - "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda", - "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da", - "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562", - "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2", - "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9" + "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad", + "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", + "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", + "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", + "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", + "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648", + "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", + "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", + "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c", + "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", + "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", + "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", + "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", + "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", + "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", + "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", + "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", + "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", + "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", + "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", + "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", + "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", + "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", + "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", + "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", + "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", + "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", + "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842", + "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", + "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", + "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", + "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e" ], "markers": "python_version >= '3.7'", - "version": "==42.0.7" + "version": "==42.0.8" }, "dbrepo": { "hashes": [ - "sha256:110db9e4e70f5656a6351409d4b022656abf7de0bd72d5e061a25685f708d9a4" + "sha256:2bdb48c70b4c99b5044fbfc12aa653c1e9281ca8913a433cc08a1e14cb4bd2ef" ], "path": "./lib/dbrepo-1.4.4.tar.gz" }, @@ -1042,12 +1042,12 @@ }, "pytest": { "hashes": [ - "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd", - "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1" + "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", + "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==8.2.1" + "version": "==8.2.2" }, "python-dateutil": { "hashes": [ @@ -1654,12 +1654,12 @@ }, "pytest": { "hashes": [ - "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd", - "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1" + "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", + "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==8.2.1" + "version": "==8.2.2" } } } diff --git a/dbrepo-search-service/app.py b/dbrepo-search-service/app.py index 844c01709f..5d3c816ffd 100644 --- a/dbrepo-search-service/app.py +++ b/dbrepo-search-service/app.py @@ -62,8 +62,7 @@ swagger_config = { { "endpoint": "api-search", "route": "/api-search.json", - "rule_filter": lambda rule: rule.endpoint.startswith('actuator') or rule.endpoint.startswith( - 'search') or rule.endpoint.startswith('database'), + "rule_filter": lambda rule: rule.endpoint.startswith('search'), "model_filter": lambda tag: True, # all in } ], @@ -75,6 +74,79 @@ swagger_config = { template = { "openapi": "3.0.0", "components": { + "schemas": { + "IndexDto": { + "required": ["results", "type"], + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + } + }, + "type": { + "type": "string", + "description": "Same as the requested type", + "enum": ["database", "table", "view", "column", "user", "identifier", "concept", "unit"] + } + } + }, + "IndexFieldsDto": { + "required": ["results"], + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IndexFieldDto" + } + } + } + }, + "IndexFieldDto": { + "required": ["attr_name", "attr_friendly_name", "type"], + "type": "object", + "properties": { + "attr_name": { + "type": "string", + "example": "name" + }, + "attr_friendly_name": { + "type": "string", + "example": "Name" + }, + "type": { + "type": "string", + "example": "string", + "description": "OpenSearch data types." + } + } + }, + "SearchResultDto": { + "required": ["results"], + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "SearchRequestDto": { + "required": ["search_term", "field_value_pairs"], + "type": "object", + "properties": { + "search_term": { + "type": "string" + }, + "field_value_pairs": { + "type": "object" + } + } + } + }, "securitySchemes": { "bearerAuth": { "type": "http", @@ -92,7 +164,7 @@ template = { "info": { "title": "Database Repository Search Service API", "description": "Service that searches the search database", - "version": "__APPVERSION__", + "version": "1.4.4", "contact": { "name": "Prof. Andreas Rauber", "email": "andreas.rauber@tuwien.ac.at" @@ -104,7 +176,7 @@ template = { }, "externalDocs": { "description": "Sourcecode Documentation", - "url": "https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/__APPVERSION__/" + "url": "https://www.ifs.tuwien.ac.at/infrastructures/dbrepo/1.4.4/" }, "servers": [ { @@ -211,7 +283,6 @@ def general_filter(index, results): @app.route("/health", methods=["GET"], endpoint="actuator_health") -@swag_from("os-yml/health.yml") def health(): return dict({"status": "UP"}), 200 @@ -356,14 +427,12 @@ def post_general_search(type): return dict({'results': response, 'type': type}), 200 -@app.route("/api/search/database/<int:database_id>", methods=["PUT"], endpoint="database_put_database") +@app.route("/api/search/database/<int:database_id>", methods=["PUT"], endpoint="search_put_database") @metrics.gauge(name='dbrepo_search_update_database', description='Time needed to update a database in the search database') @auth.login_required(role=['admin']) -@swag_from("os-yml/update_database.yml") def update_database(database_id: int) -> Database | ApiError: logging.debug(f"updating database with id: {database_id}") - logging.debug(f"====> {request.json}") try: payload: Database = Database.model_validate(request.json) except ValidationError as e: @@ -386,7 +455,6 @@ def update_database(database_id: int) -> Database | ApiError: @metrics.gauge(name='dbrepo_search_delete_database', description='Time needed to delete a database in the search database') @auth.login_required(role=['admin']) -@swag_from("os-yml/delete_database.yml") def delete_database(database_id: int): try: OpenSearchClient().delete_database(database_id) diff --git a/dbrepo-search-service/init/database.json b/dbrepo-search-service/init/database.json index 4e5200c06a..d87d33b5e2 100644 --- a/dbrepo-search-service/init/database.json +++ b/dbrepo-search-service/init/database.json @@ -2,38 +2,123 @@ "aliases": {}, "mappings": { "properties": { - "contact": { - "type": "object", + "accesses": { "properties": { - "firstname": { - "type": "keyword" + "created": { + "type": "date" }, - "id": { - "type": "keyword" + "type": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, - "lastname": { - "type": "keyword" + "user": { + "properties": { + "attributes": { + "properties": { + "theme": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "qualified_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "username": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + } + } + }, + "contact": { + "properties": { + "attributes": { + "properties": { + "theme": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } }, - "name": { - "type": "keyword" + "id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "qualified_name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "username": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "container": { - "type": "object", "properties": { "created": { - "type": "date", - "format": "strict_date_optional_time" + "type": "date" }, "host": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "id": { "type": "long" @@ -43,8 +128,7 @@ "date_formats": { "properties": { "created_at": { - "type": "date", - "format": "strict_date_optional_time" + "type": "date" }, "database_format": { "type": "text", @@ -84,6 +168,15 @@ } } }, + "driver_class": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, "id": { "type": "long" }, @@ -105,6 +198,15 @@ } } }, + "registry": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, "version": { "type": "text", "fields": { @@ -117,505 +219,406 @@ } }, "internal_name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "port": { - "type": "integer" + "type": "long" }, "sidecar_host": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "sidecar_port": { - "type": "integer" + "type": "long" }, "ui_host": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "ui_port": { - "type": "integer" + "type": "long" } } }, "created": { - "type": "date", - "format": "strict_date_optional_time" + "type": "date" }, - "description": { - "type": "text" + "creator": { + "properties": { + "attributes": { + "properties": { + "theme": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "qualified_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "username": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } }, "exchange_name": { - "type": "keyword" - }, - "exchange_type": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "id": { - "type": "keyword" + "type": "long" }, "identifiers": { - "type": "object", "properties": { "created": { - "type": "date", - "format": "strict_date_optional_time" + "type": "date" }, "creator": { - "type": "object", "properties": { - "firstname": { - "type": "keyword" - }, "id": { - "type": "keyword" - }, - "lastname": { - "type": "keyword" - }, - "name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "qualified_name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "username": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "creators": { - "type": "object", "properties": { - "affiliation": { - "type": "keyword" - }, - "affiliation_identifier": { - "type": "keyword" - }, - "affiliation_identifier_scheme": { - "type": "keyword" - }, - "affiliation_identifier_scheme_uri": { - "type": "keyword" - }, "creator_name": { - "type": "text" - }, - "firstname": { - "type": "text" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "id": { - "type": "keyword" - }, - "lastname": { - "type": "text" + "type": "long" }, "name_identifier": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "name_identifier_scheme": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "name_identifier_scheme_uri": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "name_type": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "database_id": { - "type": "keyword" + "type": "long" }, "descriptions": { - "type": "object", "properties": { "description": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "language": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "doi": { - "type": "keyword" - }, - "execution": { - "type": "date", - "format": "strict_date_optional_time" - }, - "funders": { - "type": "object", - "properties": { - "award_number": { - "type": "keyword" - }, - "award_title": { - "type": "keyword" - }, - "funder_identifier": { - "type": "keyword" - }, - "funder_identifier_type": { - "type": "keyword" - }, - "funder_name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "id": { - "type": "keyword" - }, - "scheme_uri": { - "type": "keyword" + "type": "long" } } }, "id": { - "type": "keyword" + "type": "long" }, - "language": { - "type": "keyword" + "last_modified": { + "type": "date" }, "licenses": { - "type": "object", "properties": { + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, "identifier": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "uri": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "publication_day": { - "type": "integer" + "type": "long" }, "publication_month": { - "type": "integer" + "type": "long" }, "publication_year": { - "type": "integer" + "type": "long" }, "publisher": { - "type": "text" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "query": { - "type": "text" - }, - "query_hash": { - "type": "text" - }, - "query_id": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "query_normalized": { - "type": "text" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, - "related_identifiers": { - "type": "object", - "properties": { - "created": { - "type": "date", - "format": "strict_date_optional_time" - }, - "id": { - "type": "keyword" - }, - "relation": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "keyword" + "status": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 } } }, - "result_hash": { - "type": "text" - }, - "result_number": { - "type": "long" - }, - "status": { - "type": "keyword" - }, - "table_id": { - "type": "keyword" - }, "titles": { - "type": "object", "properties": { "id": { - "type": "keyword" - }, - "language": { - "type": "keyword" + "type": "long" }, "title": { - "type": "keyword" - }, - "type": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "type": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "view_id": { - "type": "keyword" + "type": "long" + } + } + }, + "image": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 } } }, "internal_name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "is_public": { "type": "boolean" }, "name": { - "type": "keyword" - }, - "owner": { - "type": "object", - "properties": { - "firstname": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "lastname": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "qualified_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 } } }, - "subsets": { - "type": "object", + "owner": { "properties": { - "created": { - "type": "date", - "format": "strict_date_optional_time" - }, - "creators": { - "type": "object", - "properties": { - "affiliation": { - "type": "keyword" - }, - "affiliation_identifier": { - "type": "keyword" - }, - "affiliation_identifier_scheme": { - "type": "keyword" - }, - "affiliation_identifier_scheme_uri": { - "type": "keyword" - }, - "creator_name": { - "type": "text" - }, - "firstname": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "lastname": { - "type": "text" - }, - "name_identifier": { - "type": "keyword" - }, - "name_identifier_scheme": { - "type": "keyword" - }, - "name_identifier_scheme_uri": { - "type": "keyword" - }, - "name_type": { - "type": "keyword" - } - } - }, - "database_id": { - "type": "keyword" - }, - "descriptions": { - "type": "object", - "properties": { - "description": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "language": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "doi": { - "type": "keyword" - }, - "execution": { - "type": "date", - "format": "strict_date_optional_time" - }, - "funders": { - "type": "object", + "attributes": { "properties": { - "award_number": { - "type": "keyword" - }, - "award_title": { - "type": "keyword" - }, - "funder_identifier": { - "type": "keyword" - }, - "funder_identifier_type": { - "type": "keyword" - }, - "funder_name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "scheme_uri": { - "type": "keyword" + "theme": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "id": { - "type": "keyword" - }, - "language": { - "type": "keyword" - }, - "licenses": { - "type": "object", - "properties": { - "identifier": { - "type": "keyword" - }, - "uri": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 } } }, - "publication_day": { - "type": "integer" - }, - "publication_month": { - "type": "integer" - }, - "publication_year": { - "type": "integer" - }, - "publisher": { - "type": "text" - }, - "query": { - "type": "text" - }, - "query_hash": { - "type": "text" - }, - "query_id": { - "type": "keyword" - }, - "query_normalized": { - "type": "text" - }, - "related_identifiers": { - "type": "object", - "properties": { - "created": { - "type": "date", - "format": "strict_date_optional_time" - }, - "id": { - "type": "keyword" - }, - "relation": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "keyword" + "qualified_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 } } }, - "result_hash": { - "type": "text" - }, - "result_number": { - "type": "long" - }, - "status": { - "type": "keyword" - }, - "table_id": { - "type": "keyword" - }, - "titles": { - "type": "object", - "properties": { - "id": { - "type": "keyword" - }, - "language": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "type": { - "type": "keyword" + "username": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 } } - }, - "type": { - "type": "keyword" - }, - "view_id": { - "type": "keyword" } } }, "tables": { - "type": "object", "properties": { - "avg_row_length": { - "type": "long" - }, "columns": { "properties": { "auto_generated": { @@ -630,30 +633,43 @@ } } }, - "concept": { - "type": "object", + "d": { + "type": "long" + }, + "database_id": { + "type": "long" + }, + "date_format": { "properties": { - "created": { - "type": "date", - "format": "strict_date_optional_time" + "created_at": { + "type": "date" + }, + "database_format": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "has_time": { + "type": "boolean" }, "id": { "type": "long" }, - "uri": { - "type": "keyword" + "unix_format": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, - "database_id": { - "type": "long" - }, - "data_length": { - "type": "long" - }, - "description": { - "type": "text" - }, "id": { "type": "long" }, @@ -672,6 +688,9 @@ "is_public": { "type": "boolean" }, + "mean": { + "type": "float" + }, "name": { "type": "text", "fields": { @@ -681,115 +700,74 @@ } } }, - "num_rows": { - "type": "long" - }, - "max_data_length": { + "size": { "type": "long" }, - "mean": { - "type": "double" - }, - "median": { - "type": "double" - }, "std_dev": { - "type": "double" - }, - "size": { - "type": "long" + "type": "float" }, "table_id": { "type": "long" - }, - "unit": { - "type": "object", - "properties": { - "created": { - "type": "date", - "format": "strict_date_optional_time" - }, - "id": { - "type": "long" - }, - "uri": { - "type": "keyword" - } - } - }, - "val_min": { - "type": "double" - }, - "val_max": { - "type": "double" } } }, "constraints": { - "type": "object", "properties": { - "foreign_keys": { - "type": "object", + "primary_key": { "properties": { - "name": { - "type": "keyword" - }, - "columns": { - "type": "keyword" - }, - "referenced_table": { - "type": "keyword" - }, - "referenced_columns": { - "type": "keyword" + "column": { + "properties": { + "database_id": { + "type": "long" + }, + "id": { + "type": "long" + }, + "table_id": { + "type": "long" + } + } }, - "on_delete": { - "type": "keyword" + "id": { + "type": "long" }, - "on_update": { - "type": "keyword" + "table": { + "properties": { + "database_id": { + "type": "long" + }, + "id": { + "type": "long" + } + } } } }, "uniques": { - "type": "object", - "properties": { - "id": { - "type": "keyword" - } - } - }, - "checks": { - "type": "keyword" - }, - "primary_key": { - "type": "object", "properties": { - "id": { - "type": "keyword" - }, - "table": { - "type": "object", + "columns": { "properties": { + "database_id": { + "type": "long" + }, "id": { - "type": "keyword" + "type": "long" }, - "database_id": { - "type": "keyword" + "table_id": { + "type": "long" } } }, - "column": { - "type": "object", + "id": { + "type": "long" + }, + "table": { "properties": { - "id": { - "type": "keyword" - }, - "table_id": { - "type": "keyword" - }, "database_id": { - "type": "keyword" + "type": "long" + }, + "id": { + "type": "long" } } } @@ -798,223 +776,87 @@ } }, "created": { - "type": "date", - "format": "strict_date_optional_time" - }, - "database_id": { - "type": "keyword" - }, - "data_length": { - "type": "long" - }, - "description": { - "type": "text" - }, - "id": { - "type": "keyword" + "type": "date" + }, + "created_by": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, - "identifiers": { - "type": "object", + "creator": { "properties": { - "created": { - "type": "date", - "format": "strict_date_optional_time" - }, - "creators": { - "type": "object", - "properties": { - "affiliation": { - "type": "keyword" - }, - "affiliation_identifier": { - "type": "keyword" - }, - "affiliation_identifier_scheme": { - "type": "keyword" - }, - "affiliation_identifier_scheme_uri": { - "type": "keyword" - }, - "creator_name": { - "type": "text" - }, - "firstname": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "lastname": { - "type": "text" - }, - "name_identifier": { - "type": "keyword" - }, - "name_identifier_scheme": { - "type": "keyword" - }, - "name_identifier_scheme_uri": { - "type": "keyword" - }, - "name_type": { - "type": "keyword" - } - } - }, - "database_id": { - "type": "keyword" - }, - "descriptions": { - "type": "object", + "attributes": { "properties": { - "description": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "language": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "doi": { - "type": "keyword" - }, - "execution": { - "type": "date", - "format": "strict_date_optional_time" - }, - "funders": { - "type": "object", - "properties": { - "award_number": { - "type": "keyword" - }, - "award_title": { - "type": "keyword" - }, - "funder_identifier": { - "type": "keyword" - }, - "funder_identifier_type": { - "type": "keyword" - }, - "funder_name": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "scheme_uri": { - "type": "keyword" + "theme": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "id": { - "type": "keyword" - }, - "language": { - "type": "keyword" - }, - "licenses": { - "type": "object", - "properties": { - "identifier": { - "type": "keyword" - }, - "uri": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 } } }, - "publication_day": { - "type": "integer" - }, - "publication_month": { - "type": "integer" - }, - "publication_year": { - "type": "integer" - }, - "publisher": { - "type": "text" - }, - "query": { - "type": "text" - }, - "query_hash": { - "type": "text" - }, - "query_id": { - "type": "keyword" - }, - "query_normalized": { - "type": "text" - }, - "related_identifiers": { - "type": "object", - "properties": { - "created": { - "type": "date", - "format": "strict_date_optional_time" - }, - "id": { - "type": "keyword" - }, - "relation": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "keyword" + "qualified_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 } } }, - "result_hash": { - "type": "text" - }, - "result_number": { - "type": "long" - }, - "status": { - "type": "keyword" - }, - "table_id": { - "type": "keyword" - }, - "titles": { - "type": "object", - "properties": { - "id": { - "type": "keyword" - }, - "language": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "type": { - "type": "keyword" + "username": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 } } - }, - "type": { - "type": "keyword" - }, - "view_id": { - "type": "keyword" } } }, + "data_length": { + "type": "long" + }, + "database_id": { + "type": "long" + }, + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "id": { + "type": "long" + }, "internal_name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "is_public": { "type": "boolean" @@ -1023,258 +865,429 @@ "type": "boolean" }, "name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "num_rows": { "type": "long" }, - "max_data_length": { - "type": "long" - }, "owner": { - "type": "object", "properties": { - "firstname": { - "type": "keyword" + "attributes": { + "properties": { + "theme": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } }, "id": { - "type": "keyword" - }, - "lastname": { - "type": "keyword" - }, - "name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "qualified_name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "username": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "queue_name": { - "type": "keyword" - }, - "queue_type": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "routing_key": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "views": { - "type": "object", "properties": { + "columns": { + "properties": { + "auto_generated": { + "type": "boolean" + }, + "column_type": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "database_id": { + "type": "long" + }, + "date_format": { + "properties": { + "created_at": { + "type": "date" + }, + "database_format": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "has_time": { + "type": "boolean" + }, + "id": { + "type": "long" + }, + "unix_format": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "id": { + "type": "long" + }, + "internal_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "is_null_allowed": { + "type": "boolean" + }, + "is_public": { + "type": "boolean" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, "created": { - "type": "date", - "format": "strict_date_optional_time" + "type": "date" + }, + "creator": { + "properties": { + "attributes": { + "properties": { + "theme": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "qualified_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "username": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } }, "database_id": { - "type": "keyword" + "type": "long" }, "id": { - "type": "keyword" + "type": "long" }, "identifiers": { - "type": "object", "properties": { "created": { - "type": "date", - "format": "strict_date_optional_time" + "type": "date" }, - "creators": { - "type": "object", + "creator": { "properties": { - "affiliation": { - "type": "keyword" - }, - "affiliation_identifier": { - "type": "keyword" - }, - "affiliation_identifier_scheme": { - "type": "keyword" + "id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, - "affiliation_identifier_scheme_uri": { - "type": "keyword" + "qualified_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, + "username": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "creators": { + "properties": { "creator_name": { - "type": "text" - }, - "firstname": { - "type": "text" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "id": { - "type": "keyword" - }, - "lastname": { - "type": "text" + "type": "long" }, "name_identifier": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "name_identifier_scheme": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "name_identifier_scheme_uri": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "name_type": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "database_id": { - "type": "keyword" + "type": "long" }, "descriptions": { - "type": "object", "properties": { "description": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "language": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "doi": { - "type": "keyword" - }, - "execution": { - "type": "date", - "format": "strict_date_optional_time" - }, - "funders": { - "type": "object", - "properties": { - "award_number": { - "type": "keyword" - }, - "award_title": { - "type": "keyword" - }, - "funder_identifier": { - "type": "keyword" - }, - "funder_identifier_type": { - "type": "keyword" - }, - "funder_name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "id": { - "type": "keyword" - }, - "scheme_uri": { - "type": "keyword" + "type": "long" } } }, "id": { - "type": "keyword" + "type": "long" }, - "language": { - "type": "keyword" + "last_modified": { + "type": "date" }, "licenses": { - "type": "object", "properties": { + "description": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, "identifier": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "uri": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "publication_day": { - "type": "integer" + "type": "long" }, "publication_month": { - "type": "integer" + "type": "long" }, "publication_year": { - "type": "integer" + "type": "long" }, "publisher": { - "type": "text" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "query": { - "type": "text" - }, - "query_hash": { - "type": "text" - }, - "query_id": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "query_normalized": { - "type": "text" - }, - "related_identifiers": { - "type": "object", - "properties": { - "created": { - "type": "date", - "format": "strict_date_optional_time" - }, - "id": { - "type": "keyword" - }, - "relation": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 } } }, - "result_hash": { - "type": "text" - }, - "result_number": { - "type": "long" - }, "status": { - "type": "keyword" - }, - "table_id": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "titles": { - "type": "object", "properties": { "id": { - "type": "keyword" - }, - "language": { - "type": "keyword" + "type": "long" }, "title": { - "type": "keyword" - }, - "type": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } }, "type": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "view_id": { - "type": "keyword" + "type": "long" } } }, @@ -1282,19 +1295,46 @@ "type": "boolean" }, "internal_name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "is_public": { "type": "boolean" }, + "last_modified": { + "type": "date" + }, "name": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "query": { - "type": "text" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "query_hash": { - "type": "keyword" + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } } diff --git a/dbrepo-search-service/lib/dbrepo-1.4.4-py3-none-any.whl b/dbrepo-search-service/lib/dbrepo-1.4.4-py3-none-any.whl index f58e17a58e747e35bbd37f43efe6b460ba31530f..694a6fc02560b3b5d858df0e5a0bd9acf45c8f20 100644 GIT binary patch delta 14746 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 delta 14271 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 Bc!B@` diff --git a/dbrepo-search-service/lib/dbrepo-1.4.4.tar.gz b/dbrepo-search-service/lib/dbrepo-1.4.4.tar.gz index 5463f6b170c24fff05d29a434562104553292fea..f344d01026b92476d80703cbfb7d884cb7822e05 100644 GIT binary patch literal 38911 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 literal 37662 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 b!VYJdM*{5#Cj0rv{~3Q@`|HRMz{&ssMI%h+ diff --git a/dbrepo-search-service/os-yml/get_fields.yml b/dbrepo-search-service/os-yml/get_fields.yml index 6ff4c87974..bf7f487643 100644 --- a/dbrepo-search-service/os-yml/get_fields.yml +++ b/dbrepo-search-service/os-yml/get_fields.yml @@ -16,22 +16,6 @@ responses: content: application/json: schema: - type: object - properties: - results: - type: array - items: - type: object - properties: - attr_name: - type: "string" - example: "name" - attr_friendly_name: - type: "string" - example: "Name" - type: - type: "string" - example: "string" - description: OpenSearch data types. + $ref: '#/components/schemas/IndexFieldsDto' "404": description: Invalid type. diff --git a/dbrepo-search-service/os-yml/get_fuzzy_search.yml b/dbrepo-search-service/os-yml/get_fuzzy_search.yml index 3dbd5d19d5..bc54419eb9 100644 --- a/dbrepo-search-service/os-yml/get_fuzzy_search.yml +++ b/dbrepo-search-service/os-yml/get_fuzzy_search.yml @@ -8,25 +8,17 @@ consumes: produces: - application/json parameters: - - in: query + - name: q + in: query required: true schema: - type: "string" - properties: - q: - type: "string" - example: "air quality" + type: string responses: 200: description: OK, contains the elements formatted as an array of JSON arrays content: application/json: schema: - type: object - properties: - results: - type: array - items: - type: object + $ref: '#/components/schemas/SearchResultDto' 415: description: Wrong accept type diff --git a/dbrepo-search-service/os-yml/get_index.yml b/dbrepo-search-service/os-yml/get_index.yml index 48fc4ca286..fe4941810c 100644 --- a/dbrepo-search-service/os-yml/get_index.yml +++ b/dbrepo-search-service/os-yml/get_index.yml @@ -38,13 +38,4 @@ responses: content: application/json: schema: - type: object - properties: - results: - type: array - items: - type: object - type: - type: string - enum: [ database, table, view, column, user, identifier, concept, unit ] - description: "Same as the requested type" + $ref: '#/components/schemas/IndexDto' diff --git a/dbrepo-search-service/os-yml/health.yml b/dbrepo-search-service/os-yml/health.yml deleted file mode 100644 index a4b273a2bf..0000000000 --- a/dbrepo-search-service/os-yml/health.yml +++ /dev/null @@ -1,24 +0,0 @@ -summary: Return a healthcheck -description: | - Return UP if the instance is ready to serve connections. -consumes: - - application/json -produces: - - application/json -parameters: [] -definitions: - Health: - type: object - properties: - status: - type: string - description: UP -responses: - 200: - description: OK, service is up and running - schema: - $ref: "#/definitions/Column" - 404: - description: Service is not yet ready -tags: - - actuator \ No newline at end of file diff --git a/dbrepo-search-service/os-yml/post_general_search.yml b/dbrepo-search-service/os-yml/post_general_search.yml index 33cbea6367..cbff09b7fc 100644 --- a/dbrepo-search-service/os-yml/post_general_search.yml +++ b/dbrepo-search-service/os-yml/post_general_search.yml @@ -27,13 +27,7 @@ parameters: name: "body" required: true schema: - type: "object" - properties: - search_term: - type: "string" - example: "air quality" - field_value_pairs: - type: "object" + $ref: '#/components/schemas/SearchRequestDto' responses: 200: description: OK, contains the elements formatted as an array of JSON arrays diff --git a/dbrepo-search-service/os-yml/update_database.yml b/dbrepo-search-service/os-yml/update_database.yml index f1f2911d3e..e9cd0d56f9 100644 --- a/dbrepo-search-service/os-yml/update_database.yml +++ b/dbrepo-search-service/os-yml/update_database.yml @@ -12,14 +12,7 @@ parameters: name: "body" required: true schema: - type: "object" - properties: - name: - type: "string" - example: "Air Quality" - internal_name: - type: "string" - example: "air_quality_abcd" + $ref: '#/components/schemas/DatabaseDto' security: - bearerAuth: [ ] - basicAuth: [ ] diff --git a/dbrepo-ui/components/subset/Results.vue b/dbrepo-ui/components/subset/Results.vue index 9f0cb366a7..ed6f744833 100644 --- a/dbrepo-ui/components/subset/Results.vue +++ b/dbrepo-ui/components/subset/Results.vue @@ -19,12 +19,6 @@ export default { type: String, default: () => 'query' /* query or view */ }, - view: { - type: Object, - default: () => { - return {} - } - }, loading: { type: Boolean, default: () => { @@ -58,15 +52,6 @@ export default { if (this.result.headers.length !== 0) { return this.result.headers } - if (this.type === 'view' && this.view && this.view.columns) { - return this.view.columns.map((c) => { - return { - title: c.alias ? c.alias : c.internal_name, - value: c.alias ? c.alias : c.internal_name, - sortable: false - } - }) - } return [] } }, diff --git a/dbrepo-ui/components/dialogs/TimeTravel.vue b/dbrepo-ui/components/table/TableHistory.vue similarity index 86% rename from dbrepo-ui/components/dialogs/TimeTravel.vue rename to dbrepo-ui/components/table/TableHistory.vue index 12f4228503..dd3dad66e2 100644 --- a/dbrepo-ui/components/dialogs/TimeTravel.vue +++ b/dbrepo-ui/components/table/TableHistory.vue @@ -54,7 +54,7 @@ <script> import { Bar } from 'vue-chartjs' import { format } from 'date-fns' -import { useCacheStore } from '@/stores/cache' +import { useCacheStore } from '~/stores/cache.js' import { Chart as ChartJS, Title, Tooltip, BarElement, CategoryScale, LinearScale, LogarithmicScale } from 'chart.js' ChartJS.register(Title, Tooltip, BarElement, CategoryScale, LinearScale, LogarithmicScale) @@ -153,23 +153,19 @@ export default { this.datetime = this.chartData.labels[idx] console.debug('date time', this.datetime, 'idx', idx) }, - async loadHistory () { - try { - this.loading = true - const tableService = useTableService() - this.history = await tableService.history(this.table.database_id, this.table.id) - // this.chartData.labels = history.map(d => format(new Date(d.timestamp), 'dd.MM.yyyy HH:mm:ss')) - // this.chartData.datasets = [{ - // // backgroundColor: 'red', - // data: history.map(d => d.total) - // }] - this.totalChanges = history.length - console.debug('history', this.chartData) - } catch (err) { - this.error = true - console.error('failed to load table history', err) - } - this.loading = false + loadHistory () { + this.loading = true + const tableService = useTableService() + tableService.history(this.table.database_id, this.table.id) + .then((history) => { + this.loading = false + this.history = history + }) + .catch(({message}) => { + const toast = useToastInstance() + toast.error(message) + this.loading = false + }) } } } diff --git a/dbrepo-ui/components/table/TableSchema.vue b/dbrepo-ui/components/table/TableSchema.vue index 07485c8690..1cc172858c 100644 --- a/dbrepo-ui/components/table/TableSchema.vue +++ b/dbrepo-ui/components/table/TableSchema.vue @@ -1,15 +1,19 @@ <template> <div> - <v-alert - v-if="needsSequence" - class="mb-6" - border="start" - :text="$t('validation.schema.primary-key')" - color="info" /> <v-form ref="form" v-model="valid" :disabled="disabled"> + <v-row> + <v-col md="8"> + <v-alert + v-if="needsSequence" + class="mb-6" + border="start" + :text="$t('validation.schema.primary-key')" + color="info" /> + </v-col> + </v-row> <v-row v-for="(c, idx) in columns" :key="`r-${idx}`" diff --git a/dbrepo-ui/components/view/ViewToolbar.vue b/dbrepo-ui/components/view/ViewToolbar.vue index e6cb5e09db..2112576498 100644 --- a/dbrepo-ui/components/view/ViewToolbar.vue +++ b/dbrepo-ui/components/view/ViewToolbar.vue @@ -1,44 +1,43 @@ <template> - <div v-if="view"> - <v-toolbar flat> - <v-btn - class="mr-2" - size="small" - icon="mdi-arrow-left" - :to="`/database/${$route.params.database_id}/view`" /> - <v-toolbar-title - :text="title" /> - <v-spacer /> - <v-btn - v-if="canDeleteView" - prepend-icon="mdi-delete" - class="mr-2" - variant="flat" - color="error" - :text="$vuetify.display.lgAndUp ? $t('navigation.delete') : ''" - :loading="loadingDelete" - @click="deleteView" /> - <v-btn - v-if="canCreatePid" - prepend-icon="mdi-content-save-outline" - variant="flat" - color="primary" - :text="($vuetify.display.lgAndUp ? $t('toolbars.view.pid.xl') + ' ' : '') + $t('toolbars.view.pid.permanent')" - :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/persist`" /> - <template v-slot:extension> - <v-tabs - v-model="tab" - color="primary"> - <v-tab - :text="$t('navigation.info')" - :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/info`" /> - <v-tab - :text="$t('navigation.data')" - :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/data`" /> - </v-tabs> - </template> - </v-toolbar> - </div> + <v-toolbar flat> + <v-btn + class="mr-2" + size="small" + icon="mdi-arrow-left" + :to="`/database/${$route.params.database_id}/view`" /> + <v-toolbar-title + v-if="view" + :text="title" /> + <v-spacer /> + <v-btn + v-if="canDeleteView" + prepend-icon="mdi-delete" + class="mr-2" + variant="flat" + color="error" + :text="$vuetify.display.lgAndUp ? $t('navigation.delete') : ''" + :loading="loadingDelete" + @click="deleteView" /> + <v-btn + v-if="canCreatePid" + prepend-icon="mdi-content-save-outline" + variant="flat" + color="primary" + :text="($vuetify.display.lgAndUp ? $t('toolbars.view.pid.xl') + ' ' : '') + $t('toolbars.view.pid.permanent')" + :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/persist`" /> + <template v-slot:extension> + <v-tabs + v-model="tab" + color="primary"> + <v-tab + :text="$t('navigation.info')" + :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/info`" /> + <v-tab + :text="$t('navigation.data')" + :to="`/database/${$route.params.database_id}/view/${$route.params.view_id}/data`" /> + </v-tabs> + </template> + </v-toolbar> </template> <script> @@ -128,9 +127,9 @@ export default { this.cacheStore.reloadDatabase() this.$router.push(`/database/${this.$route.params.database_id}/view`) }) - .catch(({code}) => { + .catch(({code, message}) => { const toast = useToastInstance() - toast.error(this.$t(code)) + toast.error(this.$t(code) + ": " + message) }) .finally(() => { this.loadingDelete = false diff --git a/dbrepo-ui/composables/table-service.ts b/dbrepo-ui/composables/table-service.ts index 393c540f83..96be0537d0 100644 --- a/dbrepo-ui/composables/table-service.ts +++ b/dbrepo-ui/composables/table-service.ts @@ -69,6 +69,7 @@ export const useTableService = (): any => { async function getData(databaseId: number, tableId: number, page: number, size: number, timestamp: Date): Promise<QueryResultDto> { const axios = useAxiosInstance() + console.debug('====>', mapFilter(timestamp, page, size)) console.debug('get data for table with id', tableId, 'in database with id', databaseId); return new Promise<QueryResultDto>((resolve, reject) => { axios.get<QueryResultDto>(`/api/database/${databaseId}/table/${tableId}/data`, { params: mapFilter(timestamp, page, size), timeout: 30_000 }) @@ -191,7 +192,7 @@ export const useTableService = (): any => { const axios = useAxiosInstance() console.debug('suggest semantic entities for table column with id', columnId, 'of table with id', tableId, 'of database with id', databaseId) return new Promise<TableColumnEntityDto[]>((resolve, reject) => { - axios.get<TableColumnEntityDto[]>(`/api/semantic/database/${databaseId}/table/${tableId}/column/${columnId}`, {timeout: 10000}) + axios.get<TableColumnEntityDto[]>(`/api/database/${databaseId}/table/${tableId}/column/${columnId}/suggest`, {timeout: 10000}) .then((response) => { console.info('Suggested semantic entities for table column with id', columnId, 'of table with id', tableId, 'of database with id', databaseId) resolve(response.data) @@ -248,10 +249,10 @@ export const useTableService = (): any => { } function mapFilter(timestamp: Date | null, page: number | null, size: number | null) { - if (!timestamp) { + if (timestamp === null) { return {page, size} } - if (!page || !size) { + if (page === null || size === null) { return {timestamp} } return {timestamp, page, size} diff --git a/dbrepo-ui/locales/de-AT.json b/dbrepo-ui/locales/de-AT.json index 2e713bd365..bf84b68932 100644 --- a/dbrepo-ui/locales/de-AT.json +++ b/dbrepo-ui/locales/de-AT.json @@ -88,12 +88,12 @@ }, "publication-year": { "label": "Erscheinungsjahr", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "titles": { "title": { "label": "Titel", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "type": { "label": "Typ", @@ -116,15 +116,15 @@ "subtitle": "Haben Sie bereits einen DOI für diesen Datensatz?", "label": "Geben Sie hier Ihren bestehenden DOI an", "hint": "Ein DOI ermöglicht die einfache und eindeutige Zitierung Ihres Uploads. ", - "mint": "Nach dem Speichern wird ein PID erstellt." + "mint": "Nach dem Speichern wird ein PID erstellt" }, "doi": { - "mint": "Nach dem Speichern wird ein DOI erstellt." + "mint": "Nach dem Speichern wird ein DOI erstellt" }, "descriptions": { "description": { "label": "Beschreibung", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "type": { "label": "Typ", @@ -146,14 +146,14 @@ "title": "Veröffentlichungsinformationen", "subtitle": "Der Name der Entität, die die Ressource hält, archiviert, veröffentlicht, druckt, verteilt, freigibt, herausgibt oder produziert. ", "label": "Herausgeber", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "related-identifiers": { "title": "Verwandter Bezeichner", "subtitle": "Bezeichner verwandter Ressourcen. ", "identifier": { "label": "Kennung", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "type": { "label": "Typ", @@ -179,7 +179,7 @@ }, "language": { "title": "Sprache", - "subtitle": "Die primäre Sprache des Datensatzes.", + "subtitle": "Die primäre Sprache des Datensatzes", "language": { "label": "Sprache", "hint": "" @@ -187,14 +187,14 @@ }, "funders": { "title": "Finanzierungsreferenz", - "subtitle": "Informationen zur finanziellen Unterstützung (Finanzierung) für den zu registrierenden Datensatz.", + "subtitle": "Informationen zur finanziellen Unterstützung (Finanzierung) für den zu registrierenden Datensatz", "identifier": { "label": "Kennung des Geldgebers", - "hint": "Verwenden Sie eine Namenskennung, ausgedrückt als URL von ORCID*, ROR*, DOI*, ISNI, GND (Schemata mit * unterstützen den automatischen Metadatenabruf)." + "hint": "Verwenden Sie eine Namenskennung, ausgedrückt als URL von ORCID*, ROR*, DOI*, ISNI, GND (Schemata mit * unterstützen den automatischen Metadatenabruf)" }, "name": { "label": "Name des Geldgebers", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "award-number": { "label": "Auszeichnungsnummer", @@ -212,10 +212,10 @@ } }, "creators": { - "subtitle": "Die wichtigsten Forscher, die an der Erstellung der Daten beteiligt waren, in der Reihenfolge ihrer Priorität.", + "subtitle": "Die wichtigsten Forscher, die an der Erstellung der Daten beteiligt waren, in der Reihenfolge ihrer Priorität", "identifier": { "label": "Namensbezeichner", - "hint": "Verwenden Sie eine Namenskennung, ausgedrückt als URL von ORCID*, ROR*, DOI*, ISNI, GND (Schemata mit * unterstützen den automatischen Metadatenabruf)." + "hint": "Verwenden Sie eine Namenskennung, ausgedrückt als URL von ORCID*, ROR*, DOI*, ISNI, GND (Schemata mit * unterstützen den automatischen Metadatenabruf)" }, "insert": { "text": "Füge mich ein" @@ -239,11 +239,11 @@ }, "name": { "label": "Name", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "affiliation-identifier": { "label": "Zugehörigkeitskennung", - "hint": "Verwenden Sie eine Zugehörigkeitskennung, ausgedrückt als URL von ORCID*, ROR*, DOI*, ISNI, GND (Schemata mit * unterstützen den automatischen Metadatenabruf)." + "hint": "Verwenden Sie eine Zugehörigkeitskennung, ausgedrückt als URL von ORCID*, ROR*, DOI*, ISNI, GND (Schemata mit * unterstützen den automatischen Metadatenabruf)" }, "affiliation": { "label": "Zugehörigkeitsname", @@ -253,7 +253,7 @@ }, "summary": { "title": "Zusammenfassung", - "subtitle": "Details zur Kennung, die zur Identifizierung dieses Datensatzes erstellt wird.", + "subtitle": "Details zur Kennung, die zur Identifizierung dieses Datensatzes erstellt wird", "record": "Der Bezeichner beschreibt", "publisher": "Herausgeber", "license": "Lizenz", @@ -287,8 +287,8 @@ "secure": "sicher", "insecure": "unsicher", "permissions": { - "write": "Sie können in diese Tabelle schreiben.", - "read": "Sie können den gesamten Inhalt dieser Tabelle lesen." + "write": "Sie können in diese Tabelle schreiben", + "read": "Sie können den gesamten Inhalt dieser Tabelle lesen" } }, "protocol": { @@ -330,7 +330,7 @@ }, "generated": { "label": "Vorschau des Tabellennamens", - "hint": "Schreibgeschützt." + "hint": "Schreibgeschützt" }, "description": { "label": "Beschreibung", @@ -385,7 +385,7 @@ "summary": { "title": "Zusammenfassung", "prefix": "Importiert", - "suffix": "Zeilen aus dem Datensatz." + "suffix": "Zeilen aus dem Datensatz" }, "analyse": { "text": "Hochladen und analysieren" @@ -398,7 +398,7 @@ }, "name": { "label": "Tabellenname", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "description": { "label": "Tabellenbeschreibung", @@ -406,22 +406,23 @@ }, "summary": { "prefix": "Tabelle mit Namen erstellt", - "suffix": "und importierter Datensatz erfolgreich." + "suffix": "und importierter Datensatz erfolgreich" } }, "drop": { "title": "Tabelle Löschen", "warning": { "prefix": "Diese Aktion kann nicht rückgängig gemacht werden! ", - "suffix": "unten, wenn Sie es wirklich mit allen gespeicherten Daten löschen möchten." + "suffix": "unten, wenn Sie es wirklich mit allen gespeicherten Daten löschen möchten" }, "name": { "label": "Tabellenname", - "hint": "Erforderlich." + "hint": "Erforderlich" } }, "schema": { "title": "System Versioniert", + "subtitle": "Tabellenbeschränkungen", "bullet": "●", "assign": "Zuordnen", "remove": { @@ -453,14 +454,14 @@ }, "name": { "label": "Name", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "add": { "text": "Spalte hinzufügen" }, "type": { "label": "Typ", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "size": { "label": "Größe" @@ -496,23 +497,23 @@ }, "summary": { "title": "Zusammenfassung", - "text": "Tabelle mit internem Namen erfolgreich erstellt:" + "text": "Tabelle mit internem Namen erfolgreich erstellt" } }, "semantics": { - "title": "Semantische Instanz für Tabellenspalte zuweisen:", - "subtitle": "Semantische Instanzen helfen Maschinen dabei, den richtigen Kontext Ihres Datensatzes zu ermitteln.", + "title": "Semantische Instanz für Tabellenspalte zuweisen", + "subtitle": "Semantische Instanzen helfen Maschinen dabei, den richtigen Kontext Ihres Datensatzes zu ermitteln", "recommended": "Empfohlene semantische Instanzen", "bullet": "●", "info": "Die folgenden Ontologien fragen automatisch die Felder rdfs:label ab und speichern sie für diese Spalte. ", "uri": { "label": "Semantischer Instanz-URI", - "hint": "Dieser URI kann automatisch aufgelöst werden." + "hint": "Dieser URI kann automatisch aufgelöst werden" } }, "versioning": { "title": "Geschichte", - "subtitle": "Wählen Sie einen Zeitstempel aus, um die Daten für diese bestimmte Tageszeit anzuzeigen.", + "subtitle": "Wählen Sie einen Zeitstempel aus, um die Daten für diese bestimmte Tageszeit anzuzeigen", "chart": { "title": "Datenereignisse", "ylabel": "# Veranstaltungen", @@ -527,22 +528,22 @@ }, "data": { "auto": { - "hint": "Der Wert wird automatisch durch eine Sequenz generiert." + "hint": "Der Wert wird automatisch durch eine Sequenz generiert" }, "primary-key": { - "hint": "Der Wert ist ein Primärschlüssel." + "hint": "Der Wert ist ein Primärschlüssel" }, "format": { - "hint": "Der Wert muss folgendes Format haben:" + "hint": "Der Wert muss folgendes Format haben" }, "required": { "hint": "Erforderlich. " }, "float": { - "max": "max.", - "min": "Mindest.", - "before": "Ziffer(n) vor dem Punkt.", - "after": "Ziffer(n) nach dem Punkt." + "max": "max", + "min": "Mindest", + "before": "Ziffer(n) vor dem Punkt", + "after": "Ziffer(n) nach dem Punkt" } } } @@ -582,7 +583,7 @@ "subpages": { "access": { "title": "Datenbankzugriff", - "subtitle": "Übersicht über Benutzer mit ihrem Zugriff auf die Datenbank.", + "subtitle": "Übersicht über Benutzer mit ihrem Zugriff auf die Datenbank", "read": "Sie können alle Inhalte lesen", "write-own": "Sie können eigene Tabellen schreiben und alle Inhalte lesen", "write-all": "Sie können eigene Tabellen schreiben und alle Inhalte lesen", @@ -602,7 +603,7 @@ }, "create": { "title": "Datenbank erstellen", - "subtitle": "Wählen Sie einen aussagekräftigen Datenbanknamen und eine Datenbank-Engine.", + "subtitle": "Wählen Sie einen aussagekräftigen Datenbanknamen und eine Datenbank-Engine", "name": { "label": "Name", "hint": "Erforderlich. ", @@ -610,7 +611,7 @@ }, "engine": { "label": "Motor", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "submit": { "text": "Erstellen" @@ -636,7 +637,7 @@ }, "settings": { "title": "Einstellungen", - "subtitle": "Das Bild wird in einem Feld mit den maximalen Abmessungen 200x200 Pixel angezeigt.", + "subtitle": "Das Bild wird in einem Feld mit den maximalen Abmessungen 200x200 Pixel angezeigt", "image": { "label": "Teaser-Bild", "hint": "max. " @@ -649,16 +650,16 @@ }, "scheme": { "title": "Schema", - "subtitle": "Aktualisiert die Metadaten im Datenbankschema, um systemversionierte Tabellen und Ansichten in der Benutzeroberfläche anzuzeigen.", + "subtitle": "Aktualisiert die Metadaten im Datenbankschema, um systemversionierte Tabellen und Ansichten in der Benutzeroberfläche anzuzeigen", "submit": { "text": "Aktualisieren" } }, "ownership": { "title": "Eigentum", - "subtitle": "Benutzer, der Eigentümer dieser Datenbank ist.", + "subtitle": "Benutzer, der Eigentümer dieser Datenbank ist", "label": "Datenbankbesitzer", - "hint": "Erforderlich.", + "hint": "Erforderlich", "submit": { "text": "Überweisen" } @@ -668,7 +669,7 @@ "subtitle": "Private Datenbanken verbergen die Daten, während Metadaten weiterhin sichtbar sind. ", "visibility": { "label": "Datenbanksichtbarkeit", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "submit": { "text": "Ändern" @@ -681,19 +682,19 @@ "name": "Melden Sie sich an", "email": { "label": "E-Mail-Adresse", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "username": { "label": "Nutzername", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "password": { "label": "Passwort", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "confirm": { "label": "Bestätige das Passwort", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "submit": { "label": "Einreichen" @@ -703,11 +704,11 @@ "name": "Anmeldung", "username": { "label": "Nutzername", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "password": { "label": "Passwort", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "submit": { "label": "Einreichen" @@ -720,7 +721,7 @@ "subpages": { "info": { "title": "Information", - "subtitle": "Allgemeine Benutzermetadaten.", + "subtitle": "Allgemeine Benutzermetadaten", "id": { "label": "ID" }, @@ -754,7 +755,7 @@ }, "theme": { "title": "Theme", - "subtitle": "Aktualisieren Sie das Benutzerdesign, wenn Sie angemeldet sind.", + "subtitle": "Aktualisieren Sie das Benutzerdesign, wenn Sie angemeldet sind", "label": "Thema", "dark": "Dunkel", "dark-contrast": "Dunkel – hoher Kontrast", @@ -770,14 +771,14 @@ "subpages": { "authentication": { "title": "Benutzer-Passwort", - "subtitle": "Aktualisieren Sie das Benutzerkennwort, das für die Basisauthentifizierung bei allen Schnittstellen verwendet wird.", + "subtitle": "Aktualisieren Sie das Benutzerkennwort, das für die Basisauthentifizierung bei allen Schnittstellen verwendet wird", "password": { "label": "Passwort", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "confirm": { "label": "Bestätige das Passwort", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "submit": { "text": "Aktualisieren" @@ -786,7 +787,7 @@ "developer": { "token": { "title": "Token-Informationen", - "subtitle": "Sehen Sie sich Ihre Token-Geheimnisse zu Debugging-Zwecken an.", + "subtitle": "Sehen Sie sich Ihre Token-Geheimnisse zu Debugging-Zwecken an", "expiry": "Läuft ab", "access": { "label": "Zugangstoken" @@ -805,11 +806,11 @@ "title": "Wartungsmeldung", "type": { "label": "Typ", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "message": { "label": "Nachricht", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "start": { "label": "Zeitstempel starten" @@ -840,6 +841,9 @@ "info": "Info", "data": "Daten" }, + "name": { + "title": "Name" + }, "query": { "title": "Abfrage" }, @@ -857,19 +861,19 @@ "title": "Ansicht erstellen", "name": { "label": "Name", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "table": { "label": "Datentabelle", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "columns": { "label": "Datenspalten", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "visibility": { "label": "Datensichtbarkeit", - "warn": "Nur Personen mit mindestens Leserechten können die Daten einsehen.", + "warn": "Nur Personen mit mindestens Leserechten können die Daten einsehen", "hint": "Erforderlich. " } } @@ -887,7 +891,7 @@ "title": "Abfrage" }, "query-hash": { - "prefix": "sha256:", + "prefix": "sha256", "title": "Abfrage-Hash" }, "executed": { @@ -906,14 +910,14 @@ "subpages": { "create": { "title": "Teilmenge erstellen", - "generated": "Die folgende Abfrage wird ausgeführt (schreibgeschützt):", - "subtitle": "Die folgende Abfrage wird ausgeführt:", + "generated": "Die folgende Abfrage wird ausgeführt (schreibgeschützt)", + "subtitle": "Die folgende Abfrage wird ausgeführt", "simple": { "text": "Einfach" }, "expert": { "text": "Experte", - "warn": "Von der Verwendung von Kommentaren, Aggregationsfunktionen und den folgenden Vorgängen wird abgeraten:" + "warn": "Von der Verwendung von Kommentaren, Aggregationsfunktionen und den folgenden Vorgängen wird abgeraten" }, "name": { "label": "" @@ -922,15 +926,15 @@ "text": "Filter hinzufügen", "column": { "label": "Spalte", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "operator": { "label": "Operator", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "value": { "label": "Wert", - "hint": "Erforderlich." + "hint": "Erforderlich" }, "remove": { "text": "Entfernen" @@ -973,7 +977,7 @@ "hint": "" }, "publication-range": { - "hint": "Geben Sie Ihren benutzerdefinierten Veröffentlichungsjahrbereich an." + "hint": "Geben Sie Ihren benutzerdefinierten Veröffentlichungsjahrbereich an" }, "start-year": { "label": "Startjahr", @@ -984,7 +988,7 @@ "hint": "" }, "concept-unit": { - "hint": "Wenn Sie ein KONZEPT und eine EINHEIT auswählen, können Sie unabhängig von der Maßeinheit spaltenübergreifend suchen." + "hint": "Wenn Sie ein KONZEPT und eine EINHEIT auswählen, können Sie unabhängig von der Maßeinheit spaltenübergreifend suchen" }, "concept": { "label": "Konzept", @@ -1027,216 +1031,216 @@ }, "error": { "access": { - "missing": "Der Zugriff in der Metadatendatenbank konnte nicht gefunden werden." + "missing": "Der Zugriff in der Metadatendatenbank konnte nicht gefunden werden" }, "axios": { - "connection": "Es konnte keine Verbindung hergestellt werden.", - "timeout": "Zeitüberschreitung der Verbindung." + "connection": "Es konnte keine Verbindung hergestellt werden", + "timeout": "Zeitüberschreitung der Verbindung" }, "concept": { - "missing": "Das Konzept konnte in der Metadatendatenbank nicht gefunden werden." + "missing": "Das Konzept konnte in der Metadatendatenbank nicht gefunden werden" }, "container": { - "exists": "Der Container ist bereits in der Metadatendatenbank vorhanden.", - "missing": "Der Container konnte in der Metadatendatenbank nicht gefunden werden." + "exists": "Der Container ist bereits in der Metadatendatenbank vorhanden", + "missing": "Der Container konnte in der Metadatendatenbank nicht gefunden werden" }, "data": { - "invalid": "Die Kommunikation mit dem Datendienst ist fehlgeschlagen.", - "connection": "Es konnte keine Verbindung zum Datendienst hergestellt werden.", - "value": "Spaltenwert konnte nicht festgelegt werden:", - "drift": "Die Uhr Ihres Browsers ist nicht mit UTC synchronisiert und scheint um Folgendes eingestellt zu sein:" + "invalid": "Die Kommunikation mit dem Datendienst ist fehlgeschlagen", + "connection": "Es konnte keine Verbindung zum Datendienst hergestellt werden", + "value": "Spaltenwert konnte nicht festgelegt werden", + "drift": "Die Uhr Ihres Browsers ist nicht mit UTC synchronisiert und scheint um Folgendes eingestellt zu sein" }, "database": { - "connection": "Es konnte keine Verbindung zur Datenbank hergestellt werden.", - "invalid": "Aktion in der Datenbank konnte nicht ausgeführt werden.", + "connection": "Es konnte keine Verbindung zur Datenbank hergestellt werden", + "invalid": "Aktion in der Datenbank konnte nicht ausgeführt werden", "querystore": "Die Abfrage konnte nicht in den Abfragespeicher eingefügt werden", - "missing": "Die Datenbank konnte nicht in der Metadatendatenbank gefunden werden.", - "create": "Es konnte keine Verbindung zum Metadatendienst hergestellt werden." + "missing": "Die Datenbank konnte nicht in der Metadatendatenbank gefunden werden", + "create": "Es konnte keine Verbindung zum Metadatendienst hergestellt werden" }, "doi": { - "missing": "DOI konnte in der Metadatendatenbank nicht gefunden werden." + "missing": "DOI konnte in der Metadatendatenbank nicht gefunden werden" }, "exchange": { - "missing": "Im Broker-Service konnte keine Börse gefunden werden." + "missing": "Im Broker-Service konnte keine Börse gefunden werden" }, "semantic": { - "filter": "Die semantische Entität konnte im Metadatendienst nicht gefiltert werden.", - "missing": "Die semantische Entität konnte im Metadatendienst nicht gefunden werden." + "filter": "Die semantische Entität konnte im Metadatendienst nicht gefiltert werden", + "missing": "Die semantische Entität konnte im Metadatendienst nicht gefunden werden" }, "storage": { - "missing": "Datensatz im Speicherdienst konnte nicht gefunden werden.", - "invalid": "Es konnte keine Verbindung zum Speicherdienst hergestellt werden." + "missing": "Datensatz im Speicherdienst konnte nicht gefunden werden", + "invalid": "Es konnte keine Verbindung zum Speicherdienst hergestellt werden" }, "identifier": { - "format": "Die Kennung konnte im Metadatendienst nicht in das angeforderte Format umgewandelt werden.", - "missing": "Die Kennung konnte in der Metadatendatenbank nicht gefunden werden.", - "unsupported": "Es konnten keine Metadaten von einem nicht unterstützten Metadatenanbieter gefunden werden.", + "format": "Die Kennung konnte im Metadatendienst nicht in das angeforderte Format umgewandelt werden", + "missing": "Die Kennung konnte in der Metadatendatenbank nicht gefunden werden", + "unsupported": "Es konnten keine Metadaten von einem nicht unterstützten Metadatenanbieter gefunden werden", "form": "Bitte geben Sie im Formular alle erforderlichen Werte ein" }, "image": { - "exists": "Das Bild ist bereits in der Metadatendatenbank vorhanden.", - "missing": "Das Bild konnte nicht in der Metadatendatenbank gefunden werden.", - "invalid": "Bildmetadaten sind fehlerhaft." + "exists": "Das Bild ist bereits in der Metadatendatenbank vorhanden", + "missing": "Das Bild konnte nicht in der Metadatendatenbank gefunden werden", + "invalid": "Bildmetadaten sind fehlerhaft" }, "license": { - "missing": "Die Lizenz konnte in der Metadatendatenbank nicht gefunden werden." + "missing": "Die Lizenz konnte in der Metadatendatenbank nicht gefunden werden" }, "request": { - "invalid": "Die Anforderungsnutzlast wurde vom Metadatendienst abgelehnt.", - "forbidden": "Anfrage ist unzulässig, Rollen oder Authentifizierung fehlen.", - "pagination": "Die Anfrage enthält ungültige Paginierungsinformationen.", - "sort": "Die Anfrage enthält ungültige Sortierinformationen." + "invalid": "Die Anforderungsnutzlast wurde vom Metadatendienst abgelehnt", + "forbidden": "Anfrage ist unzulässig, Rollen oder Authentifizierung fehlen", + "pagination": "Die Anfrage enthält ungültige Paginierungsinformationen", + "sort": "Die Anfrage enthält ungültige Sortierinformationen" }, "message": { - "missing": "Die Nachricht konnte in der Metadatendatenbank nicht gefunden werden." + "missing": "Die Nachricht konnte in der Metadatendatenbank nicht gefunden werden" }, "ontology": { - "missing": "Die Ontologie konnte in der Metadatendatenbank nicht gefunden werden." + "missing": "Die Ontologie konnte in der Metadatendatenbank nicht gefunden werden" }, "orcid": { - "missing": "ORCID konnte im Metadatenanbieter nicht gefunden werden." + "missing": "ORCID konnte im Metadatenanbieter nicht gefunden werden" }, "query": { - "missing": "Die Abfrage konnte im Datendienst nicht gefunden werden.", - "invalid": "Die Abfrage ist ungültig (enthält beispielsweise verbotene Schlüsselwörter).", - "type.exists": "Abfrage konnte nicht erstellt werden: kein solcher Spaltentyp:", - "type.build": "Abfrage konnte nicht erstellt werden: Derzeit gibt es keine Abfrageerstellungsunterstützung für den Spaltentyp:", - "column.exists": "Abfrage konnte nicht erstellt werden: In den Datenspalten fehlt die Spalte mit dem Namen:" + "missing": "Die Abfrage konnte im Datendienst nicht gefunden werden", + "invalid": "Die Abfrage ist ungültig", + "type.exists": "Abfrage konnte nicht erstellt werden: kein solcher Spaltentyp", + "type.build": "Abfrage konnte nicht erstellt werden: Derzeit gibt es keine Abfrageerstellungsunterstützung für den Spaltentyp", + "column.exists": "Abfrage konnte nicht erstellt werden: In den Datenspalten fehlt die Spalte mit dem Namen" }, "store": { - "invalid": "Der Abfragespeicher in der Datenbank konnte nicht erstellt werden.", - "clean": "Der Abfragespeicher in der Datenbank konnte nicht durchsucht werden.", - "insert": "Die Abfrage konnte nicht in den Abfragespeicher der Datenbank eingefügt werden.", - "persist": "Die Abfrage konnte nicht im Abfragespeicher der Datenbank gespeichert werden." + "invalid": "Der Abfragespeicher in der Datenbank konnte nicht erstellt werden", + "clean": "Der Abfragespeicher in der Datenbank konnte nicht durchsucht werden", + "insert": "Die Abfrage konnte nicht in den Abfragespeicher der Datenbank eingefügt werden", + "persist": "Die Abfrage konnte nicht im Abfragespeicher der Datenbank gespeichert werden" }, "metadata": { - "privileged": "Das Abrufen privilegierter Metadaten im Datendienst ist fehlgeschlagen.", - "connection": "Es konnte keine Verbindung zum Metadatendienst hergestellt werden.", - "invalid": "Es konnten keine Authentifizierungsmetadaten im Datendienst abgerufen werden." + "privileged": "Das Abrufen privilegierter Metadaten im Datendienst ist fehlgeschlagen", + "connection": "Es konnte keine Verbindung zum Metadatendienst hergestellt werden", + "invalid": "Es konnten keine Authentifizierungsmetadaten im Datendienst abgerufen werden" }, "sidecar": { - "export": "Der Datensatz konnte nicht in den Datenbank-Sidecar exportiert werden.", - "import": "Der Datensatz konnte nicht aus dem Datenbank-Sidecar importiert werden." + "export": "Der Datensatz konnte nicht in den Datenbank-Sidecar exportiert werden", + "import": "Der Datensatz konnte nicht aus dem Datenbank-Sidecar importiert werden" }, "queue": { - "missing": "Die Warteschlange im Broker-Dienst konnte nicht gefunden werden." + "missing": "Die Warteschlange im Broker-Dienst konnte nicht gefunden werden" }, "ror": { - "missing": "ROR konnte im Metadatenanbieter nicht gefunden werden." + "missing": "ROR konnte im Metadatenanbieter nicht gefunden werden" }, "import": { - "dataset": "Der Datensatz konnte nicht importiert werden." + "dataset": "Der Datensatz konnte nicht importiert werden" }, "upload": { - "dataset": "Der Datensatz konnte nicht hochgeladen werden." + "dataset": "Der Datensatz konnte nicht hochgeladen werden" }, "schema": { - "id": "Die Spalte „id“ muss ein Primärschlüssel sein.", - "view": "Ansichtsschema konnte nicht zugeordnet werden.", - "table": "Tabellenschema konnte nicht zugeordnet werden." + "id": "Die Spalte „id“ muss ein Primärschlüssel sein", + "view": "Ansichtsschema konnte nicht zugeordnet werden", + "table": "Tabellenschema konnte nicht zugeordnet werden" }, "user": { - "exists": "Benutzer mit Benutzername ist in der Authentifizierungsdatenbank vorhanden.", - "missing": "Benutzer konnte in der Authentifizierungsdatenbank nicht gefunden werden.", - "credentials": "Ungültige Benutzername und Passwort Kombination.", - "email-exists": "Das Konto mit dieser E-Mail-Adresse existiert bereits.", - "setup": "Bitte ändern Sie Ihr Passwort." + "exists": "Benutzer mit Benutzername ist in der Authentifizierungsdatenbank vorhanden", + "missing": "Benutzer konnte in der Authentifizierungsdatenbank nicht gefunden werden", + "credentials": "Ungültige Benutzername und Passwort Kombination", + "email-exists": "Das Konto mit dieser E-Mail-Adresse existiert bereits", + "setup": "Bitte ändern Sie Ihr Passwort" }, "search": { - "connection": "Es konnte keine Verbindung zum Suchdienst hergestellt werden.", - "invalid": "Ungültige Suchanfrage." + "connection": "Es konnte keine Verbindung zum Suchdienst hergestellt werden", + "invalid": "Ungültige Suchanfrage" }, "semantics": { - "timeout": "Semantikvorschlag fehlgeschlagen: Zeitüberschreitung bei der Anfrage.", - "uri": "Der semantische URI ist fehlerhaft." + "timeout": "Semantikvorschlag fehlgeschlagen: Zeitüberschreitung bei der Anfrage", + "uri": "Der semantische URI ist fehlerhaft" }, "subset": { - "format": "Die Teilmenge konnte nicht dem angeforderten Format zugeordnet werden." + "format": "Die Teilmenge konnte nicht dem angeforderten Format zugeordnet werden" }, "pagination": { - "malformed": "Ungültige Paginierungsanforderung." + "malformed": "Ungültige Paginierungsanforderung" }, "table": { - "missing": "Die Tabelle konnte in der Metadatendatenbank nicht gefunden werden.", - "exists": "Die Tabelle mit diesem Namen existiert bereits.", - "invalid": "Die Spalten im Datendienst konnten nicht analysiert werden.", - "malformed": "Eintrag konnte nicht eingefügt werden:", - "create": "Tabelle konnte nicht erstellt werden:", - "connection": "Das Laden der Tabellendaten ist fehlgeschlagen, da die Datenbank nicht erreichbar ist." + "missing": "Die Tabelle konnte in der Metadatendatenbank nicht gefunden werden", + "exists": "Die Tabelle mit diesem Namen existiert bereits", + "invalid": "Die Spalten im Datendienst konnten nicht analysiert werden", + "malformed": "Eintrag konnte nicht eingefügt werden", + "create": "Tabelle konnte nicht erstellt werden", + "connection": "Das Laden der Tabellendaten ist fehlgeschlagen, da die Datenbank nicht erreichbar ist" }, "unit": { - "missing": "Die semantische Einheit konnte in der Metadatendatenbank nicht gefunden werden." + "missing": "Die semantische Einheit konnte in der Metadatendatenbank nicht gefunden werden" }, "view": { - "create": "Ansicht konnte nicht erstellt werden:", - "missing": "Die Ansicht konnte in der Metadatendatenbank nicht gefunden werden.", - "invalid": "Die Ansichtsabfrage konnte den Spalten im Datendienst nicht zugeordnet werden." + "create": "Ansicht konnte nicht erstellt werden", + "missing": "Die Ansicht konnte in der Metadatendatenbank nicht gefunden werden", + "invalid": "Die Ansichtsabfrage konnte den Spalten im Datendienst nicht zugeordnet werden" } }, "success": { - "signup": "Konto erfolgreich erstellt.", + "signup": "Konto erfolgreich erstellt", "clipboard": { - "user": "Benutzer-ID erfolgreich kopiert." + "user": "Benutzer-ID erfolgreich kopiert" }, "query": { "build": "Abfrage konnte nicht erstellt werden: Spalte nicht gefunden", "fatal": "Abfragen mit diesem Schema können derzeit nicht über die Benutzeroberfläche erstellt werden" }, "import": { - "dataset": "Datensatz erfolgreich importiert." + "dataset": "Datensatz erfolgreich importiert" }, "upload": { - "dataset": "Datensatz erfolgreich hochgeladen.", - "blob": "Datei erfolgreich hochgeladen." + "dataset": "Datensatz erfolgreich hochgeladen", + "blob": "Datei erfolgreich hochgeladen" }, "analyse": { - "dataset": "Datensatz erfolgreich analysiert." + "dataset": "Datensatz erfolgreich analysiert" }, "access": { - "created": "Zugriff erfolgreich bereitgestellt.", - "modified": "Zugriff erfolgreich geändert.", - "revoked": "Zugriff erfolgreich widerrufen." + "created": "Zugriff erfolgreich bereitgestellt", + "modified": "Zugriff erfolgreich geändert", + "revoked": "Zugriff erfolgreich widerrufen" }, "data": { - "add": "Dateneingabe erfolgreich hinzugefügt.", - "update": "Dateneingabe erfolgreich aktualisiert." + "add": "Dateneingabe erfolgreich hinzugefügt", + "update": "Dateneingabe erfolgreich aktualisiert" }, "table": { - "created": "Tabelle erfolgreich erstellt.", - "semantics": "Semantische Instanz erfolgreich zugewiesen." + "created": "Tabelle erfolgreich erstellt", + "semantics": "Semantische Instanz erfolgreich zugewiesen" }, "schema": { - "tables": "Die Metadaten der Datenbanktabellen wurden erfolgreich aktualisiert.", - "views": "Metadaten der Datenbankansichten wurden erfolgreich aktualisiert." + "tables": "Die Metadaten der Datenbanktabellen wurden erfolgreich aktualisiert", + "views": "Metadaten der Datenbankansichten wurden erfolgreich aktualisiert" }, "database": { - "upload": "Datenbankbild erfolgreich hochgeladen.", - "transfer": "Der Datenbankeigentümer wurde erfolgreich übertragen.", - "visibility": "Die Datenbanksichtbarkeit wurde erfolgreich aktualisiert.", + "upload": "Datenbankbild erfolgreich hochgeladen", + "transfer": "Der Datenbankeigentümer wurde erfolgreich übertragen", + "visibility": "Die Datenbanksichtbarkeit wurde erfolgreich aktualisiert", "image": { - "update": "Datenbankbild erfolgreich aktualisiert.", - "remove": "Datenbankbild erfolgreich entfernt." + "update": "Datenbankbild erfolgreich aktualisiert", + "remove": "Datenbankbild erfolgreich entfernt" } }, "pid": { - "saved": "Kennung erfolgreich gespeichert.", - "created": "Kennung erfolgreich erstellt.", - "published": "Identifikator erfolgreich veröffentlicht.", - "updated": "Kennung erfolgreich aktualisiert.", - "deleted": "Kennung erfolgreich gelöscht." + "saved": "Kennung erfolgreich gespeichert", + "created": "Kennung erfolgreich erstellt", + "published": "Identifikator erfolgreich veröffentlicht", + "updated": "Kennung erfolgreich aktualisiert", + "deleted": "Kennung erfolgreich gelöscht" }, "user": { - "info": "Benutzerinformationen erfolgreich aktualisiert.", - "theme": "Benutzerthema erfolgreich aktualisiert.", - "password": "Benutzerkennwort erfolgreich aktualisiert.", - "login": "Erfolgreich angemeldet." + "info": "Benutzerinformationen erfolgreich aktualisiert", + "theme": "Benutzerthema erfolgreich aktualisiert", + "password": "Benutzerkennwort erfolgreich aktualisiert", + "login": "Erfolgreich angemeldet" }, "view": { - "create": "Ansicht erfolgreich erstellt.", - "delete": "Ansicht erfolgreich gelöscht." + "create": "Ansicht erfolgreich erstellt", + "delete": "Ansicht erfolgreich gelöscht" }, "subset": { - "create": "Teilmenge erfolgreich erstellt." + "create": "Teilmenge erfolgreich erstellt" } }, "toolbars": { @@ -1248,7 +1252,7 @@ "semantic": { "register": { "title": "Registrieren Sie die Ontologie", - "subtitle": "Registrieren Sie einen neuen Ontologie-Endpunkt." + "subtitle": "Registrieren Sie einen neuen Ontologie-Endpunkt" }, "ontologies": { "title": "Ontologien", @@ -1266,6 +1270,7 @@ "public": "Öffentlich", "private": "Privat", "current": "Aktuelle Daten", + "history": "Historische Daten", "create": { "text": "Datenbank" }, @@ -1329,7 +1334,7 @@ }, "search": { "fuzzy": { - "placeholder": "Suchen ..." + "placeholder": "Suchen .." }, "result": "Ergebnis", "results": "Ergebnisse" @@ -1371,7 +1376,7 @@ "tuple": "Eintrag", "download": "Herunterladen", "version": "Geschichte", - "subtitle": "Stellen Sie Daten bereit, die direkt in den Datensatz eingefügt werden sollen." + "subtitle": "Stellen Sie Daten bereit, die direkt in den Datensatz eingefügt werden sollen" } } }, @@ -1386,7 +1391,7 @@ "month": "Ungültiger Monat", "schema": { "id": "Die Spalte muss als Primärschlüssel deklariert werden", - "primary-key": "Wir erstellen eine Spalte mit dem Namen „id“ mit einer automatisch ansteigenden Sequenz, die bei 1 beginnt. Bitte geben Sie eine Spalte mit Primärschlüssel an, wenn Sie dieses Verhalten nicht wünschen." + "primary-key": "Wir erstellen eine Spalte mit dem Namen „id“ mit einer automatisch ansteigenden Sequenz, die bei 1 beginnt. Bitte geben Sie eine Spalte mit Primärschlüssel an, wenn Sie dieses Verhalten nicht wünschen" }, "uri": { "pattern": "Ungültiger URI", diff --git a/dbrepo-ui/locales/en-US.json b/dbrepo-ui/locales/en-US.json index 71d8c09051..54d7a64a7a 100644 --- a/dbrepo-ui/locales/en-US.json +++ b/dbrepo-ui/locales/en-US.json @@ -88,12 +88,12 @@ }, "publication-year": { "label": "Publication Year", - "hint": "Required." + "hint": "Required" }, "titles": { "title": { "label": "Title", - "hint": "Required." + "hint": "Required" }, "type": { "label": "Type", @@ -103,7 +103,7 @@ "label": "Language", "hint": "" }, - "subtitle": "A name or title by which a resource is known. May be the title of a dataset.", + "subtitle": "A name or title by which a resource is known. May be the title of a dataset", "remove": { "text": "Remove" }, @@ -116,15 +116,15 @@ "subtitle": "Do you already have a DOI for this dataset?", "label": "Provide your existing DOI here", "hint": "A DOI allows your upload to be easily and unambiguously cited. Example: 10.1234/foo.bar", - "mint": "A PID will be minted after saving." + "mint": "A PID will be minted after saving" }, "doi": { - "mint": "A DOI will be created after saving." + "mint": "A DOI will be created after saving" }, "descriptions": { "description": { "label": "Description", - "hint": "Required." + "hint": "Required" }, "type": { "label": "Type", @@ -134,7 +134,7 @@ "label": "Language", "hint": "" }, - "subtitle": "All additional information. May be used for technical information or detailed information associated with a dataset.", + "subtitle": "All additional information. May be used for technical information or detailed information associated with a dataset", "remove": { "text": "Remove" }, @@ -144,16 +144,16 @@ }, "publisher": { "title": "Publication Information", - "subtitle": "The name of the entity that holds, archives, publishes, prints, distributes, releases, issues, or produces the resource. This property will be used to formulate the citation, so consider the prominence of the role.", + "subtitle": "The name of the entity that holds, archives, publishes, prints, distributes, releases, issues, or produces the resource. This property will be used to formulate the citation, so consider the prominence of the role", "label": "Publisher", - "hint": "Required." + "hint": "Required" }, "related-identifiers": { "title": "Related Identifier", - "subtitle": "Identifiers of related resources. These must be globally unique identifiers.", + "subtitle": "Identifiers of related resources. These must be globally unique identifiers", "identifier": { "label": "Identifier", - "hint": "Required." + "hint": "Required" }, "type": { "label": "Type", @@ -172,14 +172,14 @@ }, "licenses": { "title": "License", - "subtitle": "Identifiers of related resources. These must be globally unique identifiers.", + "subtitle": "Identifiers of related resources. These must be globally unique identifiers", "license": { "label": "License" } }, "language": { "title": "Language", - "subtitle": "The primary language of the dataset.", + "subtitle": "The primary language of the dataset", "language": { "label": "Language", "hint": "" @@ -187,14 +187,14 @@ }, "funders": { "title": "Funding Reference", - "subtitle": "Information about financial support (funding) for the dataset being registered.", + "subtitle": "Information about financial support (funding) for the dataset being registered", "identifier": { "label": "Funder Identifier", "hint": "Use a name identifier expressed as URL from ORCID*, ROR*, DOI*, ISNI, GND (schemes with * support automatic metadata retrieval)" }, "name": { "label": "Funder Name", - "hint": "Required." + "hint": "Required" }, "award-number": { "label": "Award Number", @@ -212,7 +212,7 @@ } }, "creators": { - "subtitle": "The main researchers involved in producing the data, in priority order.", + "subtitle": "The main researchers involved in producing the data, in priority order", "identifier": { "label": "Name Identifier", "hint": "Use a name identifier expressed as URL from ORCID*, ROR*, DOI*, ISNI, GND (schemes with * support automatic metadata retrieval)" @@ -239,7 +239,7 @@ }, "name": { "label": "Name", - "hint": "Required." + "hint": "Required" }, "affiliation-identifier": { "label": "Affiliation Identifier", @@ -253,7 +253,7 @@ }, "summary": { "title": "Summary", - "subtitle": "Details of the identifier that will be created to identify this record.", + "subtitle": "Details of the identifier that will be created to identify this record", "record": "The identifier describes", "publisher": "Publisher", "license": "License", @@ -287,8 +287,8 @@ "secure": "secure", "insecure": "insecure", "permissions": { - "write": "You can write to this table.", - "read": "You can read all contents of this table." + "write": "You can write to this table", + "read": "You can read all contents of this table" } }, "protocol": { @@ -322,32 +322,32 @@ }, "dataset": { "title": "Dataset Structure", - "warn": "The dataset schema does not match the target table schema. You can still force the import but it is not recommended." + "warn": "The dataset schema does not match the target table schema. You can still force the import but it is not recommended" }, "name": { "label": "Name", - "hint": "Required. Maximum length is 64 characters." + "hint": "Required. Maximum length is 64 characters" }, "generated": { "label": "Preview Table Name", - "hint": "Readonly." + "hint": "Readonly" }, "description": { "label": "Description", - "hint": "Optional. Short and concise description of the data." + "hint": "Optional. Short and concise description of the data" }, "separator": { "label": "Column Separator", - "hint": "Optional. Character that separates the columns.", + "hint": "Optional. Character that separates the columns", "warn": { "prefix": "We analysed your .csv/.tsv dataset and found that the separator you provided", "middle": "is not correct, the separator", - "suffix": "is more likely to be correct. It is advised to change the separator above." + "suffix": "is more likely to be correct. It is advised to change the separator above" } }, "skip": { "label": "Skip Rows", - "hint": "Optional. Number of rows to skip, e.g. when the first one contains header and no data." + "hint": "Optional. Number of rows to skip, e.g. when the first one contains header and no data" }, "quote": { "label": "Quote Encoding", @@ -355,11 +355,11 @@ }, "terminator": { "label": "Line Termination Encoding", - "hint": "Optional. Character that terminates the newlines.", + "hint": "Optional. Character that terminates the newlines", "warn": { "prefix": "We analysed your .csv/.tsv dataset and found that the line termination encoding you provided", "middle": "is not correct, the line termination encoding", - "suffix": "is more likely to be correct. It is advised to change the line termination encoding above." + "suffix": "is more likely to be correct. It is advised to change the line termination encoding above" } }, "null": { @@ -368,16 +368,16 @@ }, "true": { "label": "True Encoding", - "hint": "Optional. Character sequence that represents boolean true, e.g. 1, true, yes." + "hint": "Optional. Character sequence that represents boolean true, e.g. 1, true, yes" }, "false": { "label": "False Encoding", - "hint": "Optional. Character sequence that represents boolean false, e.g. 0, false, no." + "hint": "Optional. Character sequence that represents boolean false, e.g. 0, false, no" }, "file": { "title": "Dataset Upload", "label": "Dataset File", - "hint": "Required. Needs to be in .csv/.tsv file format." + "hint": "Required. Needs to be in .csv/.tsv file format" }, "preview": { "title": "Preview" @@ -385,7 +385,7 @@ "summary": { "title": "Summary", "prefix": "Imported", - "suffix": "rows from dataset." + "suffix": "rows from dataset" }, "analyse": { "text": "Upload & Analyse" @@ -398,7 +398,7 @@ }, "name": { "label": "Table Name", - "hint": "Required." + "hint": "Required" }, "description": { "label": "Table Description", @@ -406,22 +406,23 @@ }, "summary": { "prefix": "Created table with name", - "suffix": "and imported dataset successfully." + "suffix": "and imported dataset successfully" } }, "drop": { "title": "Drop table", "warning": { "prefix": "This action cannot be undone! Type the table name", - "suffix": "below if you really want to drop it with all stored data." + "suffix": "below if you really want to drop it with all stored data" }, "name": { "label": "Table Name", - "hint": "Required." + "hint": "Required" } }, "schema": { "title": "System Versioned", + "subtitle": "Table Constraints", "bullet": "●", "assign": "Assign", "remove": { @@ -453,14 +454,14 @@ }, "name": { "label": "Name", - "hint": "Required." + "hint": "Required" }, "add": { "text": "Add Column" }, "type": { "label": "Type", - "hint": "Required." + "hint": "Required" }, "size": { "label": "Size" @@ -496,23 +497,23 @@ }, "summary": { "title": "Summary", - "text": "Successfully created table with internal name:" + "text": "Successfully created table with internal name" } }, "semantics": { - "title": "Assign semantic instance for table column:", - "subtitle": "Semantic instances help machines to get the proper context of your dataset.", + "title": "Assign semantic instance for table column", + "subtitle": "Semantic instances help machines to get the proper context of your dataset", "recommended": "Recommended semantic instances", "bullet": "●", - "info": "The following ontologies automatically will query the fields rdfs:label and store it for this column. You can still use other URIs that are not matching these ontologies, the URI will be displayed instead.", + "info": "The following ontologies automatically will query the fields rdfs:label and store it for this column. You can still use other URIs that are not matching these ontologies, the URI will be displayed instead", "uri": { "label": "Semantic Instance URI", - "hint": "This URI can be automatically resolved." + "hint": "This URI can be automatically resolved" } }, "versioning": { "title": "History", - "subtitle": "Select a timestamp to view the data for this specific time of day.", + "subtitle": "Select a timestamp to view the data for this specific time of day", "chart": { "title": "Data Events", "ylabel": "# Events", @@ -527,22 +528,22 @@ }, "data": { "auto": { - "hint": "Value is automatically generated by a sequence." + "hint": "Value is automatically generated by a sequence" }, "primary-key": { - "hint": "Value is a primary key." + "hint": "Value is a primary key" }, "format": { - "hint": "Value must be in format:" + "hint": "Value must be in format" }, "required": { "hint": "Required. " }, "float": { - "max": "max.", - "min": "min.", - "before": "digit(s) before the dot.", - "after": "digit(s) after the dot." + "max": "max", + "min": "min", + "before": "digit(s) before the dot", + "after": "digit(s) after the dot" } } } @@ -582,7 +583,7 @@ "subpages": { "access": { "title": "Database Access", - "subtitle": "Overview on users with their access to the database.", + "subtitle": "Overview on users with their access to the database", "read": "You can read all contents", "write-own": "You can write own tables and read all contents", "write-all": "You can write own tables and read all contents", @@ -602,7 +603,7 @@ }, "create": { "title": "Create Database", - "subtitle": "Choose an expressive database name and select a database engine.", + "subtitle": "Choose an expressive database name and select a database engine", "name": { "label": "Name", "hint": "Required. The internal database name will be lowercase alphanumeric, others will be replaced with _", @@ -610,7 +611,7 @@ }, "engine": { "label": "Engine", - "hint": "Required." + "hint": "Required" }, "submit": { "text": "Create" @@ -636,7 +637,7 @@ }, "settings": { "title": "Settings", - "subtitle": "The image will be displayed in a box with maximum dimensions 200x200 pixels.", + "subtitle": "The image will be displayed in a box with maximum dimensions 200x200 pixels", "image": { "label": "Teaser Image", "hint": "max. 1MB file size" @@ -649,26 +650,26 @@ }, "scheme": { "title": "Schema", - "subtitle": "Update the metadata on the database schema to display system-versioned tables and views in the UI.", + "subtitle": "Update the metadata on the database schema to display system-versioned tables and views in the UI", "submit": { "text": "Refresh" } }, "ownership": { "title": "Ownership", - "subtitle": "User who has ownership over this database.", + "subtitle": "User who has ownership over this database", "label": "Database Owner", - "hint": "Required.", + "hint": "Required", "submit": { "text": "Transfer" } }, "visibility": { "title": "Visibility", - "subtitle": "Private databases hide the data while metadata is still visible. Public databases are fully transparent.", + "subtitle": "Private databases hide the data while metadata is still visible. Public databases are fully transparent", "visibility": { "label": "Database Visibility", - "hint": "Required." + "hint": "Required" }, "submit": { "text": "Modify" @@ -681,19 +682,19 @@ "name": "Signup", "email": { "label": "E-Mail Address", - "hint": "Required." + "hint": "Required" }, "username": { "label": "Username", - "hint": "Required." + "hint": "Required" }, "password": { "label": "Password", - "hint": "Required." + "hint": "Required" }, "confirm": { "label": "Confirm Password", - "hint": "Required." + "hint": "Required" }, "submit": { "label": "Submit" @@ -703,11 +704,11 @@ "name": "Login", "username": { "label": "Username", - "hint": "Required." + "hint": "Required" }, "password": { "label": "Password", - "hint": "Required." + "hint": "Required" }, "submit": { "label": "Submit" @@ -720,7 +721,7 @@ "subpages": { "info": { "title": "Information", - "subtitle": "General user metadata.", + "subtitle": "General user metadata", "id": { "label": "ID" }, @@ -754,7 +755,7 @@ }, "theme": { "title": "Theme", - "subtitle": "Update the user theme when logged in.", + "subtitle": "Update the user theme when logged in", "label": "Theme", "dark": "Dark", "dark-contrast": "Dark - High Contrast", @@ -770,14 +771,14 @@ "subpages": { "authentication": { "title": "User Password", - "subtitle": "Update the user password used for basic authentication with all interfaces.", + "subtitle": "Update the user password used for basic authentication with all interfaces", "password": { "label": "Password", - "hint": "Required." + "hint": "Required" }, "confirm": { "label": "Confirm Password", - "hint": "Required." + "hint": "Required" }, "submit": { "text": "Update" @@ -786,7 +787,7 @@ "developer": { "token": { "title": "Token Information", - "subtitle": "View your token secrets for debugging purposes.", + "subtitle": "View your token secrets for debugging purposes", "expiry": "Expires", "access": { "label": "Access Token" @@ -805,11 +806,11 @@ "title": "Maintenance Message", "type": { "label": "Type", - "hint": "Required." + "hint": "Required" }, "message": { "label": "Message", - "hint": "Required." + "hint": "Required" }, "start": { "label": "Start Timestamp" @@ -840,8 +841,11 @@ "info": "Info", "data": "Data" }, + "name": { + "title": "Name" + }, "query": { - "title": "Query" + "title": "Statement" }, "creator": { "title": "Creator" @@ -857,20 +861,20 @@ "title": "Create View", "name": { "label": "Name", - "hint": "Required." + "hint": "Required" }, "table": { "label": "Data Table", - "hint": "Required." + "hint": "Required" }, "columns": { "label": "Data Columns", - "hint": "Required." + "hint": "Required" }, "visibility": { "label": "Data Visibility", - "warn": "Only people with at least read access can view the data.", - "hint": "Required. When private, the view metadata will still be public but the data will only be visible to people with at least read access to this database." + "warn": "Only people with at least read access can view the data", + "hint": "Required. When private, the view metadata will still be public but the data will only be visible to people with at least read access to this database" } } } @@ -887,7 +891,7 @@ "title": "Query" }, "query-hash": { - "prefix": "sha256:", + "prefix": "sha256", "title": "Query Hash" }, "executed": { @@ -906,14 +910,14 @@ "subpages": { "create": { "title": "Create Subset", - "generated": "The following query will be executed (readonly):", - "subtitle": "The following query will be executed:", + "generated": "The following query will be executed (readonly)", + "subtitle": "The following query will be executed", "simple": { "text": "Simple" }, "expert": { "text": "Expert", - "warn": "It is not recommended to use comments, aggregation functions and the following operations:" + "warn": "It is not recommended to use comments, aggregation functions and the following operations" }, "name": { "label": "" @@ -922,15 +926,15 @@ "text": "Add Filter", "column": { "label": "Column", - "hint": "Required." + "hint": "Required" }, "operator": { "label": "Operator", - "hint": "Required." + "hint": "Required" }, "value": { "label": "Value", - "hint": "Required." + "hint": "Required" }, "remove": { "text": "Remove" @@ -973,7 +977,7 @@ "hint": "" }, "publication-range": { - "hint": "Specify your custom publication year range." + "hint": "Specify your custom publication year range" }, "start-year": { "label": "Start Year", @@ -984,7 +988,7 @@ "hint": "" }, "concept-unit": { - "hint": "If you select a CONCEPT and UNIT, you can search across columns regardless of their unit of measurement." + "hint": "If you select a CONCEPT and UNIT, you can search across columns regardless of their unit of measurement" }, "concept": { "label": "Concept", @@ -1027,216 +1031,216 @@ }, "error": { "access": { - "missing": "Failed to find access in metadata database." + "missing": "Failed to find access in metadata database" }, "axios": { - "connection": "Failed to establish connection.", - "timeout": "Connection timed out." + "connection": "Failed to establish connection", + "timeout": "Connection timed out" }, "concept": { - "missing": "Failed to find concept in metadata database." + "missing": "Failed to find concept in metadata database" }, "container": { - "exists": "Container already exists in metadata database.", - "missing": "Failed to find container in metadata database." + "exists": "Container already exists in metadata database", + "missing": "Failed to find container in metadata database" }, "data": { - "invalid": "Failed to communicate with data service.", - "connection": "Failed to establish connection to data service.", - "value": "Failed to set column value:", - "drift": "Your browser clock is not synchronized with UTC and seems to be off by:" + "invalid": "Failed to communicate with data service", + "connection": "Failed to establish connection to data service", + "value": "Failed to set column value", + "drift": "Your browser clock is not synchronized with UTC and seems to be off by" }, "database": { - "connection": "Failed to establis connection to the database.", - "invalid": "Failed to perform action in database.", + "connection": "Failed to establis connection to the database", + "invalid": "Failed to perform action in database", "querystore": "Failed to insert query into query store", - "missing": "Failed to find database in metadata database.", - "create": "Failed to establish connection with metadata service." + "missing": "Failed to find database in metadata database", + "create": "Failed to establish connection with metadata service" }, "doi": { - "missing": "Failed to find DOI in metadata database." + "missing": "Failed to find DOI in metadata database" }, "exchange": { - "missing": "Failed to find exchange in broker service." + "missing": "Failed to find exchange in broker service" }, "semantic": { - "filter": "Failed to filter semantic entity in metadata service.", - "missing": "Failed to find semantic entity in metadata service." + "filter": "Failed to filter semantic entity in metadata service", + "missing": "Failed to find semantic entity in metadata service" }, "storage": { - "missing": "Failed to find dataset in storage service.", - "invalid": "Failed to establish connection with storage service." + "missing": "Failed to find dataset in storage service", + "invalid": "Failed to establish connection with storage service" }, "identifier": { - "format": "Failed to transform identifier into the requested format in metadata service.", - "missing": "Failed to find identifier in metadata database.", - "unsupported": "Failed to find metadata from unsupported metadata provider.", + "format": "Failed to transform identifier into the requested format in metadata service", + "missing": "Failed to find identifier in metadata database", + "unsupported": "Failed to find metadata from unsupported metadata provider", "form": "Please provide all required values in the form" }, "image": { - "exists": "Image already exists in metadata database.", - "missing": "Failed to find image in metadata database.", - "invalid": "Image metadata is malformed." + "exists": "Image already exists in metadata database", + "missing": "Failed to find image in metadata database", + "invalid": "Image metadata is malformed" }, "license": { - "missing": "Failed to find license in metadata database." + "missing": "Failed to find license in metadata database" }, "request": { - "invalid": "Request payload was rejected by the metadata service.", - "forbidden": "Request is forbidden, roles or authentication missing.", - "pagination": "Request contains invalid pagination information.", - "sort": "Request contains invalid sort information." + "invalid": "Request payload was rejected by the metadata service", + "forbidden": "Request is forbidden, roles or authentication missing", + "pagination": "Request contains invalid pagination information", + "sort": "Request contains invalid sort information" }, "message": { - "missing": "Failed to find message in metadata database." + "missing": "Failed to find message in metadata database" }, "ontology": { - "missing": "Failed to find ontology in metadata database." + "missing": "Failed to find ontology in metadata database" }, "orcid": { - "missing": "Failed to find ORCID in metadata provider." + "missing": "Failed to find ORCID in metadata provider" }, "query": { - "missing": "Failed to find query in data service.", - "invalid": "Query is invalid (e.g. contains forbidden keywords).", - "type.exists": "Failed to build query: no such column type:", - "type.build": "Failed to build query: currently no query build support for column type:", - "column.exists": "Failed to build query: data columns are missing column with name:" + "missing": "Failed to find query in data service", + "invalid": "Query is invalid", + "type.exists": "Failed to build query: no such column type", + "type.build": "Failed to build query: currently no query build support for column type", + "column.exists": "Failed to build query: data columns are missing column with name" }, "store": { - "invalid": "Failed to create query store in the database.", - "clean": "Failed to sweep query store in the database.", - "insert": "Failed to insert query into database query store.", - "persist": "Failed to persist query in the database query store." + "invalid": "Failed to create query store in the database", + "clean": "Failed to sweep query store in the database", + "insert": "Failed to insert query into database query store", + "persist": "Failed to persist query in the database query store" }, "metadata": { - "privileged": "Failed to fetch privileged metadata in the data service.", - "connection": "Failed to establish connection to the metadata service.", - "invalid": "Failed to obtain authentication metadata in the data service." + "privileged": "Failed to fetch privileged metadata in the data service", + "connection": "Failed to establish connection to the metadata service", + "invalid": "Failed to obtain authentication metadata in the data service" }, "sidecar": { - "export": "Failed to export dataset to the database sidecar.", - "import": "Failed to import dataset from the database sidecar." + "export": "Failed to export dataset to the database sidecar", + "import": "Failed to import dataset from the database sidecar" }, "queue": { - "missing": "Failed to find queue in broker service." + "missing": "Failed to find queue in broker service" }, "ror": { - "missing": "Failed to find ROR in metadata provider." + "missing": "Failed to find ROR in metadata provider" }, "import": { - "dataset": "Failed to import dataset." + "dataset": "Failed to import dataset" }, "upload": { - "dataset": "Failed to upload dataset." + "dataset": "Failed to upload dataset" }, "schema": { - "id": "Column \"id\" must be a primary key.", - "view": "Failed to map view schema.", - "table": "Failed to map table schema." + "id": "Column \"id\" must be a primary key", + "view": "Failed to map view schema", + "table": "Failed to map table schema" }, "user": { - "exists": "User with username exists in auth database.", - "missing": "Failed to find user in auth database.", - "credentials": "Invalid username/password combination.", - "email-exists": "Account with this e-mail exists already.", - "setup": "Please change your password." + "exists": "User with username exists in auth database", + "missing": "Failed to find user in auth database", + "credentials": "Invalid username/password combination", + "email-exists": "Account with this e-mail exists already", + "setup": "Please change your password" }, "search": { - "connection": "Failed to establish connection to the search service.", - "invalid": "Malformed search request." + "connection": "Failed to establish connection to the search service", + "invalid": "Malformed search request" }, "semantics": { - "timeout": "Failed to suggest semantics: request timed out.", - "uri": "Semantic URI is malformed." + "timeout": "Failed to suggest semantics: request timed out", + "uri": "Semantic URI is malformed" }, "subset": { - "format": "Failed to map subset into requested format." + "format": "Failed to map subset into requested format" }, "pagination": { - "malformed": "Invalid pagination request." + "malformed": "Invalid pagination request" }, "table": { - "missing": "Failed to find table in metadata database.", - "exists": "Table with this name exists already.", - "invalid": "Failed to parse columns in the data service.", - "malformed": "Failed to insert entry:", - "create": "Failed to create table:", - "connection": "Failed to load table data because database is not reachable." + "missing": "Failed to find table in metadata database", + "exists": "Table with this name exists already", + "invalid": "Failed to parse columns in the data service", + "malformed": "Failed to insert entry", + "create": "Failed to create table", + "connection": "Failed to load table data because database is not reachable" }, "unit": { - "missing": "Failed to find semantic unit in metadata database." + "missing": "Failed to find semantic unit in metadata database" }, "view": { - "create": "Failed to create view:", - "missing": "Failed to find view in metadata database.", - "invalid": "Failed to map view query to columns in data service." + "create": "Failed to create view", + "missing": "Failed to find view in metadata database", + "invalid": "Failed to map view query to columns in data service" } }, "success": { - "signup": "Successfully created account.", + "signup": "Successfully created account", "clipboard": { - "user": "Successfully copied user id." + "user": "Successfully copied user id" }, "query": { "build": "Failed to build query: column not found", "fatal": "Query with this schema is not buildable through the UI at the moment" }, "import": { - "dataset": "Successfully imported dataset." + "dataset": "Successfully imported dataset" }, "upload": { - "dataset": "Successfully uploaded dataset.", - "blob": "Successfully uploaded file." + "dataset": "Successfully uploaded dataset", + "blob": "Successfully uploaded file" }, "analyse": { - "dataset": "Successfully analysed dataset." + "dataset": "Successfully analysed dataset" }, "access": { - "created": "Successfully provisioned access.", - "modified": "Successfully modified access.", - "revoked": "Successfully revoked access." + "created": "Successfully provisioned access", + "modified": "Successfully modified access", + "revoked": "Successfully revoked access" }, "data": { - "add": "Successfully added data entry.", - "update": "Successfully updated data entry." + "add": "Successfully added data entry", + "update": "Successfully updated data entry" }, "table": { - "created": "Successfully created table.", - "semantics": "Successfully assigned semantic instance." + "created": "Successfully created table", + "semantics": "Successfully assigned semantic instance" }, "schema": { - "tables": "Successfully refreshed database tables metadata.", - "views": "Successfully refreshed database views metadata." + "tables": "Successfully refreshed database tables metadata", + "views": "Successfully refreshed database views metadata" }, "database": { - "upload": "Successfully uploaded database image.", - "transfer": "Successfully transferred the database owner.", - "visibility": "Successfully updated the database visibility.", + "upload": "Successfully uploaded database image", + "transfer": "Successfully transferred the database owner", + "visibility": "Successfully updated the database visibility", "image": { - "update": "Successfully updated database image.", - "remove": "Successfully removed database image." + "update": "Successfully updated database image", + "remove": "Successfully removed database image" } }, "pid": { - "saved": "Successfully saved identifier.", - "created": "Successfully created identifier.", - "published": "Successfully published identifier.", - "updated": "Successfully updated identifier.", - "deleted": "Successfully deleted identifier." + "saved": "Successfully saved identifier", + "created": "Successfully created identifier", + "published": "Successfully published identifier", + "updated": "Successfully updated identifier", + "deleted": "Successfully deleted identifier" }, "user": { - "info": "Successfully updated user information.", - "theme": "Successfully updated user theme.", - "password": "Successfully updated user password.", - "login": "Successfully logged in." + "info": "Successfully updated user information", + "theme": "Successfully updated user theme", + "password": "Successfully updated user password", + "login": "Successfully logged in" }, "view": { - "create": "Successfully created view.", - "delete": "Successfully deleted view." + "create": "Successfully created view", + "delete": "Successfully deleted view" }, "subset": { - "create": "Successfully created subset." + "create": "Successfully created subset" } }, "toolbars": { @@ -1248,7 +1252,7 @@ "semantic": { "register": { "title": "Register Ontology", - "subtitle": "Register a new ontology endpoint." + "subtitle": "Register a new ontology endpoint" }, "ontologies": { "title": "Ontologies", @@ -1266,6 +1270,7 @@ "public": "Public", "private": "Private", "current": "Current Data", + "history": "Historic Data", "create": { "text": "Database" }, @@ -1329,7 +1334,7 @@ }, "search": { "fuzzy": { - "placeholder": "Search ..." + "placeholder": "Search .." }, "result": "Result", "results": "Results" @@ -1371,7 +1376,7 @@ "tuple": "Entry", "download": "Download", "version": "History", - "subtitle": "Provide data to be directly inserted into the dataset." + "subtitle": "Provide data to be directly inserted into the dataset" } } }, @@ -1386,7 +1391,7 @@ "month": "Invalid month", "schema": { "id": "Column needs to be declared as primary key", - "primary-key": "We create a column named id with a auto-increasing sequence starting at 1. Please specify a column with primary key if you don't want this behavior." + "primary-key": "We create a column named id with a auto-increasing sequence starting at 1. Please specify a column with primary key if you don't want this behavior" }, "uri": { "pattern": "Invalid URI", 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 3c90c14858..27191e2a60 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 @@ -88,7 +88,7 @@ v-model="pickVersionDialog" max-width="640" @close="closeVersion"> - <TimeTravel + <TableHistory ref="timeTravel" @close="pickVersion" /> </v-dialog> @@ -117,7 +117,7 @@ </template> <script> -import TimeTravel from '@/components/dialogs/TimeTravel.vue' +import TableHistory from '@/components/table/TableHistory.vue' import TimeDrift from '@/components/TimeDrift.vue' import TableToolbar from '@/components/table/TableToolbar.vue' import {formatTimestampUTC, formatDateUTC, formatTimestamp} from '@/utils' @@ -130,7 +130,7 @@ export default { components: { BlobDownload, EditTuple, - TimeTravel, + TableHistory, TableToolbar, TimeDrift }, @@ -404,7 +404,7 @@ export default { return } try { - this.headers = [{ value: 'selection', title: '', sortable: false }] + this.headers = [] this.table.columns.map((c) => { return { value: c.internal_name, @@ -448,9 +448,9 @@ export default { }) this.loadingData = false }) - .catch(({code}) => { + .catch(({code, message}) => { const toast = useToastInstance() - toast.error(this.$t(code)) + toast.error(this.$t(code) + ": " + message) this.error = true this.loadingData = false }) @@ -463,9 +463,9 @@ export default { this.total = count this.loadingCount = false }) - .catch(({code}) => { + .catch(({code, message}) => { const toast = useToastInstance() - toast.error(this.$t(code)) + toast.error(this.$t(code) + ": " + message) this.loadingCount = false }) }, 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 0509cc0c2c..3a821a730b 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 @@ -78,7 +78,7 @@ variant="flat" rounded="0" tile - :title="$t('pages.table.subpages.schema.title')"> + :title="$t('pages.table.subpages.schema.subtitle')"> <v-card-text> <v-container> <ul> @@ -87,12 +87,7 @@ (<i v-text="primaryKeysColumns" />) </li> <li v-for="(foreignKey, i) in table.constraints.foreign_keys" :key="`fk-${i}`"> - <strong>FOREIGN KEY</strong> - <span v-text="foreignKey.name" /> - (<i v-text="foreignKeyColumns(foreignKey)" />) - <strong>REFERENCES</strong> - <a :href="`/database/${database.id}/table/${foreignKey.referenced_table.id}/schema`" v-text="foreignKeyReferencedTable(foreignKey)" /> - (<i v-text="foreignKeyReferencedColumns(foreignKey)" />) + <strong>FOREIGN KEY</strong> <span v-text="foreignKey.name" /> (<i v-text="foreignKeyColumns(foreignKey)" />) <strong>REFERENCES</strong> <a :href="`/database/${database.id}/table/${foreignKey.referenced_table.id}/schema`" v-text="foreignKeyReferencedTable(foreignKey)" /> (<i v-text="foreignKeyReferencedColumns(foreignKey)" />) </li> <li v-for="(uniqueConstraint, i) in table.constraints.uniques" :key="`uk-${i}`"> <strong>UNIQUE INDEX</strong> @@ -262,7 +257,7 @@ export default { if (!foreignKey) { return null } - return foreignKey.columns.map(c => c.internal_name).join(',') + return foreignKey.references.map(r => r.column.internal_name).join(',') }, foreignKeyReferencedTable (foreignKey) { if (!foreignKey) { @@ -274,7 +269,7 @@ export default { if (!foreignKey) { return null } - return foreignKey.referenced_columns.map(c => c.internal_name).join(',') + return foreignKey.references.map(r => r.referenced_column.internal_name).join(',') }, uniqueColumns (uniqueConstraint) { if (!uniqueConstraint) { diff --git a/dbrepo-ui/pages/database/[database_id]/table/create.vue b/dbrepo-ui/pages/database/[database_id]/table/create.vue index 34c87d163d..52af4ad304 100644 --- a/dbrepo-ui/pages/database/[database_id]/table/create.vue +++ b/dbrepo-ui/pages/database/[database_id]/table/create.vue @@ -113,7 +113,7 @@ <v-container v-if="table"> <v-row dense> - <v-col> + <v-col md="8"> <v-alert border="start" color="success" 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 783ad56637..03464f5dbb 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,7 @@ <template> - <div v-if="view"> - <ViewToolbar /> + <div> + <ViewToolbar + v-if="view" /> <v-toolbar color="secondary" :title="$t('toolbars.database.current')" @@ -19,7 +20,6 @@ id="query-results" ref="queryResults" type="view" - :view="view" class="mt-0 mb-0" /> </v-card> <v-breadcrumbs :items="items" class="pa-0 mt-2" /> @@ -76,15 +76,12 @@ export default { } }, mounted () { - if (!this.view) { - return - } this.reload() }, methods: { reload () { - this.$refs.queryResults.reExecute(this.view.id) - this.$refs.queryResults.reExecuteCount(this.view.id) + this.$refs.queryResults.reExecute(Number(this.$route.params.view_id)) + this.$refs.queryResults.reExecuteCount(Number(this.$route.params.view_id)) } } } diff --git a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/info.vue b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/info.vue index 44211b6eac..3ec97f2bda 100644 --- a/dbrepo-ui/pages/database/[database_id]/view/[view_id]/info.vue +++ b/dbrepo-ui/pages/database/[database_id]/view/[view_id]/info.vue @@ -1,10 +1,10 @@ <template> - <div - v-if="view"> + <div> <ViewToolbar /> <v-window v-model="tab"> - <v-window-item> + <v-window-item + v-if="view"> <v-card variant="flat"> <Summary v-if="hasIdentifier" @@ -25,6 +25,10 @@ <v-list v-if="view" dense> + <v-list-item + :title="$t('pages.view.name.title')"> + {{ view.internal_name }} + </v-list-item> <v-list-item :title="$t('pages.view.query.title')"> <pre>{{ view.query }}</pre> diff --git a/docker-compose.yml b/docker-compose.yml index da0d6a27ea..415c754090 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -115,14 +115,15 @@ services: volumes: - "${SHARED_VOLUME:-/tmp}:/tmp" environment: - ADMIN_MAIL: "${ADMIN_MAIL:-noreply@localhost}" + ADMIN_EMAIL: "${ADMIN_EMAIL:-noreply@localhost}" ADMIN_PASSWORD: "${ADMIN_PASSWORD:-admin}" ADMIN_USERNAME: "${ADMIN_USERNAME:-admin}" + ANALYSE_SERVICE_ENDPOINT: "${ANALYSE_SERVICE_ENDPOINT:-http://gateway-service}" AUTH_SERVICE_ADMIN: ${AUTH_SERVICE_ADMIN:-fda} AUTH_SERVICE_ADMIN_PASSWORD: ${AUTH_SERVICE_ADMIN_PASSWORD:-fda} AUTH_SERVICE_CLIENT: ${AUTH_SERVICE_CLIENT:-dbrepo-client} AUTH_SERVICE_CLIENT_SECRET: ${AUTH_SERVICE_CLIENT:-MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG} - AUTH_SERVICE_ENDPOINT: ${AUTH_SERVICE_ENDPOINT:-http://auth-service:8080} + AUTH_SERVICE_ENDPOINT: ${AUTH_SERVICE_ENDPOINT:-http://gateway-service/api/auth} BASE_URL: "${BASE_URL:-http://localhost}" BROKER_EXCHANGE_NAME: ${BROKER_EXCHANGE_NAME:-dbrepo} BROKER_QUEUE_NAME: ${BROKER_QUEUE_NAME:-dbrepo} @@ -134,10 +135,8 @@ services: BROKER_VIRTUALHOST: "${BROKER_VIRTUALHOST:-dbrepo}" DATA_SERVICE_ENDPOINT: ${DATA_SERVICE_ENDPOINT:-http://data-service:8080} DELETED_RECORD: "${DELETED_RECORD:-persistent}" - GATEWAY_SERVICE_ENDPOINT: ${GATEWAY_SERVICE_ENDPOINT:-http://gateway-service} GRANULARITY: "${GRANULARITY:-YYYY-MM-DDThh:mm:ssZ}" JWT_PUBKEY: "${JWT_PUBKEY:-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB}" -# LOG_LEVEL: ${LOG_LEVEL:-info} LOG_LEVEL: trace METADATA_DB: "${METADATA_DB:-dbrepo}" METADATA_HOST: "${METADATA_HOST:-metadata-db}" @@ -146,7 +145,7 @@ services: METADATA_PASSWORD: "${METADATA_PASSWORD:-dbrepo}" PID_BASE: ${PID_BASE:-http://localhost/pid/} REPOSITORY_NAME: "${REPOSITORY_NAME:-Database Repository}" - SEARCH_SERVICE_ENDPOINT: "${SEARCH_SERVICE_ENDPOINT:-http://search-service:8080}" + SEARCH_SERVICE_ENDPOINT: "${SEARCH_SERVICE_ENDPOINT:-http://gateway-service}" S3_ACCESS_KEY_ID: "${S3_ACCESS_KEY_ID:-seaweedfsadmin}" S3_ENDPOINT: "${S3_ENDPOINT:-http://storage-service:9000}" S3_EXPORT_BUCKET: "${S3_EXPORT_BUCKET:-dbrepo-download}" @@ -154,7 +153,7 @@ services: S3_SECRET_ACCESS_KEY: "${S3_SECRET_ACCESS_KEY:-seaweedfsadmin}" SPARQL_CONNECTION_TIMEOUT: "${SPARQL_CONNECTION_TIMEOUT:-10000}" healthcheck: - test: wget -qO- localhost:8080/actuator/health/readiness | grep -q "UP" || exit 1 + test: curl -sSL localhost:8080/actuator/health/liveness | grep 'UP' || exit 1 interval: 10s timeout: 5s retries: 12 @@ -399,6 +398,8 @@ services: volumes: - ./dbrepo-storage-service/s3_config.json:/app/s3_config.json - storage-service-data:/data + ports: + - "9000:9000" healthcheck: test: echo "cluster.check" | weed shell | grep "checking master.*ok" || exit 1 interval: 10s @@ -477,11 +478,11 @@ services: BROKER_VIRTUALHOST: "${BROKER_VIRTUALHOST:-dbrepo}" CONNECTION_TIMEOUT: ${CONNECTION_TIMEOUT:-60000} EXCHANGE_NAME: ${EXCHANGE_NAME:-dbrepo} - GATEWAY_SERVICE_ENDPOINT: ${GATEWAY_SERVICE_ENDPOINT:-http://gateway-service} + METADATA_SERVICE_ENDPOINT: ${METADATA_SERVICE_ENDPOINT:-http://gateway-service} GRANT_DEFAULT_READ: "${GRANT_DEFAULT_READ:-SELECT}" GRANT_DEFAULT_WRITE: "${GRANT_DEFAULT_WRITE:-SELECT, CREATE, CREATE VIEW, CREATE ROUTINE, CREATE TEMPORARY TABLES, LOCK TABLES, INDEX, TRIGGER, INSERT, UPDATE, DELETE}" JWT_PUBKEY: "${JWT_PUBKEY:-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB}" - LOG_LEVEL: ${LOG_LEVEL:-info} + LOG_LEVEL: ${LOG_LEVEL:-trace} MIN_CONCURRENT_CONSUMERS: ${MIN_CONCURRENT_CONSUMERS:-1} MAX_CONCURRENT_CONSUMERS: ${MAX_CONCURRENT_CONSUMERS:-5} QUEUE_NAME: ${QUEUE_NAME:-dbrepo} @@ -494,7 +495,7 @@ services: S3_IMPORT_BUCKET: "${S3_IMPORT_BUCKET:-dbrepo-upload}" S3_SECRET_ACCESS_KEY: "${S3_SECRET_ACCESS_KEY:-seaweedfsadmin}" healthcheck: - test: wget -qO- localhost:8080/actuator/health/readiness | grep -q "UP" || exit 1 + test: curl -sSL localhost:8080/actuator/health/liveness | grep 'UP' || exit 1 interval: 10s timeout: 5s retries: 12 diff --git a/helm/dbrepo/.helmignore b/helm/dbrepo/.helmignore index 62d87df84f..b9029e8dea 100644 --- a/helm/dbrepo/.helmignore +++ b/helm/dbrepo/.helmignore @@ -14,6 +14,7 @@ hack/ .svn/ # Generated build/ +artifacthub-repo.yml # Common backup files *.swp *.bak diff --git a/helm/dbrepo/Makefile b/helm/dbrepo/Makefile deleted file mode 100644 index b89c9b4dec..0000000000 --- a/helm/dbrepo/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -.PHONY: all -all: - -.PHONY: build -build: ## Generate Helm values schema JSON - #helm package --sign --key 'Martin Weise' . --keyring ~/.gnupg/mweise.gpg --destination ./build - helm schema -input ./values.yaml - readme-generator-for-helm --readme README.md --values values.yaml diff --git a/helm/dbrepo/README.md b/helm/dbrepo/README.md index f7c43ba5db..01f699d9e1 100644 --- a/helm/dbrepo/README.md +++ b/helm/dbrepo/README.md @@ -145,6 +145,7 @@ The command removes all the Kubernetes components associated with the chart and | Name | Description | Value | | ----------------------------- | ----------------------------------------------------- | ------------------------------- | | `analyseservice.enabled` | Enable the Broker Service. | `true` | +| `analyseservice.endpoint` | The url of the endpoint. | `http://analyse-service` | | `analyseservice.s3.endpoint` | The S3-capable endpoint the microservice connects to. | `http://storageservice-s3:9000` | | `analyseservice.replicaCount` | The number of replicas. | `2` | @@ -153,6 +154,7 @@ The command removes all the Kubernetes components associated with the chart and | Name | Description | Value | | ------------------------------------------ | --------------------------------------------------------------------- | ------------------------------- | | `metadataservice.enabled` | Enable the Metadata Service. | `true` | +| `metadataservice.endpoint` | The Metadata Service endpoint. | `http://metadata-service` | | `metadataservice.admin.email` | The OAI-PMH exposed admin e-mail. | `noreply@example.com` | | `metadataservice.deletedRecord` | The OAI-PMH exposed delete policy. | `permanent` | | `metadataservice.repositoryName` | The OAI-PMH exposed repository name. | `Database Repository` | diff --git a/helm/artifacthub-repo.yml b/helm/dbrepo/artifacthub-repo.yml similarity index 100% rename from helm/artifacthub-repo.yml rename to helm/dbrepo/artifacthub-repo.yml diff --git a/helm/dbrepo/templates/data-secret.yaml b/helm/dbrepo/templates/data-secret.yaml index b5307a23d9..7797626672 100644 --- a/helm/dbrepo/templates/data-secret.yaml +++ b/helm/dbrepo/templates/data-secret.yaml @@ -31,6 +31,7 @@ stringData: DEFAULT_TIMESTAMP_FORMAT_ID: "{{ .Values.dataservice.default.timestamp }}" JWT_PUBKEY: "{{ .Values.authservice.jwt.pubkey }}" LOG_LEVEL: "{{ ternary "debug" "info" .Values.dataservice.image.debug }}" + METADATA_SERVICE_ENDPOINT: "{{ .Values.metadataservice.endpoint }}" MIN_CONCURRENT_CONSUMERS: "{{ .Values.dataservice.consumerConcurrentMin }}" MAX_CONCURRENT_CONSUMERS: "{{ .Values.dataservice.consumerConcurrentMax }}" REQUEUE_REJECTED: "{{ .Values.dataservice.requeueRejected }}" diff --git a/helm/dbrepo/templates/metadata-configmap.yaml b/helm/dbrepo/templates/metadata-configmap.yaml index 88c761643a..4bb2eb136b 100644 --- a/helm/dbrepo/templates/metadata-configmap.yaml +++ b/helm/dbrepo/templates/metadata-configmap.yaml @@ -13,11 +13,10 @@ data: BEGIN; INSERT INTO `mdb_containers` (name, internal_name, image_id, host, port, sidecar_host, sidecar_port, privileged_username, privileged_password) VALUES ('MariaDB Galera 11.1.3', 'mariadb_11_1_3', 1, 'data-db', 3306, 'data-db', 80, 'root', 'dbrepo'); - INSERT INTO `mdb_banner_messages` (type, message) - VALUES ('INFO', 'You are currently working on our test environment. Any data upload to this system may be deleted.'); COMMIT; 01-setup-schema.sql: | BEGIN; + CREATE TABLE IF NOT EXISTS `mdb_users` ( id character varying(36) NOT NULL, @@ -138,13 +137,13 @@ data: CREATE TABLE IF NOT EXISTS `mdb_tables` ( - ID bigint NOT NULL AUTO_INCREMENT, - tDBID bigint NOT NULL, - internal_name character varying(255) NOT NULL, - queue_name character varying(255) NOT NULL, - routing_key character varying(255), - tName VARCHAR(50), - tDescription TEXT, + ID bigint NOT NULL AUTO_INCREMENT, + tDBID bigint NOT NULL, + tName VARCHAR(64) NOT NULL, + internal_name VARCHAR(64) NOT NULL, + queue_name VARCHAR(255) NOT NULL, + routing_key VARCHAR(255), + tDescription VARCHAR(2048), num_rows BIGINT, data_length BIGINT, max_data_length BIGINT, @@ -156,12 +155,13 @@ data: element_true VARCHAR(50), element_false VARCHAR(50), Version TEXT, - created timestamp NOT NULL DEFAULT NOW(), - versioned boolean not null default true, - created_by character varying(36) NOT NULL, - owned_by character varying(36) NOT NULL, + created timestamp NOT NULL DEFAULT NOW(), + versioned boolean not null default true, + created_by character varying(36) NOT NULL, + owned_by character varying(36) NOT NULL, last_modified timestamp, PRIMARY KEY (ID), + UNIQUE (tDBID, internal_name), FOREIGN KEY (tDBID) REFERENCES mdb_databases (id), FOREIGN KEY (created_by) REFERENCES mdb_users (id), FOREIGN KEY (owned_by) REFERENCES mdb_users (id) @@ -169,25 +169,26 @@ data: CREATE TABLE IF NOT EXISTS `mdb_columns` ( - ID BIGINT NOT NULL AUTO_INCREMENT, - tID BIGINT NOT NULL, + ID BIGINT NOT NULL AUTO_INCREMENT, + tID BIGINT NOT NULL, dfID BIGINT, - cName VARCHAR(100), - internal_name VARCHAR(100) NOT NULL, + cName VARCHAR(64), + internal_name VARCHAR(64) NOT NULL, Datatype ENUM ('CHAR','VARCHAR','BINARY','VARBINARY','TINYBLOB','TINYTEXT','TEXT','BLOB','MEDIUMTEXT','MEDIUMBLOB','LONGTEXT','LONGBLOB','ENUM','SET','BIT','TINYINT','BOOL','SMALLINT','MEDIUMINT','INT','BIGINT','FLOAT','DOUBLE','DECIMAL','DATE','DATETIME','TIMESTAMP','TIME','YEAR'), - length BIGINT NULL, - ordinal_position INTEGER NOT NULL, - index_length BIGINT NULL, + length BIGINT NULL, + ordinal_position INTEGER NOT NULL, + index_length BIGINT NULL, + description VARCHAR(2048), size BIGINT, d BIGINT, - auto_generated BOOLEAN DEFAULT false, - is_null_allowed BOOLEAN NOT NULL DEFAULT true, - val_min NUMERIC NULL, - val_max NUMERIC NULL, - mean NUMERIC NULL, - median NUMERIC NULL, - std_dev Numeric NULL, - created timestamp NOT NULL DEFAULT NOW(), + auto_generated BOOLEAN DEFAULT false, + is_null_allowed BOOLEAN NOT NULL DEFAULT true, + val_min NUMERIC NULL, + val_max NUMERIC NULL, + mean NUMERIC NULL, + median NUMERIC NULL, + std_dev Numeric NULL, + created timestamp NOT NULL DEFAULT NOW(), last_modified timestamp, FOREIGN KEY (tID) REFERENCES mdb_tables (ID) ON DELETE CASCADE, PRIMARY KEY (ID) @@ -344,8 +345,8 @@ data: ( id bigint NOT NULL AUTO_INCREMENT, vdbid bigint NOT NULL, - vName VARCHAR(255) NOT NULL, - internal_name VARCHAR(255) NOT NULL, + vName VARCHAR(64) NOT NULL, + internal_name VARCHAR(64) NOT NULL, Query TEXT NOT NULL, query_hash VARCHAR(255) NOT NULL, Public BOOLEAN NOT NULL, @@ -387,14 +388,19 @@ data: CREATE TABLE IF NOT EXISTS `mdb_view_columns` ( - id BIGINT NOT NULL AUTO_INCREMENT, - cid BIGINT NOT NULL, - vid BIGINT NOT NULL, - alias VARCHAR(100), - ordinal_position INTEGER, + id BIGINT NOT NULL AUTO_INCREMENT, + view_id BIGINT NOT NULL, + dfID BIGINT, + name VARCHAR(64), + internal_name VARCHAR(64) NOT NULL, + column_type ENUM ('CHAR','VARCHAR','BINARY','VARBINARY','TINYBLOB','TINYTEXT','TEXT','BLOB','MEDIUMTEXT','MEDIUMBLOB','LONGTEXT','LONGBLOB','ENUM','SET','BIT','TINYINT','BOOL','SMALLINT','MEDIUMINT','INT','BIGINT','FLOAT','DOUBLE','DECIMAL','DATE','DATETIME','TIMESTAMP','TIME','YEAR'), + ordinal_position INTEGER NOT NULL, + size BIGINT, + d BIGINT, + auto_generated BOOLEAN DEFAULT false, + is_null_allowed BOOLEAN NOT NULL DEFAULT true, PRIMARY KEY (id), - FOREIGN KEY (vid) REFERENCES mdb_view (id), - FOREIGN KEY (cid) REFERENCES mdb_columns (ID) + FOREIGN KEY (view_id) REFERENCES mdb_view (id) ) WITH SYSTEM VERSIONING; CREATE TABLE IF NOT EXISTS `mdb_identifiers` @@ -503,18 +509,6 @@ data: FOREIGN KEY (pid) REFERENCES mdb_identifiers (id) ) WITH SYSTEM VERSIONING; - CREATE TABLE IF NOT EXISTS `mdb_feed` - ( - fDBID bigint, - fID bigint, - fUserId character varying(36) not null, - fDataID bigint REFERENCES mdb_data (ID), - created timestamp NOT NULL DEFAULT NOW(), - PRIMARY KEY (fDBID, fID, fUserId, fDataID), - FOREIGN KEY (fDBID, fID) REFERENCES mdb_tables (tDBID, ID), - FOREIGN KEY (fUserId) REFERENCES mdb_users (id) - ) WITH SYSTEM VERSIONING; - CREATE TABLE IF NOT EXISTS `mdb_update` ( uUserID character varying(255) NOT NULL, diff --git a/helm/dbrepo/templates/metadata-secret.yaml b/helm/dbrepo/templates/metadata-secret.yaml index db8328b7a8..3beda17fc5 100644 --- a/helm/dbrepo/templates/metadata-secret.yaml +++ b/helm/dbrepo/templates/metadata-secret.yaml @@ -7,8 +7,9 @@ metadata: namespace: {{ .Values.namespace }} stringData: ADMIN_EMAIL: "{{ .Values.metadataservice.admin.email }}" - ADMIN_USERNAME: "{{ .Values.admin.username }}" ADMIN_PASSWORD: "{{ .Values.admin.password }}" + ADMIN_USERNAME: "{{ .Values.admin.username }}" + ANALYSE_SERVICE_ENDPOINT: "{{ .Values.analyseservice.endpoint }}" AUTH_SERVICE_ADMIN: "{{ .Values.authservice.auth.adminUser }}" AUTH_SERVICE_ADMIN_PASSWORD: "{{ .Values.authservice.auth.adminPassword }}" AUTH_SERVICE_CLIENT: "{{ .Values.authservice.client.id }}" @@ -29,7 +30,6 @@ stringData: DATACITE_USERNAME: "{{ .Values.metadataservice.datacite.username }}" DATACITE_PASSWORD: "{{ .Values.metadataservice.datacite.password }}" DELETED_RECORD: "{{ .Values.metadataservice.deletedRecord }}" - GATEWAY_SERVICE_ENDPOINT: "{{ .Values.gateway }}" GRANULARITY: "{{ .Values.metadataservice.granularity }}" JWT_PUBKEY: "{{ .Values.authservice.jwt.pubkey }}" LOG_LEVEL: "{{ ternary "trace" "info" .Values.metadataservice.image.debug }}" diff --git a/helm/dbrepo/values.prod.yaml b/helm/dbrepo/values.prod.yaml deleted file mode 100644 index 93c0bf3d72..0000000000 --- a/helm/dbrepo/values.prod.yaml +++ /dev/null @@ -1,514 +0,0 @@ -namespace: dbrepo - -hostname: example.com - -metadataDb: - fullnameOverride: metadata-db - image: - debug: false - host: metadata-db - rootUser: - user: root - password: dbrepo - jdbcExtraArgs: "" - db: - name: fda - metrics: - enabled: false - galera: - mariabackup: - user: mariabackup - password: mariabackup - initdbScriptsConfigMap: metadata-db-setup - service: - type: ClusterIP - annotations: { } - loadBalancerIP: "" - loadBalancerSourceRanges: [ ] - persistence: - enabled: true - replicaCount: 3 # uneven 3,5,7 - -authService: - fullnameOverride: auth-service - image: - debug: false - auth: - adminUser: fda - adminPassword: fda - postgresql: - enabled: false # not needed - extraStartupArgs: "--import-realm" - tls: - enabled: true - existingSecret: ingress-cert - usePem: true - metrics: - enabled: true - externalDatabase: - existingSecret: auth-service-secret - existingSecretDatabaseKey: db-name - existingSecretHostKey: db-host - existingSecretPortKey: db-port - existingSecretUserKey: db-username - existingSecretPasswordKey: db-password - client: - id: dbrepo-client - secret: MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG - extraEnvVarsCM: auth-service-config - extraVolumes: - - name: config-map - configMap: - name: auth-service-setup - extraVolumeMounts: - - name: config-map - mountPath: /opt/bitnami/keycloak/data/import - replicaCount: 2 - -authDb: - fullnameOverride: auth-db - host: auth-db-pgpool - port: 5432 - postgresql: - postgresPassword: postgres - username: metrics # implicit requirement for metrics container - password: metrics # implicit requirement for metrics container - repmgrPassword: repmgr # implicit requirement for rolling updates - database: keycloak - replicaCount: 3 - pgpool: - adminUsername: admin - adminPassword: admin - metrics: - enabled: true - service: - type: ClusterIP - annotations: { } - loadBalancerIP: "" - loadBalancerSourceRanges: [ ] - persistence: - enabled: true - size: 10Gi - -dataDb: - fullnameOverride: data-db - image: - debug: false - extraFlags: "--character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci" - rootUser: - user: root - password: dbrepo - metrics: - enabled: true - galera: - mariabackup: - user: mariabackup - password: mariabackup - sidecars: - - name: sidecar - image: s210.dl.hpc.tuwien.ac.at/dbrepo/data-db-sidecar:1.4.2 - imagePullPolicy: Always - securityContext: - runAsUser: 1001 - runAsGroup: 1001 - ports: - - containerPort: 3305 - protocol: TCP - env: - - name: S3_STORAGE_ENDPOINT - value: http://storageservice-s3:9000 - - name: S3_ACCESS_KEY_ID - value: seaweedfsadmin - - name: S3_SECRET_ACCESS_KEY - value: seaweedfsadmin - volumeMounts: - - name: tmp # share between sidecar and galera container - mountPath: /tmp - service: - type: ClusterIP - annotations: { } - #loadBalancerIP: 1.2.3.4 - loadBalancerSourceRanges: [ ] - extraPorts: - - name: "sidecar" - port: 3305 - targetPort: 3305 - protocol: TCP - extraVolumeMounts: - - name: tmp - mountPath: /tmp - extraVolumes: - # - name: tmp - # emptyDir: {} - - name: tmp - persistentVolumeClaim: - claimName: data-db-shared - persistence: - enabled: true - size: 10Gi - replicaCount: 3 # uneven 3,5,7 - -searchdb: - fullnameOverride: search-db - host: search-db - port: 9200 - protocol: http - username: admin - password: admin - clusterName: search-db - masterService: search-db - replicas: 3 # uneven 3,5,7 - image: - debug: false - sysctlInit: - enabled: true - persistence: - enabled: true - size: 10Gi - service: - type: ClusterIP - annotations: { } - loadBalancerSourceRanges: [ ] - extraEnvs: - - name: DISABLE_INSTALL_DEMO_CONFIG - value: "true" - extraVolumeMounts: - - name: node-cert - mountPath: /usr/share/opensearch/config/tls - readOnly: true - extraVolumes: - - name: node-cert - secret: - secretName: search-db-cert - config: - opensearch.yml: | - cluster.name: search-db - network.host: 0.0.0.0 - plugins: - security: - ssl: - transport: - pemcert_filepath: tls/tls.crt - pemkey_filepath: tls/tls.key - pemtrustedcas_filepath: tls/ca.crt - enforce_hostname_verification: false - http: - #enabled: true # uncomment to force ssl connections - pemcert_filepath: tls/tls.crt - pemkey_filepath: tls/tls.key - pemtrustedcas_filepath: tls/ca.crt - allow_unsafe_democertificates: false - allow_default_init_securityindex: true - authcz: - admin_dn: - - CN=search-db - nodes_dn: - - CN=search-db - audit.type: internal_opensearch - enable_snapshot_restore_privilege: true - check_snapshot_restore_write_privileges: true - restapi: - roles_enabled: [ "all_access", "security_rest_api_access" ] - system_indices: - enabled: true - indices: - [ - ".opendistro-alerting-config", - ".opendistro-alerting-alert*", - ".opendistro-anomaly-results*", - ".opendistro-anomaly-detector*", - ".opendistro-anomaly-checkpoints", - ".opendistro-anomaly-detection-state", - ".opendistro-reports-*", - ".opendistro-notifications-*", - ".opendistro-notebooks", - ".opendistro-asynchronous-search-response*", - ] - -searchDbDashboard: - fullnameOverride: search-db-dashboard - opensearchHosts: http://search-db:9200 - extraInitContainers: - - name: init - image: s210.dl.hpc.tuwien.ac.at/dbrepo/search-db-init:1.4.2 - imagePullPolicy: Always - env: - - name: OPENSEARCH_HOST - value: http://search-db:9200 - extraVolumeMounts: - - name: tls - mountPath: /usr/share/opensearch-dashboards/tls - readOnly: true - - name: config - mountPath: /usr/share/opensearch-dashboards/config/opensearch_dashboards.yml - subPath: opensearch_dashboards.yml - readOnly: true - extraVolumes: - - name: tls - secret: - secretName: ingress-cert - - name: config - secret: - secretName: search-db-dashboard-secret - replicaCount: 2 - -uploadService: - enabled: true - image: - registry: docker.io - repository: tusproject/tusd - tag: v1.12 - replicaCount: 2 - -brokerService: - fullnameOverride: broker-service - image: - debug: true - url: http://broker-service:15672 - host: broker-service - port: 5672 - virtualHost: dbrepo - queueName: dbrepo - exchangeName: dbrepo - routingKey: dbrepo.# - connectionTimeout: 60000 - auth: - tls: - enabled: false - sslOptionsVerify: true - failIfNoPeerCert: true - existingSecret: ingress-cert - username: broker - password: broker - extraConfiguration: |- - default_vhost = dbrepo - default_user_tags.administrator = true - default_permissions.configure = .* - default_permissions.read = .* - default_permissions.write = .* - load_definitions = /etc/rabbitmq/definitions.json - log.console = true - listeners.tcp.1 = 0.0.0.0:5672 - auth_backends.1 = rabbit_auth_backend_oauth2 - auth_backends.2 = rabbit_auth_backend_internal - auth_oauth2.resource_server_id = rabbitmq - auth_oauth2.preferred_username_claims.1 = client_id - auth_oauth2.default_key = t2OCeCheJ9uwoBbNQjG_nN6WKiLcceTIAZmiTbGODFM - auth_oauth2.signing_keys.t2OCeCheJ9uwoBbNQjG_nN6WKiLcceTIAZmiTbGODFM = /etc/rabbitmq/cert.pem - auth_oauth2.signing_keys.id2 = /etc/rabbitmq/pubkey.pem - auth_oauth2.algorithms.1 = HS256 - auth_oauth2.algorithms.2 = RS256 - loadDefinition: - enabled: true - file: /etc/rabbitmq/definitions.json - existingSecret: broker-service-secret - extraVolumeMounts: - - name: secret-map - mountPath: /etc/rabbitmq/definitions.json - subPath: definitions.json - readOnly: true - - name: secret-map - mountPath: /etc/rabbitmq/pubkey.pem - subPath: pubkey.pem - readOnly: true - - name: secret-map - mountPath: /etc/rabbitmq/cert.pem - subPath: cert.pem - readOnly: true - extraVolumes: - - name: secret-map - secret: - secretName: broker-service-secret - extraPlugins: rabbitmq_prometheus rabbitmq_auth_backend_oauth2 rabbitmq_auth_mechanism_ssl - persistence: - enabled: false - size: 5Gi - service: - type: ClusterIP - # loadBalancerIP: - replicaCount: 2 - -analyseService: - enabled: true - image: - name: s210.dl.hpc.tuwien.ac.at/dbrepo/analyse-service:1.4.2 - pullPolicy: Always - debug: false - replicaCount: 2 - -metadataService: - enabled: true - image: - name: s210.dl.hpc.tuwien.ac.at/dbrepo/metadata-service:1.4.2 - pullPolicy: Always - debug: false - adminEmail: noreply@example.com - authService: - url: http://auth-service - website: http://example.com - repositoryName: Database Repository - datacite: - enabled: false - url: https://api.datacite.org - prefix: "" - username: "" - password: "" - rates: - deleteStaleFiles: 60 - mirror: 60 - obtainMetadata: 60 - deleteStaleQueries: 60 - replicaCount: 2 - -dataService: - enabled: true - image: - name: s210.dl.hpc.tuwien.ac.at/dbrepo/data-service:1.4.2 - pullPolicy: Always - debug: false - jwt: - pubkey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB" - consumerConcurrentMin: 1 - consumerConcurrentMax: 5 - requeueRejected: false - replicaCount: 2 - -searchService: - enabled: true - image: - name: s210.dl.hpc.tuwien.ac.at/dbrepo/search-service:1.4.2 - pullPolicy: Always - debug: false - replicaCount: 2 - -storageservice: - master: - enabled: true - filer: - enabled: true - replicas: 2 - enablePVC: false - storage: 25Gi - s3: - enabled: true - allowEmptyFolder: true - port: 9000 - enableAuth: true - skipAuthSecretCreation: true - existingConfigSecret: seaweedfs-s3-secret - volume: - enabled: true - replicas: 2 - s3: - enabled: true - replicas: 2 - port: 9000 - metricsPort: 9091 - enableAuth: true - skipAuthSecretCreation: true - existingConfigSecret: seaweedfs-s3-secret - auth: - username: seaweedfsadmin - password: seaweedfsadmin - -logservice: - fullnameOverride: log-service - config: - outputs: | - [OUTPUT] - Name opensearch - Match kube.* - Host search-db - Port 9200 - HTTP_User admin - HTTP_Passwd admin - Logstash_Format On - Replace_Dots On - Type _doc - Retry_Limit False - Suppress_Type_Name On - - [OUTPUT] - Name opensearch - Match host.* - Host search-db - Port 9200 - HTTP_User admin - HTTP_Passwd admin - Logstash_Format On - Logstash_Prefix node - Replace_Dots On - Type _doc - Retry_Limit False - Suppress_Type_Name On -# Replace_Dots On -# Suppress_Type_Name On - -ui: - enabled: true - image: - name: s210.dl.hpc.tuwien.ac.at/dbrepo/ui:1.4.2 - pullPolicy: Always - debug: false - public: - api: - client: {} - server: {} - title: "Database Repository" - logo: "/logo.svg" - icon: "/favicon.ico" - touch: "/apple-touch-icon.png" - broker: - host: example.com - port: - 5671: true - 5672: false - extra: "128.130.0.0/15" - database: - extra: "128.130.0.0/15" - pid: - default: - publisher: "Example University" - doi: - enabled: false - endpoint: https://doi.org - replicaCount: 2 - extraVolumes: [ ] - # - name: images-map - # configMap: - # name: ui-config - extraVolumeMounts: [ ] - # - name: images-map - # mountPath: /static/logo.svg - # subPath: logo.svg - -ingress: - enabled: true - className: nginx - tls: - enabled: true - secretName: ingress-cert - annotations: - basic: {} -# cert-manager.io/cluster-issuer: letsencrypt-cluster-issuer - secure: -# cert-manager.io/cluster-issuer: letsencrypt-cluster-issuer - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" - upload: -# cert-manager.io/cluster-issuer: letsencrypt-cluster-issuer - nginx.ingress.kubernetes.io/proxy-body-size: 2G - rewriteApi: -# cert-manager.io/cluster-issuer: letsencrypt-cluster-issuer - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/rewrite-target: /api/$1 - rewriteRoot: -# cert-manager.io/cluster-issuer: letsencrypt-cluster-issuer - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/rewrite-target: /$1 - rewritePid: -# cert-manager.io/cluster-issuer: letsencrypt-cluster-issuer - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/rewrite-target: /api/pid/$1 diff --git a/helm/dbrepo/values.yaml b/helm/dbrepo/values.yaml index d15d46c305..dc8cd7bdba 100644 --- a/helm/dbrepo/values.yaml +++ b/helm/dbrepo/values.yaml @@ -173,12 +173,13 @@ datadb: service: extraPorts: - name: "sidecar" - port: 80 + port: 8080 targetPort: 8080 protocol: TCP sidecars: - name: sidecar image: s210.dl.hpc.tuwien.ac.at/dbrepo/data-db-sidecar:1.4.4 + imagePullPolicy: Always securityContext: runAsUser: 1001 runAsGroup: 0 @@ -430,6 +431,7 @@ brokerservice: ## @param analyseservice.enabled Enable the Broker Service. ## @skip analyseservice.image +## @param analyseservice.endpoint The url of the endpoint. ## @param analyseservice.s3.endpoint The S3-capable endpoint the microservice connects to. ## @param analyseservice.replicaCount The number of replicas. ## @@ -439,6 +441,7 @@ analyseservice: name: s210.dl.hpc.tuwien.ac.at/dbrepo/analyse-service:1.4.4 pullPolicy: Always debug: false + endpoint: http://analyse-service s3: endpoint: http://storageservice-s3:9000 replicaCount: 2 @@ -447,6 +450,7 @@ analyseservice: ## @param metadataservice.enabled Enable the Metadata Service. ## @skip metadataservice.image +## @param metadataservice.endpoint The Metadata Service endpoint. ## @param metadataservice.admin.email The OAI-PMH exposed admin e-mail. ## @param metadataservice.deletedRecord The OAI-PMH exposed delete policy. ## @param metadataservice.repositoryName The OAI-PMH exposed repository name. @@ -469,6 +473,7 @@ metadataservice: name: s210.dl.hpc.tuwien.ac.at/dbrepo/metadata-service:1.4.4 pullPolicy: Always debug: false + endpoint: http://metadata-service admin: email: noreply@example.com deletedRecord: permanent diff --git a/install.sh b/install.sh index eeba0c1d45..9850ccd35e 100644 --- a/install.sh +++ b/install.sh @@ -58,7 +58,7 @@ fi # environment echo "[🚀] Gathering environment ..." mkdir -p ./dist -curl -sSL -o ./docker-compose.yml "https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/release-${VERSION}/docker-compose.prod.yml" +curl -sSL -o ./docker-compose.yml "https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/release-${VERSION}/.docker/docker-compose.yml" curl -sSL -o ./dist/2_setup-data.sql "https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/release-${VERSION}/dbrepo-metadata-db/2_setup-data.sql" curl -sSL -o ./dist/rabbitmq.conf "https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/release-${VERSION}/dbrepo-broker-service/rabbitmq.conf" curl -sSL -o ./dist/enabled_plugins "https://gitlab.phaidra.org/fair-data-austria-db-repository/fda-services/-/raw/release-${VERSION}/dbrepo-broker-service/enabled_plugins" diff --git a/lib/python/Pipfile.lock b/lib/python/Pipfile.lock index 554a33747d..953bcf20f3 100644 --- a/lib/python/Pipfile.lock +++ b/lib/python/Pipfile.lock @@ -18,85 +18,85 @@ "default": { "aiohttp": { "hashes": [ - "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168", - "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb", - "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5", - "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f", - "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc", - "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c", - "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29", - "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4", - "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc", - "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc", - "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63", - "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e", - "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d", - "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a", - "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60", - "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38", - "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b", - "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2", - "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53", - "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5", - "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4", - "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96", - "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58", - "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa", - "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321", - "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae", - "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce", - "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8", - "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194", - "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c", - "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf", - "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d", - "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869", - "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b", - "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52", - "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528", - "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5", - "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1", - "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4", - "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8", - "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d", - "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7", - "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5", - "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54", - "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3", - "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5", - "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c", - "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29", - "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3", - "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747", - "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672", - "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5", - "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11", - "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca", - "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768", - "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6", - "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2", - "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533", - "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6", - "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266", - "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d", - "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec", - "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5", - "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1", - "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b", - "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679", - "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283", - "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb", - "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b", - "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3", - "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051", - "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511", - "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e", - "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d", - "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542", - "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f" + "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8", + "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c", + "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475", + "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed", + "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf", + "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372", + "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81", + "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f", + "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1", + "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd", + "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a", + "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb", + "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46", + "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de", + "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78", + "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c", + "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771", + "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb", + "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430", + "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233", + "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156", + "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9", + "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59", + "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888", + "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c", + "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c", + "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da", + "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424", + "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2", + "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb", + "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8", + "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a", + "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10", + "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0", + "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09", + "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031", + "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4", + "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3", + "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa", + "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a", + "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe", + "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a", + "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2", + "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1", + "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323", + "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b", + "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b", + "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106", + "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac", + "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6", + "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832", + "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75", + "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6", + "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d", + "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72", + "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db", + "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a", + "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da", + "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678", + "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b", + "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24", + "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed", + "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f", + "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e", + "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58", + "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a", + "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342", + "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558", + "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2", + "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551", + "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595", + "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee", + "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11", + "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d", + "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7", + "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f" ], "markers": "python_version >= '3.8'", - "version": "==3.9.3" + "version": "==3.9.5" }, "aiosignal": { "hashes": [ @@ -108,11 +108,11 @@ }, "annotated-types": { "hashes": [ - "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", - "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" + "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" ], "markers": "python_version >= '3.8'", - "version": "==0.6.0" + "version": "==0.7.0" }, "attrs": { "hashes": [ @@ -124,11 +124,11 @@ }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", + "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.6.2" }, "charset-normalizer": { "hashes": [ @@ -311,11 +311,11 @@ }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "multidict": { "hashes": [ @@ -457,39 +457,38 @@ }, "pandas": { "hashes": [ - "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee", - "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e", - "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572", - "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944", - "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403", - "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89", - "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab", - "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6", - "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb", - "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9", - "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019", - "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be", - "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd", - "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c", - "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88", - "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0", - "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397", - "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc", - "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2", - "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7", - "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06", - "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51", - "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0", - "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a", - "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16", - "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02", - "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359", - "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b", - "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df" + "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863", + "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2", + "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1", + "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad", + "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db", + "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76", + "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51", + "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32", + "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08", + "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b", + "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4", + "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921", + "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288", + "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee", + "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0", + "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24", + "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99", + "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151", + "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd", + "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce", + "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57", + "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef", + "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54", + "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a", + "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238", + "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23", + "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772", + "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce", + "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==2.2.1" + "version": "==2.2.2" }, "pika": { "hashes": [ @@ -497,102 +496,100 @@ "sha256:b2a327ddddf8570b4965b3576ac77091b850262d34ce8c1d8cb4e4146aa4145f" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==1.3.2" }, "pydantic": { "hashes": [ - "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6", - "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5" + "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e", + "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.6.4" + "version": "==2.7.3" }, "pydantic-core": { "hashes": [ - "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a", - "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed", - "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979", - "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff", - "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5", - "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45", - "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340", - "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad", - "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23", - "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6", - "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7", - "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241", - "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda", - "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187", - "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba", - "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c", - "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2", - "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c", - "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132", - "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf", - "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972", - "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db", - "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade", - "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4", - "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8", - "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f", - "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9", - "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48", - "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec", - "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d", - "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9", - "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb", - "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4", - "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89", - "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c", - "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9", - "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da", - "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac", - "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b", - "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf", - "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e", - "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137", - "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1", - "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b", - "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8", - "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e", - "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053", - "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01", - "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe", - "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd", - "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805", - "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183", - "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8", - "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99", - "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820", - "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074", - "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256", - "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8", - "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975", - "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad", - "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e", - "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca", - "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df", - "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b", - "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a", - "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a", - "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721", - "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a", - "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f", - "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2", - "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97", - "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6", - "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed", - "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc", - "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1", - "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe", - "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120", - "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f", - "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a" + "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3", + "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8", + "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8", + "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30", + "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a", + "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8", + "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d", + "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc", + "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2", + "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab", + "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077", + "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e", + "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9", + "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9", + "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef", + "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1", + "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507", + "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528", + "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558", + "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b", + "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154", + "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724", + "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695", + "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9", + "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851", + "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805", + "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a", + "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5", + "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94", + "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c", + "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d", + "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef", + "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26", + "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2", + "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c", + "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0", + "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2", + "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4", + "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d", + "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2", + "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce", + "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34", + "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f", + "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d", + "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b", + "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07", + "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312", + "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057", + "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d", + "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af", + "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb", + "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd", + "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78", + "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b", + "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223", + "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a", + "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4", + "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5", + "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23", + "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a", + "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4", + "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8", + "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d", + "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443", + "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e", + "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f", + "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e", + "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d", + "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc", + "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443", + "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be", + "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2", + "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee", + "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f", + "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae", + "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864", + "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4", + "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951", + "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc" ], "markers": "python_version >= '3.8'", - "version": "==2.16.3" + "version": "==2.18.4" }, "python-dateutil": { "hashes": [ @@ -611,12 +608,11 @@ }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "version": "==2.32.3" }, "six": { "hashes": [ @@ -640,16 +636,15 @@ "sha256:024d3d1745120098a85635e42242039ca6b1bc787f561ec974fffb45fc775c1b" ], "index": "pypi", - "markers": "python_full_version >= '3.5.3'", "version": "==1.0.3" }, "typing-extensions": { "hashes": [ - "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", - "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a", + "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1" ], "markers": "python_version >= '3.8'", - "version": "==4.11.0" + "version": "==4.12.1" }, "tzdata": { "hashes": [ @@ -775,19 +770,19 @@ }, "babel": { "hashes": [ - "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363", - "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287" + "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb", + "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413" ], - "markers": "python_version >= '3.7'", - "version": "==2.14.0" + "markers": "python_version >= '3.8'", + "version": "==2.15.0" }, "backports.tarfile": { "hashes": [ - "sha256:2688f159c21afd56a07b75f01306f9f52c79aebcc5f4a117fb8fbb4445352c75", - "sha256:bcd36290d9684beb524d3fe74f4a2db056824c47746583f090b8e55daf0776e4" + "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", + "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" ], "markers": "python_version < '3.12'", - "version": "==1.0.0" + "version": "==1.2.0" }, "beautifulsoup4": { "hashes": [ @@ -803,16 +798,15 @@ "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==1.2.1" }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", + "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.6.2" }, "cffi": { "hashes": [ @@ -970,125 +964,123 @@ }, "coverage": { "hashes": [ - "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", - "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", - "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", - "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", - "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", - "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", - "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", - "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", - "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", - "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", - "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", - "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", - "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", - "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", - "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", - "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", - "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", - "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", - "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", - "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", - "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", - "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", - "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", - "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", - "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", - "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", - "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", - "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", - "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", - "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", - "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", - "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", - "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", - "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", - "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", - "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", - "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", - "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", - "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", - "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", - "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", - "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", - "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", - "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", - "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", - "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", - "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", - "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", - "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", - "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", - "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", - "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4" + "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523", + "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f", + "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d", + "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb", + "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0", + "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c", + "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98", + "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83", + "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8", + "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7", + "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac", + "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84", + "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb", + "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3", + "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884", + "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614", + "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd", + "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807", + "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd", + "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8", + "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc", + "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db", + "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0", + "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08", + "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232", + "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d", + "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a", + "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1", + "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286", + "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303", + "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341", + "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84", + "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45", + "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc", + "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec", + "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd", + "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155", + "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52", + "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d", + "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485", + "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31", + "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d", + "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d", + "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d", + "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85", + "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce", + "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb", + "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974", + "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24", + "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56", + "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9", + "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==7.4.4" + "version": "==7.5.3" }, "cryptography": { "hashes": [ - "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", - "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", - "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", - "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", - "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", - "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", - "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", - "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", - "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", - "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", - "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", - "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", - "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", - "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", - "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", - "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", - "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", - "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", - "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", - "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", - "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", - "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", - "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", - "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", - "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", - "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", - "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", - "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", - "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", - "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", - "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", - "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" + "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad", + "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", + "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", + "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", + "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", + "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648", + "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", + "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", + "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c", + "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", + "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", + "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", + "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", + "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", + "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", + "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", + "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", + "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", + "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", + "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", + "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", + "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", + "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", + "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", + "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", + "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", + "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", + "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842", + "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", + "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", + "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", + "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e" ], "markers": "python_version >= '3.7'", - "version": "==42.0.5" + "version": "==42.0.8" }, "docutils": { "hashes": [ - "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", - "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b" + "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", + "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" ], - "markers": "python_version >= '3.7'", - "version": "==0.20.1" + "markers": "python_version >= '3.9'", + "version": "==0.21.2" }, "furo": { "hashes": [ - "sha256:3548be2cef45a32f8cdc0272d415fcb3e5fa6a0eb4ddfe21df3ecf1fe45a13cf", - "sha256:4d6b2fe3f10a6e36eb9cc24c1e7beb38d7a23fc7b3c382867503b7fcac8a1e02" + "sha256:490a00d08c0a37ecc90de03ae9227e8eb5d6f7f750edf9807f398a2bdf2358de", + "sha256:81f205a6605ebccbb883350432b4831c0196dd3d1bc92f61e1f459045b3d2b0b" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2024.1.29" + "version": "==2024.5.6" }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "imagesize": { "hashes": [ @@ -1132,11 +1124,11 @@ }, "jaraco.functools": { "hashes": [ - "sha256:c279cb24c93d694ef7270f970d499cab4d3813f4e08273f95398651a634f0925", - "sha256:daf276ddf234bea897ef14f43c4e1bf9eefeac7b7a82a4dd69228ac20acff68d" + "sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664", + "sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8" ], "markers": "python_version >= '3.8'", - "version": "==4.0.0" + "version": "==4.0.1" }, "jeepney": { "hashes": [ @@ -1148,19 +1140,19 @@ }, "jinja2": { "hashes": [ - "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", - "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" ], "markers": "python_version >= '3.7'", - "version": "==3.1.3" + "version": "==3.1.4" }, "keyring": { "hashes": [ - "sha256:26fc12e6a329d61d24aa47b22a7c5c3f35753df7d8f2860973cf94f4e1fb3427", - "sha256:7230ea690525133f6ad536a9b5def74a4bd52642abe594761028fc044d7c7893" + "sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50", + "sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b" ], "markers": "python_version >= '3.8'", - "version": "==25.1.0" + "version": "==25.2.1" }, "markdown-it-py": { "hashes": [ @@ -1283,19 +1275,19 @@ }, "pkginfo": { "hashes": [ - "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", - "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097" + "sha256:6d4998d1cd42c297af72cc0eab5f5bab1d356fb8a55b828fa914173f8bc1ba05", + "sha256:dba885aa82e31e80d615119874384923f4e011c2a39b0c4b7104359e36cb7087" ], - "markers": "python_version >= '3.6'", - "version": "==1.10.0" + "markers": "python_version >= '3.8'", + "version": "==1.11.0" }, "pluggy": { "hashes": [ - "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", - "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" ], "markers": "python_version >= '3.8'", - "version": "==1.4.0" + "version": "==1.5.0" }, "pycparser": { "hashes": [ @@ -1307,28 +1299,27 @@ }, "pygments": { "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" + "markers": "python_version >= '3.8'", + "version": "==2.18.0" }, "pyproject-hooks": { "hashes": [ - "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8", - "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5" + "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965", + "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2" ], "markers": "python_version >= '3.7'", - "version": "==1.0.0" + "version": "==1.1.0" }, "pytest": { "hashes": [ - "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7", - "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044" + "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", + "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==8.1.1" + "version": "==8.2.2" }, "readme-renderer": { "hashes": [ @@ -1340,12 +1331,11 @@ }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "version": "==2.32.3" }, "requests-mock": { "hashes": [ @@ -1353,7 +1343,6 @@ "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401" ], "index": "pypi", - "markers": "python_version >= '3.5'", "version": "==1.12.1" }, "requests-toolbelt": { @@ -1390,12 +1379,11 @@ }, "setuptools": { "hashes": [ - "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e", - "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c" + "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", + "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==69.2.0" + "version": "==70.0.0" }, "snowballstemmer": { "hashes": [ @@ -1414,11 +1402,11 @@ }, "sphinx": { "hashes": [ - "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560", - "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5" + "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3", + "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc" ], "markers": "python_version >= '3.9'", - "version": "==7.2.6" + "version": "==7.3.7" }, "sphinx-basic-ng": { "hashes": [ @@ -1478,12 +1466,11 @@ }, "twine": { "hashes": [ - "sha256:89b0cc7d370a4b66421cc6102f269aa910fe0f1861c124f573cf2ddedbc10cf4", - "sha256:a262933de0b484c53408f9edae2e7821c1c45a3314ff2df9bdd343aa7ab8edc0" + "sha256:4d74770c88c4fcaf8134d2a6a9d863e40f08255ff7d8e2acb3cbbd57d25f6e9d", + "sha256:fe1d814395bfe50cfbe27783cb74efe93abeac3f66deaeb6c8390e4e92bacb43" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==5.0.0" + "version": "==5.1.0" }, "urllib3": { "hashes": [ @@ -1495,11 +1482,11 @@ }, "zipp": { "hashes": [ - "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", - "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" + "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19", + "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c" ], "markers": "python_version >= '3.8'", - "version": "==3.18.1" + "version": "==3.19.2" } } } diff --git a/lib/python/dbrepo/RestClient.py b/lib/python/dbrepo/RestClient.py index c833079e0c..0a03fd669b 100644 --- a/lib/python/dbrepo/RestClient.py +++ b/lib/python/dbrepo/RestClient.py @@ -128,7 +128,7 @@ class RestClient: logging.info(f"No username set!") return None - def get_users(self) -> List[User]: + def get_users(self) -> List[UserBrief]: """ Get all users. @@ -140,7 +140,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return TypeAdapter(List[User]).validate_python(body) + return TypeAdapter(List[UserBrief]).validate_python(body) raise ResponseCodeError(f'Failed to find users: response code: {response.status_code} is not 200 (OK)') def get_user(self, user_id: str) -> User: @@ -439,7 +439,7 @@ class RestClient: :raises NameExistsError: If a table with this name already exists. :raises ForbiddenError: If the action is not allowed. :raises MalformedError: If the payload is rejected by the service. - :raises NotExistsError: If thecontainer does not exist. + :raises NotExistsError: If the container does not exist. """ url = f'/api/database/{database_id}/table' response = self._wrapper(method="post", url=url, force_auth=True, @@ -459,7 +459,7 @@ class RestClient: raise ResponseCodeError( f'Failed to create table: response code: {response.status_code} is not 201 (CREATED)') - def get_tables(self, database_id: int) -> List[Table]: + def get_tables(self, database_id: int) -> List[TableBrief]: """ Get all tables. @@ -473,7 +473,7 @@ class RestClient: response = self._wrapper(method="get", url=url) if response.status_code == 200: body = response.json() - return TypeAdapter(List[Table]).validate_python(body) + return TypeAdapter(List[TableBrief]).validate_python(body) raise ResponseCodeError(f'Failed to find tables: response code: {response.status_code} is not 200 (OK)') def get_table(self, database_id: int, table_id: int) -> Table: @@ -758,7 +758,7 @@ class RestClient: """ url = f'/api/database/{database_id}/table/{table_id}/data' response = self._wrapper(method="post", url=url, force_auth=True, payload=CreateData(data=data)) - if response.status_code == 202: + if response.status_code == 201: return if response.status_code == 400 or response.status_code == 410: raise MalformedError(f'Failed to insert table data: service rejected malformed payload') @@ -767,7 +767,7 @@ class RestClient: if response.status_code == 404: raise NotExistsError(f'Failed to insert table data: not found') raise ResponseCodeError( - f'Failed to insert table data: response code: {response.status_code} is not 202 (ACCEPTED)') + f'Failed to insert table data: response code: {response.status_code} is not 201 (CREATED)') def import_table_data(self, database_id: int, table_id: int, separator: str, file_path: str, quote: str = None, skip_lines: int = 0, false_encoding: str = None, diff --git a/lib/python/dbrepo/api/dto.py b/lib/python/dbrepo/api/dto.py index 6f00b54549..5eae072f35 100644 --- a/lib/python/dbrepo/api/dto.py +++ b/lib/python/dbrepo/api/dto.py @@ -1,6 +1,5 @@ from __future__ import annotations -import uuid from dataclasses import field from enum import Enum import datetime @@ -28,7 +27,7 @@ class JwtAuth(BaseModel): refresh_expires_in: int not_before_policy: int = Field(alias='not-before-policy') scope: str - session_state: uuid.UUID + session_state: str token_type: str @@ -75,11 +74,11 @@ class UpdateUser(BaseModel): class UserBrief(BaseModel): id: str username: str - name: str - orcid: str - qualified_name: str - given_name: str - family_name: str + name: Optional[str] = None + orcid: Optional[str] = None + qualified_name: Optional[str] = None + given_name: Optional[str] = None + family_name: Optional[str] = None class Container(BaseModel): @@ -118,12 +117,12 @@ class ColumnBrief(BaseModel): class TableBrief(BaseModel): id: int + database_id: int name: str - description: str - owner: UserBrief - columns: List[ColumnBrief] + description: Optional[str] internal_name: str is_versioned: bool + owner: UserBrief class UserAttributes(BaseModel): @@ -150,16 +149,6 @@ class UpdateUserPassword(BaseModel): password: str -class UserBrief(BaseModel): - id: str - username: str - name: Optional[str] = None - orcid: Optional[str] = None - qualified_name: Optional[str] = None - given_name: Optional[str] = None - family_name: Optional[str] = None - - class AccessType(str, Enum): """ Enumeration of database access. @@ -515,6 +504,8 @@ class CreateTableColumn(BaseModel): name: str type: ColumnType null_allowed: bool + concept_uri: Optional[str] = None + unit_uri: Optional[str] = None index_length: Optional[int] = None size: Optional[int] = None d: Optional[int] = None @@ -996,22 +987,44 @@ class Unique(BaseModel): columns: List[ColumnMinimal] +class ForeignKeyReference(BaseModel): + id: int + foreign_key: ForeignKeyMinimal + column: ColumnMinimal + referenced_column: ColumnMinimal + + +class ReferenceType(str, Enum): + """ + Enumeration of reference types. + """ + RESTRICT = "restrict" + CASCADE = "cascade" + SET_NULL = "set_null" + NO_ACTION = "no_action" + SET_DEFAULT = "set_default" + + +class ForeignKeyMinimal(BaseModel): + id: int + + class ForeignKey(BaseModel): id: int name: str - columns: List[ColumnMinimal] + references: List[ForeignKeyReference] + table: TableMinimal referenced_table: TableMinimal - referenced_columns: List[ColumnMinimal] - on_update: Optional[str] = None - on_delete: Optional[str] = None + on_update: Optional[ReferenceType] = None + on_delete: Optional[ReferenceType] = None class CreateForeignKey(BaseModel): - columns: List[Column] - referenced_table: Table - referenced_columns: List[Column] - on_update: Optional[str] = None - on_delete: Optional[str] = None + columns: List[str] + referenced_table: str + referenced_columns: List[str] + on_update: Optional[ReferenceType] = None + on_delete: Optional[ReferenceType] = None class PrimaryKey(BaseModel): diff --git a/lib/python/tests/test_component_user.py b/lib/python/tests/test_component_user.py deleted file mode 100644 index bc9961bc7e..0000000000 --- a/lib/python/tests/test_component_user.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -import unittest -import random -import string - -from dbrepo.RestClient import RestClient - - -def rand(size=6, chars=string.ascii_uppercase + string.digits): - return ''.join(random.choice(chars) for _ in range(size)) - - -class UserComponentTest(unittest.TestCase): - - def test_create_user_find_whoami_login_basic_oidc(self): - # params - username = rand(size=8).lower() - password = rand(size=8) - email = rand(size=8) + "@example.com" - print(f"creating user {username} with password {password} with email {email}") - # create user - client = RestClient(endpoint="http://localhost") - response = client.create_user(username=username, password=password, email=email) - self.assertEqual(username, response.username) - self.assertIsNotNone(response.id) - user_id = response.id - # find user - client = RestClient(endpoint="http://localhost", username=username, password=password) - response = client.get_user(user_id=user_id) - self.assertEqual(username, response.username) - self.assertEqual(user_id, response.id) - # whoami - response = client.whoami() - self.assertEqual(username, response) - # login basic - response = client.get_jwt_auth(username=username, password=password) - self.assertIsNotNone(response.id_token) - access_token = response.access_token - self.assertIsNotNone(response.access_token) - self.assertIsNotNone(response.refresh_token) - self.assertEqual(0, response.not_before_policy) - self.assertIsNotNone(response.expires_in) - self.assertIsNotNone(response.refresh_expires_in) - self.assertIsNotNone(response.session_state) - self.assertIsNotNone(response.scope) - # login oidc - client = RestClient(endpoint="http://localhost", password=access_token) - response = client.update_user(user_id=user_id, theme="light", language="en") - - -if __name__ == "__main__": - unittest.main() diff --git a/lib/python/tests/test_unit_table.py b/lib/python/tests/test_unit_table.py index 03f5aca1af..5dff01582e 100644 --- a/lib/python/tests/test_unit_table.py +++ b/lib/python/tests/test_unit_table.py @@ -8,7 +8,7 @@ from dbrepo.RestClient import RestClient from pandas import DataFrame from dbrepo.api.dto import Table, CreateTableConstraints, UserAttributes, User, Column, Constraints, ColumnType, Result, \ - Concept, Unit, TableStatistics, ColumnStatistic, PrimaryKey, TableMinimal, ColumnMinimal + Concept, Unit, TableStatistics, ColumnStatistic, PrimaryKey, TableMinimal, ColumnMinimal, TableBrief, UserBrief from dbrepo.api.exceptions import MalformedError, ForbiddenError, NotExistsError, NameExistsError, QueryStoreError, \ AuthenticationError @@ -126,38 +126,14 @@ class TableUnitTest(unittest.TestCase): def test_get_tables_succeeds(self): with requests_mock.Mocker() as mock: - exp = [Table(id=2, - name="Test", - description="Test Table", - database_id=1, - internal_name="test", - creator=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', - attributes=UserAttributes(theme='light')), - owner=User(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', - attributes=UserAttributes(theme='light')), - created=datetime.datetime(2024, 1, 1, 0, 0, 0, 0, datetime.timezone.utc), - is_versioned=True, - created_by='8638c043-5145-4be8-a3e4-4b79991b0a16', - queue_name='test', - routing_key='dbrepo.test_database_1234.test', - is_public=True, - constraints=Constraints(uniques=[], - foreign_keys=[], - checks=[], - primary_key=[PrimaryKey(id=1, - table=TableMinimal(id=2, database_id=1), - column=ColumnMinimal(id=1, table_id=2, - database_id=1))]), - columns=[Column(id=1, - name="ID", - database_id=1, - table_id=2, - internal_name="id", - auto_generated=True, - is_primary_key=True, - column_type=ColumnType.BIGINT, - is_public=True, - is_null_allowed=False)])] + exp = [TableBrief(id=2, + name="Test", + description="Test Table", + database_id=1, + internal_name="test", + owner=UserBrief(id='8638c043-5145-4be8-a3e4-4b79991b0a16', username='mweise', + attributes=UserAttributes(theme='light')), + is_versioned=True)] # mock mock.get('/api/database/1/table', json=[exp[0].model_dump()]) # test @@ -380,7 +356,7 @@ class TableUnitTest(unittest.TestCase): def test_create_table_data_succeeds(self): with requests_mock.Mocker() as mock: # mock - mock.post('/api/database/1/table/9/data', status_code=202) + mock.post('/api/database/1/table/9/data', status_code=201) # test client = RestClient(username="a", password="b") client.create_table_data(database_id=1, table_id=9, diff --git a/make/gen.mk b/make/gen.mk index e6fceaf2ba..14206d6633 100644 --- a/make/gen.mk +++ b/make/gen.mk @@ -10,6 +10,12 @@ gen-swagger-doc-fe: build-images ## Generate Swagger documentation and fetch. bash .docs/.swagger/swagger-generate.sh bash .docs/.swagger/swagger-site.sh docker compose down + openapi-merge-cli --config .docs/.swagger/openapi-merge.json + +.PHONY: gen-helm-doc +gen-helm-doc: build-helm ## Generate Helm documentation and schema + helm schema -input ./helm/dbrepo/values.yaml + readme-generator-for-helm --readme ./helm/dbrepo/README.md --values ./helm/dbrepo/values.yaml .PHONY: gen-dbrepo-doc gen-docs-doc: ## Generate DBRepo documentation. diff --git a/make/rel.mk b/make/rel.mk index bf73c6bb8e..c06bb23433 100644 --- a/make/rel.mk +++ b/make/rel.mk @@ -2,50 +2,28 @@ .PHONY: tag-images tag-images: build-images ## Tag the docker images. - docker tag dbrepo-analyse-service:latest "${REPOSITORY_1_URL}/analyse-service:${APP_VERSION}" - docker tag dbrepo-analyse-service:latest "${REPOSITORY_2_URL}/analyse-service:${APP_VERSION}" - docker tag dbrepo-auth-service:latest "${REPOSITORY_1_URL}/auth-service:${APP_VERSION}" - docker tag dbrepo-auth-service:latest "${REPOSITORY_2_URL}/auth-service:${APP_VERSION}" - docker tag dbrepo-metadata-db:latest "${REPOSITORY_1_URL}/metadata-db:${APP_VERSION}" - docker tag dbrepo-metadata-db:latest "${REPOSITORY_2_URL}/metadata-db:${APP_VERSION}" - docker tag dbrepo-ui:latest "${REPOSITORY_1_URL}/ui:${APP_VERSION}" - docker tag dbrepo-ui:latest "${REPOSITORY_2_URL}/ui:${APP_VERSION}" - docker tag dbrepo-data-service:latest "${REPOSITORY_1_URL}/data-service:${APP_VERSION}" - docker tag dbrepo-data-service:latest "${REPOSITORY_2_URL}/data-service:${APP_VERSION}" - docker tag dbrepo-metadata-service:latest "${REPOSITORY_1_URL}/metadata-service:${APP_VERSION}" - docker tag dbrepo-metadata-service:latest "${REPOSITORY_2_URL}/metadata-service:${APP_VERSION}" - docker tag dbrepo-search-db:latest "${REPOSITORY_1_URL}/search-db:${APP_VERSION}" - docker tag dbrepo-search-db:latest "${REPOSITORY_2_URL}/search-db:${APP_VERSION}" - docker tag dbrepo-data-db-sidecar:latest "${REPOSITORY_1_URL}/data-db-sidecar:${APP_VERSION}" - docker tag dbrepo-data-db-sidecar:latest "${REPOSITORY_2_URL}/data-db-sidecar:${APP_VERSION}" - docker tag dbrepo-search-service:latest "${REPOSITORY_1_URL}/search-service:${APP_VERSION}" - docker tag dbrepo-search-service:latest "${REPOSITORY_2_URL}/search-service:${APP_VERSION}" - docker tag dbrepo-search-service-init:latest "${REPOSITORY_1_URL}/search-service-init:${APP_VERSION}" - docker tag dbrepo-search-service-init:latest "${REPOSITORY_2_URL}/search-service-init:${APP_VERSION}" - docker tag dbrepo-storage-service-init:latest "${REPOSITORY_1_URL}/storage-service-init:${APP_VERSION}" - docker tag dbrepo-storage-service-init:latest "${REPOSITORY_2_URL}/storage-service-init:${APP_VERSION}" + docker tag dbrepo-analyse-service:latest "${REPOSITORY_URL}/analyse-service:${APP_VERSION}" + docker tag dbrepo-auth-service:latest "${REPOSITORY_URL}/auth-service:${APP_VERSION}" + docker tag dbrepo-metadata-db:latest "${REPOSITORY_URL}/metadata-db:${APP_VERSION}" + docker tag dbrepo-ui:latest "${REPOSITORY_URL}/ui:${APP_VERSION}" + docker tag dbrepo-data-service:latest "${REPOSITORY_URL}/data-service:${APP_VERSION}" + docker tag dbrepo-metadata-service:latest "${REPOSITORY_URL}/metadata-service:${APP_VERSION}" + docker tag dbrepo-search-db:latest "${REPOSITORY_URL}/search-db:${APP_VERSION}" + docker tag dbrepo-data-db-sidecar:latest "${REPOSITORY_URL}/data-db-sidecar:${APP_VERSION}" + docker tag dbrepo-search-service:latest "${REPOSITORY_URL}/search-service:${APP_VERSION}" + docker tag dbrepo-search-service-init:latest "${REPOSITORY_URL}/search-service-init:${APP_VERSION}" + docker tag dbrepo-storage-service-init:latest "${REPOSITORY_URL}/storage-service-init:${APP_VERSION}" .PHONY: release-images release-images: tag-images ## Release the docker images. - docker push "${REPOSITORY_1_URL}/analyse-service:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/analyse-service:${APP_VERSION}" - docker push "${REPOSITORY_1_URL}/auth-service:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/auth-service:${APP_VERSION}" - docker push "${REPOSITORY_1_URL}/metadata-db:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/metadata-db:${APP_VERSION}" - docker push "${REPOSITORY_1_URL}/ui:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/ui:${APP_VERSION}" - docker push "${REPOSITORY_1_URL}/data-service:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/data-service:${APP_VERSION}" - docker push "${REPOSITORY_1_URL}/search-db:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/search-db:${APP_VERSION}" - docker push "${REPOSITORY_1_URL}/data-db-sidecar:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/data-db-sidecar:${APP_VERSION}" - docker push "${REPOSITORY_1_URL}/metadata-service:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/metadata-service:${APP_VERSION}" - docker push "${REPOSITORY_1_URL}/search-service:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/search-service:${APP_VERSION}" - docker push "${REPOSITORY_1_URL}/search-service-init:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/search-service-init:${APP_VERSION}" - docker push "${REPOSITORY_1_URL}/storage-service-init:${APP_VERSION}" - docker push "${REPOSITORY_2_URL}/storage-service-init:${APP_VERSION}" + docker push "${REPOSITORY_URL}/analyse-service:${APP_VERSION}" + docker push "${REPOSITORY_URL}/auth-service:${APP_VERSION}" + docker push "${REPOSITORY_URL}/metadata-db:${APP_VERSION}" + docker push "${REPOSITORY_URL}/ui:${APP_VERSION}" + docker push "${REPOSITORY_URL}/data-service:${APP_VERSION}" + docker push "${REPOSITORY_URL}/search-db:${APP_VERSION}" + docker push "${REPOSITORY_URL}/data-db-sidecar:${APP_VERSION}" + docker push "${REPOSITORY_URL}/metadata-service:${APP_VERSION}" + docker push "${REPOSITORY_URL}/search-service:${APP_VERSION}" + docker push "${REPOSITORY_URL}/search-service-init:${APP_VERSION}" + docker push "${REPOSITORY_URL}/storage-service-init:${APP_VERSION}" diff --git a/values.schema.json b/values.schema.json new file mode 100644 index 0000000000..2cc52abfed --- /dev/null +++ b/values.schema.json @@ -0,0 +1,1459 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "admin": { + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "analyseservice": { + "properties": { + "enabled": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "image": { + "properties": { + "debug": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "pullPolicy": { + "type": "string" + } + }, + "type": "object" + }, + "replicaCount": { + "type": "integer" + }, + "s3": { + "properties": { + "endpoint": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "authservice": { + "properties": { + "auth": { + "properties": { + "adminPassword": { + "type": "string" + }, + "adminUser": { + "type": "string" + } + }, + "type": "object" + }, + "client": { + "properties": { + "id": { + "type": "string" + }, + "secret": { + "type": "string" + } + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "extraEnvVarsCM": { + "type": "string" + }, + "extraStartupArgs": { + "type": "string" + }, + "extraVolumeMounts": { + "items": { + "properties": { + "mountPath": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "extraVolumes": { + "items": { + "properties": { + "configMap": { + "properties": { + "name": { + "type": "string" + } + }, + "type": "object" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "fullnameOverride": { + "type": "string" + }, + "image": { + "properties": { + "debug": { + "type": "boolean" + } + }, + "type": "object" + }, + "jwt": { + "properties": { + "pubkey": { + "type": "string" + } + }, + "type": "object" + }, + "metrics": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, + "postgresql": { + "properties": { + "auth": { + "properties": { + "postgresPassword": { + "type": "string" + } + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, + "replicaCount": { + "type": "integer" + }, + "tls": { + "properties": { + "enabled": { + "type": "boolean" + }, + "existingSecret": { + "type": "string" + }, + "usePem": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "brokerservice": { + "properties": { + "auth": { + "properties": { + "password": { + "type": "string" + }, + "tls": { + "properties": { + "enabled": { + "type": "boolean" + }, + "existingSecret": { + "type": "string" + }, + "failIfNoPeerCert": { + "type": "boolean" + }, + "sslOptionsVerify": { + "type": "boolean" + } + }, + "type": "object" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "connectionTimeout": { + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "exchangeName": { + "type": "string" + }, + "extraConfiguration": { + "type": "string" + }, + "extraPlugins": { + "type": "string" + }, + "extraVolumes": { + "items": { + "properties": { + "name": { + "type": "string" + }, + "secret": { + "properties": { + "secretName": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "type": "array" + }, + "fullnameOverride": { + "type": "string" + }, + "host": { + "type": "string" + }, + "image": { + "properties": { + "debug": { + "type": "boolean" + } + }, + "type": "object" + }, + "loadDefinition": { + "properties": { + "enabled": { + "type": "boolean" + }, + "existingSecret": { + "type": "string" + } + }, + "type": "object" + }, + "persistence": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, + "port": { + "type": "integer" + }, + "queueName": { + "type": "string" + }, + "replicaCount": { + "type": "integer" + }, + "routingKey": { + "type": "string" + }, + "service": { + "properties": { + "managerPortEnabled": { + "type": "boolean" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "virtualHost": { + "type": "string" + } + }, + "type": "object" + }, + "clusterDomain": { + "type": "string" + }, + "datadb": { + "properties": { + "enabled": { + "type": "boolean" + }, + "extraFlags": { + "type": "string" + }, + "extraVolumeMounts": { + "items": { + "properties": { + "mountPath": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "extraVolumes": { + "items": { + "properties": { + "emptyDir": { + "properties": {}, + "type": "object" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "fullnameOverride": { + "type": "string" + }, + "galera": { + "properties": { + "mariabackup": { + "properties": { + "password": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "image": { + "properties": { + "debug": { + "type": "boolean" + } + }, + "type": "object" + }, + "metrics": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, + "persistence": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, + "replicaCount": { + "type": "integer" + }, + "rootUser": { + "properties": { + "password": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "type": "object" + }, + "service": { + "properties": { + "extraPorts": { + "items": { + "properties": { + "name": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "protocol": { + "type": "string" + }, + "targetPort": { + "type": "integer" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "sidecars": { + "items": { + "properties": { + "envFrom": { + "items": { + "properties": { + "secretRef": { + "properties": { + "name": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "type": "array" + }, + "image": { + "type": "string" + }, + "imagePullPolicy": { + "type": "string" + }, + "livenessProbe": { + "properties": { + "exec": { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "initialDelaySeconds": { + "type": "integer" + }, + "periodSeconds": { + "type": "integer" + } + }, + "type": "object" + }, + "name": { + "type": "string" + }, + "ports": { + "items": { + "properties": { + "containerPort": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "readinessProbe": { + "properties": { + "exec": { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "initialDelaySeconds": { + "type": "integer" + }, + "periodSeconds": { + "type": "integer" + } + }, + "type": "object" + }, + "securityContext": { + "properties": { + "allowPrivilegeEscalation": { + "type": "boolean" + }, + "capabilities": { + "properties": { + "drop": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "runAsGroup": { + "type": "integer" + }, + "runAsNonRoot": { + "type": "boolean" + }, + "runAsUser": { + "type": "integer" + }, + "seccompProfile": { + "properties": { + "type": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "volumeMounts": { + "items": { + "properties": { + "mountPath": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "dataservice": { + "properties": { + "consumerConcurrentMax": { + "type": "integer" + }, + "consumerConcurrentMin": { + "type": "integer" + }, + "default": { + "properties": { + "date": { + "type": "integer" + }, + "time": { + "type": "integer" + }, + "timestamp": { + "type": "integer" + } + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "grant": { + "properties": { + "read": { + "type": "string" + }, + "write": { + "type": "string" + } + }, + "type": "object" + }, + "image": { + "properties": { + "debug": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "pullPolicy": { + "type": "string" + } + }, + "type": "object" + }, + "replicaCount": { + "type": "integer" + }, + "requeueRejected": { + "type": "boolean" + }, + "s3": { + "properties": { + "auth": { + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "bucket": { + "properties": { + "export": { + "type": "string" + }, + "import": { + "type": "string" + } + }, + "type": "object" + }, + "endpoint": { + "type": "string" + }, + "filePath": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "gateway": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "ingress": { + "properties": { + "annotations": { + "properties": { + "basic": { + "properties": {}, + "type": "object" + }, + "rewriteApi": { + "properties": { + "nginx.ingress.kubernetes.io/rewrite-target": { + "type": "string" + }, + "nginx.ingress.kubernetes.io/use-regex": { + "type": "string" + } + }, + "type": "object" + }, + "rewritePid": { + "properties": { + "nginx.ingress.kubernetes.io/rewrite-target": { + "type": "string" + }, + "nginx.ingress.kubernetes.io/use-regex": { + "type": "string" + } + }, + "type": "object" + }, + "rewriteRoot": { + "properties": { + "nginx.ingress.kubernetes.io/rewrite-target": { + "type": "string" + }, + "nginx.ingress.kubernetes.io/use-regex": { + "type": "string" + } + }, + "type": "object" + }, + "rewriteRootSecure": { + "properties": { + "nginx.ingress.kubernetes.io/backend-protocol": { + "type": "string" + }, + "nginx.ingress.kubernetes.io/force-ssl-redirect": { + "type": "string" + }, + "nginx.ingress.kubernetes.io/rewrite-target": { + "type": "string" + }, + "nginx.ingress.kubernetes.io/use-regex": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "className": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "tls": { + "properties": { + "enabled": { + "type": "boolean" + }, + "secretName": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "metadatadb": { + "properties": { + "db": { + "properties": { + "name": { + "type": "string" + } + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "extraInitDbScripts": { + "properties": {}, + "type": "object" + }, + "fullnameOverride": { + "type": "string" + }, + "galera": { + "properties": { + "mariabackup": { + "properties": { + "password": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "host": { + "type": "string" + }, + "image": { + "properties": { + "debug": { + "type": "boolean" + } + }, + "type": "object" + }, + "initdbScriptsConfigMap": { + "type": "string" + }, + "jdbcExtraArgs": { + "type": "string" + }, + "metrics": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, + "persistence": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, + "replicaCount": { + "type": "integer" + }, + "rootUser": { + "properties": { + "password": { + "type": "string" + }, + "user": { + "type": "string" + } + }, + "type": "object" + }, + "service": { + "properties": { + "annotations": { + "properties": {}, + "type": "object" + }, + "loadBalancerIP": { + "type": "string" + }, + "loadBalancerSourceRanges": { + "type": "array" + }, + "type": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "metadataservice": { + "properties": { + "admin": { + "properties": { + "email": { + "type": "string" + } + }, + "type": "object" + }, + "datacite": { + "properties": { + "enabled": { + "type": "boolean" + }, + "password": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "deletedRecord": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "granularity": { + "type": "string" + }, + "image": { + "properties": { + "debug": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "pullPolicy": { + "type": "string" + } + }, + "type": "object" + }, + "replicaCount": { + "type": "integer" + }, + "repositoryName": { + "type": "string" + }, + "s3": { + "properties": { + "auth": { + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "bucket": { + "properties": { + "export": { + "type": "string" + }, + "import": { + "type": "string" + } + }, + "type": "object" + }, + "endpoint": { + "type": "string" + } + }, + "type": "object" + }, + "sparql": { + "properties": { + "connectionTimeout": { + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "namespace": { + "type": "string" + }, + "searchdb": { + "properties": { + "clusterName": { + "type": "string" + }, + "config": { + "properties": { + "opensearch.yml": { + "type": "string" + } + }, + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "extraEnvs": { + "items": { + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "extraVolumeMounts": { + "items": { + "properties": { + "mountPath": { + "type": "string" + }, + "name": { + "type": "string" + }, + "readOnly": { + "type": "boolean" + } + }, + "type": "object" + }, + "type": "array" + }, + "extraVolumes": { + "items": { + "properties": { + "name": { + "type": "string" + }, + "secret": { + "properties": { + "secretName": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "type": "array" + }, + "fullnameOverride": { + "type": "string" + }, + "host": { + "type": "string" + }, + "masterService": { + "type": "string" + }, + "password": { + "type": "string" + }, + "persistence": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, + "port": { + "type": "integer" + }, + "protocol": { + "type": "string" + }, + "replicas": { + "type": "integer" + }, + "service": { + "properties": { + "annotations": { + "properties": {}, + "type": "object" + }, + "loadBalancerSourceRanges": { + "type": "array" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "sysctlInit": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "searchservice": { + "properties": { + "enabled": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "image": { + "properties": { + "debug": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "pullPolicy": { + "type": "string" + } + }, + "type": "object" + }, + "init": { + "properties": { + "image": { + "properties": { + "name": { + "type": "string" + }, + "pullPolicy": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "replicaCount": { + "type": "integer" + } + }, + "type": "object" + }, + "storageservice": { + "properties": { + "enabled": { + "type": "boolean" + }, + "filer": { + "properties": { + "enablePVC": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "replicas": { + "type": "integer" + }, + "s3": { + "properties": { + "allowEmptyFolder": { + "type": "boolean" + }, + "enableAuth": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "existingConfigSecret": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "skipAuthSecretCreation": { + "type": "boolean" + } + }, + "type": "object" + }, + "storage": { + "type": "string" + } + }, + "type": "object" + }, + "init": { + "properties": { + "image": { + "type": "string" + }, + "pullPolicy": { + "type": "string" + } + }, + "type": "object" + }, + "master": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, + "s3": { + "properties": { + "auth": { + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object" + }, + "bucket": { + "properties": { + "export": { + "type": "string" + }, + "import": { + "type": "string" + } + }, + "type": "object" + }, + "enableAuth": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "existingConfigSecret": { + "type": "string" + }, + "metricsPort": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "replicas": { + "type": "integer" + }, + "skipAuthSecretCreation": { + "type": "boolean" + } + }, + "type": "object" + }, + "volume": { + "properties": { + "enabled": { + "type": "boolean" + }, + "replicas": { + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "strategyType": { + "type": "string" + }, + "ui": { + "properties": { + "enabled": { + "type": "boolean" + }, + "extraVolumeMounts": { + "type": "array" + }, + "extraVolumes": { + "type": "array" + }, + "image": { + "properties": { + "debug": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "pullPolicy": { + "type": "string" + } + }, + "type": "object" + }, + "public": { + "properties": { + "api": { + "properties": { + "client": { + "type": "string" + }, + "server": { + "type": "string" + } + }, + "type": "object" + }, + "broker": { + "properties": { + "extra": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "properties": { + "5671": { + "type": "boolean" + }, + "5672": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "database": { + "properties": { + "extra": { + "type": "string" + } + }, + "type": "object" + }, + "doi": { + "properties": { + "enabled": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + } + }, + "type": "object" + }, + "icon": { + "type": "string" + }, + "links": { + "properties": { + "keycloak": { + "properties": { + "href": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "type": "object" + }, + "rabbitmq": { + "properties": { + "href": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "logo": { + "type": "string" + }, + "pid": { + "properties": { + "default": { + "properties": { + "publisher": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "title": { + "type": "string" + }, + "touch": { + "type": "string" + } + }, + "type": "object" + }, + "replicaCount": { + "type": "integer" + } + }, + "type": "object" + }, + "uploadservice": { + "properties": { + "containerArgs": { + "items": { + "type": "string" + }, + "type": "array" + }, + "enabled": { + "type": "boolean" + }, + "envFrom": { + "items": { + "properties": { + "secretRef": { + "properties": { + "name": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "type": "array" + }, + "fullnameOverride": { + "type": "string" + }, + "image": { + "properties": { + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + }, + "type": "object" + }, + "replicaCount": { + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" +} -- GitLab