From 7bca0c67540b330b5db6ec0f5a0ebed0c8e3092c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Sun, 20 Aug 2023 15:06:30 -0500 Subject: [PATCH 01/67] docs: Update `README` (#18) --- README.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index eeb20e8..018d517 100644 --- a/README.md +++ b/README.md @@ -6,41 +6,55 @@ ## Development +### Local database + +You can run a local postgres database and pgadmin using the following command: + +```bash +docker-compose -f docker-compose.dev.yml up +``` + +After that, you can access the pgadmin using the following url: `http://localhost:5050/` and the credentials: + +| Email | Password | +| --------------------- | -------- | +| postgres@postgres.com | postgres | + ### Create packages -You can create new packages with default folders (`domain`, `application`, `infraestructure` and `test`) using the following command: +You can create new packages with default folders (`domain`, `application`, `infraestructure` and `test`) using the following command: ```bash -make create +make create ``` After running the command you'll be prompted to enter the name of the package. ### Remove packages -You can remove packages using the following command: +You can remove packages using the following command: ```bash -make remove +make remove ``` After running the command you'll be prompted to enter the name of the package. ## Tests -1. Make sure you have `sbt` installed in your computer: +1. Make sure you have `sbt` installed in your computer: ```bash sbt --version ``` -2. Run the tests and generate the coverage report: +2. Run the tests and generate the coverage report: ```bash sbt clean coverage test coverageReport ``` -3. (Optional) Open the `html` coverage file located in: +3. (Optional) Open the `html` coverage file located in: ```bash cd target/scala-2.13/scoverage-report @@ -49,4 +63,4 @@ cd target/scala-2.13/scoverage-report ## Coverage | [![circle](https://codecov.io/gh/hawks-atlanta/metadata-scala/graphs/sunburst.svg?token=M9CJCEEIBK)](https://app.codecov.io/gh/hawks-atlanta/metadata-scala) | [![square](https://codecov.io/gh/hawks-atlanta/metadata-scala/graphs/tree.svg?token=M9CJCEEIBK)](https://app.codecov.io/gh/hawks-atlanta/metadata-scala) | -| ------------------------------------------------------------ | ------------------------------------------------------------ | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | From 8ff1a42668adf82d805e95c327c227cf98e44ad3 Mon Sep 17 00:00:00 2001 From: Antonio Donis Date: Sun, 20 Aug 2023 20:06:59 +0000 Subject: [PATCH 02/67] [ci skip] chore(release): 0.0.9 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd9384e..92e9cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### 0.0.9 (2023-08-20) + ### [0.0.8](https://github.com-university/hawks-atlanta/metadata-scala/compare/v0.0.7...v0.0.8) (2023-08-20) ### 0.0.7 (2023-08-20) diff --git a/package-lock.json b/package-lock.json index 767b1c4..c613eb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "metadata-scala", - "version": "0.0.8", + "version": "0.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "0.0.8", + "version": "0.0.9", "devDependencies": { "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" diff --git a/package.json b/package.json index b5444f4..65fd999 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,5 @@ "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" }, - "version": "0.0.8" + "version": "0.0.9" } From b502749d51d3149d585972f8d19bc6f4c19b7fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Mon, 21 Aug 2023 14:42:09 -0500 Subject: [PATCH 03/67] feat: PostgreSQL connection and Migrations (#20) * feat(db): Configure database migrations * feat(db): Sample PostgreSQL queries * docs: Add information about ports and env variables --- .dockerignore | 6 +++ .github/workflows/coverage.yaml | 4 +- .github/workflows/testing.yaml | 10 +++- CLI.md | 13 +++++ Dockerfile | 7 ++- Makefile | 5 +- README.md | 34 +++++++++++-- build.sbt | 17 +++++++ db/migrations/V1__init.sql | 7 +++ db/migrations/V2__rename.sql | 1 + ...er-compose.dev.yaml => docker-compose.yaml | 3 +- src/main/scala/Main.scala | 5 +- .../infrastructure/PostgreSQLRepository.scala | 50 +++++++++++++++++++ .../migrations/PostgreSQLMigration.scala | 29 +++++++++++ .../shared/infrastructure/Environment.scala | 10 ++++ .../infrastructure/PostgreSQLPool.scala | 23 +++++++++ ....scala => FruitMemoryRepositoryTest.scala} | 4 +- .../fruit/FruitPostgreSQLRepositoryTest.scala | 38 ++++++++++++++ volumes/.gitkeep | 0 19 files changed, 249 insertions(+), 17 deletions(-) create mode 100644 CLI.md create mode 100644 db/migrations/V1__init.sql create mode 100644 db/migrations/V2__rename.sql rename docker-compose.dev.yaml => docker-compose.yaml (82%) create mode 100644 src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala create mode 100644 src/main/scala/migrations/PostgreSQLMigration.scala create mode 100644 src/main/scala/shared/infrastructure/Environment.scala create mode 100644 src/main/scala/shared/infrastructure/PostgreSQLPool.scala rename src/test/scala/fruit/{FruitTest.scala => FruitMemoryRepositoryTest.scala} (90%) create mode 100644 src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala create mode 100644 volumes/.gitkeep diff --git a/.dockerignore b/.dockerignore index 4c26e77..f19c448 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,9 @@ # IntelliJ IDEA files ./.bsp ./.idea + +# github +./.github + +# docker +./volumes \ No newline at end of file diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 051f883..69d4cfa 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -16,7 +16,7 @@ jobs: java-version: openjdk@1.11.0-2 - name: Set up docker environment - run: docker-compose -f docker-compose.dev.yaml up -d + run: docker-compose up -d - name: Clean and test run: sbt clean coverage test coverageReport @@ -28,4 +28,4 @@ jobs: fail_ci_if_error: true - name: Clean docker environment - run: docker compose -f docker-compose.dev.yaml down --rmi all -v --remove-orphans \ No newline at end of file + run: docker-compose down --rmi all -v --remove-orphans \ No newline at end of file diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index d7dd184..8cae311 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -17,7 +17,7 @@ jobs: # Assemble the jar file without running tests - name: Clean and build - run: sbt "set test in assembly := {}" clean assembly + run: sbt "set assembly / test := {}" clean assembly test: runs-on: ubuntu-22.04 @@ -29,5 +29,11 @@ jobs: with: java-version: openjdk@1.11.0-2 + - name: Setup docker environment + run: docker-compose up -d + - name: Clean and test - run: sbt clean test \ No newline at end of file + run: sbt clean test + + - name: Clean docker environment + run: docker-compose down --rmi all -v --remove-orphans \ No newline at end of file diff --git a/CLI.md b/CLI.md new file mode 100644 index 0000000..deb2c3b --- /dev/null +++ b/CLI.md @@ -0,0 +1,13 @@ +# CLI + +This document describes how to use the service as a CLI tool. + +## Environment variables + +| Variable | Description | Example | +| ------------------- | ------------------------------------------- | ----------- | +| `DATABASE_HOST` | IP address or hostname of the database host | `localhost` | +| `DATABASE_PORT` | Port of the database host | `5432` | +| `DATABASE_NAME` | Name of the database | `metadata` | +| `DATABASE_USER` | Username of the database | `postgres` | +| `DATABASE_PASSWORD` | Password of the database | `postgres` | diff --git a/Dockerfile b/Dockerfile index 49ea38c..79846e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,5 +35,10 @@ WORKDIR /app COPY --from=builder /app/target/scala-2.13/bundle.jar . # Run -# EXPOSE 8080 +EXPOSE 8080 +ENV DATABASE_HOST "localhost" +ENV DATABASE_PORT "5432" +ENV DATABASE_NAME "metadata" +ENV DATABASE_USER "postgres" +ENV DATABASE_PASSWORD "postgres" CMD ["java", "-jar", "/app/bundle.jar"] \ No newline at end of file diff --git a/Makefile b/Makefile index 4154ed9..7478661 100644 --- a/Makefile +++ b/Makefile @@ -23,4 +23,7 @@ remove: rm -rf $$package_name; \ cd $(WORKING_DIR); \ cd $(BASE_TEST_DIR); \ - rm -rf $$package_name; \ No newline at end of file + rm -rf $$package_name; + +coverage: + sbt clean coverage test coverageReport \ No newline at end of file diff --git a/README.md b/README.md index 018d517..e9001ef 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,30 @@ ### Local database -You can run a local postgres database and pgadmin using the following command: +1. Run the `docker-compose` command: ```bash docker-compose -f docker-compose.dev.yml up ``` -After that, you can access the pgadmin using the following url: `http://localhost:5050/` and the credentials: +2. (Optional) Open the `pgadmin` page in `http://localhost:5050/` and login with the following credentials: | Email | Password | | --------------------- | -------- | | postgres@postgres.com | postgres | +Note that sometimes the `pgadmin` container doesn't start properly, so you'll need to run the command again. This usually occurs the first time you run the command. + +3. (Optional) Create a new server in `pgadmin` with the following credentials: + +| Field | Value | +|----------------------|-------------| +| Host | postgres-db | +| Port | 5432 | +| Maintenance database | metadata | +| User | postgres | +| Password | postgres | + ### Create packages You can create new packages with default folders (`domain`, `application`, `infraestructure` and `test`) using the following command: @@ -40,7 +52,7 @@ make remove After running the command you'll be prompted to enter the name of the package. -## Tests +## Local tests 1. Make sure you have `sbt` installed in your computer: @@ -48,13 +60,25 @@ After running the command you'll be prompted to enter the name of the package. sbt --version ``` -2. Run the tests and generate the coverage report: +2. Setup the local database: + +```bash +docker-compose up +``` + +3. Run the tests and generate the coverage report: ```bash sbt clean coverage test coverageReport ``` -3. (Optional) Open the `html` coverage file located in: +Or: + +```bash +make coverage +``` + +4. (Optional) Open the `html` coverage file located in: ```bash cd target/scala-2.13/scoverage-report diff --git a/build.sbt b/build.sbt index 4643cc2..ffe119d 100644 --- a/build.sbt +++ b/build.sbt @@ -7,9 +7,26 @@ lazy val root = (project in file(".")) idePackagePrefix := Some("org.hawksatlanta.metadata") ) +// Strategy to solve duplicate files in the assembly process +assembly / assemblyMergeStrategy := { + case PathList("META-INF", _*) => MergeStrategy.discard + case _ => MergeStrategy.first +} + // Testing dependencies libraryDependencies ++= Seq( "org.scalatest" %% "scalatest" % "3.2.15" % Test, "junit" % "junit" % "4.13.2" % Test, "org.scalatestplus" %% "junit-4-13" % "3.2.15.0" % Test ) + +// Migration dependencies +libraryDependencies ++= Seq( + "org.flywaydb" % "flyway-core" % "9.16.0" +) + +// Database connection dependencies +libraryDependencies ++= Seq( + "com.zaxxer" % "HikariCP" % "5.0.1", + "org.postgresql" % "postgresql" % "42.5.4" +) \ No newline at end of file diff --git a/db/migrations/V1__init.sql b/db/migrations/V1__init.sql new file mode 100644 index 0000000..ed5a6fb --- /dev/null +++ b/db/migrations/V1__init.sql @@ -0,0 +1,7 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE IF NOT EXISTS "FRUITS" ( + "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "name" VARCHAR(255) NOT NULL UNIQUE, + "price" DECIMAL(10, 2) NOT NULL +); \ No newline at end of file diff --git a/db/migrations/V2__rename.sql b/db/migrations/V2__rename.sql new file mode 100644 index 0000000..ae8607d --- /dev/null +++ b/db/migrations/V2__rename.sql @@ -0,0 +1 @@ +ALTER TABLE "FRUITS" RENAME TO fruits; \ No newline at end of file diff --git a/docker-compose.dev.yaml b/docker-compose.yaml similarity index 82% rename from docker-compose.dev.yaml rename to docker-compose.yaml index b8c3c00..d413afa 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.yaml @@ -12,8 +12,7 @@ services: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=metadata volumes: - # - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql - - ./postgres-volume:/var/lib/postgresql/data + - ./volumes/postgres:/var/lib/postgresql/data postgres-admin: image: dpage/pgadmin4 diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala index 5d171e8..41151b4 100644 --- a/src/main/scala/Main.scala +++ b/src/main/scala/Main.scala @@ -1,8 +1,9 @@ package org.hawksatlanta.metadata +import migrations.PostgreSQLMigration + object Main { def main(args: Array[String]): Unit = { - // TODO: Implement me! - println("Hello from Scala") + PostgreSQLMigration.migrate() } } diff --git a/src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala b/src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala new file mode 100644 index 0000000..a499425 --- /dev/null +++ b/src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala @@ -0,0 +1,50 @@ +package org.hawksatlanta.metadata +package fruit.infrastructure + +import fruit.domain.{Fruit, Repository} +import shared.infrastructure.PostgreSQLPool + +import com.zaxxer.hikari.HikariDataSource + +class PostgreSQLRepository extends Repository{ + private val pool: HikariDataSource = PostgreSQLPool.getInstance() + + override def get_fruits(): List[Fruit] = { + val connection = pool.getConnection() + + try { + // Execute the query + val statement = connection.createStatement() + val resultSet = statement.executeQuery("SELECT id, name, price FROM fruits") + + // Parse into domain entity + var fruits: List[Fruit] = List() + + while (resultSet.next()) { + fruits = fruits :+ Fruit( + id = resultSet.getString("id"), + name = resultSet.getString("name"), + price = resultSet.getFloat("price") + ) + } + + // Return the resulting list + fruits + } finally { + connection.close() + } + } + + override def save(fruit: Fruit): Unit = { + val connection = pool.getConnection() + + try { + val statement = connection.prepareStatement("INSERT INTO fruits (name, price) VALUES (?, ?)") + statement.setString(1, fruit.name) + statement.setFloat(2, fruit.price) + statement.executeUpdate() + } finally { + connection.close() + } + } +} diff --git a/src/main/scala/migrations/PostgreSQLMigration.scala b/src/main/scala/migrations/PostgreSQLMigration.scala new file mode 100644 index 0000000..ebc7118 --- /dev/null +++ b/src/main/scala/migrations/PostgreSQLMigration.scala @@ -0,0 +1,29 @@ +package org.hawksatlanta.metadata +package migrations + +import org.flywaydb.core.Flyway +import org.hawksatlanta.metadata.shared.infrastructure.Environment + +object PostgreSQLMigration { + def migrate(): Boolean = { + val flyway = Flyway.configure() + .dataSource( + s"jdbc:postgresql://${Environment.dbHost}:${Environment.dbPort}/${Environment.dbName}", + Environment.dbUser, + Environment.dbPassword + ) + .locations("filesystem:db/migrations") + .load() + + try { + val migrationResult = flyway.migrate() + print(s"${migrationResult.migrationsExecuted} migrations were successfully executed.") + migrationResult.success + }catch { + case e: Exception => { + print(s"Migration failed: ${e.getMessage}") + } + false + } + } +} \ No newline at end of file diff --git a/src/main/scala/shared/infrastructure/Environment.scala b/src/main/scala/shared/infrastructure/Environment.scala new file mode 100644 index 0000000..b0f776e --- /dev/null +++ b/src/main/scala/shared/infrastructure/Environment.scala @@ -0,0 +1,10 @@ +package org.hawksatlanta.metadata +package shared.infrastructure + +object Environment { + val dbHost = sys.env.getOrElse("DATABASE_HOST", "localhost") + val dbPort = sys.env.getOrElse("DATABASE_PORT", "5432").toInt + val dbName = sys.env.getOrElse("DATABASE_NAME", "metadata") + val dbUser = sys.env.getOrElse("DATABASE_USER", "postgres") + val dbPassword = sys.env.getOrElse("DATABASE_PASSWORD", "postgres") +} diff --git a/src/main/scala/shared/infrastructure/PostgreSQLPool.scala b/src/main/scala/shared/infrastructure/PostgreSQLPool.scala new file mode 100644 index 0000000..a26b317 --- /dev/null +++ b/src/main/scala/shared/infrastructure/PostgreSQLPool.scala @@ -0,0 +1,23 @@ +package org.hawksatlanta.metadata +package shared.infrastructure + +import com.zaxxer.hikari.{HikariConfig, HikariDataSource} + +object PostgreSQLPool { + // Singleton instance + private var pool: HikariDataSource = _; + + def getInstance(): HikariDataSource = { + if (pool == null) { + val config: HikariConfig = new HikariConfig() + config.setJdbcUrl(s"jdbc:postgresql://${Environment.dbHost}:${Environment.dbPort}/${Environment.dbName}") + config.setUsername(Environment.dbUser) + config.setPassword(Environment.dbPassword) + config.setMaximumPoolSize(10) + + pool = new HikariDataSource(config) + } + + pool + } +} diff --git a/src/test/scala/fruit/FruitTest.scala b/src/test/scala/fruit/FruitMemoryRepositoryTest.scala similarity index 90% rename from src/test/scala/fruit/FruitTest.scala rename to src/test/scala/fruit/FruitMemoryRepositoryTest.scala index aaf8529..5d743ee 100644 --- a/src/test/scala/fruit/FruitTest.scala +++ b/src/test/scala/fruit/FruitMemoryRepositoryTest.scala @@ -5,10 +5,10 @@ import fruit.application.UseCases import fruit.domain.Fruit import fruit.infrastructure.InMemoryRepository -import org.junit.Test +import org.junit.{Test} import org.scalatestplus.junit.JUnitSuite -class FruitTest extends JUnitSuite { +class FruitMemoryRepositoryTest extends JUnitSuite { val fruit_repository = new InMemoryRepository() val use_cases = new UseCases(fruit_repository) diff --git a/src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala b/src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala new file mode 100644 index 0000000..276238e --- /dev/null +++ b/src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala @@ -0,0 +1,38 @@ +package org.hawksatlanta.metadata +package fruit + +import migrations.PostgreSQLMigration +import fruit.domain.Fruit +import fruit.application.UseCases +import fruit.infrastructure.PostgreSQLRepository + +import org.junit.Test +import org.junit.runner.OrderWith +import org.junit.runner.manipulation.Alphanumeric +import org.scalatestplus.junit.JUnitSuite + +@OrderWith(classOf[Alphanumeric]) +class FruitPostgreSQLRepositoryTest extends JUnitSuite { + val fruit_repository = new PostgreSQLRepository() + val use_cases = new UseCases(fruit_repository) + + @Test + def t1_migration(): Unit = { + val migrationResult = PostgreSQLMigration.migrate() + assert(migrationResult) + } + + @Test + def t2_create_fruit(): Unit = { + val fruit = Fruit("1", "Apple", 1.0f) + use_cases.create_fruit(fruit) + assert(fruit_repository.get_fruits().length === 1) + } + + @Test + def t3_list_fruits(): Unit = { + val fruit2 = Fruit("2", "Orange", 2.0f) + use_cases.create_fruit(fruit2) + assert(use_cases.get_fruits().length === 2) + } +} diff --git a/volumes/.gitkeep b/volumes/.gitkeep new file mode 100644 index 0000000..e69de29 From 4faa6035ace8b32043950ca4ffd31d847216b44c Mon Sep 17 00:00:00 2001 From: Antonio Donis Date: Mon, 21 Aug 2023 19:42:32 +0000 Subject: [PATCH 04/67] [ci skip] chore(release): 0.0.10 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92e9cb0..fafbbfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### 0.0.10 (2023-08-21) + + +### Features + +* PostgreSQL connection and Migrations ([#20](https://github.com/hawks-atlanta/metadata-scala/issues/20)) ([b502749](https://github.com/hawks-atlanta/metadata-scala/commit/b502749d51d3149d585972f8d19bc6f4c19b7fbc)) + ### 0.0.9 (2023-08-20) ### [0.0.8](https://github.com-university/hawks-atlanta/metadata-scala/compare/v0.0.7...v0.0.8) (2023-08-20) diff --git a/package-lock.json b/package-lock.json index c613eb2..b303b3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "metadata-scala", - "version": "0.0.9", + "version": "0.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "0.0.9", + "version": "0.0.10", "devDependencies": { "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" diff --git a/package.json b/package.json index 65fd999..c7a9e87 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,5 @@ "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" }, - "version": "0.0.9" + "version": "0.0.10" } From e0518b9d5f1c3946b7dfa74b293c3ddf187dae8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Wed, 23 Aug 2023 09:17:11 -0500 Subject: [PATCH 05/67] docs: Openapi spec (#23) * docs: Initial openapi spec --- README.md | 11 +- docs/spec.openapi.yaml | 258 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 docs/spec.openapi.yaml diff --git a/README.md b/README.md index e9001ef..21e5663 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,15 @@ [![Tagging](https://github.com/hawks-atlanta/metadata-scala/actions/workflows/tagging.yaml/badge.svg?branch=dev)](https://github.com/hawks-atlanta/metadata-scala/actions/workflows/tagging.yaml) [![codecov](https://codecov.io/gh/hawks-atlanta/metadata-scala/graph/badge.svg?token=M9CJCEEIBK)](https://codecov.io/gh/hawks-atlanta/metadata-scala) +## Documentation + +| Document | URL | +|:-------------------:|:-----------------------------------------------------------------------------------:| +| CLI documentation | [CLI.md](CLI.md) | +| Database models | [Database.md](https://github.com/hawks-atlanta/docs/blob/main/Database.md#metadata) | +| CONTRIBUTING | [CONTRIBUTING.md](https://github.com/hawks-atlanta/docs/blob/main/CONTRIBUTING.md) | +| OpenAPI specification | [Specification](docs/spec.openapi.yaml) | + ## Development ### Local database @@ -52,7 +61,7 @@ make remove After running the command you'll be prompted to enter the name of the package. -## Local tests +## Testing 1. Make sure you have `sbt` installed in your computer: diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml new file mode 100644 index 0000000..50ef3c0 --- /dev/null +++ b/docs/spec.openapi.yaml @@ -0,0 +1,258 @@ +openapi: 3.0.3 + +info: + title: Metadata Scala + + license: + name: MIT + url: https://github.com/hawks-atlanta/metadata-scala/blob/main/LICENSE + + version: TBA + +tags: + - name: File + +paths: + /files/{user_uuid}: + get: + tags: [ 'File' ] + description: List files in the given directory. List the user's root directory by default when the parent_uuid query parameter is not provided. + parameters: + - in: path + name: user_uuid + schema: + type: string + example: '658b4e63-b5ac-46a7-ac43-efb6a1415130' + required: true + - in: query + name: parent_uuid + schema: + type: string + example: '5ad724f0-4091-453a-914a-c2d11d69d1e3' + required: false + responses: + '200': + description: Ok. The directory was listed. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/file' + '403': + description: Forbidden. The directory is not owned by the user. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '404': + description: Not found. No directory with the given parent_uuid was found. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + + post: + tags: [ 'File' ] + description: Save the metadata for a new file or directory. + parameters: + - in: path + name: user_uuid + schema: + type: string + example: '658b4e63-b5ac-46a7-ac43-efb6a1415130' + required: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/file_creation_request' + responses: + '201': + description: Created. The metadata was saved sucessfully. + content: + application/json: + schema: + type: object + properties: + file_uuid: + type: string + example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + + /files/{user_uuid}/{file_uuid}: + get: + tags: [ 'File' ] + description: Get the metadata of the given file. This endpoint is suposed to only be used by the gateway service to obtain the location (files/volume/archive_uuid) of the file. + parameters: + - in: path + name: user_uuid + schema: + type: string + example: '658b4e63-b5ac-46a7-ac43-efb6a1415130' + required: true + - in: path + name: file_uuid + schema: + type: string + example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' + required: true + responses: + '200': + description: Ok. The metadata was returned successfuly. + content: + application/json: + schema: + $ref: '#/components/schemas/metadata' + '403': + description: Forbidden. The file is not owned by the user. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '404': + description: Not found. No file with the given file_uuid was found. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + delete: + tags: [ 'File' ] + description: Delete the metadata of the given file. + parameters: + - in: path + name: user_uuid + schema: + type: string + example: '658b4e63-b5ac-46a7-ac43-efb6a1415130' + required: true + - in: path + name: file_uuid + schema: + type: string + example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' + required: true + responses: + '204': + description: No content. The metadata of the file was deleted. + '403': + description: Forbidden. The user tried to delete the metadata of a file that doesn´t own. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '404': + description: Not found. No file with the given file_uuid was found. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + + /files/{file_uuid}/ready: + put: + tags: [ 'File' ] + description: Mark the given file as ready (Stored in the filesystem). + parameters: + - in: path + name: file_uuid + schema: + type: string + example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' + required: true + responses: + '204': + description: No content. The metadata of the file was updated. + '404': + description: Not found. No file with the given file_uuid was found. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + +components: + schemas: + file: + type: object + properties: + uuid: + type: string + example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' + type: + type: string + enum: ['file', 'directory'] + description: "Whether the file is a directory or an archive" + example: 'file' + name: + type: string + example: 'filename' + + metadata: + type: object + properties: + volume: + type: string + example: 'VOLUME_1' + archive_uuid: + type: string + example: '0b82495a-350b-4f4f-95ce-0119998466d4' + + file_creation_request: + type: object + properties: + parent_uuid: + type: string + example: '5ad724f0-4091-453a-914a-c2d11d69d1e3' + hash_sum: + type: string + example: '56d50f755d5dbca915cf93779d3b51d6562e6183' + type: + type: string + enum: ['file', 'directory'] + description: "Whether the file is a directory or an archive" + example: 'file' + name: + type: string + example: 'filename' + size: + type: number + description: 'Size in KB' + example: 3072 + + error_response: + type: object + properties: + error: + type: boolean + example: true + message: + type: string + example: Something went wrong. Try again later. \ No newline at end of file From 862a9f11ca09520ec4e48874efc7de7d3490c66f Mon Sep 17 00:00:00 2001 From: Antonio Donis Date: Wed, 23 Aug 2023 14:17:34 +0000 Subject: [PATCH 06/67] [ci skip] chore(release): 0.0.11 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fafbbfd..8d369d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### 0.0.11 (2023-08-23) + ### 0.0.10 (2023-08-21) diff --git a/package-lock.json b/package-lock.json index b303b3c..f1e2c6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "metadata-scala", - "version": "0.0.10", + "version": "0.0.11", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "0.0.10", + "version": "0.0.11", "devDependencies": { "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" diff --git a/package.json b/package.json index c7a9e87..90587f1 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,5 @@ "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" }, - "version": "0.0.10" + "version": "0.0.11" } From a30711f8aad4df840d11b4337ca32d9d78334491 Mon Sep 17 00:00:00 2001 From: Antonio Donis Date: Wed, 23 Aug 2023 14:23:02 +0000 Subject: [PATCH 07/67] [ci skip] chore(release): 0.0.12 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5323f..1ee239b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### 0.0.12 (2023-08-23) + ### 0.0.11 (2023-08-23) ### 0.0.10 (2023-08-21) diff --git a/package-lock.json b/package-lock.json index f1e2c6e..2c370b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "metadata-scala", - "version": "0.0.11", + "version": "0.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "0.0.11", + "version": "0.0.12", "devDependencies": { "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" diff --git a/package.json b/package.json index 90587f1..00bcab8 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,5 @@ "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" }, - "version": "0.0.11" + "version": "0.0.12" } From c1edb5d596dc573d467704d25ad93ee5f568b900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Fri, 1 Sep 2023 06:28:07 -0500 Subject: [PATCH 08/67] fix(cd): Build the Docker image without running tests (#35) --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 79846e0..73d8724 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,9 +18,8 @@ RUN mv ~/.local/share/coursier/bin/** /usr/local/bin # Copy project files COPY . . -# Build project -RUN sbt clean && \ - sbt assembly +# Clean and build without running tests +RUN sbt "set assembly / test := {}" clean assembly # Rename jar file RUN mv target/scala-2.13/*.jar target/scala-2.13/bundle.jar From 9b9f69bb68abbe15f42941c10bd4fff566f1ea83 Mon Sep 17 00:00:00 2001 From: Antonio Donis Date: Fri, 1 Sep 2023 11:28:26 +0000 Subject: [PATCH 09/67] [ci skip] chore(release): 0.0.13 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ee239b..194a2d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### 0.0.13 (2023-09-01) + + +### Bug Fixes + +* **cd:** Build the Docker image without running tests ([#35](https://github.com/hawks-atlanta/metadata-scala/issues/35)) ([c1edb5d](https://github.com/hawks-atlanta/metadata-scala/commit/c1edb5d596dc573d467704d25ad93ee5f568b900)) + ### 0.0.12 (2023-08-23) ### 0.0.11 (2023-08-23) diff --git a/package-lock.json b/package-lock.json index 2c370b8..3b4872e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "metadata-scala", - "version": "0.0.12", + "version": "0.0.13", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "0.0.12", + "version": "0.0.13", "devDependencies": { "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" diff --git a/package.json b/package.json index 00bcab8..3b61069 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,5 @@ "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" }, - "version": "0.0.12" + "version": "0.0.13" } From 94e1444b2561275e1debd1fb96a1bc0e2b497fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:40:16 -0500 Subject: [PATCH 10/67] Feat: Save metadata (#31) * feat(db): Initial database scheme * feat: Endpoint to save metadata of a given file * fix: Save metadata of a given directory * fix(cd): Define main class to be runned from Docker * docs: Add examples of REST request for the new endpoint * style: Format files with `scalafmt` * chore: Remove sample package * chore: Delete sample tests package --- .scalafmt.conf | 61 +++++ build.sbt | 34 ++- db/migrations/V1__init.sql | 53 +++- db/migrations/V2__rename.sql | 1 - docs/rest/save_metadata.http | 51 ++++ docs/spec.openapi.yaml | 39 ++- src/main/scala/Main.scala | 4 +- .../application/FilesMetaUseCases.scala | 44 ++++ .../files_metadata/domain/ArchivesMeta.scala | 16 ++ .../domain/DomainExceptions.scala | 16 ++ .../files_metadata/domain/FileMeta.scala | 30 +++ .../domain/FilesMetaRepository.scala | 30 +++ .../FilesMetaPostgresRepository.scala | 238 ++++++++++++++++++ .../infrastructure/MetadataControllers.scala | 148 +++++++++++ .../infrastructure/MetadataRoutes.scala | 21 ++ .../requests/CreationReqSchema.scala | 60 +++++ .../scala/fruit/application/UseCases.scala | 21 -- src/main/scala/fruit/domain/Fruit.scala | 4 - src/main/scala/fruit/domain/Repository.scala | 7 - .../infrastructure/InMemoryRepository.scala | 16 -- .../infrastructure/PostgreSQLRepository.scala | 50 ---- .../migrations/PostgreSQLMigration.scala | 24 +- .../infrastructure/CaskHTTPRouter.scala | 12 + .../infrastructure/CommonValidator.scala | 12 + .../shared/infrastructure/Environment.scala | 10 +- .../infrastructure/PostgreSQLPool.scala | 15 +- .../SaveFileMetadataTests.scala | 235 +++++++++++++++++ .../fruit/FruitMemoryRepositoryTest.scala | 32 --- .../fruit/FruitPostgreSQLRepositoryTest.scala | 38 --- 29 files changed, 1105 insertions(+), 217 deletions(-) create mode 100644 .scalafmt.conf delete mode 100644 db/migrations/V2__rename.sql create mode 100644 docs/rest/save_metadata.http create mode 100644 src/main/scala/files_metadata/application/FilesMetaUseCases.scala create mode 100644 src/main/scala/files_metadata/domain/ArchivesMeta.scala create mode 100644 src/main/scala/files_metadata/domain/DomainExceptions.scala create mode 100644 src/main/scala/files_metadata/domain/FileMeta.scala create mode 100644 src/main/scala/files_metadata/domain/FilesMetaRepository.scala create mode 100644 src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala create mode 100644 src/main/scala/files_metadata/infrastructure/MetadataControllers.scala create mode 100644 src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala create mode 100644 src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala delete mode 100644 src/main/scala/fruit/application/UseCases.scala delete mode 100644 src/main/scala/fruit/domain/Fruit.scala delete mode 100644 src/main/scala/fruit/domain/Repository.scala delete mode 100644 src/main/scala/fruit/infrastructure/InMemoryRepository.scala delete mode 100644 src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala create mode 100644 src/main/scala/shared/infrastructure/CaskHTTPRouter.scala create mode 100644 src/main/scala/shared/infrastructure/CommonValidator.scala create mode 100644 src/test/scala/files_metadata/SaveFileMetadataTests.scala delete mode 100644 src/test/scala/fruit/FruitMemoryRepositoryTest.scala delete mode 100644 src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..5ed0b7c --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,61 @@ +# ---- ---- ---- ---- +# Note that this file was copied from the PlayFramework repository +# https://github.com/playframework/playframework/blob/main/.scalafmt.conf +# ---- ---- ---- ---- + +version = 3.7.11 +runner.dialect = scala213 + +preset = default + +maxColumn = 80 +assumeStandardLibraryStripMargin = true + +align { + preset = more + allowOverflow = true +} + +newlines { + alwaysBeforeMultilineDef = false + implicitParamListModifierPrefer = before + beforeCurlyLambdaParams = multilineWithCaseOnly + inInterpolation = "avoid" +} + +comments { + wrap = trailing # Custom rule +} + +docstrings { + style = Asterisk + wrap = no, + removeEmpty = true # Custom rule +} + +spaces { + inImportCurlyBraces = true, + inParentheses = true, # Custom rule, + inInterpolatedStringCurlyBraces = true, # Custom rule +} + +project { + git = true +} + +rewrite { + rules = [ + AvoidInfix, + RedundantParens, + SortModifiers, + PreferCurlyFors, + Imports, + ] + sortModifiers.order = ["private", "protected", "final", "sealed", "abstract", "implicit", "override", "lazy"] + imports { + expand = true + sort = original + groups = [["java(x)?\\..*"], ["scala\\..*"], ["sbt\\..*"]] + } + trailingCommas.style = never +} \ No newline at end of file diff --git a/build.sbt b/build.sbt index ffe119d..37f6a32 100644 --- a/build.sbt +++ b/build.sbt @@ -1,23 +1,25 @@ -ThisBuild / version := "0.1.0-SNAPSHOT" +ThisBuild / version := "0.1.0-SNAPSHOT" ThisBuild / scalaVersion := "2.13.11" +ThisBuild / mainClass := Some( "org.hawksatlanta.metadata.Main" ) -lazy val root = (project in file(".")) +lazy val root = ( project in file( "." ) ) .settings( - name := "metadata", - idePackagePrefix := Some("org.hawksatlanta.metadata") + name := "metadata", + idePackagePrefix := Some( "org.hawksatlanta.metadata" ) ) // Strategy to solve duplicate files in the assembly process assembly / assemblyMergeStrategy := { - case PathList("META-INF", _*) => MergeStrategy.discard - case _ => MergeStrategy.first + case PathList( "META-INF", _* ) => MergeStrategy.discard + case _ => MergeStrategy.first } // Testing dependencies libraryDependencies ++= Seq( - "org.scalatest" %% "scalatest" % "3.2.15" % Test, - "junit" % "junit" % "4.13.2" % Test, - "org.scalatestplus" %% "junit-4-13" % "3.2.15.0" % Test + "org.scalatest" %% "scalatest" % "3.2.15" % Test, + "junit" % "junit" % "4.13.2" % Test, + "io.rest-assured" % "rest-assured" % "5.3.0" % Test, + "org.scalatestplus" %% "junit-4-13" % "3.2.15.0" % Test ) // Migration dependencies @@ -27,6 +29,16 @@ libraryDependencies ++= Seq( // Database connection dependencies libraryDependencies ++= Seq( - "com.zaxxer" % "HikariCP" % "5.0.1", + "com.zaxxer" % "HikariCP" % "5.0.1", "org.postgresql" % "postgresql" % "42.5.4" -) \ No newline at end of file +) + +// HTTP dependencies +libraryDependencies ++= Seq( + "com.lihaoyi" %% "cask" % "0.9.0" +) + +// Validation libraries +libraryDependencies ++= Seq( + "com.wix" %% "accord-core" % "0.7.6" +) diff --git a/db/migrations/V1__init.sql b/db/migrations/V1__init.sql index ed5a6fb..f9d54e7 100644 --- a/db/migrations/V1__init.sql +++ b/db/migrations/V1__init.sql @@ -1,7 +1,50 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -CREATE TABLE IF NOT EXISTS "FRUITS" ( - "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "name" VARCHAR(255) NOT NULL UNIQUE, - "price" DECIMAL(10, 2) NOT NULL -); \ No newline at end of file +-- Tables +CREATE TABLE IF NOT EXISTS archives ( + "uuid" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "hash_sum" VARCHAR(64) NOT NULL, + "size" BIGINT NOT NULL, + "ready" BOOLEAN NOT NULL DEFAULT FALSE, + "created_at" TIMESTAMP NOT NULL DEFAULT NOW(), + "updated_at" TIMESTAMP NOT NULL DEFAULT NOW(), + + CONSTRAINT "archive_size_positive" CHECK ("size" > 0) +); + +CREATE TABLE IF NOT EXISTS files ( + "uuid" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "owner_uuid" UUID NOT NULL, + "parent_uuid" UUID DEFAULT NULL REFERENCES files("uuid"), + "archive_uuid" UUID DEFAULT NULL REFERENCES archives("uuid"), + "volume" VARCHAR(32) DEFAULT NULL, + "name" VARCHAR(128) NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT NOW(), + "updated_at" TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- Indexes +CREATE INDEX IF NOT EXISTS "files_owner_uuid_index" ON files ("owner_uuid"); +CREATE INDEX IF NOT EXISTS "files_parent_uuid_index" ON files ("parent_uuid"); +CREATE UNIQUE INDEX IF NOT EXISTS "files_unique_triplet_index" ON files ("owner_uuid", "parent_uuid", "name"); + +-- Triggers +CREATE OR REPLACE FUNCTION "update_updated_at_column"() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ +BEGIN + NEW."updated_at" = NOW(); + RETURN NEW; +END $$ +; + +CREATE OR REPLACE TRIGGER "archives_updated_at_trigger" + BEFORE UPDATE ON archives + FOR EACH ROW + EXECUTE PROCEDURE "update_updated_at_column"(); + +CREATE OR REPLACE TRIGGER "files_updated_at_trigger" + BEFORE UPDATE ON files + FOR EACH ROW + EXECUTE PROCEDURE "update_updated_at_column"(); \ No newline at end of file diff --git a/db/migrations/V2__rename.sql b/db/migrations/V2__rename.sql deleted file mode 100644 index ae8607d..0000000 --- a/db/migrations/V2__rename.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "FRUITS" RENAME TO fruits; \ No newline at end of file diff --git a/docs/rest/save_metadata.http b/docs/rest/save_metadata.http new file mode 100644 index 0000000..889a31e --- /dev/null +++ b/docs/rest/save_metadata.http @@ -0,0 +1,51 @@ +### Save metadata for an archive file stored in the root directory + +POST http://localhost:8080/api/v1/files/9ae0ee6f-0644-46c8-b364-ee36c9f9bd81 HTTP/1.1 +Content-Type: application/json + +{ + "parentUUID": null, + "hashSum": "fb3a2b16764328a3c90f2122cdb4e583d2b344c9499fdf9bd1f846170e05cb52", + "fileType": "archive", + "fileName": "project.txt", + "fileSize": 32, +} + +### Save metadata for a directory stored in the root directory + +POST http://localhost:8080/api/v1/files/9ae0ee6f-0644-46c8-b364-ee36c9f9bd81 HTTP/1.1 +Content-Type: application/json + +{ + "parentUUID": null, + "hashSum": "", + "fileType": "directory", + "fileName": "university", + "fileSize": 0, +} + +### Save metadata for an archive file stored in a parent directory + +POST http://localhost:8080/api/v1/files/9ae0ee6f-0644-46c8-b364-ee36c9f9bd81 HTTP/1.1 +Content-Type: application/json + +{ + "parentUUID": "92467e11-38e8-41f1-a088-0538f43811bd", + "hashSum": "5a362b73f98d8a4123ba318e9c5bead3135caa33eada95e80e17290ce9bbf4be", + "fileType": "archive", + "fileName": "nested.txt", + "fileSize": 16, +} + +### Save metadata for a directory stored in a parent directory + +POST http://localhost:8080/api/v1/files/9ae0ee6f-0644-46c8-b364-ee36c9f9bd81 HTTP/1.1 +Content-Type: application/json + +{ + "parentUUID": "92467e11-38e8-41f1-a088-0538f43811bd", + "hashSum": "", + "fileType": "directory", + "fileName": "nested", + "fileSize": 0, +} \ No newline at end of file diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml index 50ef3c0..f169eb9 100644 --- a/docs/spec.openapi.yaml +++ b/docs/spec.openapi.yaml @@ -81,9 +81,27 @@ paths: schema: type: object properties: - file_uuid: + uuid: type: string example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' + '400': + description: Bad request. The provided user_uuid parameter wasn´t a valid UUID or the JSON body does´t fullfill the validations. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '404': + description: Not found. No parent directory with the given UUID was found. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '409': + description: Conflict. There is already a file or directory with the given UUID in the given location. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' '500': description: Internal server error content: @@ -206,12 +224,12 @@ components: uuid: type: string example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' - type: + fileType: type: string - enum: ['file', 'directory'] + enum: ['archive', 'directory'] description: "Whether the file is a directory or an archive" example: 'file' - name: + fileName: type: string example: 'filename' @@ -221,28 +239,29 @@ components: volume: type: string example: 'VOLUME_1' - archive_uuid: + archiveUUID: type: string example: '0b82495a-350b-4f4f-95ce-0119998466d4' file_creation_request: type: object properties: - parent_uuid: + parentUUID: type: string example: '5ad724f0-4091-453a-914a-c2d11d69d1e3' - hash_sum: + hashSum: type: string + description: 'SHA256 hash of the file' example: '56d50f755d5dbca915cf93779d3b51d6562e6183' - type: + fileType: type: string enum: ['file', 'directory'] description: "Whether the file is a directory or an archive" example: 'file' - name: + fileName: type: string example: 'filename' - size: + fileSize: type: number description: 'Size in KB' example: 3072 diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala index 41151b4..d83da0d 100644 --- a/src/main/scala/Main.scala +++ b/src/main/scala/Main.scala @@ -1,9 +1,11 @@ package org.hawksatlanta.metadata import migrations.PostgreSQLMigration +import shared.infrastructure.CaskHTTPRouter object Main { - def main(args: Array[String]): Unit = { + def main( args: Array[String] ): Unit = { PostgreSQLMigration.migrate() + CaskHTTPRouter.main( args ) } } diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala new file mode 100644 index 0000000..55648e2 --- /dev/null +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -0,0 +1,44 @@ +package org.hawksatlanta.metadata +package files_metadata.application + +import java.util.UUID + +import files_metadata.domain.ArchivesMeta +import files_metadata.domain.DomainExceptions +import files_metadata.domain.FileMeta +import files_metadata.domain.FilesMetaRepository + +class FilesMetaUseCases { + private var repository: FilesMetaRepository = _ + + def this( repository: FilesMetaRepository ) { + this() + this.repository = repository + } + + def saveMetadata( archiveMeta: ArchivesMeta, fileMeta: FileMeta ): UUID = { + // Check if the file already exists + val existingFileMeta = repository.searchFileInDirectory( + ownerUuid = fileMeta.ownerUuid, + directoryUuid = fileMeta.parentUuid, + fileName = fileMeta.name + ) + + if (existingFileMeta.isDefined) { + throw DomainExceptions.FileAlreadyExistsException( + "A file with the same name already exists in the given directory" + ) + } + + // If a parent directory is given, check if it exists + if (fileMeta.parentUuid.isDefined) { + repository.getFileMeta( + ownerUuid = fileMeta.ownerUuid, + uuid = fileMeta.parentUuid.get + ) + } + + // Save the metadata + repository.saveFileMeta( archiveMeta, fileMeta ) + } +} diff --git a/src/main/scala/files_metadata/domain/ArchivesMeta.scala b/src/main/scala/files_metadata/domain/ArchivesMeta.scala new file mode 100644 index 0000000..7397911 --- /dev/null +++ b/src/main/scala/files_metadata/domain/ArchivesMeta.scala @@ -0,0 +1,16 @@ +package org.hawksatlanta.metadata +package files_metadata.domain + +import java.util.UUID + +case class ArchivesMeta( + uuid: UUID, + hashSum: String, + size: Long, + ready: Boolean +) + +object ArchivesMeta { + def createNewArchive( hashSum: String, size: Long ): ArchivesMeta = + new ArchivesMeta( null, hashSum, size, false ) +} diff --git a/src/main/scala/files_metadata/domain/DomainExceptions.scala b/src/main/scala/files_metadata/domain/DomainExceptions.scala new file mode 100644 index 0000000..e5072a4 --- /dev/null +++ b/src/main/scala/files_metadata/domain/DomainExceptions.scala @@ -0,0 +1,16 @@ +package org.hawksatlanta.metadata +package files_metadata.domain + +object DomainExceptions { + case class FileNotFoundException( message: String ) + extends Exception( message ) + + case class FileAlreadyExistsException( message: String ) + extends Exception( message ) + + case class ArchiveNotSavedException( message: String ) + extends Exception( message ) + + case class FileNotSavedException( message: String ) + extends Exception( message ) +} diff --git a/src/main/scala/files_metadata/domain/FileMeta.scala b/src/main/scala/files_metadata/domain/FileMeta.scala new file mode 100644 index 0000000..927e5be --- /dev/null +++ b/src/main/scala/files_metadata/domain/FileMeta.scala @@ -0,0 +1,30 @@ +package org.hawksatlanta.metadata +package files_metadata.domain + +import java.util.UUID + +case class FileMeta( + uuid: UUID, + ownerUuid: UUID, + parentUuid: Option[UUID], + archiveUuid: Option[UUID], + volume: String, + name: String +) + +object FileMeta { + def createNewFile( + ownerUuid: UUID, + parentUuid: Option[UUID], // Can be empty if it's in the root directory + name: String + ): FileMeta = { + FileMeta( + uuid = null, + ownerUuid = ownerUuid, + parentUuid = parentUuid, + archiveUuid = null, + volume = null, + name = name + ) + } +} diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala new file mode 100644 index 0000000..81cc03b --- /dev/null +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -0,0 +1,30 @@ +package org.hawksatlanta.metadata +package files_metadata.domain + +import java.util.UUID + +trait FilesMetaRepository { + // --- Create --- + def saveFileMeta( archiveMeta: ArchivesMeta, fileMeta: FileMeta ): UUID + + // --- Read --- + def getFilesMetaInRoot( ownerUuid: UUID ): Seq[FileMeta] + + def getFilesMetaInDirectory( + ownerUuid: UUID, + directoryUuid: UUID + ): Seq[FileMeta] + + def getFileMeta( ownerUuid: UUID, uuid: UUID ): FileMeta + + def searchFileInDirectory( + ownerUuid: UUID, + directoryUuid: Option[UUID], + fileName: String + ): Option[FileMeta] + // --- Update --- + def updateFileStatus( uuid: UUID, ready: Boolean ): Unit + + // --- Delete --- + def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit +} diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala new file mode 100644 index 0000000..69c9422 --- /dev/null +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -0,0 +1,238 @@ +package org.hawksatlanta.metadata +package files_metadata.infrastructure + +import java.sql.Connection +import java.sql.PreparedStatement +import java.util.UUID + +import com.zaxxer.hikari.HikariDataSource +import files_metadata.domain.ArchivesMeta +import files_metadata.domain.DomainExceptions +import files_metadata.domain.FileMeta +import files_metadata.domain.FilesMetaRepository +import shared.infrastructure.PostgreSQLPool + +class FilesMetaPostgresRepository extends FilesMetaRepository { + private val pool: HikariDataSource = PostgreSQLPool.getInstance() + + private def saveDirectory( fileMeta: FileMeta ): UUID = { + val connection: Connection = pool.getConnection() + + try { + val statemet = connection.prepareStatement( + "INSERT INTO files (owner_uuid, parent_uuid, name) VALUES (?, ?, ?) RETURNING uuid" + ) + + statemet.setObject( 1, fileMeta.ownerUuid ) + statemet.setObject( 2, fileMeta.parentUuid.orNull ) + statemet.setString( 3, fileMeta.name ) + + val result = statemet.executeQuery() + var insertedUUID: UUID = null + + if (result.next()) { + val parsedUUID = UUID.fromString( result.getString( "uuid" ) ) + insertedUUID = parsedUUID + } else { + throw DomainExceptions.FileNotSavedException( + "There was an error while saving the directory" + ) + } + + insertedUUID + } catch { + case exception: Exception => throw exception + } finally { + connection.close() + } + } + + private def saveArchive( + archivesMeta: ArchivesMeta, + fileMeta: FileMeta + ): UUID = { + val connection: Connection = pool.getConnection() + + try { + // Start a transancion + connection.setAutoCommit( false ) + + // 1. Insert the archive + val archiveStatemet = connection.prepareStatement( + "INSERT INTO archives (hash_sum, size, ready) VALUES (?, ?, ?) RETURNING uuid" + ) + + archiveStatemet.setString( 1, archivesMeta.hashSum ) + archiveStatemet.setLong( 2, archivesMeta.size ) + archiveStatemet.setBoolean( 3, false ) + + val archiveResult = archiveStatemet.executeQuery() + var archiveUUID: Option[UUID] = None + + if (archiveResult.next()) { + val parsedUUID = UUID.fromString( archiveResult.getString( "uuid" ) ) + archiveUUID = Some( parsedUUID ) + } + + if (archiveUUID.isEmpty) { + throw DomainExceptions.ArchiveNotSavedException( + "There was an error while saving the archive" + ) + } + + // 2. Insert the file + val fileStatemet = connection.prepareStatement( + "INSERT INTO files (owner_uuid, parent_uuid, archive_uuid, name) VALUES (?, ?, ?, ?) RETURNING uuid" + ) + + fileStatemet.setObject( 1, fileMeta.ownerUuid ) + fileStatemet.setObject( 2, fileMeta.parentUuid.orNull ) + fileStatemet.setObject( 3, archiveUUID.get ) + fileStatemet.setString( 4, fileMeta.name ) + + val fileResult = fileStatemet.executeQuery() + var fileUUID: UUID = null + + if (fileResult.next()) { + val parsedUUID = UUID.fromString( fileResult.getString( "uuid" ) ) + fileUUID = parsedUUID + } else { + throw DomainExceptions.FileNotSavedException( + "There was an error while saving the file" + ) + } + + // Commit the transaction + connection.commit() + fileUUID + } catch { + case exception: Exception => + connection.rollback() + throw exception + } finally { + connection.close() + } + } + + override def saveFileMeta( + archiveMeta: ArchivesMeta, + fileMeta: FileMeta + ): UUID = { + if (archiveMeta.hashSum.isEmpty) saveDirectory( fileMeta ) + else saveArchive( archiveMeta, fileMeta ) + } + + override def getFilesMetaInRoot( ownerUuid: UUID ): Seq[FileMeta] = ??? + + override def getFilesMetaInDirectory( + ownerUuid: UUID, + directoryUuid: UUID + ): Seq[FileMeta] = ??? + + override def getFileMeta( ownerUuid: UUID, uuid: UUID ): FileMeta = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name FROM files WHERE owner_uuid = ? AND uuid = ?" + ) + + statement.setObject( 1, ownerUuid ) + statement.setObject( 2, uuid ) + + val result = statement.executeQuery() + if (result.next()) { + val parentUUIDString = result.getString( "parent_uuid" ) + val archiveUUIDString = result.getString( "archive_uuid" ) + + val parentUUID = + if (parentUUIDString == null) None + else Some( UUID.fromString( parentUUIDString ) ) + val archiveUUID = + if (archiveUUIDString == null) None + else Some( UUID.fromString( archiveUUIDString ) ) + + FileMeta( + uuid = UUID.fromString( result.getString( "uuid" ) ), + ownerUuid = UUID.fromString( result.getString( "owner_uuid" ) ), + parentUuid = parentUUID, + archiveUuid = archiveUUID, + volume = result.getString( "volume" ), + name = result.getString( "name" ) + ) + } else { + throw DomainExceptions.FileNotFoundException( + "The user does not own a file or directory with the given UUID" + ) + } + } catch { + case exception: Exception => throw exception + } finally { + connection.close() + } + } + + override def searchFileInDirectory( + ownerUuid: UUID, + directoryUuid: Option[UUID], + fileName: String + ): Option[FileMeta] = { + val connection: Connection = pool.getConnection() + + try { + var statement: PreparedStatement = null + + if (directoryUuid.isEmpty) { + statement = connection.prepareStatement( + "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name FROM files WHERE owner_uuid = ? AND parent_uuid IS NULL AND name = ? Limit 1" + ) + + statement.setObject( 1, ownerUuid ) + statement.setString( 2, fileName ) + } else { + statement = connection.prepareStatement( + "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name FROM files WHERE owner_uuid = ? AND parent_uuid = ? AND name = ? Limit 1" + ) + + statement.setObject( 1, ownerUuid ) + statement.setObject( 2, directoryUuid.get ) + statement.setString( 3, fileName ) + } + + val result = statement.executeQuery() + + if (result.next()) { + val parentUUIDString = result.getString( "parent_uuid" ) + val archiveUUIDString = result.getString( "archive_uuid" ) + + val parentUUID = + if (parentUUIDString == null) None + else Some( UUID.fromString( parentUUIDString ) ) + val archiveUUID = + if (archiveUUIDString == null) None + else Some( UUID.fromString( archiveUUIDString ) ) + + Some( + FileMeta( + uuid = UUID.fromString( result.getString( "uuid" ) ), + ownerUuid = UUID.fromString( result.getString( "owner_uuid" ) ), + parentUuid = parentUUID, + archiveUuid = archiveUUID, + volume = result.getString( "volume" ), + name = result.getString( "name" ) + ) + ) + } else { + None + } + } catch { + case _: Exception => None + } finally { + connection.close() + } + } + + override def updateFileStatus( uuid: UUID, ready: Boolean ): Unit = ??? + + override def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit = ??? +} diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala new file mode 100644 index 0000000..03c08a1 --- /dev/null +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -0,0 +1,148 @@ +package org.hawksatlanta.metadata +package files_metadata.infrastructure + +import java.util.UUID + +import com.wix.accord.validate +import com.wix.accord.Validator +import files_metadata.application.FilesMetaUseCases +import files_metadata.domain.ArchivesMeta +import files_metadata.domain.DomainExceptions +import files_metadata.domain.FileMeta +import files_metadata.domain.FilesMetaRepository +import files_metadata.infrastructure.requests.CreationReqSchema +import shared.infrastructure.CommonValidator +import ujson.Obj +import upickle.default.read + +class MetadataControllers { + private var useCases: FilesMetaUseCases = _ + + def _init(): Unit = { + val repository: FilesMetaRepository = + new FilesMetaPostgresRepository() + + useCases = new FilesMetaUseCases( repository ) + } + + def SaveMetadataController( + request: cask.Request, + userUUID: String + ): cask.Response[Obj] = { + // Check if the given user UUID is valid + val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) + if (!isUserUUIDValid) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "The user_uuid parameter was not valid" + ), + statusCode = 400 + ) + } + + try { + // Decode the JSON payload + val decoded: CreationReqSchema = read[CreationReqSchema]( + request.text() + ) + + // Validate the payload + var validationRule: Validator[CreationReqSchema] = null + + if (decoded.fileType == "archive") + validationRule = CreationReqSchema.fileSchemaValidator + else if (decoded.fileType == "directory") + validationRule = CreationReqSchema.directorySchemaValidator + else + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "The fileType should be either 'archive' or 'directory'" + ), + statusCode = 400 + ) + + val validationResult = validate[CreationReqSchema]( decoded )( + validationRule + ) + + if (validationResult.isFailure) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + // Save the metadata + val receivedArchiveMeta = ArchivesMeta.createNewArchive( + decoded.hashSum, + decoded.fileSize + ) + + val parentUUID = + if (decoded.parentUUID == null) None + else Some( UUID.fromString( decoded.parentUUID ) ) + + val receivedFileMeta = FileMeta.createNewFile( + ownerUuid = UUID.fromString( userUUID ), + parentUuid = parentUUID, + name = decoded.fileName + ) + + // Save the metadata + val savedUUID = + useCases.saveMetadata( receivedArchiveMeta, receivedFileMeta ) + cask.Response( + ujson.Obj( + "error" -> false, + "message" -> "Metadata was saved successfully", + "uuid" -> savedUUID.toString() + ), + statusCode = 201 + ) + } catch { + // Unable to parse the given JSON payload + case _: upickle.core.AbortException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "JSON payload wasn't valid" + ), + statusCode = 400 + ) + + case conflict: DomainExceptions.FileAlreadyExistsException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> conflict.getMessage() + ), + statusCode = 409 + ) + + case parentDirectoryNotFound: DomainExceptions.FileNotFoundException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> parentDirectoryNotFound.getMessage() + ), + statusCode = 404 + ) + + // Any other error + case _: Exception => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "There was an error while saving the metadata" + ), + statusCode = 500 + ) + + } + } +} diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala new file mode 100644 index 0000000..794d51c --- /dev/null +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -0,0 +1,21 @@ +package org.hawksatlanta.metadata +package files_metadata.infrastructure + +import ujson.Obj + +case class MetadataRoutes() extends cask.Routes { + private val basePath = "/api/v1/files" + + private val controllers = new MetadataControllers() + controllers._init() + + @cask.post( s"${ basePath }/:userUUID" ) + def SaveMetadataHandler( + request: cask.Request, + userUUID: String + ): cask.Response[Obj] = { + controllers.SaveMetadataController( request, userUUID ) + } + + initialize() +} diff --git a/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala b/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala new file mode 100644 index 0000000..1d04950 --- /dev/null +++ b/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala @@ -0,0 +1,60 @@ +package org.hawksatlanta.metadata +package files_metadata.infrastructure.requests + +import com.wix.accord.dsl._ +import com.wix.accord.Validator +import shared.infrastructure.CommonValidator + +// --- Base class --- +case class CreationReqSchema( + parentUUID: String, + hashSum: String, + fileType: String, + fileName: String, + fileSize: Long +) + +object CreationReqSchema { + // --- Automatic JSON (de)serialization --- + import upickle.default._ + + implicit def rw: ReadWriter[CreationReqSchema] = + macroRW[CreationReqSchema] + + // --- Validation --- + val fileSchemaValidator: Validator[CreationReqSchema] = + validator[CreationReqSchema] { request => + { + request.parentUUID + .is( aNull ) // Can be empty if it's in the root directory + .or( // Otherwise should be a valid UUID + request.parentUUID should matchRegex( + CommonValidator.uuidRegex + ) + ) + request.hashSum.has( size == 64 ) // SHA-256 + request.fileType.is( equalTo( "archive" ) ) + request.fileName.is( notEmpty ) + request.fileName.has( size <= 128 ) + request.fileSize should be > 0L + } + } + + val directorySchemaValidator: Validator[CreationReqSchema] = + validator[CreationReqSchema] { request => + { + request.parentUUID + .is( aNull ) + .or( + request.parentUUID should matchRegex( + CommonValidator.uuidRegex + ) + ) + request.hashSum.is( empty ) // HashSum is not needed for a directory + request.fileType.is( equalTo( "directory" ) ) + request.fileName.is( notEmpty ) + request.fileName.has( size <= 128 ) + request.fileSize.is( equalTo( 0L ) ) // File size is 0 for a directory + } + } +} diff --git a/src/main/scala/fruit/application/UseCases.scala b/src/main/scala/fruit/application/UseCases.scala deleted file mode 100644 index 4ca0419..0000000 --- a/src/main/scala/fruit/application/UseCases.scala +++ /dev/null @@ -1,21 +0,0 @@ -package org.hawksatlanta.metadata -package fruit.application - -import fruit.domain.{Fruit, Repository} - -class UseCases { - private var repository: Repository = _; - - def this(repository: Repository) { - this() - this.repository = repository - } - - def get_fruits(): List[Fruit] = { - return repository.get_fruits() - } - - def create_fruit(fruit: Fruit): Unit = { - repository.save(fruit) - } -} diff --git a/src/main/scala/fruit/domain/Fruit.scala b/src/main/scala/fruit/domain/Fruit.scala deleted file mode 100644 index 4616c26..0000000 --- a/src/main/scala/fruit/domain/Fruit.scala +++ /dev/null @@ -1,4 +0,0 @@ -package org.hawksatlanta.metadata -package fruit.domain - -case class Fruit(id: String, name: String, price: Float) \ No newline at end of file diff --git a/src/main/scala/fruit/domain/Repository.scala b/src/main/scala/fruit/domain/Repository.scala deleted file mode 100644 index f576d2b..0000000 --- a/src/main/scala/fruit/domain/Repository.scala +++ /dev/null @@ -1,7 +0,0 @@ -package org.hawksatlanta.metadata -package fruit.domain - -trait Repository { - def get_fruits(): List[Fruit] - def save(fruit: Fruit): Unit -} diff --git a/src/main/scala/fruit/infrastructure/InMemoryRepository.scala b/src/main/scala/fruit/infrastructure/InMemoryRepository.scala deleted file mode 100644 index 1373b33..0000000 --- a/src/main/scala/fruit/infrastructure/InMemoryRepository.scala +++ /dev/null @@ -1,16 +0,0 @@ -package org.hawksatlanta.metadata -package fruit.infrastructure - -import fruit.domain.{Fruit, Repository} - -class InMemoryRepository extends Repository{ - private var fruitsStore: List[Fruit] = List[Fruit]() - - def get_fruits(): List[Fruit] = { - return fruitsStore - } - - def save(fruit: Fruit): Unit = { - fruitsStore = fruitsStore :+ fruit - } -} diff --git a/src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala b/src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala deleted file mode 100644 index a499425..0000000 --- a/src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala +++ /dev/null @@ -1,50 +0,0 @@ -package org.hawksatlanta.metadata -package fruit.infrastructure - -import fruit.domain.{Fruit, Repository} -import shared.infrastructure.PostgreSQLPool - -import com.zaxxer.hikari.HikariDataSource - -class PostgreSQLRepository extends Repository{ - private val pool: HikariDataSource = PostgreSQLPool.getInstance() - - override def get_fruits(): List[Fruit] = { - val connection = pool.getConnection() - - try { - // Execute the query - val statement = connection.createStatement() - val resultSet = statement.executeQuery("SELECT id, name, price FROM fruits") - - // Parse into domain entity - var fruits: List[Fruit] = List() - - while (resultSet.next()) { - fruits = fruits :+ Fruit( - id = resultSet.getString("id"), - name = resultSet.getString("name"), - price = resultSet.getFloat("price") - ) - } - - // Return the resulting list - fruits - } finally { - connection.close() - } - } - - override def save(fruit: Fruit): Unit = { - val connection = pool.getConnection() - - try { - val statement = connection.prepareStatement("INSERT INTO fruits (name, price) VALUES (?, ?)") - statement.setString(1, fruit.name) - statement.setFloat(2, fruit.price) - statement.executeUpdate() - } finally { - connection.close() - } - } -} diff --git a/src/main/scala/migrations/PostgreSQLMigration.scala b/src/main/scala/migrations/PostgreSQLMigration.scala index ebc7118..8bd058b 100644 --- a/src/main/scala/migrations/PostgreSQLMigration.scala +++ b/src/main/scala/migrations/PostgreSQLMigration.scala @@ -6,24 +6,28 @@ import org.hawksatlanta.metadata.shared.infrastructure.Environment object PostgreSQLMigration { def migrate(): Boolean = { - val flyway = Flyway.configure() + val flyway = Flyway + .configure() .dataSource( - s"jdbc:postgresql://${Environment.dbHost}:${Environment.dbPort}/${Environment.dbName}", + s"jdbc:postgresql://${ Environment.dbHost }:${ Environment.dbPort }/${ Environment.dbName }", Environment.dbUser, Environment.dbPassword ) - .locations("filesystem:db/migrations") + .locations( "filesystem:db/migrations" ) .load() try { val migrationResult = flyway.migrate() - print(s"${migrationResult.migrationsExecuted} migrations were successfully executed.") + print( + s"${ migrationResult.migrationsExecuted } migrations were successfully executed." + ) migrationResult.success - }catch { - case e: Exception => { - print(s"Migration failed: ${e.getMessage}") - } - false + } catch { + case e: Exception => + { + print( s"Migration failed: ${ e.getMessage }" ) + } + false } } -} \ No newline at end of file +} diff --git a/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala b/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala new file mode 100644 index 0000000..120e7c1 --- /dev/null +++ b/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala @@ -0,0 +1,12 @@ +package org.hawksatlanta.metadata +package shared.infrastructure + +import org.hawksatlanta.metadata.files_metadata.infrastructure.MetadataRoutes + +object CaskHTTPRouter extends cask.Main { + override def port: Int = 8080 + + val allRoutes = Seq( + MetadataRoutes() + ) +} diff --git a/src/main/scala/shared/infrastructure/CommonValidator.scala b/src/main/scala/shared/infrastructure/CommonValidator.scala new file mode 100644 index 0000000..bf5f80b --- /dev/null +++ b/src/main/scala/shared/infrastructure/CommonValidator.scala @@ -0,0 +1,12 @@ +package org.hawksatlanta.metadata +package shared.infrastructure + +import scala.util.matching.Regex + +object CommonValidator { + val uuidRegex: Regex = """[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}""".r + + def validateUUID( uuid: String ): Boolean = { + uuidRegex.matches( uuid ) + } +} diff --git a/src/main/scala/shared/infrastructure/Environment.scala b/src/main/scala/shared/infrastructure/Environment.scala index b0f776e..8094c3a 100644 --- a/src/main/scala/shared/infrastructure/Environment.scala +++ b/src/main/scala/shared/infrastructure/Environment.scala @@ -2,9 +2,9 @@ package org.hawksatlanta.metadata package shared.infrastructure object Environment { - val dbHost = sys.env.getOrElse("DATABASE_HOST", "localhost") - val dbPort = sys.env.getOrElse("DATABASE_PORT", "5432").toInt - val dbName = sys.env.getOrElse("DATABASE_NAME", "metadata") - val dbUser = sys.env.getOrElse("DATABASE_USER", "postgres") - val dbPassword = sys.env.getOrElse("DATABASE_PASSWORD", "postgres") + val dbHost = sys.env.getOrElse( "DATABASE_HOST", "localhost" ) + val dbPort = sys.env.getOrElse( "DATABASE_PORT", "5432" ).toInt + val dbName = sys.env.getOrElse( "DATABASE_NAME", "metadata" ) + val dbUser = sys.env.getOrElse( "DATABASE_USER", "postgres" ) + val dbPassword = sys.env.getOrElse( "DATABASE_PASSWORD", "postgres" ) } diff --git a/src/main/scala/shared/infrastructure/PostgreSQLPool.scala b/src/main/scala/shared/infrastructure/PostgreSQLPool.scala index a26b317..2200d74 100644 --- a/src/main/scala/shared/infrastructure/PostgreSQLPool.scala +++ b/src/main/scala/shared/infrastructure/PostgreSQLPool.scala @@ -1,7 +1,8 @@ package org.hawksatlanta.metadata package shared.infrastructure -import com.zaxxer.hikari.{HikariConfig, HikariDataSource} +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource object PostgreSQLPool { // Singleton instance @@ -10,12 +11,14 @@ object PostgreSQLPool { def getInstance(): HikariDataSource = { if (pool == null) { val config: HikariConfig = new HikariConfig() - config.setJdbcUrl(s"jdbc:postgresql://${Environment.dbHost}:${Environment.dbPort}/${Environment.dbName}") - config.setUsername(Environment.dbUser) - config.setPassword(Environment.dbPassword) - config.setMaximumPoolSize(10) + config.setJdbcUrl( + s"jdbc:postgresql://${ Environment.dbHost }:${ Environment.dbPort }/${ Environment.dbName }" + ) + config.setUsername( Environment.dbUser ) + config.setPassword( Environment.dbPassword ) + config.setMaximumPoolSize( 8 ) - pool = new HikariDataSource(config) + pool = new HikariDataSource( config ) } pool diff --git a/src/test/scala/files_metadata/SaveFileMetadataTests.scala b/src/test/scala/files_metadata/SaveFileMetadataTests.scala new file mode 100644 index 0000000..54f01a7 --- /dev/null +++ b/src/test/scala/files_metadata/SaveFileMetadataTests.scala @@ -0,0 +1,235 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util +import java.util.UUID + +import io.restassured.RestAssured.`given` +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite +import shared.infrastructure.CommonValidator + +object TestData { + val API_PREFIX: String = "/api/v1/files" + val USER_UUID: UUID = UUID.randomUUID() + + private var filePayload: util.HashMap[String, Any] = _ + var directoryUUID: UUID = _ + + def getFilePayload(): util.HashMap[String, Any] = { + if (filePayload == null) { + filePayload = new util.HashMap[String, Any] + filePayload.put( "parentUUID", null ) + filePayload.put( + "hashSum", + "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" + ) + filePayload.put( "fileType", "archive" ) + filePayload.put( "fileName", "project.txt" ) + filePayload.put( "fileSize", 150 ) + } + + filePayload.clone().asInstanceOf[util.HashMap[String, Any]] + } + + def setDirectoryUUID( uuid: UUID ): Unit = { + directoryUUID = uuid + } +} + +@OrderWith( classOf[Alphanumeric] ) +class SaveFileMetadataTests extends JUnitSuite { + // Setup routes and perform migrations + @Test + def T0_setup(): Unit = { + Main.main( Array() ) + } + + @Test + // POST /api/v1/files/:user_uuid Success: Save file metadata + def T1_SaveArchiveMetadataSuccess(): Unit = { + // --- Request --- + val response = `given`() + .port( 8080 ) + .body( TestData.getFilePayload() ) + .contentType( "application/json" ) + .when() + .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + val responseJSON = response.jsonPath() + + // --- Assertions --- + assert( response.statusCode() == 201 ) + assert( !responseJSON.getBoolean( "error" ) ) + assert( + responseJSON.getString( "message" ) == "Metadata was saved successfully" + ) + assert( + CommonValidator.validateUUID( + responseJSON.getString( "uuid" ) + ) + ) + } + + @Test + // POST /api/v1/files/:user_uuid Success: Save directory metadata + def T2_SaveDirectoryMetadataSuccess(): Unit = { + // --- Test data --- + val payload = new util.HashMap[String, Any]() + payload.put( "parentUUID", null ) + payload.put( "hashSum", "" ) + payload.put( "fileType", "directory" ) + payload.put( "fileName", "project" ) + payload.put( "fileSize", 0 ) + + // --- Request --- + val response = `given`() + .port( 8080 ) + .body( payload ) + .contentType( "application/json" ) + .when() + .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + val responseJSON = response.jsonPath() + + // --- Assertions --- + assert( response.statusCode() == 201 ) + assert( !responseJSON.getBoolean( "error" ) ) + assert( + responseJSON.getString( "message" ) == "Metadata was saved successfully" + ) + + // --- Save the directory UUID for the next test --- + val directoryUUID = responseJSON.getString( "uuid" ) + assert( CommonValidator.validateUUID( directoryUUID ) ) + TestData.setDirectoryUUID( UUID.fromString( directoryUUID ) ) + } + + @Test + /* POST /api/v1/files/:user_uuid Success: Save file metadata with parent + * directory */ + def T3_SaveArchiveMetadataWithParentSuccess(): Unit = { + // --- Test data --- + val payload = TestData.getFilePayload() + payload.put( "parentUUID", TestData.directoryUUID.toString ) + + // --- Request --- + val response = `given`() + .port( 8080 ) + .body( payload ) + .contentType( "application/json" ) + .when() + .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + val responseJSON = response.jsonPath() + + // --- Assertions --- + assert( response.statusCode() == 201 ) + assert( !responseJSON.getBoolean( "error" ) ) + assert( + responseJSON.getString( "message" ) == "Metadata was saved successfully" + ) + assert( + CommonValidator.validateUUID( + responseJSON.getString( "uuid" ) + ) + ) + } + + @Test + // POST /api/v1/files/:user_uuid Bad Request: Bad user_uuid parameter + def T4_SaveArchiveMetadataBadParameter(): Unit = { + // --- Request --- + val response = `given`() + .port( 8080 ) + .when() + .post( s"${ TestData.API_PREFIX }/1" ) + + val responseJSON = response.jsonPath() + + // --- Assertions --- + assert( response.statusCode() == 400 ) + assert( responseJSON.getBoolean( "error" ) ) + assert( + responseJSON.getString( + "message" + ) == "The user_uuid parameter was not valid" + ) + } + + @Test + // POST /api/v1/files/:user_uuid Conflict: File already exists + def T5_SaveArchiveMetadataConflict(): Unit = { + // --- Request --- + val response = `given`() + .port( 8080 ) + .body( TestData.getFilePayload() ) + .contentType( "application/json" ) + .when() + .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + val responseJSON = response.jsonPath() + + // --- Assertions --- + assert( response.statusCode() == 409 ) + assert( responseJSON.getBoolean( "error" ) ) + assert( + responseJSON.getString( + "message" + ) == "A file with the same name already exists in the given directory" + ) + } + + @Test + /* POST /api/v1/files/:user_uuid Conflict: File in parent directory already + * exists */ + def T6_SaveArchiveMetadataWithParentConflict(): Unit = { + // --- Test data --- + val payload = TestData.getFilePayload() + payload.put( "parentUUID", TestData.directoryUUID.toString ) + + print( s">> Parent: ${ TestData.directoryUUID.toString } <<" ) + + // --- Request --- + val response = `given`() + .port( 8080 ) + .body( payload ) + .contentType( "application/json" ) + .when() + .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + val responseJSON = response.jsonPath() + + // --- Assertions --- + assert( response.statusCode() == 409 ) + assert( responseJSON.getBoolean( "error" ) ) + assert( + responseJSON.getString( + "message" + ) == "A file with the same name already exists in the given directory" + ) + } + + @Test + // POST /api/v1/files/:user_uuid Not found: Parent directory does not exist + def T7_SaveArchiveMetadataWithParentNotFound(): Unit = { + // --- Test data --- + val payload = TestData.getFilePayload() + payload.put( "parentUUID", UUID.randomUUID().toString ) + + // --- Request --- + val response = `given`() + .port( 8080 ) + .body( payload ) + .contentType( "application/json" ) + .when() + .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + val responseJSON = response.jsonPath() + + // --- Assertions --- + assert( response.statusCode() == 404 ) + assert( responseJSON.getBoolean( "error" ) ) + assert( + responseJSON.getString( + "message" + ) == "The user does not own a file or directory with the given UUID" + ) + } +} diff --git a/src/test/scala/fruit/FruitMemoryRepositoryTest.scala b/src/test/scala/fruit/FruitMemoryRepositoryTest.scala deleted file mode 100644 index 5d743ee..0000000 --- a/src/test/scala/fruit/FruitMemoryRepositoryTest.scala +++ /dev/null @@ -1,32 +0,0 @@ -package org.hawksatlanta.metadata -package fruit - -import fruit.application.UseCases -import fruit.domain.Fruit -import fruit.infrastructure.InMemoryRepository - -import org.junit.{Test} -import org.scalatestplus.junit.JUnitSuite - -class FruitMemoryRepositoryTest extends JUnitSuite { - val fruit_repository = new InMemoryRepository() - val use_cases = new UseCases(fruit_repository) - - @Test - def test_create_fruit(): Unit = { - val fruit = Fruit("1", "Apple", 1.0f) - use_cases.create_fruit(fruit) - assert(fruit_repository.get_fruits().length === 1) - } - - @Test - def test_get_fruits(): Unit = { - val fruit = Fruit("1", "Apple", 1.0f) - use_cases.create_fruit(fruit) - - val fruit2 = Fruit("2", "Orange", 2.0f) - use_cases.create_fruit(fruit2) - - assert(use_cases.get_fruits().length === 2) - } -} diff --git a/src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala b/src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala deleted file mode 100644 index 276238e..0000000 --- a/src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala +++ /dev/null @@ -1,38 +0,0 @@ -package org.hawksatlanta.metadata -package fruit - -import migrations.PostgreSQLMigration -import fruit.domain.Fruit -import fruit.application.UseCases -import fruit.infrastructure.PostgreSQLRepository - -import org.junit.Test -import org.junit.runner.OrderWith -import org.junit.runner.manipulation.Alphanumeric -import org.scalatestplus.junit.JUnitSuite - -@OrderWith(classOf[Alphanumeric]) -class FruitPostgreSQLRepositoryTest extends JUnitSuite { - val fruit_repository = new PostgreSQLRepository() - val use_cases = new UseCases(fruit_repository) - - @Test - def t1_migration(): Unit = { - val migrationResult = PostgreSQLMigration.migrate() - assert(migrationResult) - } - - @Test - def t2_create_fruit(): Unit = { - val fruit = Fruit("1", "Apple", 1.0f) - use_cases.create_fruit(fruit) - assert(fruit_repository.get_fruits().length === 1) - } - - @Test - def t3_list_fruits(): Unit = { - val fruit2 = Fruit("2", "Orange", 2.0f) - use_cases.create_fruit(fruit2) - assert(use_cases.get_fruits().length === 2) - } -} From 442c6422879cec537ecefb1e9471a7d6f295ea1e Mon Sep 17 00:00:00 2001 From: Antonio Donis Date: Tue, 5 Sep 2023 23:48:05 +0000 Subject: [PATCH 11/67] [ci skip] chore(release): 0.0.14 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 194a2d4..ccaeb02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### 0.0.14 (2023-09-05) + + +### Features + +* Save metadata ([#31](https://github.com/hawks-atlanta/metadata-scala/issues/31)) ([94e1444](https://github.com/hawks-atlanta/metadata-scala/commit/94e1444b2561275e1debd1fb96a1bc0e2b497fbd)) + ### 0.0.13 (2023-09-01) diff --git a/package-lock.json b/package-lock.json index 3b4872e..2ba39ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "metadata-scala", - "version": "0.0.13", + "version": "0.0.14", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "0.0.13", + "version": "0.0.14", "devDependencies": { "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" diff --git a/package.json b/package.json index 3b61069..00b2297 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,5 @@ "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" }, - "version": "0.0.13" + "version": "0.0.14" } From 9c2ec5f7cab084ff4ab7502b95969fef1f90d99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Wed, 6 Sep 2023 07:47:06 -0500 Subject: [PATCH 12/67] ci: Add lint step (#36) --- .github/workflows/testing.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 8cae311..a3999b3 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -19,8 +19,25 @@ jobs: - name: Clean and build run: sbt "set assembly / test := {}" clean assembly + lint: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + + - uses: coursier/cache-action@v6 + + - uses: coursier/setup-action@v1 + with: + jvm: adopt:1.11.0.2 + apps: scalafmt + + - name: Check format + run: scalafmt --check src/ + test: runs-on: ubuntu-22.04 + needs: lint + steps: - uses: actions/checkout@v3 From 9294959c7b5a22412a32289ece7131b150f016f3 Mon Sep 17 00:00:00 2001 From: Antonio Donis Date: Wed, 6 Sep 2023 12:47:29 +0000 Subject: [PATCH 13/67] [ci skip] chore(release): 0.0.15 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccaeb02..3553634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### 0.0.15 (2023-09-06) + ### 0.0.14 (2023-09-05) diff --git a/package-lock.json b/package-lock.json index 2ba39ec..e3758de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "metadata-scala", - "version": "0.0.14", + "version": "0.0.15", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "0.0.14", + "version": "0.0.15", "devDependencies": { "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" diff --git a/package.json b/package.json index 00b2297..2901873 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,5 @@ "git-semver-tags": "^4.1.1", "standard-version": "^9.5.0" }, - "version": "0.0.14" + "version": "0.0.15" } From b1f745e000a8c22e418ded737fa3bd8b822aa8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Thu, 7 Sep 2023 18:05:34 -0500 Subject: [PATCH 14/67] ci: Replace tagging pipeline (#42) --- .github/workflows/tagging.yaml | 20 +- CHANGELOG.md | 63 - package-lock.json | 2095 -------------------------------- package.json | 7 - version.json | 3 + version.py | 2 +- 6 files changed, 10 insertions(+), 2180 deletions(-) delete mode 100644 CHANGELOG.md delete mode 100644 package-lock.json delete mode 100644 package.json create mode 100644 version.json diff --git a/.github/workflows/tagging.yaml b/.github/workflows/tagging.yaml index f985489..5ab70c4 100644 --- a/.github/workflows/tagging.yaml +++ b/.github/workflows/tagging.yaml @@ -11,18 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Setup node 18.x - uses: actions/setup-node@v3 + + - uses: TriPSs/conventional-changelog-action@v3 + name: Tagging and Changelog with: - node-version: 18.x - cache: "npm" - - name: Git Identity - run: | - git checkout dev - git fetch --all --tags - git config --global user.email "antoniojosedonishung@gmail.com" - git config --global user.name "Antonio Donis" - - name: Changelog - run: 'npx standard-version --message "[ci skip] chore(release): %s"' - - name: Push changes - run: git push --follow-tags --force origin dev \ No newline at end of file + git-user-nane: "Antonio Donis" + git-user-email: "antoniojosedonishung@gmail.com" + git-message: "chore(release): {version}" diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 3553634..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,63 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - -### 0.0.15 (2023-09-06) - -### 0.0.14 (2023-09-05) - - -### Features - -* Save metadata ([#31](https://github.com/hawks-atlanta/metadata-scala/issues/31)) ([94e1444](https://github.com/hawks-atlanta/metadata-scala/commit/94e1444b2561275e1debd1fb96a1bc0e2b497fbd)) - -### 0.0.13 (2023-09-01) - - -### Bug Fixes - -* **cd:** Build the Docker image without running tests ([#35](https://github.com/hawks-atlanta/metadata-scala/issues/35)) ([c1edb5d](https://github.com/hawks-atlanta/metadata-scala/commit/c1edb5d596dc573d467704d25ad93ee5f568b900)) - -### 0.0.12 (2023-08-23) - -### 0.0.11 (2023-08-23) - -### 0.0.10 (2023-08-21) - -### Features - -* PostgreSQL connection and Migrations ([#20](https://github.com/hawks-atlanta/metadata-scala/issues/20)) ([b502749](https://github.com/hawks-atlanta/metadata-scala/commit/b502749d51d3149d585972f8d19bc6f4c19b7fbc)) - -### 0.0.9 (2023-08-20) - -### [0.0.8](https://github.com-university/hawks-atlanta/metadata-scala/compare/v0.0.7...v0.0.8) (2023-08-20) - -### 0.0.7 (2023-08-20) - - -### Bug Fixes - -* **ci:** Coverage pipeline ([#16](https://github.com-university/hawks-atlanta/metadata-scala/issues/16)) ([973e936](https://github.com-university/hawks-atlanta/metadata-scala/commit/973e936759affd769f80b900d02924422e2de698)), closes [#7](https://github.com-university/hawks-atlanta/metadata-scala/issues/7) [#9](https://github.com-university/hawks-atlanta/metadata-scala/issues/9) [#10](https://github.com-university/hawks-atlanta/metadata-scala/issues/10) [#11](https://github.com-university/hawks-atlanta/metadata-scala/issues/11) [#15](https://github.com-university/hawks-atlanta/metadata-scala/issues/15) [#8](https://github.com-university/hawks-atlanta/metadata-scala/issues/8) [#7](https://github.com-university/hawks-atlanta/metadata-scala/issues/7) [#9](https://github.com-university/hawks-atlanta/metadata-scala/issues/9) [#10](https://github.com-university/hawks-atlanta/metadata-scala/issues/10) [#11](https://github.com-university/hawks-atlanta/metadata-scala/issues/11) -* **ci:** Fix coverage pipeline ([#14](https://github.com-university/hawks-atlanta/metadata-scala/issues/14)) ([013c956](https://github.com-university/hawks-atlanta/metadata-scala/commit/013c956ab386707a9db33f76a376dad6c1130cd2)) -* **ci:** Update test pipeline ([#8](https://github.com-university/hawks-atlanta/metadata-scala/issues/8)) ([1da251d](https://github.com-university/hawks-atlanta/metadata-scala/commit/1da251d344ba2f8af61efa8a339716672abec56f)) -* Trigger pipelines ([#13](https://github.com-university/hawks-atlanta/metadata-scala/issues/13)) ([791a672](https://github.com-university/hawks-atlanta/metadata-scala/commit/791a672b646753bb42a7aedaa20de30e44e05c1f)) - -### 0.0.6 (2023-08-20) - -### 0.0.5 (2023-08-20) - -### Bug Fixes - -* **ci:** Detach docker compose ([#15](https://github.com/hawks-atlanta/metadata-scala/issues/15)) ([7dfbe61](https://github.com/hawks-atlanta/metadata-scala/commit/7dfbe610279e448e4362409e452bbff269fa6f0c)), closes [#8](https://github.com/hawks-atlanta/metadata-scala/issues/8) [#7](https://github.com/hawks-atlanta/metadata-scala/issues/7) [#9](https://github.com/hawks-atlanta/metadata-scala/issues/9) [#10](https://github.com/hawks-atlanta/metadata-scala/issues/10) [#11](https://github.com/hawks-atlanta/metadata-scala/issues/11) - -### 0.0.4 (2023-08-20) - -### 0.0.3 (2023-08-20) - -### 0.0.2 (2023-08-20) - -### 0.0.1 (2023-08-20) - -### Bug Fixes - -* **ci:** Update test pipeline ([#7](https://github.com/hawks-atlanta/metadata-scala/issues/7)) ([7e9080b](https://github.com/hawks-atlanta/metadata-scala/commit/7e9080bcf9d4ddd34a778aa30a67d74614988f32)) diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index e3758de..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2095 +0,0 @@ -{ - "name": "metadata-scala", - "version": "0.0.15", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "version": "0.0.15", - "devDependencies": { - "git-semver-tags": "^4.1.1", - "standard-version": "^9.5.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", - "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.22.10", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", - "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@hutson/parse-repository-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", - "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true - }, - "node_modules/add-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", - "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", - "dev": true - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "dev": true, - "engines": [ - "node >= 6.0" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/conventional-changelog": { - "version": "3.1.25", - "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz", - "integrity": "sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==", - "dev": true, - "dependencies": { - "conventional-changelog-angular": "^5.0.12", - "conventional-changelog-atom": "^2.0.8", - "conventional-changelog-codemirror": "^2.0.8", - "conventional-changelog-conventionalcommits": "^4.5.0", - "conventional-changelog-core": "^4.2.1", - "conventional-changelog-ember": "^2.0.9", - "conventional-changelog-eslint": "^3.0.9", - "conventional-changelog-express": "^2.0.6", - "conventional-changelog-jquery": "^3.0.11", - "conventional-changelog-jshint": "^2.0.9", - "conventional-changelog-preset-loader": "^2.3.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-angular": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", - "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", - "dev": true, - "dependencies": { - "compare-func": "^2.0.0", - "q": "^1.5.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-atom": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz", - "integrity": "sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==", - "dev": true, - "dependencies": { - "q": "^1.5.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-codemirror": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz", - "integrity": "sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==", - "dev": true, - "dependencies": { - "q": "^1.5.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-config-spec": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-config-spec/-/conventional-changelog-config-spec-2.1.0.tgz", - "integrity": "sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==", - "dev": true - }, - "node_modules/conventional-changelog-conventionalcommits": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz", - "integrity": "sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==", - "dev": true, - "dependencies": { - "compare-func": "^2.0.0", - "lodash": "^4.17.15", - "q": "^1.5.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-core": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz", - "integrity": "sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==", - "dev": true, - "dependencies": { - "add-stream": "^1.0.0", - "conventional-changelog-writer": "^5.0.0", - "conventional-commits-parser": "^3.2.0", - "dateformat": "^3.0.0", - "get-pkg-repo": "^4.0.0", - "git-raw-commits": "^2.0.8", - "git-remote-origin-url": "^2.0.0", - "git-semver-tags": "^4.1.1", - "lodash": "^4.17.15", - "normalize-package-data": "^3.0.0", - "q": "^1.5.1", - "read-pkg": "^3.0.0", - "read-pkg-up": "^3.0.0", - "through2": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-core/node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/conventional-changelog-core/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/conventional-changelog-core/node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/conventional-changelog-core/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/conventional-changelog-core/node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/conventional-changelog-core/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/conventional-changelog-core/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/conventional-changelog-core/node_modules/read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", - "dev": true, - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/conventional-changelog-core/node_modules/read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", - "dev": true, - "dependencies": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/conventional-changelog-core/node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/conventional-changelog-core/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/conventional-changelog-ember": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz", - "integrity": "sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==", - "dev": true, - "dependencies": { - "q": "^1.5.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-eslint": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz", - "integrity": "sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==", - "dev": true, - "dependencies": { - "q": "^1.5.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-express": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz", - "integrity": "sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==", - "dev": true, - "dependencies": { - "q": "^1.5.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-jquery": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz", - "integrity": "sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==", - "dev": true, - "dependencies": { - "q": "^1.5.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-jshint": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz", - "integrity": "sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==", - "dev": true, - "dependencies": { - "compare-func": "^2.0.0", - "q": "^1.5.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-preset-loader": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz", - "integrity": "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-writer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz", - "integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==", - "dev": true, - "dependencies": { - "conventional-commits-filter": "^2.0.7", - "dateformat": "^3.0.0", - "handlebars": "^4.7.7", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.15", - "meow": "^8.0.0", - "semver": "^6.0.0", - "split": "^1.0.0", - "through2": "^4.0.0" - }, - "bin": { - "conventional-changelog-writer": "cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-commits-filter": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz", - "integrity": "sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==", - "dev": true, - "dependencies": { - "lodash.ismatch": "^4.4.0", - "modify-values": "^1.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-commits-parser": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", - "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", - "dev": true, - "dependencies": { - "is-text-path": "^1.0.1", - "JSONStream": "^1.0.4", - "lodash": "^4.17.15", - "meow": "^8.0.0", - "split2": "^3.0.0", - "through2": "^4.0.0" - }, - "bin": { - "conventional-commits-parser": "cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-recommended-bump": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz", - "integrity": "sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw==", - "dev": true, - "dependencies": { - "concat-stream": "^2.0.0", - "conventional-changelog-preset-loader": "^2.3.4", - "conventional-commits-filter": "^2.0.7", - "conventional-commits-parser": "^3.2.0", - "git-raw-commits": "^2.0.8", - "git-semver-tags": "^4.1.1", - "meow": "^8.0.0", - "q": "^1.5.1" - }, - "bin": { - "conventional-recommended-bump": "cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "node_modules/dargs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", - "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotgitignore": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/dotgitignore/-/dotgitignore-2.1.0.tgz", - "integrity": "sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dotgitignore/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dotgitignore/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dotgitignore/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dotgitignore/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-pkg-repo": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz", - "integrity": "sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==", - "dev": true, - "dependencies": { - "@hutson/parse-repository-url": "^3.0.0", - "hosted-git-info": "^4.0.0", - "through2": "^2.0.0", - "yargs": "^16.2.0" - }, - "bin": { - "get-pkg-repo": "src/cli.js" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-pkg-repo/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/get-pkg-repo/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/get-pkg-repo/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/get-pkg-repo/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/git-raw-commits": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", - "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", - "dev": true, - "dependencies": { - "dargs": "^7.0.0", - "lodash": "^4.17.15", - "meow": "^8.0.0", - "split2": "^3.0.0", - "through2": "^4.0.0" - }, - "bin": { - "git-raw-commits": "cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/git-remote-origin-url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", - "integrity": "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==", - "dev": true, - "dependencies": { - "gitconfiglocal": "^1.0.0", - "pify": "^2.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/git-semver-tags": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz", - "integrity": "sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==", - "dev": true, - "dependencies": { - "meow": "^8.0.0", - "semver": "^6.0.0" - }, - "bin": { - "git-semver-tags": "cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gitconfiglocal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", - "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", - "dev": true, - "dependencies": { - "ini": "^1.3.2" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-text-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", - "dev": true, - "dependencies": { - "text-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ] - }, - "node_modules/JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "dependencies": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - }, - "bin": { - "JSONStream": "bin.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.ismatch": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", - "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", - "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", - "dev": true, - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/modify-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", - "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-type/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "dev": true, - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", - "dev": true - }, - "node_modules/split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "dev": true, - "dependencies": { - "readable-stream": "^3.0.0" - } - }, - "node_modules/standard-version": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/standard-version/-/standard-version-9.5.0.tgz", - "integrity": "sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q==", - "dev": true, - "dependencies": { - "chalk": "^2.4.2", - "conventional-changelog": "3.1.25", - "conventional-changelog-config-spec": "2.1.0", - "conventional-changelog-conventionalcommits": "4.6.3", - "conventional-recommended-bump": "6.1.0", - "detect-indent": "^6.0.0", - "detect-newline": "^3.1.0", - "dotgitignore": "^2.1.0", - "figures": "^3.1.0", - "find-up": "^5.0.0", - "git-semver-tags": "^4.0.0", - "semver": "^7.1.1", - "stringify-package": "^1.0.1", - "yargs": "^16.0.0" - }, - "bin": { - "standard-version": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/standard-version/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/standard-version/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/standard-version/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/standard-version/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/standard-version/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/stringify-package": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz", - "integrity": "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==", - "deprecated": "This module is not used anymore, and has been replaced by @npmcli/package-json", - "dev": true - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-extensions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", - "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "dependencies": { - "readable-stream": "3" - } - }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, - "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "dev": true, - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 2901873..0000000 --- a/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "devDependencies": { - "git-semver-tags": "^4.1.1", - "standard-version": "^9.5.0" - }, - "version": "0.0.15" -} diff --git a/version.json b/version.json new file mode 100644 index 0000000..c158d5b --- /dev/null +++ b/version.json @@ -0,0 +1,3 @@ +{ + "version": "0.0.0" +} \ No newline at end of file diff --git a/version.py b/version.py index c54f138..20e1e3a 100644 --- a/version.py +++ b/version.py @@ -1,5 +1,5 @@ import json -with open("package.json", "rb") as pkg_file: +with open("version.json", "rb") as pkg_file: pkg = json.load(pkg_file) print(f"version={pkg['version']}") \ No newline at end of file From 7530a2bdd9ff3bb7b146b1e6e9e48b876ea830b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Thu, 7 Sep 2023 18:10:19 -0500 Subject: [PATCH 15/67] fix(ci): Fix tagging pipeline (#43) --- .github/workflows/tagging.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tagging.yaml b/.github/workflows/tagging.yaml index 5ab70c4..cf661bf 100644 --- a/.github/workflows/tagging.yaml +++ b/.github/workflows/tagging.yaml @@ -18,3 +18,4 @@ jobs: git-user-nane: "Antonio Donis" git-user-email: "antoniojosedonishung@gmail.com" git-message: "chore(release): {version}" + version-file: "version.json" From 7ca8be07f34d5c16f282cc145c0195adcb37d1b7 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Thu, 7 Sep 2023 23:10:35 +0000 Subject: [PATCH 16/67] chore(release): v0.1.0 [skip ci] --- CHANGELOG.md | 19 +++++++++++++++++++ version.json | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bbead91 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# 0.1.0 (2023-09-07) + + +### Bug Fixes + +* **cd:** Build the Docker image without running tests ([#35](https://github.com/hawks-atlanta/metadata-scala/issues/35)) ([c1edb5d](https://github.com/hawks-atlanta/metadata-scala/commit/c1edb5d596dc573d467704d25ad93ee5f568b900)) +* **ci:** Coverage pipeline ([#16](https://github.com/hawks-atlanta/metadata-scala/issues/16)) ([973e936](https://github.com/hawks-atlanta/metadata-scala/commit/973e936759affd769f80b900d02924422e2de698)), closes [#7](https://github.com/hawks-atlanta/metadata-scala/issues/7) [#9](https://github.com/hawks-atlanta/metadata-scala/issues/9) [#10](https://github.com/hawks-atlanta/metadata-scala/issues/10) [#11](https://github.com/hawks-atlanta/metadata-scala/issues/11) [#15](https://github.com/hawks-atlanta/metadata-scala/issues/15) [#8](https://github.com/hawks-atlanta/metadata-scala/issues/8) [#7](https://github.com/hawks-atlanta/metadata-scala/issues/7) [#9](https://github.com/hawks-atlanta/metadata-scala/issues/9) [#10](https://github.com/hawks-atlanta/metadata-scala/issues/10) [#11](https://github.com/hawks-atlanta/metadata-scala/issues/11) +* **ci:** Fix coverage pipeline ([#14](https://github.com/hawks-atlanta/metadata-scala/issues/14)) ([013c956](https://github.com/hawks-atlanta/metadata-scala/commit/013c956ab386707a9db33f76a376dad6c1130cd2)) +* **ci:** Fix tagging pipeline ([#43](https://github.com/hawks-atlanta/metadata-scala/issues/43)) ([7530a2b](https://github.com/hawks-atlanta/metadata-scala/commit/7530a2bdd9ff3bb7b146b1e6e9e48b876ea830b5)) +* **ci:** Update test pipeline ([#8](https://github.com/hawks-atlanta/metadata-scala/issues/8)) ([1da251d](https://github.com/hawks-atlanta/metadata-scala/commit/1da251d344ba2f8af61efa8a339716672abec56f)) +* Trigger pipelines ([#13](https://github.com/hawks-atlanta/metadata-scala/issues/13)) ([791a672](https://github.com/hawks-atlanta/metadata-scala/commit/791a672b646753bb42a7aedaa20de30e44e05c1f)) + + +### Features + +* PostgreSQL connection and Migrations ([#20](https://github.com/hawks-atlanta/metadata-scala/issues/20)) ([b502749](https://github.com/hawks-atlanta/metadata-scala/commit/b502749d51d3149d585972f8d19bc6f4c19b7fbc)) + + + diff --git a/version.json b/version.json index c158d5b..6793ca7 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.0.0" + "version": "0.1.0" } \ No newline at end of file From a1140d5c8767278defc5e767c3c3eb87271ba81b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Sun, 10 Sep 2023 06:25:10 -0500 Subject: [PATCH 17/67] feat: New files are unshared by default (#44) --- db/migrations/V1__init.sql | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/db/migrations/V1__init.sql b/db/migrations/V1__init.sql index f9d54e7..58695f1 100644 --- a/db/migrations/V1__init.sql +++ b/db/migrations/V1__init.sql @@ -19,10 +19,17 @@ CREATE TABLE IF NOT EXISTS files ( "archive_uuid" UUID DEFAULT NULL REFERENCES archives("uuid"), "volume" VARCHAR(32) DEFAULT NULL, "name" VARCHAR(128) NOT NULL, + "is_shared" BOOLEAN NOT NULL DEFAULT FALSE, "created_at" TIMESTAMP NOT NULL DEFAULT NOW(), "updated_at" TIMESTAMP NOT NULL DEFAULT NOW() ); +CREATE TABLE IF NOT EXISTS shared_files ( + "uuid" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "file_uuid" UUID NOT NULL REFERENCES files("uuid"), + "user_uuid" UUID NOT NULL +); + -- Indexes CREATE INDEX IF NOT EXISTS "files_owner_uuid_index" ON files ("owner_uuid"); CREATE INDEX IF NOT EXISTS "files_parent_uuid_index" ON files ("parent_uuid"); From c900db661913f347db9344f60e2c10099d2c83fc Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Sun, 10 Sep 2023 11:25:26 +0000 Subject: [PATCH 18/67] chore(release): v0.2.0 [skip ci] --- CHANGELOG.md | 11 ++++++++++- version.json | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbead91..67f495e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ -# 0.1.0 (2023-09-07) +# [0.2.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.1.0...v0.2.0) (2023-09-10) + + +### Features + +* New files are unshared by default ([#44](https://github.com/hawks-atlanta/metadata-scala/issues/44)) ([a1140d5](https://github.com/hawks-atlanta/metadata-scala/commit/a1140d5c8767278defc5e767c3c3eb87271ba81b)) + + + +# [0.1.0](https://github.com/hawks-atlanta/metadata-scala/compare/1da251d344ba2f8af61efa8a339716672abec56f...v0.1.0) (2023-09-07) ### Bug Fixes diff --git a/version.json b/version.json index 6793ca7..d787358 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.1.0" + "version": "0.2.0" } \ No newline at end of file From 539d84c92f12f3d35b761a9efee2a8e293cb03a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Mon, 11 Sep 2023 07:12:45 -0500 Subject: [PATCH 19/67] docs: Update openapi spec (#45) * docs(openapi): Add new endpoints * chore(db): Add unique index for the shared files table --- db/migrations/V1__init.sql | 1 + docs/spec.openapi.yaml | 197 ++++++++++++++++++++++++++++++++++++- 2 files changed, 196 insertions(+), 2 deletions(-) diff --git a/db/migrations/V1__init.sql b/db/migrations/V1__init.sql index 58695f1..990ad64 100644 --- a/db/migrations/V1__init.sql +++ b/db/migrations/V1__init.sql @@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS shared_files ( CREATE INDEX IF NOT EXISTS "files_owner_uuid_index" ON files ("owner_uuid"); CREATE INDEX IF NOT EXISTS "files_parent_uuid_index" ON files ("parent_uuid"); CREATE UNIQUE INDEX IF NOT EXISTS "files_unique_triplet_index" ON files ("owner_uuid", "parent_uuid", "name"); +CREATE UNIQUE INDEX IF NOT EXISTS "shared_files_unique_tuple_index" ON shared_files ("file_uuid", "user_uuid"); -- Triggers CREATE OR REPLACE FUNCTION "update_updated_at_column"() diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml index f169eb9..5320ad5 100644 --- a/docs/spec.openapi.yaml +++ b/docs/spec.openapi.yaml @@ -109,10 +109,10 @@ paths: schema: $ref: '#/components/schemas/error_response' - /files/{user_uuid}/{file_uuid}: + /files/{user_uuid}/shared_with_me: get: tags: [ 'File' ] - description: Get the metadata of the given file. This endpoint is suposed to only be used by the gateway service to obtain the location (files/volume/archive_uuid) of the file. + description: List the files shared with the given user. parameters: - in: path name: user_uuid @@ -120,6 +120,66 @@ paths: type: string example: '658b4e63-b5ac-46a7-ac43-efb6a1415130' required: true + responses: + '200': + description: Ok. The directory was listed. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/file' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + + /files/{user_uuid}/{file_uuid}/can_read: + get: + tags: [ 'File' ] + description: Check if the given user can read the given file. + parameters: + - in: path + name: user_uuid + schema: + type: string + example: '658b4e63-b5ac-46a7-ac43-efb6a1415130' + required: true + - in: path + name: file_uuid + schema: + type: string + example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' + required: true + responses: + '204': + description: The user can read the file. + '403': + description: The user can't read the file. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '404': + description: No file with the given file_uuid was found. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + + /files/{file_uuid}: + get: + tags: [ 'File' ] + description: Get the metadata of the given file. This endpoint is suposed to only be used by the gateway service to obtain the location (files/volume/archive_uuid) of the file. + parameters: - in: path name: file_uuid schema: @@ -151,6 +211,38 @@ paths: application/json: schema: $ref: '#/components/schemas/error_response' + + /files/{file_uuid}/shared_with: + get: + tags: [ 'File' ] + parameters: + - in: path + name: file_uuid + schema: + type: string + example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' + required: true + responses: + '200': + description: The list of UUIDs of the users which the file was shared is returned. + content: + application/json: + schema: + type: object + properties: + shared_with: + type: array + items: + type: string + example: '6cec2ad8-7329-47f8-8b76-7daf7036945c' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + + /files/{user_uuid}/{file_uuid}: delete: tags: [ 'File' ] description: Delete the metadata of the given file. @@ -189,6 +281,100 @@ paths: schema: $ref: '#/components/schemas/error_response' + /files/{owner_uuid}/{file_uuid}/share: + post: + tags: [ 'File' ] + description: Share the given file with the given user + parameters: + - in: path + name: owner_uuid + schema: + type: string + example: '658b4e63-b5ac-46a7-ac43-efb6a1415130' + required: true + - in: path + name: file_uuid + schema: + type: string + example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' + required: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/toggle_share_request' + responses: + '204': + description: The file was shared + '400': + description: The owner_uuid or file_uuid were not a valid UUID or the JSON body does´t fullfill the validations. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '403': + description: The file is not owned by the user. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '409': + description: The file is already shared with the given user. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + + /files/{owner_uuid}/{file_uuid}/unshare: + post: + tags: [ 'File' ] + description: Unshare the given file with the given user + parameters: + - in: path + name: owner_uuid + schema: + type: string + example: '658b4e63-b5ac-46a7-ac43-efb6a1415130' + required: true + - in: path + name: file_uuid + schema: + type: string + example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' + required: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/toggle_share_request' + responses: + '204': + description: The file was unshared. + '400': + description: The owner_uuid or file_uuid were not a valid UUID or the JSON body does´t fullfill the validations. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '403': + description: The file is not owned by the user. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + /files/{file_uuid}/ready: put: tags: [ 'File' ] @@ -266,6 +452,13 @@ components: description: 'Size in KB' example: 3072 + toggle_share_request: + type: object + properties: + otherUserUUID: + type: string + example: '6cec2ad8-7329-47f8-8b76-7daf7036945c' + error_response: type: object properties: From d4063bdcd3441e17cb76733599d00cefef3446ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Mon, 11 Sep 2023 07:46:11 -0500 Subject: [PATCH 20/67] docs(openapi): Fix overlapping routes error (#46) --- docs/spec.openapi.yaml | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml index 5320ad5..426c179 100644 --- a/docs/spec.openapi.yaml +++ b/docs/spec.openapi.yaml @@ -13,7 +13,7 @@ tags: - name: File paths: - /files/{user_uuid}: + /files/list/{user_uuid}: get: tags: [ 'File' ] description: List files in the given directory. List the user's root directory by default when the parent_uuid query parameter is not provided. @@ -58,16 +58,10 @@ paths: schema: $ref: '#/components/schemas/error_response' + /files: post: tags: [ 'File' ] description: Save the metadata for a new file or directory. - parameters: - - in: path - name: user_uuid - schema: - type: string - example: '658b4e63-b5ac-46a7-ac43-efb6a1415130' - required: true requestBody: content: application/json: @@ -109,7 +103,7 @@ paths: schema: $ref: '#/components/schemas/error_response' - /files/{user_uuid}/shared_with_me: + /files/shared_with_me/{user_uuid}: get: tags: [ 'File' ] description: List the files shared with the given user. @@ -136,7 +130,7 @@ paths: schema: $ref: '#/components/schemas/error_response' - /files/{user_uuid}/{file_uuid}/can_read: + /files/can_read/{user_uuid}/{file_uuid}: get: tags: [ 'File' ] description: Check if the given user can read the given file. @@ -175,7 +169,7 @@ paths: schema: $ref: '#/components/schemas/error_response' - /files/{file_uuid}: + /files/metadata/{file_uuid}: get: tags: [ 'File' ] description: Get the metadata of the given file. This endpoint is suposed to only be used by the gateway service to obtain the location (files/volume/archive_uuid) of the file. @@ -212,7 +206,7 @@ paths: schema: $ref: '#/components/schemas/error_response' - /files/{file_uuid}/shared_with: + /files/shared_with_who/{file_uuid}: get: tags: [ 'File' ] parameters: @@ -281,7 +275,7 @@ paths: schema: $ref: '#/components/schemas/error_response' - /files/{owner_uuid}/{file_uuid}/share: + /files/share/{owner_uuid}/{file_uuid}: post: tags: [ 'File' ] description: Share the given file with the given user @@ -331,7 +325,7 @@ paths: schema: $ref: '#/components/schemas/error_response' - /files/{owner_uuid}/{file_uuid}/unshare: + /files/unshare/{owner_uuid}/{file_uuid}: post: tags: [ 'File' ] description: Unshare the given file with the given user @@ -375,7 +369,7 @@ paths: schema: $ref: '#/components/schemas/error_response' - /files/{file_uuid}/ready: + /files/ready/{file_uuid}: put: tags: [ 'File' ] description: Mark the given file as ready (Stored in the filesystem). @@ -432,6 +426,9 @@ components: file_creation_request: type: object properties: + userUUID: + type: string + example: '658b4e63-b5ac-46a7-ac43-efb6a1415130' parentUUID: type: string example: '5ad724f0-4091-453a-914a-c2d11d69d1e3' From 5e2ea56fac46705a9c77a7a2c13a3379391b0800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Mon, 11 Sep 2023 08:06:44 -0500 Subject: [PATCH 21/67] fix: Update save metadata endpoint to avoid endpoints overlapping (#47) --- .../infrastructure/MetadataControllers.scala | 17 +------- .../infrastructure/MetadataRoutes.scala | 7 ++- .../requests/CreationReqSchema.scala | 7 +++ .../SaveFileMetadataTests.scala | 43 +++++-------------- 4 files changed, 23 insertions(+), 51 deletions(-) diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 03c08a1..2e07e0e 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -26,21 +26,8 @@ class MetadataControllers { } def SaveMetadataController( - request: cask.Request, - userUUID: String + request: cask.Request ): cask.Response[Obj] = { - // Check if the given user UUID is valid - val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) - if (!isUserUUIDValid) { - return cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "The user_uuid parameter was not valid" - ), - statusCode = 400 - ) - } - try { // Decode the JSON payload val decoded: CreationReqSchema = read[CreationReqSchema]( @@ -88,7 +75,7 @@ class MetadataControllers { else Some( UUID.fromString( decoded.parentUUID ) ) val receivedFileMeta = FileMeta.createNewFile( - ownerUuid = UUID.fromString( userUUID ), + ownerUuid = UUID.fromString( decoded.userUUID ), parentUuid = parentUUID, name = decoded.fileName ) diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index 794d51c..49312cc 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -9,12 +9,11 @@ case class MetadataRoutes() extends cask.Routes { private val controllers = new MetadataControllers() controllers._init() - @cask.post( s"${ basePath }/:userUUID" ) + @cask.post( s"${ basePath }" ) def SaveMetadataHandler( - request: cask.Request, - userUUID: String + request: cask.Request ): cask.Response[Obj] = { - controllers.SaveMetadataController( request, userUUID ) + controllers.SaveMetadataController( request ) } initialize() diff --git a/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala b/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala index 1d04950..013b4c0 100644 --- a/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala +++ b/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala @@ -7,6 +7,7 @@ import shared.infrastructure.CommonValidator // --- Base class --- case class CreationReqSchema( + userUUID: String, parentUUID: String, hashSum: String, fileType: String, @@ -25,6 +26,9 @@ object CreationReqSchema { val fileSchemaValidator: Validator[CreationReqSchema] = validator[CreationReqSchema] { request => { + request.userUUID should matchRegex( + CommonValidator.uuidRegex + ) request.parentUUID .is( aNull ) // Can be empty if it's in the root directory .or( // Otherwise should be a valid UUID @@ -43,6 +47,9 @@ object CreationReqSchema { val directorySchemaValidator: Validator[CreationReqSchema] = validator[CreationReqSchema] { request => { + request.userUUID should matchRegex( + CommonValidator.uuidRegex + ) request.parentUUID .is( aNull ) .or( diff --git a/src/test/scala/files_metadata/SaveFileMetadataTests.scala b/src/test/scala/files_metadata/SaveFileMetadataTests.scala index 54f01a7..09b527f 100644 --- a/src/test/scala/files_metadata/SaveFileMetadataTests.scala +++ b/src/test/scala/files_metadata/SaveFileMetadataTests.scala @@ -21,6 +21,7 @@ object TestData { def getFilePayload(): util.HashMap[String, Any] = { if (filePayload == null) { filePayload = new util.HashMap[String, Any] + filePayload.put( "userUUID", USER_UUID.toString ) filePayload.put( "parentUUID", null ) filePayload.put( "hashSum", @@ -56,7 +57,7 @@ class SaveFileMetadataTests extends JUnitSuite { .body( TestData.getFilePayload() ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + .post( s"${ TestData.API_PREFIX }" ) val responseJSON = response.jsonPath() // --- Assertions --- @@ -77,6 +78,7 @@ class SaveFileMetadataTests extends JUnitSuite { def T2_SaveDirectoryMetadataSuccess(): Unit = { // --- Test data --- val payload = new util.HashMap[String, Any]() + payload.put( "userUUID", TestData.USER_UUID.toString ) payload.put( "parentUUID", null ) payload.put( "hashSum", "" ) payload.put( "fileType", "directory" ) @@ -89,7 +91,7 @@ class SaveFileMetadataTests extends JUnitSuite { .body( payload ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + .post( s"${ TestData.API_PREFIX }" ) val responseJSON = response.jsonPath() // --- Assertions --- @@ -119,7 +121,7 @@ class SaveFileMetadataTests extends JUnitSuite { .body( payload ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + .post( s"${ TestData.API_PREFIX }" ) val responseJSON = response.jsonPath() // --- Assertions --- @@ -135,37 +137,16 @@ class SaveFileMetadataTests extends JUnitSuite { ) } - @Test - // POST /api/v1/files/:user_uuid Bad Request: Bad user_uuid parameter - def T4_SaveArchiveMetadataBadParameter(): Unit = { - // --- Request --- - val response = `given`() - .port( 8080 ) - .when() - .post( s"${ TestData.API_PREFIX }/1" ) - - val responseJSON = response.jsonPath() - - // --- Assertions --- - assert( response.statusCode() == 400 ) - assert( responseJSON.getBoolean( "error" ) ) - assert( - responseJSON.getString( - "message" - ) == "The user_uuid parameter was not valid" - ) - } - @Test // POST /api/v1/files/:user_uuid Conflict: File already exists - def T5_SaveArchiveMetadataConflict(): Unit = { + def T4_SaveArchiveMetadataConflict(): Unit = { // --- Request --- val response = `given`() .port( 8080 ) .body( TestData.getFilePayload() ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + .post( s"${ TestData.API_PREFIX }" ) val responseJSON = response.jsonPath() // --- Assertions --- @@ -181,20 +162,18 @@ class SaveFileMetadataTests extends JUnitSuite { @Test /* POST /api/v1/files/:user_uuid Conflict: File in parent directory already * exists */ - def T6_SaveArchiveMetadataWithParentConflict(): Unit = { + def T5_SaveArchiveMetadataWithParentConflict(): Unit = { // --- Test data --- val payload = TestData.getFilePayload() payload.put( "parentUUID", TestData.directoryUUID.toString ) - print( s">> Parent: ${ TestData.directoryUUID.toString } <<" ) - // --- Request --- val response = `given`() .port( 8080 ) .body( payload ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + .post( s"${ TestData.API_PREFIX }" ) val responseJSON = response.jsonPath() // --- Assertions --- @@ -209,7 +188,7 @@ class SaveFileMetadataTests extends JUnitSuite { @Test // POST /api/v1/files/:user_uuid Not found: Parent directory does not exist - def T7_SaveArchiveMetadataWithParentNotFound(): Unit = { + def T6_SaveArchiveMetadataWithParentNotFound(): Unit = { // --- Test data --- val payload = TestData.getFilePayload() payload.put( "parentUUID", UUID.randomUUID().toString ) @@ -220,7 +199,7 @@ class SaveFileMetadataTests extends JUnitSuite { .body( payload ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }/${ TestData.USER_UUID.toString }" ) + .post( s"${ TestData.API_PREFIX }" ) val responseJSON = response.jsonPath() // --- Assertions --- From aa04f138680c8c9c1d477022bf1cf2bb3d57d362 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Mon, 11 Sep 2023 13:07:02 +0000 Subject: [PATCH 22/67] chore(release): v0.2.1 [skip ci] --- CHANGELOG.md | 9 +++++++++ version.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67f495e..fcd9a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [0.2.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.2.0...v0.2.1) (2023-09-11) + + +### Bug Fixes + +* Update save metadata endpoint to avoid endpoints overlapping ([#47](https://github.com/hawks-atlanta/metadata-scala/issues/47)) ([5e2ea56](https://github.com/hawks-atlanta/metadata-scala/commit/5e2ea56fac46705a9c77a7a2c13a3379391b0800)) + + + # [0.2.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.1.0...v0.2.0) (2023-09-10) diff --git a/version.json b/version.json index d787358..4bd29bc 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.2.0" + "version": "0.2.1" } \ No newline at end of file From 1c2e8ea772c7c0ae51e17ab143f7f581cace8f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:41:03 -0500 Subject: [PATCH 23/67] feat: Share files (#48) * feat: Endpoint to share a file * fix: Get file metadata without the owner UUID * test: Add tests for the new endpoint * docs: Update HTTP examples --- docs/rest/save_metadata.http | 12 +- docs/rest/share_file.http | 8 + .../application/FilesMetaUseCases.scala | 26 ++- .../domain/DomainExceptions.scala | 6 + .../domain/FilesMetaRepository.scala | 10 +- .../FilesMetaPostgresRepository.scala | 55 ++++- .../infrastructure/MetadataControllers.scala | 88 ++++++++ .../infrastructure/MetadataRoutes.scala | 9 + .../requests/ShareReqSchema.scala | 28 +++ .../SaveFileMetadataTests.scala | 63 ++---- .../scala/files_metadata/ShareFileTests.scala | 212 ++++++++++++++++++ 11 files changed, 463 insertions(+), 54 deletions(-) create mode 100644 docs/rest/share_file.http create mode 100644 src/main/scala/files_metadata/infrastructure/requests/ShareReqSchema.scala create mode 100644 src/test/scala/files_metadata/ShareFileTests.scala diff --git a/docs/rest/save_metadata.http b/docs/rest/save_metadata.http index 889a31e..bd2bd48 100644 --- a/docs/rest/save_metadata.http +++ b/docs/rest/save_metadata.http @@ -1,9 +1,10 @@ ### Save metadata for an archive file stored in the root directory -POST http://localhost:8080/api/v1/files/9ae0ee6f-0644-46c8-b364-ee36c9f9bd81 HTTP/1.1 +POST http://localhost:8080/api/v1/files/ HTTP/1.1 Content-Type: application/json { + "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", "parentUUID": null, "hashSum": "fb3a2b16764328a3c90f2122cdb4e583d2b344c9499fdf9bd1f846170e05cb52", "fileType": "archive", @@ -13,10 +14,11 @@ Content-Type: application/json ### Save metadata for a directory stored in the root directory -POST http://localhost:8080/api/v1/files/9ae0ee6f-0644-46c8-b364-ee36c9f9bd81 HTTP/1.1 +POST http://localhost:8080/api/v1/files/ HTTP/1.1 Content-Type: application/json { + "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", "parentUUID": null, "hashSum": "", "fileType": "directory", @@ -26,10 +28,11 @@ Content-Type: application/json ### Save metadata for an archive file stored in a parent directory -POST http://localhost:8080/api/v1/files/9ae0ee6f-0644-46c8-b364-ee36c9f9bd81 HTTP/1.1 +POST http://localhost:8080/api/v1/files/ HTTP/1.1 Content-Type: application/json { + "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", "parentUUID": "92467e11-38e8-41f1-a088-0538f43811bd", "hashSum": "5a362b73f98d8a4123ba318e9c5bead3135caa33eada95e80e17290ce9bbf4be", "fileType": "archive", @@ -39,10 +42,11 @@ Content-Type: application/json ### Save metadata for a directory stored in a parent directory -POST http://localhost:8080/api/v1/files/9ae0ee6f-0644-46c8-b364-ee36c9f9bd81 HTTP/1.1 +POST http://localhost:8080/api/v1/files/ HTTP/1.1 Content-Type: application/json { + "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", "parentUUID": "92467e11-38e8-41f1-a088-0538f43811bd", "hashSum": "", "fileType": "directory", diff --git a/docs/rest/share_file.http b/docs/rest/share_file.http new file mode 100644 index 0000000..c3daa18 --- /dev/null +++ b/docs/rest/share_file.http @@ -0,0 +1,8 @@ +### Share a file (archive or directory) + +POST http://localhost:8080/api/v1/files/share/{ownerUUID}/{fileUUID} HTTP/1.1 +Content-Type: application/json + +{ + "otherUserUUID": "928f7a86-a091-4199-b767-58a0e1147b72" +} \ No newline at end of file diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index 55648e2..3f360b2 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -33,7 +33,6 @@ class FilesMetaUseCases { // If a parent directory is given, check if it exists if (fileMeta.parentUuid.isDefined) { repository.getFileMeta( - ownerUuid = fileMeta.ownerUuid, uuid = fileMeta.parentUuid.get ) } @@ -41,4 +40,29 @@ class FilesMetaUseCases { // Save the metadata repository.saveFileMeta( archiveMeta, fileMeta ) } + + def shareFile( + ownerUUID: UUID, + fileUUID: UUID, + otherUserUUID: UUID + ): Unit = { + val fileMeta = repository.getFileMeta( fileUUID ) + + if (fileMeta.ownerUuid != ownerUUID) { + throw DomainExceptions.FileNotOwnedException( + "The user does not own the file" + ) + } + + if ( + fileMeta.ownerUuid == otherUserUUID || + repository.isFileDirectlySharedWithUser( fileUUID, otherUserUUID ) + ) { + throw DomainExceptions.FileAlreadySharedException( + "The file is already shared with the user" + ) + } + + repository.shareFile( fileUUID, otherUserUUID ) + } } diff --git a/src/main/scala/files_metadata/domain/DomainExceptions.scala b/src/main/scala/files_metadata/domain/DomainExceptions.scala index e5072a4..696cd3c 100644 --- a/src/main/scala/files_metadata/domain/DomainExceptions.scala +++ b/src/main/scala/files_metadata/domain/DomainExceptions.scala @@ -8,9 +8,15 @@ object DomainExceptions { case class FileAlreadyExistsException( message: String ) extends Exception( message ) + case class FileNotOwnedException( message: String ) + extends Exception( message ) + case class ArchiveNotSavedException( message: String ) extends Exception( message ) case class FileNotSavedException( message: String ) extends Exception( message ) + + case class FileAlreadySharedException( message: String ) + extends Exception( message ) } diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index 81cc03b..002edf4 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -7,6 +7,8 @@ trait FilesMetaRepository { // --- Create --- def saveFileMeta( archiveMeta: ArchivesMeta, fileMeta: FileMeta ): UUID + def shareFile( fileUUID: UUID, userUUID: UUID ): Unit + // --- Read --- def getFilesMetaInRoot( ownerUuid: UUID ): Seq[FileMeta] @@ -15,13 +17,19 @@ trait FilesMetaRepository { directoryUuid: UUID ): Seq[FileMeta] - def getFileMeta( ownerUuid: UUID, uuid: UUID ): FileMeta + def getFileMeta( uuid: UUID ): FileMeta def searchFileInDirectory( ownerUuid: UUID, directoryUuid: Option[UUID], fileName: String ): Option[FileMeta] + + def isFileDirectlySharedWithUser( + fileUuid: UUID, + userUuid: UUID + ): Boolean + // --- Update --- def updateFileStatus( uuid: UUID, ready: Boolean ): Unit diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 69c9422..8ad2f9e 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -129,16 +129,14 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { directoryUuid: UUID ): Seq[FileMeta] = ??? - override def getFileMeta( ownerUuid: UUID, uuid: UUID ): FileMeta = { + override def getFileMeta( uuid: UUID ): FileMeta = { val connection: Connection = pool.getConnection() try { val statement = connection.prepareStatement( - "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name FROM files WHERE owner_uuid = ? AND uuid = ?" + "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name FROM files WHERE uuid = ?" ) - - statement.setObject( 1, ownerUuid ) - statement.setObject( 2, uuid ) + statement.setObject( 1, uuid ) val result = statement.executeQuery() if (result.next()) { @@ -232,6 +230,53 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } + override def isFileDirectlySharedWithUser( + fileUuid: UUID, + userUuid: UUID + ): Boolean = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + "SELECT COUNT(*) FROM shared_files WHERE file_uuid = ? AND user_uuid = ?" + ) + + statement.setObject( 1, fileUuid ) + statement.setObject( 2, userUuid ) + + val result = statement.executeQuery() + + if (result.next()) { + result.getInt( 1 ) > 0 + } else { + false + } + } catch { + case _: Exception => false + } finally { + connection.close() + } + } + + override def shareFile( fileUUID: UUID, userUUID: UUID ): Unit = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + "INSERT INTO shared_files (file_uuid, user_uuid) VALUES (?, ?)" + ) + + statement.setObject( 1, fileUUID ) + statement.setObject( 2, userUUID ) + + statement.executeUpdate() + } catch { + case exception: Exception => throw exception + } finally { + connection.close() + } + } + override def updateFileStatus( uuid: UUID, ready: Boolean ): Unit = ??? override def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit = ??? diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 2e07e0e..ace0422 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -11,6 +11,7 @@ import files_metadata.domain.DomainExceptions import files_metadata.domain.FileMeta import files_metadata.domain.FilesMetaRepository import files_metadata.infrastructure.requests.CreationReqSchema +import files_metadata.infrastructure.requests.ShareReqSchema import shared.infrastructure.CommonValidator import ujson.Obj import upickle.default.read @@ -132,4 +133,91 @@ class MetadataControllers { } } + + def ShareFileController( + request: cask.Request, + ownerUUID: String, + fileUUID: String + ): cask.Response[Obj] = { + try { + val decoded: ShareReqSchema = read[ShareReqSchema]( + request.text() + ) + + val isOwnerUUIDValid = CommonValidator.validateUUID( ownerUUID ) + val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) + + val validationRule: Validator[ShareReqSchema] = + ShareReqSchema.shareSchemaValidator + val validationResult = validate[ShareReqSchema]( decoded )( + validationRule + ) + + if (!isOwnerUUIDValid || !isFileUUIDValid || validationResult.isFailure) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + useCases.shareFile( + ownerUUID = UUID.fromString( ownerUUID ), + fileUUID = UUID.fromString( fileUUID ), + otherUserUUID = UUID.fromString( decoded.otherUserUUID ) + ) + + cask.Response( + None, + statusCode = 204 + ) + } catch { + case _: upickle.core.AbortException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "JSON payload wasn't valid" + ), + statusCode = 400 + ) + + case _: DomainExceptions.FileNotFoundException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "The file wasn't found" + ), + statusCode = 404 + ) + + case _: DomainExceptions.FileNotOwnedException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "The user does not own the file" + ), + statusCode = 403 + ) + + case _: DomainExceptions.FileAlreadySharedException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "The file is already shared with the given user" + ), + statusCode = 409 + ) + + case e: Exception => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "There was an error while sharing the file" + ), + statusCode = 500 + ) + } + } } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index 49312cc..abef270 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -16,5 +16,14 @@ case class MetadataRoutes() extends cask.Routes { controllers.SaveMetadataController( request ) } + @cask.post( s"${ basePath }/share/:ownerUUID/:fileUUID" ) + def ShareMetadataHandler( + request: cask.Request, + ownerUUID: String, + fileUUID: String + ): cask.Response[Obj] = { + controllers.ShareFileController( request, ownerUUID, fileUUID ) + } + initialize() } diff --git a/src/main/scala/files_metadata/infrastructure/requests/ShareReqSchema.scala b/src/main/scala/files_metadata/infrastructure/requests/ShareReqSchema.scala new file mode 100644 index 0000000..eaa5126 --- /dev/null +++ b/src/main/scala/files_metadata/infrastructure/requests/ShareReqSchema.scala @@ -0,0 +1,28 @@ +package org.hawksatlanta.metadata +package files_metadata.infrastructure.requests + +import com.wix.accord.dsl._ +import com.wix.accord.Validator +import shared.infrastructure.CommonValidator + +case class ShareReqSchema( + otherUserUUID: String +) + +object ShareReqSchema { + // --- Automatic JSON (de)serialization --- + import upickle.default._ + + implicit def rw: ReadWriter[ShareReqSchema] = + macroRW[ShareReqSchema] + + // --- Validation --- + val shareSchemaValidator: Validator[ShareReqSchema] = + validator[ShareReqSchema] { request => + { + request.otherUserUUID should matchRegex( + CommonValidator.uuidRegex + ) + } + } +} diff --git a/src/test/scala/files_metadata/SaveFileMetadataTests.scala b/src/test/scala/files_metadata/SaveFileMetadataTests.scala index 09b527f..d6061d9 100644 --- a/src/test/scala/files_metadata/SaveFileMetadataTests.scala +++ b/src/test/scala/files_metadata/SaveFileMetadataTests.scala @@ -11,14 +11,14 @@ import org.junit.Test import org.scalatestplus.junit.JUnitSuite import shared.infrastructure.CommonValidator -object TestData { +object SaveFileTestsData { val API_PREFIX: String = "/api/v1/files" val USER_UUID: UUID = UUID.randomUUID() private var filePayload: util.HashMap[String, Any] = _ - var directoryUUID: UUID = _ + var savedDirectoryUUID: UUID = _ - def getFilePayload(): util.HashMap[String, Any] = { + def getFilePayloadCopy(): util.HashMap[String, Any] = { if (filePayload == null) { filePayload = new util.HashMap[String, Any] filePayload.put( "userUUID", USER_UUID.toString ) @@ -28,7 +28,7 @@ object TestData { "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" ) filePayload.put( "fileType", "archive" ) - filePayload.put( "fileName", "project.txt" ) + filePayload.put( "fileName", "save.txt" ) filePayload.put( "fileSize", 150 ) } @@ -36,31 +36,23 @@ object TestData { } def setDirectoryUUID( uuid: UUID ): Unit = { - directoryUUID = uuid + savedDirectoryUUID = uuid } } @OrderWith( classOf[Alphanumeric] ) class SaveFileMetadataTests extends JUnitSuite { - // Setup routes and perform migrations - @Test - def T0_setup(): Unit = { - Main.main( Array() ) - } - @Test // POST /api/v1/files/:user_uuid Success: Save file metadata def T1_SaveArchiveMetadataSuccess(): Unit = { - // --- Request --- val response = `given`() .port( 8080 ) - .body( TestData.getFilePayload() ) + .body( SaveFileTestsData.getFilePayloadCopy() ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }" ) + .post( s"${ SaveFileTestsData.API_PREFIX }" ) val responseJSON = response.jsonPath() - // --- Assertions --- assert( response.statusCode() == 201 ) assert( !responseJSON.getBoolean( "error" ) ) assert( @@ -76,55 +68,48 @@ class SaveFileMetadataTests extends JUnitSuite { @Test // POST /api/v1/files/:user_uuid Success: Save directory metadata def T2_SaveDirectoryMetadataSuccess(): Unit = { - // --- Test data --- val payload = new util.HashMap[String, Any]() - payload.put( "userUUID", TestData.USER_UUID.toString ) + payload.put( "userUUID", SaveFileTestsData.USER_UUID.toString ) payload.put( "parentUUID", null ) payload.put( "hashSum", "" ) payload.put( "fileType", "directory" ) payload.put( "fileName", "project" ) payload.put( "fileSize", 0 ) - // --- Request --- val response = `given`() .port( 8080 ) .body( payload ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }" ) + .post( s"${ SaveFileTestsData.API_PREFIX }" ) val responseJSON = response.jsonPath() - // --- Assertions --- assert( response.statusCode() == 201 ) assert( !responseJSON.getBoolean( "error" ) ) assert( responseJSON.getString( "message" ) == "Metadata was saved successfully" ) - // --- Save the directory UUID for the next test --- val directoryUUID = responseJSON.getString( "uuid" ) assert( CommonValidator.validateUUID( directoryUUID ) ) - TestData.setDirectoryUUID( UUID.fromString( directoryUUID ) ) + SaveFileTestsData.setDirectoryUUID( UUID.fromString( directoryUUID ) ) } @Test /* POST /api/v1/files/:user_uuid Success: Save file metadata with parent * directory */ def T3_SaveArchiveMetadataWithParentSuccess(): Unit = { - // --- Test data --- - val payload = TestData.getFilePayload() - payload.put( "parentUUID", TestData.directoryUUID.toString ) + val payload = SaveFileTestsData.getFilePayloadCopy() + payload.put( "parentUUID", SaveFileTestsData.savedDirectoryUUID.toString ) - // --- Request --- val response = `given`() .port( 8080 ) .body( payload ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }" ) + .post( s"${ SaveFileTestsData.API_PREFIX }" ) val responseJSON = response.jsonPath() - // --- Assertions --- assert( response.statusCode() == 201 ) assert( !responseJSON.getBoolean( "error" ) ) assert( @@ -140,16 +125,14 @@ class SaveFileMetadataTests extends JUnitSuite { @Test // POST /api/v1/files/:user_uuid Conflict: File already exists def T4_SaveArchiveMetadataConflict(): Unit = { - // --- Request --- val response = `given`() .port( 8080 ) - .body( TestData.getFilePayload() ) + .body( SaveFileTestsData.getFilePayloadCopy() ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }" ) + .post( s"${ SaveFileTestsData.API_PREFIX }" ) val responseJSON = response.jsonPath() - // --- Assertions --- assert( response.statusCode() == 409 ) assert( responseJSON.getBoolean( "error" ) ) assert( @@ -163,20 +146,17 @@ class SaveFileMetadataTests extends JUnitSuite { /* POST /api/v1/files/:user_uuid Conflict: File in parent directory already * exists */ def T5_SaveArchiveMetadataWithParentConflict(): Unit = { - // --- Test data --- - val payload = TestData.getFilePayload() - payload.put( "parentUUID", TestData.directoryUUID.toString ) + val payload = SaveFileTestsData.getFilePayloadCopy() + payload.put( "parentUUID", SaveFileTestsData.savedDirectoryUUID.toString ) - // --- Request --- val response = `given`() .port( 8080 ) .body( payload ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }" ) + .post( s"${ SaveFileTestsData.API_PREFIX }" ) val responseJSON = response.jsonPath() - // --- Assertions --- assert( response.statusCode() == 409 ) assert( responseJSON.getBoolean( "error" ) ) assert( @@ -189,20 +169,17 @@ class SaveFileMetadataTests extends JUnitSuite { @Test // POST /api/v1/files/:user_uuid Not found: Parent directory does not exist def T6_SaveArchiveMetadataWithParentNotFound(): Unit = { - // --- Test data --- - val payload = TestData.getFilePayload() + val payload = SaveFileTestsData.getFilePayloadCopy() payload.put( "parentUUID", UUID.randomUUID().toString ) - // --- Request --- val response = `given`() .port( 8080 ) .body( payload ) .contentType( "application/json" ) .when() - .post( s"${ TestData.API_PREFIX }" ) + .post( s"${ SaveFileTestsData.API_PREFIX }" ) val responseJSON = response.jsonPath() - // --- Assertions --- assert( response.statusCode() == 404 ) assert( responseJSON.getBoolean( "error" ) ) assert( diff --git a/src/test/scala/files_metadata/ShareFileTests.scala b/src/test/scala/files_metadata/ShareFileTests.scala new file mode 100644 index 0000000..8690c1e --- /dev/null +++ b/src/test/scala/files_metadata/ShareFileTests.scala @@ -0,0 +1,212 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util.UUID + +import io.restassured.RestAssured.`given` +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +object ShareFileTestsData { + val API_PREFIX: String = "/api/v1/files/share" + val OWNER_USER_UUID: UUID = UUID.randomUUID() + val OTHER_USER_UUID: UUID = UUID.randomUUID() + + private var sharePayload: java.util.HashMap[String, Any] = _ + var savedDirectoryUUID: UUID = _ + var savedFileUUID: UUID = _ + + def getSharePayload(): java.util.HashMap[String, Any] = { + if (sharePayload == null) { + sharePayload = new java.util.HashMap[String, Any] + sharePayload.put( "otherUserUUID", OTHER_USER_UUID.toString ) + } + + sharePayload.clone().asInstanceOf[java.util.HashMap[String, Any]] + } +} + +@OrderWith( classOf[Alphanumeric] ) +class ShareFileTests extends JUnitSuite { + def saveFilesToShare(): Unit = { + // Save a file to share + val saveFilePayload = new java.util.HashMap[String, Any]() + saveFilePayload.put( + "userUUID", + ShareFileTestsData.OWNER_USER_UUID.toString + ) + saveFilePayload.put( "parentUUID", null ) + saveFilePayload.put( + "hashSum", + "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" + ) + saveFilePayload.put( "fileType", "archive" ) + saveFilePayload.put( "fileName", "share.txt" ) + saveFilePayload.put( "fileSize", 150 ) + + val saveFileResponse = `given`() + .port( 8080 ) + .body( saveFilePayload ) + .contentType( "application/json" ) + .when() + .post( s"${ SaveFileTestsData.API_PREFIX }" ) + + ShareFileTestsData.savedFileUUID = + UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) + + // Save a directory to share + val saveDirectoryPayload = new java.util.HashMap[String, Any]() + saveDirectoryPayload.put( + "userUUID", + ShareFileTestsData.OWNER_USER_UUID.toString + ) + saveDirectoryPayload.put( "parentUUID", null ) + saveDirectoryPayload.put( "hashSum", "" ) + saveDirectoryPayload.put( "fileType", "directory" ) + saveDirectoryPayload.put( "fileName", "share" ) + saveDirectoryPayload.put( "fileSize", 0 ) + + val saveDirectoryResponse = `given`() + .port( 8080 ) + .body( saveDirectoryPayload ) + .contentType( "application/json" ) + .when() + .post( s"${ SaveFileTestsData.API_PREFIX }" ) + + ShareFileTestsData.savedDirectoryUUID = + UUID.fromString( saveDirectoryResponse.jsonPath().get( "uuid" ) ) + } + + @Test + def T0_setup(): Unit = { + // Setup routes and perform migrations + Main.main( Array() ) + } + + @Test + // POST /api/v1/files/share/:user_uuid/:file_uuid Bad request + def T1_ShareFileBadRequest(): Unit = { + // 1. Bad otherUserUUID + val requestBody = ShareFileTestsData.getSharePayload() + requestBody.put( "otherUserUUID", "Not an UUID" ) + + val response = `given`() + .port( 8080 ) + .body( requestBody ) + .contentType( "application/json" ) + .when() + .post( + s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/${ ShareFileTestsData.savedFileUUID }" + ) + + assert( response.statusCode() == 400 ) + assert( response.jsonPath().getBoolean( "error" ) ) + + // 2. Bad ownerUserUUID + val response2 = `given`() + .port( 8080 ) + .body( ShareFileTestsData.getSharePayload() ) + .contentType( "application/json" ) + .when() + .post( + s"${ ShareFileTestsData.API_PREFIX }/NotAnUUID/${ ShareFileTestsData.savedFileUUID }" + ) + + assert( response2.statusCode() == 400 ) + assert( response2.jsonPath().getBoolean( "error" ) ) + + // 3. Bad fileUUID + val response3 = `given`() + .port( 8080 ) + .body( ShareFileTestsData.getSharePayload() ) + .contentType( "application/json" ) + .when() + .post( + s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/NotAnUUID" + ) + + assert( response3.statusCode() == 400 ) + assert( response3.jsonPath().getBoolean( "error" ) ) + } + + @Test + // POST /api/v1/files/share/:user_uuid/:file_uuid Success: Share file + def T2_ShareFileSuccess(): Unit = { + saveFilesToShare() + + // Share the file + val response = `given`() + .port( 8080 ) + .body( ShareFileTestsData.getSharePayload() ) + .contentType( "application/json" ) + .when() + .post( + s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/${ ShareFileTestsData.savedFileUUID }" + ) + + assert( response.statusCode() == 204 ) + + // Share the directory + val directoryResponse = `given`() + .port( 8080 ) + .body( ShareFileTestsData.getSharePayload() ) + .contentType( "application/json" ) + .when() + .post( + s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/${ ShareFileTestsData.savedDirectoryUUID }" + ) + + assert( directoryResponse.statusCode() == 204 ) + } + + @Test + // POST /api/v1/files/share/:user_uuid/:file_uuid Not found + def T3_ShareFileNotFound(): Unit = { + val response = `given`() + .port( 8080 ) + .body( ShareFileTestsData.getSharePayload() ) + .contentType( "application/json" ) + .when() + .post( + s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/${ UUID.randomUUID() }" + ) + + assert( response.statusCode() == 404 ) + assert( response.jsonPath().getBoolean( "error" ) ) + } + + @Test + // POST /api/v1/files/share/:user_uuid/:file_uuid Conflict + def T4_ShareFileConflict(): Unit = { + // Share the same file as in "T2" again + val response = `given`() + .port( 8080 ) + .body( ShareFileTestsData.getSharePayload() ) + .contentType( "application/json" ) + .when() + .post( + s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/${ ShareFileTestsData.savedFileUUID }" + ) + + assert( response.statusCode() == 409 ) + assert( response.jsonPath().getBoolean( "error" ) ) + } + + @Test + // POST /api/v1/files/share/:user_uuid/:file_uuid Forbidden + def T5_ShareFileForbidden(): Unit = { + val response = `given`() + .port( 8080 ) + .body( ShareFileTestsData.getSharePayload() ) + .contentType( "application/json" ) + .when() + .post( + s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OTHER_USER_UUID }/${ ShareFileTestsData.savedFileUUID }" + ) + + assert( response.statusCode() == 403 ) + assert( response.jsonPath().getBoolean( "error" ) ) + } +} From 093ba2e2c8750cd36a4b1f71a880c3529d19bee5 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Mon, 11 Sep 2023 16:41:20 +0000 Subject: [PATCH 24/67] chore(release): v0.3.0 [skip ci] --- CHANGELOG.md | 9 +++++++++ version.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd9a2a..1fd1e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [0.3.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.2.1...v0.3.0) (2023-09-11) + + +### Features + +* Share files ([#48](https://github.com/hawks-atlanta/metadata-scala/issues/48)) ([1c2e8ea](https://github.com/hawks-atlanta/metadata-scala/commit/1c2e8ea772c7c0ae51e17ab143f7f581cace8f34)) + + + ## [0.2.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.2.0...v0.2.1) (2023-09-11) diff --git a/version.json b/version.json index 4bd29bc..085e3ba 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.2.1" + "version": "0.3.0" } \ No newline at end of file From e1577c26ede19cf8160fc828ec4ef45bea2663ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:55:38 -0500 Subject: [PATCH 25/67] feat: Can read endpoint (#49) * feat: Endpoint to check if an user can read a file * refactor(tests): Create shared function to save metadata * refactor(tests): Create shared function to share files * test: Can read endpoint * docs: Add rest example for the new endpoint * chore: Remove clean step in the coverage report script --- Makefile | 2 +- db/migrations/V1.1.0__can_read_procedure.sql | 34 +++ docs/rest/can_read.http | 3 + .../application/FilesMetaUseCases.scala | 9 + .../domain/FilesMetaRepository.scala | 2 + .../FilesMetaPostgresRepository.scala | 15 ++ .../infrastructure/MetadataControllers.scala | 56 ++++- .../infrastructure/MetadataRoutes.scala | 9 + .../files_metadata/CanReadFileTests.scala | 213 ++++++++++++++++++ .../files_metadata/FilesTestsUtils.scala | 33 +++ .../SaveFileMetadataTests.scala | 46 +--- .../scala/files_metadata/ShareFileTests.scala | 141 ++++-------- 12 files changed, 430 insertions(+), 133 deletions(-) create mode 100644 db/migrations/V1.1.0__can_read_procedure.sql create mode 100644 docs/rest/can_read.http create mode 100644 src/test/scala/files_metadata/CanReadFileTests.scala create mode 100644 src/test/scala/files_metadata/FilesTestsUtils.scala diff --git a/Makefile b/Makefile index 7478661..e86b747 100644 --- a/Makefile +++ b/Makefile @@ -26,4 +26,4 @@ remove: rm -rf $$package_name; coverage: - sbt clean coverage test coverageReport \ No newline at end of file + sbt coverage test coverageReport \ No newline at end of file diff --git a/db/migrations/V1.1.0__can_read_procedure.sql b/db/migrations/V1.1.0__can_read_procedure.sql new file mode 100644 index 0000000..1b0f0f4 --- /dev/null +++ b/db/migrations/V1.1.0__can_read_procedure.sql @@ -0,0 +1,34 @@ +CREATE OR REPLACE FUNCTION can_read(user_uuid_arg UUID, file_uuid_arg UUID) + RETURNS BOOLEAN + LANGUAGE PLPGSQL + AS $$ +DECLARE + folder_parent_uuid UUID; + is_shared BOOLEAN; +BEGIN + -- Check if the file was directly shared with the user + SELECT COUNT(uuid) > 0 + INTO is_shared + FROM shared_files + WHERE + shared_files.file_uuid = file_uuid_arg AND + shared_files.user_uuid = user_uuid_arg; + + IF is_shared THEN + RETURN TRUE; + END IF; + + -- Check if the file is contained in a directory shared with the user + SELECT files.parent_uuid + INTO folder_parent_uuid + FROM files + WHERE + files.uuid = file_uuid_arg; + + IF folder_parent_uuid IS NULL THEN + RETURN FALSE; + ELSE + RETURN can_read(user_uuid_arg, folder_parent_uuid); + END IF; +END $$ +; \ No newline at end of file diff --git a/docs/rest/can_read.http b/docs/rest/can_read.http new file mode 100644 index 0000000..a9e4d50 --- /dev/null +++ b/docs/rest/can_read.http @@ -0,0 +1,3 @@ +### Check if an user can read a file (archive or directory) + +GET http://localhost:8080/api/v1/files/can_read/{userUUID}/{fileUUID} HTTP/1.1 diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index 3f360b2..f33ae97 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -65,4 +65,13 @@ class FilesMetaUseCases { repository.shareFile( fileUUID, otherUserUUID ) } + + def canReadFile( + userUUID: UUID, + fileUUID: UUID + ): Boolean = { + val fileMeta = repository.getFileMeta( fileUUID ) + if (fileMeta.ownerUuid == userUUID) return true + repository.canUserReadFile( userUUID, fileUUID ) + } } diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index 002edf4..049ccf7 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -30,6 +30,8 @@ trait FilesMetaRepository { userUuid: UUID ): Boolean + def canUserReadFile( userUuid: UUID, fileUuid: UUID ): Boolean + // --- Update --- def updateFileStatus( uuid: UUID, ready: Boolean ): Unit diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 8ad2f9e..9776656 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -277,6 +277,21 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } + override def canUserReadFile( userUuid: UUID, fileUuid: UUID ): Boolean = { + val connection: Connection = pool.getConnection() + + val statement = connection.prepareStatement( "SELECT can_read(?, ?)" ) + statement.setObject( 1, userUuid ) + statement.setObject( 2, fileUuid ) + + val result = statement.executeQuery() + if (result.next()) { + result.getBoolean( 1 ) + } else { + false + } + } + override def updateFileStatus( uuid: UUID, ready: Boolean ): Unit = ??? override def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit = ??? diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index ace0422..7d7cc62 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -152,7 +152,6 @@ class MetadataControllers { val validationResult = validate[ShareReqSchema]( decoded )( validationRule ) - if (!isOwnerUUIDValid || !isFileUUIDValid || validationResult.isFailure) { return cask.Response( ujson.Obj( @@ -220,4 +219,59 @@ class MetadataControllers { ) } } + + def CanReadFileController( + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { + try { + val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) + val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) + if (!isUserUUIDValid || !isFileUUIDValid) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + val canRead = useCases.canReadFile( + userUUID = UUID.fromString( userUUID ), + fileUUID = UUID.fromString( fileUUID ) + ) + + if (!canRead) { + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "The user can't read the file" + ), + statusCode = 403 + ) + } else { + cask.Response( None, statusCode = 204 ) + } + } catch { + case _: DomainExceptions.FileNotFoundException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "The file wasn't found" + ), + statusCode = 404 + ) + + case _: Exception => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "There was an error while checking if the user can read the file" + ), + statusCode = 500 + ) + } + } } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index abef270..d0600c8 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -25,5 +25,14 @@ case class MetadataRoutes() extends cask.Routes { controllers.ShareFileController( request, ownerUUID, fileUUID ) } + @cask.get( s"${ basePath }/can_read/:userUUID/:fileUUID" ) + def CanReadMetadataHandler( + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { + controllers.CanReadFileController( request, userUUID, fileUUID ) + } + initialize() } diff --git a/src/test/scala/files_metadata/CanReadFileTests.scala b/src/test/scala/files_metadata/CanReadFileTests.scala new file mode 100644 index 0000000..2a59a40 --- /dev/null +++ b/src/test/scala/files_metadata/CanReadFileTests.scala @@ -0,0 +1,213 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util.UUID + +import io.restassured.RestAssured.`given` +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +object CanReadFileTestsData { + val API_PREFIX: String = "/api/v1/files/can_read" + val OWNER_USER_UUID: UUID = UUID.randomUUID() + val OTHER_USER_UUID: UUID = UUID.randomUUID() + + var savedFirstLevelDirectoryUUID: UUID = _ + var savedSecondLevelDirectoryUUID: UUID = _ + var savedThirdLevelDirectoryUUID: UUID = _ + + var directlySharedFileUUID: UUID = _ + var indirectlySharedFileUUID: UUID = _ + var unsharedFileUUID: UUID = _ +} + +@OrderWith( classOf[Alphanumeric] ) +class CanReadFileTests extends JUnitSuite { + def saveAndShareFilesToCheck(): Unit = { + // Save the directories + val thirdLvlDirectoryPayload = new java.util.HashMap[String, Any]() + thirdLvlDirectoryPayload.put( + "userUUID", + CanReadFileTestsData.OWNER_USER_UUID.toString + ) + thirdLvlDirectoryPayload.put( "parentUUID", null ) + thirdLvlDirectoryPayload.put( "hashSum", "" ) + thirdLvlDirectoryPayload.put( "fileType", "directory" ) + thirdLvlDirectoryPayload.put( "fileName", "Third Level Directory" ) + thirdLvlDirectoryPayload.put( "fileSize", 0 ) + + val thirdLvlDirectoryResponse = + FilesTestsUtils.SaveFile( thirdLvlDirectoryPayload ) + + CanReadFileTestsData.savedThirdLevelDirectoryUUID = UUID.fromString( + thirdLvlDirectoryResponse.jsonPath().get( "uuid" ) + ) + + val secondLvlDirectoryPayload = new java.util.HashMap[String, Any]() + secondLvlDirectoryPayload.put( + "userUUID", + CanReadFileTestsData.OWNER_USER_UUID.toString + ) + secondLvlDirectoryPayload.put( + "parentUUID", + CanReadFileTestsData.savedThirdLevelDirectoryUUID.toString + ) + secondLvlDirectoryPayload.put( "hashSum", "" ) + secondLvlDirectoryPayload.put( "fileType", "directory" ) + secondLvlDirectoryPayload.put( "fileName", "Second Level Directory" ) + secondLvlDirectoryPayload.put( "fileSize", 0 ) + + val secondLvlDirectoryResponse = + FilesTestsUtils.SaveFile( secondLvlDirectoryPayload ) + + CanReadFileTestsData.savedSecondLevelDirectoryUUID = UUID.fromString( + secondLvlDirectoryResponse.jsonPath().get( "uuid" ) + ) + + val firstLvlDirectoryPayload = new java.util.HashMap[String, Any]() + firstLvlDirectoryPayload.put( + "userUUID", + CanReadFileTestsData.OWNER_USER_UUID.toString + ) + firstLvlDirectoryPayload.put( + "parentUUID", + CanReadFileTestsData.savedSecondLevelDirectoryUUID.toString + ) + firstLvlDirectoryPayload.put( "hashSum", "" ) + firstLvlDirectoryPayload.put( "fileType", "directory" ) + firstLvlDirectoryPayload.put( "fileName", "First Level Directory" ) + firstLvlDirectoryPayload.put( "fileSize", 0 ) + + val firstLvlDirectoryResponse = + FilesTestsUtils.SaveFile( firstLvlDirectoryPayload ) + + CanReadFileTestsData.savedFirstLevelDirectoryUUID = UUID.fromString( + firstLvlDirectoryResponse.jsonPath().get( "uuid" ) + ) + + // Save the first file + val saveFilePayload = new java.util.HashMap[String, Any]() + saveFilePayload.put( + "userUUID", + CanReadFileTestsData.OWNER_USER_UUID.toString + ) + saveFilePayload.put( + "parentUUID", + CanReadFileTestsData.savedFirstLevelDirectoryUUID.toString + ) + saveFilePayload.put( + "hashSum", + "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" + ) + saveFilePayload.put( "fileType", "archive" ) + saveFilePayload.put( "fileName", "indirectly_shared.txt" ) + saveFilePayload.put( "fileSize", 150 ) + + val saveFileResponse = FilesTestsUtils.SaveFile( saveFilePayload ) + CanReadFileTestsData.indirectlySharedFileUUID = + UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) + + // Save the second file + saveFilePayload.put( "parentUUID", null ) + saveFilePayload.put( "fileName", "directly_shared.txt" ) + + val saveFileResponse2 = FilesTestsUtils.SaveFile( saveFilePayload ) + CanReadFileTestsData.directlySharedFileUUID = + UUID.fromString( saveFileResponse2.jsonPath().get( "uuid" ) ) + + // Save the third file + saveFilePayload.put( "fileName", "unshared.txt" ) + + val saveFileResponse3 = FilesTestsUtils.SaveFile( saveFilePayload ) + CanReadFileTestsData.unsharedFileUUID = + UUID.fromString( saveFileResponse3.jsonPath().get( "uuid" ) ) + + // Share the third level directory + val sharePayload = new java.util.HashMap[String, Any]() + sharePayload.put( + "otherUserUUID", + CanReadFileTestsData.OTHER_USER_UUID.toString + ) + FilesTestsUtils.ShareFile( + ownerUUID = CanReadFileTestsData.OWNER_USER_UUID.toString, + fileUUID = CanReadFileTestsData.savedThirdLevelDirectoryUUID.toString, + payload = sharePayload + ) + + // Share the directly shared file + FilesTestsUtils.ShareFile( + ownerUUID = CanReadFileTestsData.OWNER_USER_UUID.toString, + fileUUID = CanReadFileTestsData.directlySharedFileUUID.toString, + payload = sharePayload + ) + } + + @Test + def T0_setup(): Unit = { + // Setup routes and perform migrations + Main.main( Array() ) + } + + @Test + // GET /api/v1/files/can_read/:userUUID/:fileUUID Success + def T1_OwnersCanReadTheirFiles(): Unit = { + saveAndShareFilesToCheck() + + val firstFileResponse = `given`() + .port( 8080 ) + .when() + .get( + s"${ CanReadFileTestsData.API_PREFIX }/${ CanReadFileTestsData.OWNER_USER_UUID.toString }/${ CanReadFileTestsData.indirectlySharedFileUUID.toString }" + ) + assert( firstFileResponse.statusCode() == 204 ) + + val secondFileResponse = `given`() + .port( 8080 ) + .when() + .get( + s"${ CanReadFileTestsData.API_PREFIX }/${ CanReadFileTestsData.OWNER_USER_UUID.toString }/${ CanReadFileTestsData.directlySharedFileUUID.toString }" + ) + assert( secondFileResponse.statusCode() == 204 ) + + val thirdFileResponse = `given`() + .port( 8080 ) + .when() + .get( + s"${ CanReadFileTestsData.API_PREFIX }/${ CanReadFileTestsData.OWNER_USER_UUID.toString }/${ CanReadFileTestsData.unsharedFileUUID.toString }" + ) + } + + @Test + // GET /api/v1/files/can_read/:userUUID/:fileUUID Success + def T2_SharedUsersCanReadFiles(): Unit = { + val directlySharedFileResponse = `given`() + .port( 8080 ) + .when() + .get( + s"${ CanReadFileTestsData.API_PREFIX }/${ CanReadFileTestsData.OTHER_USER_UUID.toString }/${ CanReadFileTestsData.directlySharedFileUUID.toString }" + ) + assert( directlySharedFileResponse.statusCode() == 204 ) + + val indirectlySharedFileResponse = `given`() + .port( 8080 ) + .when() + .get( + s"${ CanReadFileTestsData.API_PREFIX }/${ CanReadFileTestsData.OTHER_USER_UUID.toString }/${ CanReadFileTestsData.indirectlySharedFileUUID.toString }" + ) + assert( indirectlySharedFileResponse.statusCode() == 204 ) + } + + @Test + // GET /api/v1/files/can_read/:userUUID/:fileUUID Forbidden + def T3_UnsharedUsersCannotReadFiles(): Unit = { + val unsharedFileResponse = `given`() + .port( 8080 ) + .when() + .get( + s"${ CanReadFileTestsData.API_PREFIX }/${ CanReadFileTestsData.OTHER_USER_UUID.toString }/${ CanReadFileTestsData.unsharedFileUUID.toString }" + ) + assert( unsharedFileResponse.statusCode() == 403 ) + } +} diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala new file mode 100644 index 0000000..312d252 --- /dev/null +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -0,0 +1,33 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util + +import io.restassured.response.Response +import io.restassured.RestAssured.`given` + +object FilesTestsUtils { + def SaveFile( payload: util.HashMap[String, Any] ): Response = { + `given`() + .port( 8080 ) + .contentType( "application/json" ) + .body( payload ) + .when() + .post( s"${ SaveFileTestsData.API_PREFIX }" ) + } + + def ShareFile( + ownerUUID: String, + fileUUID: String, + payload: util.HashMap[String, Any] + ): Response = { + `given`() + .port( 8080 ) + .contentType( "application/json" ) + .body( payload ) + .when() + .post( + s"${ ShareFileTestsData.API_PREFIX }/${ ownerUUID }/${ fileUUID }" + ) + } +} diff --git a/src/test/scala/files_metadata/SaveFileMetadataTests.scala b/src/test/scala/files_metadata/SaveFileMetadataTests.scala index d6061d9..2d448ee 100644 --- a/src/test/scala/files_metadata/SaveFileMetadataTests.scala +++ b/src/test/scala/files_metadata/SaveFileMetadataTests.scala @@ -45,12 +45,9 @@ class SaveFileMetadataTests extends JUnitSuite { @Test // POST /api/v1/files/:user_uuid Success: Save file metadata def T1_SaveArchiveMetadataSuccess(): Unit = { - val response = `given`() - .port( 8080 ) - .body( SaveFileTestsData.getFilePayloadCopy() ) - .contentType( "application/json" ) - .when() - .post( s"${ SaveFileTestsData.API_PREFIX }" ) + val response = FilesTestsUtils.SaveFile( + SaveFileTestsData.getFilePayloadCopy() + ) val responseJSON = response.jsonPath() assert( response.statusCode() == 201 ) @@ -76,12 +73,7 @@ class SaveFileMetadataTests extends JUnitSuite { payload.put( "fileName", "project" ) payload.put( "fileSize", 0 ) - val response = `given`() - .port( 8080 ) - .body( payload ) - .contentType( "application/json" ) - .when() - .post( s"${ SaveFileTestsData.API_PREFIX }" ) + val response = FilesTestsUtils.SaveFile( payload ) val responseJSON = response.jsonPath() assert( response.statusCode() == 201 ) @@ -102,12 +94,7 @@ class SaveFileMetadataTests extends JUnitSuite { val payload = SaveFileTestsData.getFilePayloadCopy() payload.put( "parentUUID", SaveFileTestsData.savedDirectoryUUID.toString ) - val response = `given`() - .port( 8080 ) - .body( payload ) - .contentType( "application/json" ) - .when() - .post( s"${ SaveFileTestsData.API_PREFIX }" ) + val response = FilesTestsUtils.SaveFile( payload ) val responseJSON = response.jsonPath() assert( response.statusCode() == 201 ) @@ -125,12 +112,9 @@ class SaveFileMetadataTests extends JUnitSuite { @Test // POST /api/v1/files/:user_uuid Conflict: File already exists def T4_SaveArchiveMetadataConflict(): Unit = { - val response = `given`() - .port( 8080 ) - .body( SaveFileTestsData.getFilePayloadCopy() ) - .contentType( "application/json" ) - .when() - .post( s"${ SaveFileTestsData.API_PREFIX }" ) + val response = FilesTestsUtils.SaveFile( + SaveFileTestsData.getFilePayloadCopy() + ) val responseJSON = response.jsonPath() assert( response.statusCode() == 409 ) @@ -149,12 +133,7 @@ class SaveFileMetadataTests extends JUnitSuite { val payload = SaveFileTestsData.getFilePayloadCopy() payload.put( "parentUUID", SaveFileTestsData.savedDirectoryUUID.toString ) - val response = `given`() - .port( 8080 ) - .body( payload ) - .contentType( "application/json" ) - .when() - .post( s"${ SaveFileTestsData.API_PREFIX }" ) + val response = FilesTestsUtils.SaveFile( payload ) val responseJSON = response.jsonPath() assert( response.statusCode() == 409 ) @@ -172,12 +151,7 @@ class SaveFileMetadataTests extends JUnitSuite { val payload = SaveFileTestsData.getFilePayloadCopy() payload.put( "parentUUID", UUID.randomUUID().toString ) - val response = `given`() - .port( 8080 ) - .body( payload ) - .contentType( "application/json" ) - .when() - .post( s"${ SaveFileTestsData.API_PREFIX }" ) + val response = FilesTestsUtils.SaveFile( payload ) val responseJSON = response.jsonPath() assert( response.statusCode() == 404 ) diff --git a/src/test/scala/files_metadata/ShareFileTests.scala b/src/test/scala/files_metadata/ShareFileTests.scala index 8690c1e..1724f9f 100644 --- a/src/test/scala/files_metadata/ShareFileTests.scala +++ b/src/test/scala/files_metadata/ShareFileTests.scala @@ -3,7 +3,6 @@ package files_metadata import java.util.UUID -import io.restassured.RestAssured.`given` import org.junit.runner.manipulation.Alphanumeric import org.junit.runner.OrderWith import org.junit.Test @@ -46,13 +45,7 @@ class ShareFileTests extends JUnitSuite { saveFilePayload.put( "fileName", "share.txt" ) saveFilePayload.put( "fileSize", 150 ) - val saveFileResponse = `given`() - .port( 8080 ) - .body( saveFilePayload ) - .contentType( "application/json" ) - .when() - .post( s"${ SaveFileTestsData.API_PREFIX }" ) - + val saveFileResponse = FilesTestsUtils.SaveFile( saveFilePayload ) ShareFileTestsData.savedFileUUID = UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) @@ -68,65 +61,44 @@ class ShareFileTests extends JUnitSuite { saveDirectoryPayload.put( "fileName", "share" ) saveDirectoryPayload.put( "fileSize", 0 ) - val saveDirectoryResponse = `given`() - .port( 8080 ) - .body( saveDirectoryPayload ) - .contentType( "application/json" ) - .when() - .post( s"${ SaveFileTestsData.API_PREFIX }" ) - + val saveDirectoryResponse = + FilesTestsUtils.SaveFile( saveDirectoryPayload ) ShareFileTestsData.savedDirectoryUUID = UUID.fromString( saveDirectoryResponse.jsonPath().get( "uuid" ) ) } - @Test - def T0_setup(): Unit = { - // Setup routes and perform migrations - Main.main( Array() ) - } - @Test // POST /api/v1/files/share/:user_uuid/:file_uuid Bad request def T1_ShareFileBadRequest(): Unit = { + saveFilesToShare() + // 1. Bad otherUserUUID val requestBody = ShareFileTestsData.getSharePayload() requestBody.put( "otherUserUUID", "Not an UUID" ) - val response = `given`() - .port( 8080 ) - .body( requestBody ) - .contentType( "application/json" ) - .when() - .post( - s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/${ ShareFileTestsData.savedFileUUID }" - ) - + val response = FilesTestsUtils.ShareFile( + ownerUUID = ShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = ShareFileTestsData.savedFileUUID.toString, + payload = requestBody + ) assert( response.statusCode() == 400 ) assert( response.jsonPath().getBoolean( "error" ) ) // 2. Bad ownerUserUUID - val response2 = `given`() - .port( 8080 ) - .body( ShareFileTestsData.getSharePayload() ) - .contentType( "application/json" ) - .when() - .post( - s"${ ShareFileTestsData.API_PREFIX }/NotAnUUID/${ ShareFileTestsData.savedFileUUID }" - ) - + val response2 = FilesTestsUtils.ShareFile( + ownerUUID = "Not an UUID", + fileUUID = ShareFileTestsData.savedFileUUID.toString, + payload = ShareFileTestsData.getSharePayload() + ) assert( response2.statusCode() == 400 ) assert( response2.jsonPath().getBoolean( "error" ) ) // 3. Bad fileUUID - val response3 = `given`() - .port( 8080 ) - .body( ShareFileTestsData.getSharePayload() ) - .contentType( "application/json" ) - .when() - .post( - s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/NotAnUUID" - ) - + val response3 = FilesTestsUtils.ShareFile( + ownerUUID = ShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = "Not an UUID", + payload = ShareFileTestsData.getSharePayload() + ) assert( response3.statusCode() == 400 ) assert( response3.jsonPath().getBoolean( "error" ) ) } @@ -134,29 +106,20 @@ class ShareFileTests extends JUnitSuite { @Test // POST /api/v1/files/share/:user_uuid/:file_uuid Success: Share file def T2_ShareFileSuccess(): Unit = { - saveFilesToShare() - // Share the file - val response = `given`() - .port( 8080 ) - .body( ShareFileTestsData.getSharePayload() ) - .contentType( "application/json" ) - .when() - .post( - s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/${ ShareFileTestsData.savedFileUUID }" - ) - - assert( response.statusCode() == 204 ) + val fileResponse = FilesTestsUtils.ShareFile( + ownerUUID = ShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = ShareFileTestsData.savedFileUUID.toString, + payload = ShareFileTestsData.getSharePayload() + ) + assert( fileResponse.statusCode() == 204 ) // Share the directory - val directoryResponse = `given`() - .port( 8080 ) - .body( ShareFileTestsData.getSharePayload() ) - .contentType( "application/json" ) - .when() - .post( - s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/${ ShareFileTestsData.savedDirectoryUUID }" - ) + val directoryResponse = FilesTestsUtils.ShareFile( + ownerUUID = ShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = ShareFileTestsData.savedDirectoryUUID.toString, + payload = ShareFileTestsData.getSharePayload() + ) assert( directoryResponse.statusCode() == 204 ) } @@ -164,15 +127,11 @@ class ShareFileTests extends JUnitSuite { @Test // POST /api/v1/files/share/:user_uuid/:file_uuid Not found def T3_ShareFileNotFound(): Unit = { - val response = `given`() - .port( 8080 ) - .body( ShareFileTestsData.getSharePayload() ) - .contentType( "application/json" ) - .when() - .post( - s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/${ UUID.randomUUID() }" - ) - + val response = FilesTestsUtils.ShareFile( + ownerUUID = ShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = UUID.randomUUID().toString, + payload = ShareFileTestsData.getSharePayload() + ) assert( response.statusCode() == 404 ) assert( response.jsonPath().getBoolean( "error" ) ) } @@ -181,15 +140,11 @@ class ShareFileTests extends JUnitSuite { // POST /api/v1/files/share/:user_uuid/:file_uuid Conflict def T4_ShareFileConflict(): Unit = { // Share the same file as in "T2" again - val response = `given`() - .port( 8080 ) - .body( ShareFileTestsData.getSharePayload() ) - .contentType( "application/json" ) - .when() - .post( - s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OWNER_USER_UUID }/${ ShareFileTestsData.savedFileUUID }" - ) - + val response = FilesTestsUtils.ShareFile( + ownerUUID = ShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = ShareFileTestsData.savedFileUUID.toString, + payload = ShareFileTestsData.getSharePayload() + ) assert( response.statusCode() == 409 ) assert( response.jsonPath().getBoolean( "error" ) ) } @@ -197,15 +152,11 @@ class ShareFileTests extends JUnitSuite { @Test // POST /api/v1/files/share/:user_uuid/:file_uuid Forbidden def T5_ShareFileForbidden(): Unit = { - val response = `given`() - .port( 8080 ) - .body( ShareFileTestsData.getSharePayload() ) - .contentType( "application/json" ) - .when() - .post( - s"${ ShareFileTestsData.API_PREFIX }/${ ShareFileTestsData.OTHER_USER_UUID }/${ ShareFileTestsData.savedFileUUID }" - ) - + val response = FilesTestsUtils.ShareFile( + ownerUUID = ShareFileTestsData.OTHER_USER_UUID.toString, + fileUUID = ShareFileTestsData.savedFileUUID.toString, + payload = ShareFileTestsData.getSharePayload() + ) assert( response.statusCode() == 403 ) assert( response.jsonPath().getBoolean( "error" ) ) } From b4b35f96535326a0645a001b1eba8c36c0ef45ea Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Mon, 11 Sep 2023 18:55:55 +0000 Subject: [PATCH 26/67] chore(release): v0.4.0 [skip ci] --- CHANGELOG.md | 9 +++++++++ version.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fd1e4d..cf408e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [0.4.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.3.0...v0.4.0) (2023-09-11) + + +### Features + +* Can read endpoint ([#49](https://github.com/hawks-atlanta/metadata-scala/issues/49)) ([e1577c2](https://github.com/hawks-atlanta/metadata-scala/commit/e1577c26ede19cf8160fc828ec4ef45bea2663ab)) + + + # [0.3.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.2.1...v0.3.0) (2023-09-11) diff --git a/version.json b/version.json index 085e3ba..39a8c86 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.3.0" + "version": "0.4.0" } \ No newline at end of file From 2ef830f54893a15f81b7756e2c580a2a49e9c36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Mon, 11 Sep 2023 15:33:16 -0500 Subject: [PATCH 27/67] refactor(tests): Global setup of HTTP server (#51) --- src/test/scala/files_metadata/CanReadFileTests.scala | 8 ++++---- src/test/scala/files_metadata/FilesTestsUtils.scala | 11 +++++++++++ .../scala/files_metadata/SaveFileMetadataTests.scala | 7 ++++++- src/test/scala/files_metadata/ShareFileTests.scala | 6 ++++++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/test/scala/files_metadata/CanReadFileTests.scala b/src/test/scala/files_metadata/CanReadFileTests.scala index 2a59a40..e13dc87 100644 --- a/src/test/scala/files_metadata/CanReadFileTests.scala +++ b/src/test/scala/files_metadata/CanReadFileTests.scala @@ -6,6 +6,7 @@ import java.util.UUID import io.restassured.RestAssured.`given` import org.junit.runner.manipulation.Alphanumeric import org.junit.runner.OrderWith +import org.junit.Before import org.junit.Test import org.scalatestplus.junit.JUnitSuite @@ -144,10 +145,9 @@ class CanReadFileTests extends JUnitSuite { ) } - @Test - def T0_setup(): Unit = { - // Setup routes and perform migrations - Main.main( Array() ) + @Before + def startHttpServer(): Unit = { + FilesTestsUtils.StartHttpServer() } @Test diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index 312d252..3382d1b 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -2,11 +2,22 @@ package org.hawksatlanta.metadata package files_metadata import java.util +import java.util.concurrent.atomic.AtomicBoolean import io.restassured.response.Response import io.restassured.RestAssured.`given` object FilesTestsUtils { + var wasHttpServerInitializationCalled: AtomicBoolean = new AtomicBoolean( + false + ) + + def StartHttpServer(): Unit = { + if (wasHttpServerInitializationCalled.compareAndSet( false, true )) { + Main.main( Array[String]() ) + } + } + def SaveFile( payload: util.HashMap[String, Any] ): Response = { `given`() .port( 8080 ) diff --git a/src/test/scala/files_metadata/SaveFileMetadataTests.scala b/src/test/scala/files_metadata/SaveFileMetadataTests.scala index 2d448ee..2a880f0 100644 --- a/src/test/scala/files_metadata/SaveFileMetadataTests.scala +++ b/src/test/scala/files_metadata/SaveFileMetadataTests.scala @@ -4,9 +4,9 @@ package files_metadata import java.util import java.util.UUID -import io.restassured.RestAssured.`given` import org.junit.runner.manipulation.Alphanumeric import org.junit.runner.OrderWith +import org.junit.Before import org.junit.Test import org.scalatestplus.junit.JUnitSuite import shared.infrastructure.CommonValidator @@ -42,6 +42,11 @@ object SaveFileTestsData { @OrderWith( classOf[Alphanumeric] ) class SaveFileMetadataTests extends JUnitSuite { + @Before + def startHttpServer(): Unit = { + FilesTestsUtils.StartHttpServer() + } + @Test // POST /api/v1/files/:user_uuid Success: Save file metadata def T1_SaveArchiveMetadataSuccess(): Unit = { diff --git a/src/test/scala/files_metadata/ShareFileTests.scala b/src/test/scala/files_metadata/ShareFileTests.scala index 1724f9f..e5cb4ef 100644 --- a/src/test/scala/files_metadata/ShareFileTests.scala +++ b/src/test/scala/files_metadata/ShareFileTests.scala @@ -5,6 +5,7 @@ import java.util.UUID import org.junit.runner.manipulation.Alphanumeric import org.junit.runner.OrderWith +import org.junit.Before import org.junit.Test import org.scalatestplus.junit.JUnitSuite @@ -67,6 +68,11 @@ class ShareFileTests extends JUnitSuite { UUID.fromString( saveDirectoryResponse.jsonPath().get( "uuid" ) ) } + @Before + def startHttpServer(): Unit = { + FilesTestsUtils.StartHttpServer() + } + @Test // POST /api/v1/files/share/:user_uuid/:file_uuid Bad request def T1_ShareFileBadRequest(): Unit = { From f66a70a8669be258bfdc714c45cc1f82eef16f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:46:39 -0500 Subject: [PATCH 28/67] feat: Mark files as ready (#52) * feat: Endpoint to mark an archive as ready * refactor: Create base domain exception class * chore: Remove message field validation in tests * docs(openapi): Update /ready endpoint spec --- docker-compose.yaml | 5 +- docs/spec.openapi.yaml | 10 ++ .../application/FilesMetaUseCases.scala | 20 +++ .../domain/DomainExceptions.scala | 41 ++++-- .../domain/FilesMetaRepository.scala | 8 +- .../FilesMetaPostgresRepository.scala | 98 ++++++++++++- .../infrastructure/MetadataControllers.scala | 129 ++++++++++++------ .../infrastructure/MetadataRoutes.scala | 8 ++ .../requests/MarkAsReadyReqSchema.scala | 20 +++ .../SaveFileMetadataTests.scala | 24 ---- 10 files changed, 282 insertions(+), 81 deletions(-) create mode 100644 src/main/scala/files_metadata/infrastructure/requests/MarkAsReadyReqSchema.scala diff --git a/docker-compose.yaml b/docker-compose.yaml index d413afa..921158a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,7 +15,7 @@ services: - ./volumes/postgres:/var/lib/postgresql/data postgres-admin: - image: dpage/pgadmin4 + image: dpage/pgadmin4:snapshot container_name: postgres-admin ports: - "5050:80" @@ -23,4 +23,5 @@ services: - PGADMIN_DEFAULT_EMAIL=postgres@postgres.com - PGADMIN_DEFAULT_PASSWORD=postgres depends_on: - - postgres-db \ No newline at end of file + - postgres-db + restart: on-failure \ No newline at end of file diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml index 426c179..cda31ee 100644 --- a/docs/spec.openapi.yaml +++ b/docs/spec.openapi.yaml @@ -380,6 +380,16 @@ paths: type: string example: 'b96bdc16-8f27-44aa-9758-b4e5f13060fe' required: true + requestBody: + content: + application/json: + schema: + type: object + properties: + volume: + type: string + description: The volume in which the file was saved + example: "volume_1" responses: '204': description: No content. The metadata of the file was updated. diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index f33ae97..2f08e67 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -74,4 +74,24 @@ class FilesMetaUseCases { if (fileMeta.ownerUuid == userUUID) return true repository.canUserReadFile( userUUID, fileUUID ) } + + def updateSavedFile( fileUUID: UUID, volume: String ): Unit = { + val fileMetadata = repository.getFileMeta( fileUUID ) + + // If the file is an archive, update the archive status + if (fileMetadata.archiveUuid.isDefined) { + val archiveMetadata = + repository.getArchiveMeta( fileMetadata.archiveUuid.get ) + + if (archiveMetadata.ready || fileMetadata.volume != null) + throw DomainExceptions.FileAlreadyMarkedAsReadyException( + "The file was already marked as ready" + ) + + repository.updateArchiveStatus( archiveMetadata.uuid, ready = true ) + } + + // Update the file volume + repository.updateFileVolume( fileUUID, volume ) + } } diff --git a/src/main/scala/files_metadata/domain/DomainExceptions.scala b/src/main/scala/files_metadata/domain/DomainExceptions.scala index 696cd3c..037aaef 100644 --- a/src/main/scala/files_metadata/domain/DomainExceptions.scala +++ b/src/main/scala/files_metadata/domain/DomainExceptions.scala @@ -1,22 +1,39 @@ package org.hawksatlanta.metadata package files_metadata.domain +abstract class BaseDomainException extends Exception { + private var _statusCode: Int = _ + private var _message: String = _ + + def this( message: String, statusCode: Int ) { + this() + _statusCode = statusCode + _message = message + } + + def statusCode: Int = _statusCode + def message: String = _message +} + object DomainExceptions { - case class FileNotFoundException( message: String ) - extends Exception( message ) + case class FileNotFoundException( override val message: String ) + extends BaseDomainException( message, 404 ) + + case class FileAlreadyExistsException( override val message: String ) + extends BaseDomainException( message, 409 ) - case class FileAlreadyExistsException( message: String ) - extends Exception( message ) + case class FileNotOwnedException( override val message: String ) + extends BaseDomainException( message, 403 ) - case class FileNotOwnedException( message: String ) - extends Exception( message ) + case class ArchiveNotSavedException( override val message: String ) + extends BaseDomainException( message, 500 ) - case class ArchiveNotSavedException( message: String ) - extends Exception( message ) + case class FileNotSavedException( override val message: String ) + extends BaseDomainException( message, 500 ) - case class FileNotSavedException( message: String ) - extends Exception( message ) + case class FileAlreadySharedException( override val message: String ) + extends BaseDomainException( message, 409 ) - case class FileAlreadySharedException( message: String ) - extends Exception( message ) + case class FileAlreadyMarkedAsReadyException( override val message: String ) + extends BaseDomainException( message, 409 ) } diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index 049ccf7..ef1c2f7 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -19,6 +19,10 @@ trait FilesMetaRepository { def getFileMeta( uuid: UUID ): FileMeta + def getArchiveMeta( uuid: UUID ): ArchivesMeta + + def getFileMetaByArchiveUuid( archiveUuid: UUID ): FileMeta + def searchFileInDirectory( ownerUuid: UUID, directoryUuid: Option[UUID], @@ -33,7 +37,9 @@ trait FilesMetaRepository { def canUserReadFile( userUuid: UUID, fileUuid: UUID ): Boolean // --- Update --- - def updateFileStatus( uuid: UUID, ready: Boolean ): Unit + def updateArchiveStatus( archiveUUID: UUID, ready: Boolean ): Unit + + def updateFileVolume( fileUUID: UUID, volume: String ): Unit // --- Delete --- def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 9776656..31bda68 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -170,6 +170,74 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } + override def getArchiveMeta( uuid: UUID ): ArchivesMeta = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + "SELECT uuid, hash_sum, size, ready FROM archives WHERE uuid = ?" + ) + statement.setObject( 1, uuid ) + + val result = statement.executeQuery() + if (!result.next()) { + throw DomainExceptions.FileNotFoundException( + "There is no archive with the given UUID" + ) + } + + ArchivesMeta( + uuid = UUID.fromString( result.getString( "uuid" ) ), + hashSum = result.getString( "hash_sum" ), + size = result.getLong( "size" ), + ready = result.getBoolean( "ready" ) + ) + } finally { + connection.close() + } + } + + override def getFileMetaByArchiveUuid( archiveUuid: UUID ): FileMeta = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name FROM files WHERE archive_uuid = ?" + ) + statement.setObject( 1, archiveUuid ) + + val result = statement.executeQuery() + if (result.next()) { + val parentUUIDString = result.getString( "parent_uuid" ) + val archiveUUIDString = result.getString( "archive_uuid" ) + + val parentUUID = + if (parentUUIDString == null) None + else Some( UUID.fromString( parentUUIDString ) ) + val archiveUUID = + if (archiveUUIDString == null) None + else Some( UUID.fromString( archiveUUIDString ) ) + + FileMeta( + uuid = UUID.fromString( result.getString( "uuid" ) ), + ownerUuid = UUID.fromString( result.getString( "owner_uuid" ) ), + parentUuid = parentUUID, + archiveUuid = archiveUUID, + volume = result.getString( "volume" ), + name = result.getString( "name" ) + ) + } else { + throw DomainExceptions.FileNotFoundException( + "There is no file with the given archive UUID" + ) + } + } catch { + case exception: Exception => throw exception + } finally { + connection.close() + } + } + override def searchFileInDirectory( ownerUuid: UUID, directoryUuid: Option[UUID], @@ -292,7 +360,35 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } - override def updateFileStatus( uuid: UUID, ready: Boolean ): Unit = ??? + override def updateArchiveStatus( + archiveUUID: UUID, + ready: Boolean + ): Unit = { + val connection: Connection = pool.getConnection() + + val statement = connection.prepareStatement( + "UPDATE archives SET ready = ? WHERE uuid = ?" + ) + statement.setBoolean( 1, ready ) + statement.setObject( 2, archiveUUID ) + + statement.executeUpdate() + } + + def updateFileVolume( + fileUUID: UUID, + volume: String + ): Unit = { + val connection: Connection = pool.getConnection() + + val statement = connection.prepareStatement( + "UPDATE files SET volume = ? WHERE uuid = ?" + ) + statement.setString( 1, volume ) + statement.setObject( 2, fileUUID ) + + statement.executeUpdate() + } override def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit = ??? } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 7d7cc62..09280a6 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -7,10 +7,12 @@ import com.wix.accord.validate import com.wix.accord.Validator import files_metadata.application.FilesMetaUseCases import files_metadata.domain.ArchivesMeta +import files_metadata.domain.BaseDomainException import files_metadata.domain.DomainExceptions import files_metadata.domain.FileMeta import files_metadata.domain.FilesMetaRepository import files_metadata.infrastructure.requests.CreationReqSchema +import files_metadata.infrastructure.requests.MarkAsReadyReqSchema import files_metadata.infrastructure.requests.ShareReqSchema import shared.infrastructure.CommonValidator import ujson.Obj @@ -93,35 +95,24 @@ class MetadataControllers { statusCode = 201 ) } catch { - // Unable to parse the given JSON payload case _: upickle.core.AbortException => cask.Response( ujson.Obj( "error" -> true, - "message" -> "JSON payload wasn't valid" + "message" -> "Unable to decode JSON body" ), statusCode = 400 ) - case conflict: DomainExceptions.FileAlreadyExistsException => + case e: BaseDomainException => cask.Response( ujson.Obj( "error" -> true, - "message" -> conflict.getMessage() + "message" -> e.message ), - statusCode = 409 + statusCode = e.statusCode ) - case parentDirectoryNotFound: DomainExceptions.FileNotFoundException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> parentDirectoryNotFound.getMessage() - ), - statusCode = 404 - ) - - // Any other error case _: Exception => cask.Response( ujson.Obj( @@ -177,39 +168,21 @@ class MetadataControllers { cask.Response( ujson.Obj( "error" -> true, - "message" -> "JSON payload wasn't valid" + "message" -> "Unable to decode JSON body" ), statusCode = 400 ) - case _: DomainExceptions.FileNotFoundException => + case e: BaseDomainException => cask.Response( ujson.Obj( "error" -> true, - "message" -> "The file wasn't found" + "message" -> e.message ), - statusCode = 404 + statusCode = e.statusCode ) - case _: DomainExceptions.FileNotOwnedException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "The user does not own the file" - ), - statusCode = 403 - ) - - case _: DomainExceptions.FileAlreadySharedException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "The file is already shared with the given user" - ), - statusCode = 409 - ) - - case e: Exception => + case _: Exception => cask.Response( ujson.Obj( "error" -> true, @@ -255,13 +228,13 @@ class MetadataControllers { cask.Response( None, statusCode = 204 ) } } catch { - case _: DomainExceptions.FileNotFoundException => + case e: BaseDomainException => cask.Response( ujson.Obj( "error" -> true, - "message" -> "The file wasn't found" + "message" -> e.message ), - statusCode = 404 + statusCode = e.statusCode ) case _: Exception => @@ -274,4 +247,78 @@ class MetadataControllers { ) } } + + def MarkFileAsReadyController( + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { + try { + val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) + if (!isFileUUIDValid) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + val decoded: MarkAsReadyReqSchema = read[MarkAsReadyReqSchema]( + request.text() + ) + + val validationRule: Validator[MarkAsReadyReqSchema] = + MarkAsReadyReqSchema.schemaValidator + val validationResult = validate[MarkAsReadyReqSchema]( decoded )( + validationRule + ) + if (validationResult.isFailure) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + useCases.updateSavedFile( + fileUUID = UUID.fromString( fileUUID ), + volume = decoded.volume + ) + + cask.Response( + None, + statusCode = 204 + ) + } catch { + case _: upickle.core.AbortException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Unable to decode JSON body" + ), + statusCode = 400 + ) + + case e: BaseDomainException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> e.message + ), + statusCode = e.statusCode + ) + + case e: Exception => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "There was an error while marking the file as ready" + ), + statusCode = 500 + ) + } + } } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index d0600c8..6111262 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -34,5 +34,13 @@ case class MetadataRoutes() extends cask.Routes { controllers.CanReadFileController( request, userUUID, fileUUID ) } + @cask.put( s"${ basePath }/ready/:fileUUID" ) + def ReadyMetadataHandler( + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { + controllers.MarkFileAsReadyController( request, fileUUID ) + } + initialize() } diff --git a/src/main/scala/files_metadata/infrastructure/requests/MarkAsReadyReqSchema.scala b/src/main/scala/files_metadata/infrastructure/requests/MarkAsReadyReqSchema.scala new file mode 100644 index 0000000..8b51a4e --- /dev/null +++ b/src/main/scala/files_metadata/infrastructure/requests/MarkAsReadyReqSchema.scala @@ -0,0 +1,20 @@ +package org.hawksatlanta.metadata +package files_metadata.infrastructure.requests + +import com.wix.accord.dsl._ +import com.wix.accord.Validator + +case class MarkAsReadyReqSchema( + volume: String +) + +object MarkAsReadyReqSchema { + import upickle.default._ + implicit def rw: ReadWriter[MarkAsReadyReqSchema] = + macroRW[MarkAsReadyReqSchema] + + val schemaValidator: Validator[MarkAsReadyReqSchema] = + validator[MarkAsReadyReqSchema] { req => + req.volume.is( notEmpty ) + } +} diff --git a/src/test/scala/files_metadata/SaveFileMetadataTests.scala b/src/test/scala/files_metadata/SaveFileMetadataTests.scala index 2a880f0..b21ed4b 100644 --- a/src/test/scala/files_metadata/SaveFileMetadataTests.scala +++ b/src/test/scala/files_metadata/SaveFileMetadataTests.scala @@ -57,9 +57,6 @@ class SaveFileMetadataTests extends JUnitSuite { assert( response.statusCode() == 201 ) assert( !responseJSON.getBoolean( "error" ) ) - assert( - responseJSON.getString( "message" ) == "Metadata was saved successfully" - ) assert( CommonValidator.validateUUID( responseJSON.getString( "uuid" ) @@ -83,9 +80,6 @@ class SaveFileMetadataTests extends JUnitSuite { assert( response.statusCode() == 201 ) assert( !responseJSON.getBoolean( "error" ) ) - assert( - responseJSON.getString( "message" ) == "Metadata was saved successfully" - ) val directoryUUID = responseJSON.getString( "uuid" ) assert( CommonValidator.validateUUID( directoryUUID ) ) @@ -104,9 +98,6 @@ class SaveFileMetadataTests extends JUnitSuite { assert( response.statusCode() == 201 ) assert( !responseJSON.getBoolean( "error" ) ) - assert( - responseJSON.getString( "message" ) == "Metadata was saved successfully" - ) assert( CommonValidator.validateUUID( responseJSON.getString( "uuid" ) @@ -124,11 +115,6 @@ class SaveFileMetadataTests extends JUnitSuite { assert( response.statusCode() == 409 ) assert( responseJSON.getBoolean( "error" ) ) - assert( - responseJSON.getString( - "message" - ) == "A file with the same name already exists in the given directory" - ) } @Test @@ -143,11 +129,6 @@ class SaveFileMetadataTests extends JUnitSuite { assert( response.statusCode() == 409 ) assert( responseJSON.getBoolean( "error" ) ) - assert( - responseJSON.getString( - "message" - ) == "A file with the same name already exists in the given directory" - ) } @Test @@ -161,10 +142,5 @@ class SaveFileMetadataTests extends JUnitSuite { assert( response.statusCode() == 404 ) assert( responseJSON.getBoolean( "error" ) ) - assert( - responseJSON.getString( - "message" - ) == "The user does not own a file or directory with the given UUID" - ) } } From 93a5befaf9caa98c5bb7b1c426f978d02a4f23e8 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Tue, 12 Sep 2023 19:46:51 +0000 Subject: [PATCH 29/67] chore(release): v0.5.0 [skip ci] --- CHANGELOG.md | 28 +++++++++------------------- version.json | 2 +- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf408e7..ab75be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [0.5.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.4.0...v0.5.0) (2023-09-12) + + +### Features + +* Mark files as ready ([#52](https://github.com/hawks-atlanta/metadata-scala/issues/52)) ([f66a70a](https://github.com/hawks-atlanta/metadata-scala/commit/f66a70a8669be258bfdc714c45cc1f82eef16f4f)) + + + # [0.4.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.3.0...v0.4.0) (2023-09-11) @@ -34,22 +43,3 @@ -# [0.1.0](https://github.com/hawks-atlanta/metadata-scala/compare/1da251d344ba2f8af61efa8a339716672abec56f...v0.1.0) (2023-09-07) - - -### Bug Fixes - -* **cd:** Build the Docker image without running tests ([#35](https://github.com/hawks-atlanta/metadata-scala/issues/35)) ([c1edb5d](https://github.com/hawks-atlanta/metadata-scala/commit/c1edb5d596dc573d467704d25ad93ee5f568b900)) -* **ci:** Coverage pipeline ([#16](https://github.com/hawks-atlanta/metadata-scala/issues/16)) ([973e936](https://github.com/hawks-atlanta/metadata-scala/commit/973e936759affd769f80b900d02924422e2de698)), closes [#7](https://github.com/hawks-atlanta/metadata-scala/issues/7) [#9](https://github.com/hawks-atlanta/metadata-scala/issues/9) [#10](https://github.com/hawks-atlanta/metadata-scala/issues/10) [#11](https://github.com/hawks-atlanta/metadata-scala/issues/11) [#15](https://github.com/hawks-atlanta/metadata-scala/issues/15) [#8](https://github.com/hawks-atlanta/metadata-scala/issues/8) [#7](https://github.com/hawks-atlanta/metadata-scala/issues/7) [#9](https://github.com/hawks-atlanta/metadata-scala/issues/9) [#10](https://github.com/hawks-atlanta/metadata-scala/issues/10) [#11](https://github.com/hawks-atlanta/metadata-scala/issues/11) -* **ci:** Fix coverage pipeline ([#14](https://github.com/hawks-atlanta/metadata-scala/issues/14)) ([013c956](https://github.com/hawks-atlanta/metadata-scala/commit/013c956ab386707a9db33f76a376dad6c1130cd2)) -* **ci:** Fix tagging pipeline ([#43](https://github.com/hawks-atlanta/metadata-scala/issues/43)) ([7530a2b](https://github.com/hawks-atlanta/metadata-scala/commit/7530a2bdd9ff3bb7b146b1e6e9e48b876ea830b5)) -* **ci:** Update test pipeline ([#8](https://github.com/hawks-atlanta/metadata-scala/issues/8)) ([1da251d](https://github.com/hawks-atlanta/metadata-scala/commit/1da251d344ba2f8af61efa8a339716672abec56f)) -* Trigger pipelines ([#13](https://github.com/hawks-atlanta/metadata-scala/issues/13)) ([791a672](https://github.com/hawks-atlanta/metadata-scala/commit/791a672b646753bb42a7aedaa20de30e44e05c1f)) - - -### Features - -* PostgreSQL connection and Migrations ([#20](https://github.com/hawks-atlanta/metadata-scala/issues/20)) ([b502749](https://github.com/hawks-atlanta/metadata-scala/commit/b502749d51d3149d585972f8d19bc6f4c19b7fbc)) - - - diff --git a/version.json b/version.json index 39a8c86..381a76d 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.4.0" + "version": "0.5.0" } \ No newline at end of file From 22542c6e66cd95bd27ec3e4f30079ea9f54bb03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:34:07 -0500 Subject: [PATCH 30/67] feat: Obtain file metadata (#53) * feat: Endpoint to get metadata of a given file * test: Add tests for the /ready endpoint * chore: Remove unused method * fix(db): Remove pool connections limit * test: Add tests for the /metadata endpoint --- .../application/FilesMetaUseCases.scala | 19 ++- .../domain/FilesMetaRepository.scala | 2 - .../FilesMetaPostgresRepository.scala | 43 +---- .../infrastructure/MetadataControllers.scala | 78 ++++++++- .../infrastructure/MetadataRoutes.scala | 8 + .../infrastructure/PostgreSQLPool.scala | 1 - .../files_metadata/FilesTestsUtils.scala | 23 +++ .../files_metadata/GetFileMetadataTests.scala | 139 ++++++++++++++++ .../files_metadata/UpdateReadyFile.scala | 148 ++++++++++++++++++ 9 files changed, 410 insertions(+), 51 deletions(-) create mode 100644 src/test/scala/files_metadata/GetFileMetadataTests.scala create mode 100644 src/test/scala/files_metadata/UpdateReadyFile.scala diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index 2f08e67..cefef30 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -78,20 +78,29 @@ class FilesMetaUseCases { def updateSavedFile( fileUUID: UUID, volume: String ): Unit = { val fileMetadata = repository.getFileMeta( fileUUID ) + if (fileMetadata.volume != null) { + throw DomainExceptions.FileAlreadyMarkedAsReadyException( + "The file was already marked as ready" + ) + } + // If the file is an archive, update the archive status if (fileMetadata.archiveUuid.isDefined) { val archiveMetadata = repository.getArchiveMeta( fileMetadata.archiveUuid.get ) - if (archiveMetadata.ready || fileMetadata.volume != null) - throw DomainExceptions.FileAlreadyMarkedAsReadyException( - "The file was already marked as ready" - ) - repository.updateArchiveStatus( archiveMetadata.uuid, ready = true ) } // Update the file volume repository.updateFileVolume( fileUUID, volume ) } + + def getFileMetadata( fileUUID: UUID ): FileMeta = { + repository.getFileMeta( fileUUID ) + } + + def getArchiveMetadata( archiveUUID: UUID ): ArchivesMeta = { + repository.getArchiveMeta( archiveUUID ) + } } diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index ef1c2f7..da8cf2a 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -21,8 +21,6 @@ trait FilesMetaRepository { def getArchiveMeta( uuid: UUID ): ArchivesMeta - def getFileMetaByArchiveUuid( archiveUuid: UUID ): FileMeta - def searchFileInDirectory( ownerUuid: UUID, directoryUuid: Option[UUID], diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 31bda68..4a7f170 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -160,7 +160,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { ) } else { throw DomainExceptions.FileNotFoundException( - "The user does not own a file or directory with the given UUID" + "There is no file with the given UUID" ) } } catch { @@ -197,47 +197,6 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } - override def getFileMetaByArchiveUuid( archiveUuid: UUID ): FileMeta = { - val connection: Connection = pool.getConnection() - - try { - val statement = connection.prepareStatement( - "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name FROM files WHERE archive_uuid = ?" - ) - statement.setObject( 1, archiveUuid ) - - val result = statement.executeQuery() - if (result.next()) { - val parentUUIDString = result.getString( "parent_uuid" ) - val archiveUUIDString = result.getString( "archive_uuid" ) - - val parentUUID = - if (parentUUIDString == null) None - else Some( UUID.fromString( parentUUIDString ) ) - val archiveUUID = - if (archiveUUIDString == null) None - else Some( UUID.fromString( archiveUUIDString ) ) - - FileMeta( - uuid = UUID.fromString( result.getString( "uuid" ) ), - ownerUuid = UUID.fromString( result.getString( "owner_uuid" ) ), - parentUuid = parentUUID, - archiveUuid = archiveUUID, - volume = result.getString( "volume" ), - name = result.getString( "name" ) - ) - } else { - throw DomainExceptions.FileNotFoundException( - "There is no file with the given archive UUID" - ) - } - } catch { - case exception: Exception => throw exception - } finally { - connection.close() - } - } - override def searchFileInDirectory( ownerUuid: UUID, directoryUuid: Option[UUID], diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 09280a6..638b6b0 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -8,7 +8,6 @@ import com.wix.accord.Validator import files_metadata.application.FilesMetaUseCases import files_metadata.domain.ArchivesMeta import files_metadata.domain.BaseDomainException -import files_metadata.domain.DomainExceptions import files_metadata.domain.FileMeta import files_metadata.domain.FilesMetaRepository import files_metadata.infrastructure.requests.CreationReqSchema @@ -248,6 +247,83 @@ class MetadataControllers { } } + def GetFileMetadataController( + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { + try { + val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) + if (!isFileUUIDValid) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + val fileMeta = useCases.getFileMetadata( + fileUUID = UUID.fromString( fileUUID ) + ) + + if (fileMeta.volume == null) { + return cask.Response( + ujson.Obj( + "message" -> "The file is not ready yet" + ), + statusCode = 202 + ) + } + + if (fileMeta.archiveUuid.isEmpty) { + // Directories metadata + cask.Response( + ujson.Obj( + "archiveUUID" -> ujson.Null, // Needs to be a "custom" null value + "volume" -> fileMeta.volume, + "size" -> 0, + "hashSum" -> "" + ), + statusCode = 200 + ) + } else { + // Archives metadata + val archivesMeta = useCases.getArchiveMetadata( + archiveUUID = fileMeta.archiveUuid.get + ) + + cask.Response( + ujson.Obj( + "archiveUUID" -> fileMeta.archiveUuid.get.toString, + "volume" -> fileMeta.volume, + "size" -> archivesMeta.size, + "hashSum" -> archivesMeta.hashSum + ), + statusCode = 200 + ) + } + } catch { + case e: BaseDomainException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> e.message + ), + statusCode = e.statusCode + ) + + case _: Exception => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "There was an error while getting the file metadata" + ), + statusCode = 500 + ) + } + } + def MarkFileAsReadyController( request: cask.Request, fileUUID: String diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index 6111262..8e35c66 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -42,5 +42,13 @@ case class MetadataRoutes() extends cask.Routes { controllers.MarkFileAsReadyController( request, fileUUID ) } + @cask.get( s"${ basePath }/metadata/:fileUUID" ) + def GetFileMetadataHandler( + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { + controllers.GetFileMetadataController( request, fileUUID ) + } + initialize() } diff --git a/src/main/scala/shared/infrastructure/PostgreSQLPool.scala b/src/main/scala/shared/infrastructure/PostgreSQLPool.scala index 2200d74..8f02852 100644 --- a/src/main/scala/shared/infrastructure/PostgreSQLPool.scala +++ b/src/main/scala/shared/infrastructure/PostgreSQLPool.scala @@ -16,7 +16,6 @@ object PostgreSQLPool { ) config.setUsername( Environment.dbUser ) config.setPassword( Environment.dbPassword ) - config.setMaximumPoolSize( 8 ) pool = new HikariDataSource( config ) } diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index 3382d1b..9e43077 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -41,4 +41,27 @@ object FilesTestsUtils { s"${ ShareFileTestsData.API_PREFIX }/${ ownerUUID }/${ fileUUID }" ) } + + def UpdateReadyFile( + fileUUID: String, + payload: util.HashMap[String, Any] + ): Response = { + `given`() + .port( 8080 ) + .contentType( "application/json" ) + .body( payload ) + .when() + .put( + s"${ UpdateReadyFileTestsData.API_PREFIX }/${ fileUUID }" + ) + } + + def GetFileMetadata( fileUUID: String ): Response = { + `given`() + .port( 8080 ) + .when() + .get( + s"${ GetFileMetadataTestsData.API_PREFIX }/${ fileUUID }" + ) + } } diff --git a/src/test/scala/files_metadata/GetFileMetadataTests.scala b/src/test/scala/files_metadata/GetFileMetadataTests.scala new file mode 100644 index 0000000..11e0a4e --- /dev/null +++ b/src/test/scala/files_metadata/GetFileMetadataTests.scala @@ -0,0 +1,139 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util.UUID + +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Before +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +object GetFileMetadataTestsData { + val API_PREFIX: String = "/api/v1/files/metadata" + val OWNER_UUID: UUID = UUID.randomUUID() + val VOLUME_NAME: String = "volume_x" + + var savedFileUUID: UUID = _ + var savedDirectoryUUID: UUID = _ +} +@OrderWith( classOf[Alphanumeric] ) +class GetFileMetadataTests extends JUnitSuite { + def saveFilesToBeObtained(): Unit = { + val filePayload = new java.util.HashMap[String, Any]() + filePayload.put( + "userUUID", + GetFileMetadataTestsData.OWNER_UUID.toString + ) + filePayload.put( "parentUUID", null ) + filePayload.put( + "hashSum", + "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" + ) + filePayload.put( "fileType", "archive" ) + filePayload.put( "fileName", "File to get metadata.txt" ) + filePayload.put( "fileSize", 15 ) + + val fileResponse = FilesTestsUtils.SaveFile( filePayload ) + GetFileMetadataTestsData.savedFileUUID = UUID.fromString( + fileResponse.body().jsonPath().getString( "uuid" ) + ) + + val directoryPayload = new java.util.HashMap[String, Any]() + directoryPayload.put( + "userUUID", + GetFileMetadataTestsData.OWNER_UUID.toString + ) + directoryPayload.put( "parentUUID", null ) + directoryPayload.put( "hashSum", "" ) + directoryPayload.put( "fileType", "directory" ) + directoryPayload.put( "fileName", "Directory to get metadata" ) + directoryPayload.put( "fileSize", 0 ) + + val directoryResponse = FilesTestsUtils.SaveFile( directoryPayload ) + GetFileMetadataTestsData.savedDirectoryUUID = UUID.fromString( + directoryResponse.body().jsonPath().getString( "uuid" ) + ) + } + + def markFilesAsReady(): Unit = { + val updatePayload = new java.util.HashMap[String, Any]() + updatePayload.put( "volume", GetFileMetadataTestsData.VOLUME_NAME ) + + FilesTestsUtils.UpdateReadyFile( + GetFileMetadataTestsData.savedFileUUID.toString, + updatePayload + ) + + FilesTestsUtils.UpdateReadyFile( + GetFileMetadataTestsData.savedDirectoryUUID.toString, + updatePayload + ) + } + + @Before + def startHttpServer(): Unit = { + FilesTestsUtils.StartHttpServer() + } + + @Test + // GET /api/v1/files/metadata/{fileUUID} Bad request + def fileMetadataWithInvalidUUID(): Unit = { + val response = FilesTestsUtils.GetFileMetadata( "not_an_uuid" ) + assert( response.statusCode() == 400 ) + } + + @Test + // GET /api/v1/files/metadata/{fileUUID} File not found + def fileMetadataWithNonExistentUUID(): Unit = { + val response = FilesTestsUtils.GetFileMetadata( UUID.randomUUID().toString ) + assert( response.statusCode() == 404 ) + } + + @Test + // GET /api/v1/files/metadata/{fileUUID} Non-ready file + def fileMetadataWithNonReadyFileUUID(): Unit = { + saveFilesToBeObtained() + val response = FilesTestsUtils.GetFileMetadata( + GetFileMetadataTestsData.savedFileUUID.toString + ) + assert( response.statusCode() == 202 ) + } + + @Test + // GET /api/v1/files/metadata/{fileUUID} Success + def fileMetadataWithReadyFileUUID(): Unit = { + markFilesAsReady() + + // Get the file + val response = FilesTestsUtils.GetFileMetadata( + GetFileMetadataTestsData.savedFileUUID.toString + ) + assert( response.statusCode() == 200 ) + assert( + response + .body() + .jsonPath() + .getString( "volume" ) == GetFileMetadataTestsData.VOLUME_NAME + ) + + // Get the directory + val directoryResponse = FilesTestsUtils.GetFileMetadata( + GetFileMetadataTestsData.savedDirectoryUUID.toString + ) + + assert( directoryResponse.statusCode() == 200 ) + assert( + directoryResponse + .body() + .jsonPath() + .getString( "volume" ) == GetFileMetadataTestsData.VOLUME_NAME + ) + assert( + directoryResponse + .body() + .jsonPath() + .getString( "archiveUUID" ) == null + ) + } +} diff --git a/src/test/scala/files_metadata/UpdateReadyFile.scala b/src/test/scala/files_metadata/UpdateReadyFile.scala new file mode 100644 index 0000000..4888aac --- /dev/null +++ b/src/test/scala/files_metadata/UpdateReadyFile.scala @@ -0,0 +1,148 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util.UUID + +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Before +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +object UpdateReadyFileTestsData { + val API_PREFIX: String = "/api/v1/files/ready" + val OWNER_UUID: UUID = UUID.randomUUID() + + var payload: java.util.HashMap[String, Any] = _ + var savedFileUUID: UUID = _ + var savedDirectoryUUID: UUID = _ + + def getPayloadCopy(): java.util.HashMap[String, Any] = { + if (payload == null) { + payload = new java.util.HashMap[String, Any]() + payload.put( "volume", "volume_x" ) + } + + payload.clone().asInstanceOf[java.util.HashMap[String, Any]] + } +} + +@OrderWith( classOf[Alphanumeric] ) +class UpdateReadyFile extends JUnitSuite { + def saveFilesToBeUpdated(): Unit = { + val filePayload = new java.util.HashMap[String, Any]() + filePayload.put( + "userUUID", + UpdateReadyFileTestsData.OWNER_UUID.toString + ) + filePayload.put( "parentUUID", null ) + filePayload.put( + "hashSum", + "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" + ) + filePayload.put( "fileType", "archive" ) + filePayload.put( "fileName", "File to mark as ready.txt" ) + filePayload.put( "fileSize", 15 ) + + val fileResponse = FilesTestsUtils.SaveFile( filePayload ) + UpdateReadyFileTestsData.savedFileUUID = UUID.fromString( + fileResponse.body().jsonPath().getString( "uuid" ) + ) + + val directoryPayload = new java.util.HashMap[String, Any]() + directoryPayload.put( + "userUUID", + UpdateReadyFileTestsData.OWNER_UUID.toString + ) + directoryPayload.put( "parentUUID", null ) + directoryPayload.put( "hashSum", "" ) + directoryPayload.put( "fileType", "directory" ) + directoryPayload.put( "fileName", "Directory to mark as ready" ) + directoryPayload.put( "fileSize", 0 ) + + val directoryResponse = FilesTestsUtils.SaveFile( directoryPayload ) + UpdateReadyFileTestsData.savedDirectoryUUID = UUID.fromString( + directoryResponse.body().jsonPath().getString( "uuid" ) + ) + } + @Before + def startHttpServer(): Unit = { + FilesTestsUtils.StartHttpServer() + } + + @Test + // PUT /api/v1/files/ready/{fileUUID} Bad Request + def T1_UpdateReadyFileBadRequest(): Unit = { + saveFilesToBeUpdated() + + // Bad fileUUID + val response = FilesTestsUtils.UpdateReadyFile( + "not_an_uuid", + UpdateReadyFileTestsData.getPayloadCopy() + ) + + assert( response.statusCode() == 400 ) + + // Bad payload + val payload = UpdateReadyFileTestsData.getPayloadCopy() + payload.put( "volume", "" ) + + val response2 = FilesTestsUtils.UpdateReadyFile( + UpdateReadyFileTestsData.savedFileUUID.toString, + payload + ) + + assert( response2.statusCode() == 400 ) + } + + @Test + // PUT /api/v1/files/ready/{fileUUID} Not Found + def T2_UpdateReadyFileNotFound(): Unit = { + val response = FilesTestsUtils.UpdateReadyFile( + UUID.randomUUID().toString, + UpdateReadyFileTestsData.getPayloadCopy() + ) + + assert( response.statusCode() == 404 ) + } + + @Test + // PUT /api/v1/files/ready/{fileUUID} Success + def T3_UpdateReadyFileSuccess(): Unit = { + // Update file + val updateFileResponse = FilesTestsUtils.UpdateReadyFile( + UpdateReadyFileTestsData.savedFileUUID.toString, + UpdateReadyFileTestsData.getPayloadCopy() + ) + + assert( updateFileResponse.statusCode() == 204 ) + + // Update directory + val updateDirectoryResponse = FilesTestsUtils.UpdateReadyFile( + UpdateReadyFileTestsData.savedDirectoryUUID.toString, + UpdateReadyFileTestsData.getPayloadCopy() + ) + + assert( updateDirectoryResponse.statusCode() == 204 ) + } + + @Test + // PUT /api/v1/files/read/{fileUUID} Conflict + def T4_UpdateReadyFileConflict(): Unit = { + // Try to mark the file as ready again + val updateFileResponse = FilesTestsUtils.UpdateReadyFile( + UpdateReadyFileTestsData.savedFileUUID.toString, + UpdateReadyFileTestsData.getPayloadCopy() + ) + + assert( updateFileResponse.statusCode() == 409 ) + + // Try to mark the directory as ready again + val updateDirectoryResponse = FilesTestsUtils.UpdateReadyFile( + UpdateReadyFileTestsData.savedDirectoryUUID.toString, + UpdateReadyFileTestsData.getPayloadCopy() + ) + + assert( updateDirectoryResponse.statusCode() == 409 ) + } +} From 8930e8e6287f7c7170b5ad1935ee80c33a06a306 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Tue, 12 Sep 2023 20:34:21 +0000 Subject: [PATCH 31/67] chore(release): v0.6.0 [skip ci] --- CHANGELOG.md | 18 +++++++++--------- version.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab75be6..a4fa2fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [0.6.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.5.0...v0.6.0) (2023-09-12) + + +### Features + +* Obtain file metadata ([#53](https://github.com/hawks-atlanta/metadata-scala/issues/53)) ([22542c6](https://github.com/hawks-atlanta/metadata-scala/commit/22542c6e66cd95bd27ec3e4f30079ea9f54bb03c)) + + + # [0.5.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.4.0...v0.5.0) (2023-09-12) @@ -34,12 +43,3 @@ -# [0.2.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.1.0...v0.2.0) (2023-09-10) - - -### Features - -* New files are unshared by default ([#44](https://github.com/hawks-atlanta/metadata-scala/issues/44)) ([a1140d5](https://github.com/hawks-atlanta/metadata-scala/commit/a1140d5c8767278defc5e767c3c3eb87271ba81b)) - - - diff --git a/version.json b/version.json index 381a76d..8284914 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.5.0" + "version": "0.6.0" } \ No newline at end of file From 2abf2f042dedf7dd6950ff9c9341081da6bee455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:45:29 -0500 Subject: [PATCH 32/67] docs(openapi): Update spec (#54) --- docs/spec.openapi.yaml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml index cda31ee..f730e79 100644 --- a/docs/spec.openapi.yaml +++ b/docs/spec.openapi.yaml @@ -187,8 +187,14 @@ paths: application/json: schema: $ref: '#/components/schemas/metadata' - '403': - description: Forbidden. The file is not owned by the user. + '202': + description: The file is not ready yet + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '400': + description: The file_uuid parameter was not a valid UUID. content: application/json: schema: @@ -399,6 +405,12 @@ paths: application/json: schema: $ref: '#/components/schemas/error_response' + '409': + description: The file was already marked as ready. + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' '500': description: Internal server error content: @@ -426,12 +438,18 @@ components: metadata: type: object properties: + archiveUUID: + type: string + example: '0b82495a-350b-4f4f-95ce-0119998466d4' volume: type: string example: 'VOLUME_1' - archiveUUID: + hashSum: type: string - example: '0b82495a-350b-4f4f-95ce-0119998466d4' + example: '56d50f755d5dbca915cf93779d3b51d6562e6183' + size: + type: number + example: 3072 file_creation_request: type: object From b7d1409e53d190eab35fe6bb6590c031ae7e5814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= Date: Tue, 12 Sep 2023 16:08:42 -0500 Subject: [PATCH 33/67] ci: Restore lint step --- .github/workflows/testing.yaml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index a85be98..37aeaa2 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -17,11 +17,25 @@ jobs: # Assemble the jar file without running tests - name: Clean and build - run: sbt "set test in assembly := {}" clean assembly + run: sbt "set assembly / test := {}" clean assembly + + lint: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + + - uses: coursier/cache-action@v6 + + - uses: coursier/setup-action@v1 + with: + jvm: adopt:1.11.0.2 + apps: scalafmt + + - name: Check format + run: scalafmt --check src/ test: runs-on: ubuntu-22.04 - needs: lint steps: - uses: actions/checkout@v3 @@ -34,14 +48,8 @@ jobs: - name: Setup docker environment run: docker-compose up -d - - name: Setup docker environment - run: docker-compose up -d - - name: Clean and test run: sbt clean test - name: Clean docker environment run: docker-compose down --rmi all -v --remove-orphans - - - name: Clean docker environment - run: docker-compose down --rmi all -v --remove-orphans From d09e2250fde387ba1ff4674daebbb7c0b4819ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= Date: Tue, 12 Sep 2023 16:11:21 -0500 Subject: [PATCH 34/67] chore: Remove old files --- db/migrations/V2__rename.sql | 1 - .../infrastructure/PostgreSQLRepository.scala | 50 ------------------- .../fruit/FruitPostgreSQLRepositoryTest.scala | 38 -------------- 3 files changed, 89 deletions(-) delete mode 100644 db/migrations/V2__rename.sql delete mode 100644 src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala delete mode 100644 src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala diff --git a/db/migrations/V2__rename.sql b/db/migrations/V2__rename.sql deleted file mode 100644 index ae8607d..0000000 --- a/db/migrations/V2__rename.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "FRUITS" RENAME TO fruits; \ No newline at end of file diff --git a/src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala b/src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala deleted file mode 100644 index a499425..0000000 --- a/src/main/scala/fruit/infrastructure/PostgreSQLRepository.scala +++ /dev/null @@ -1,50 +0,0 @@ -package org.hawksatlanta.metadata -package fruit.infrastructure - -import fruit.domain.{Fruit, Repository} -import shared.infrastructure.PostgreSQLPool - -import com.zaxxer.hikari.HikariDataSource - -class PostgreSQLRepository extends Repository{ - private val pool: HikariDataSource = PostgreSQLPool.getInstance() - - override def get_fruits(): List[Fruit] = { - val connection = pool.getConnection() - - try { - // Execute the query - val statement = connection.createStatement() - val resultSet = statement.executeQuery("SELECT id, name, price FROM fruits") - - // Parse into domain entity - var fruits: List[Fruit] = List() - - while (resultSet.next()) { - fruits = fruits :+ Fruit( - id = resultSet.getString("id"), - name = resultSet.getString("name"), - price = resultSet.getFloat("price") - ) - } - - // Return the resulting list - fruits - } finally { - connection.close() - } - } - - override def save(fruit: Fruit): Unit = { - val connection = pool.getConnection() - - try { - val statement = connection.prepareStatement("INSERT INTO fruits (name, price) VALUES (?, ?)") - statement.setString(1, fruit.name) - statement.setFloat(2, fruit.price) - statement.executeUpdate() - } finally { - connection.close() - } - } -} diff --git a/src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala b/src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala deleted file mode 100644 index 276238e..0000000 --- a/src/test/scala/fruit/FruitPostgreSQLRepositoryTest.scala +++ /dev/null @@ -1,38 +0,0 @@ -package org.hawksatlanta.metadata -package fruit - -import migrations.PostgreSQLMigration -import fruit.domain.Fruit -import fruit.application.UseCases -import fruit.infrastructure.PostgreSQLRepository - -import org.junit.Test -import org.junit.runner.OrderWith -import org.junit.runner.manipulation.Alphanumeric -import org.scalatestplus.junit.JUnitSuite - -@OrderWith(classOf[Alphanumeric]) -class FruitPostgreSQLRepositoryTest extends JUnitSuite { - val fruit_repository = new PostgreSQLRepository() - val use_cases = new UseCases(fruit_repository) - - @Test - def t1_migration(): Unit = { - val migrationResult = PostgreSQLMigration.migrate() - assert(migrationResult) - } - - @Test - def t2_create_fruit(): Unit = { - val fruit = Fruit("1", "Apple", 1.0f) - use_cases.create_fruit(fruit) - assert(fruit_repository.get_fruits().length === 1) - } - - @Test - def t3_list_fruits(): Unit = { - val fruit2 = Fruit("2", "Orange", 2.0f) - use_cases.create_fruit(fruit2) - assert(use_cases.get_fruits().length === 2) - } -} From 4111feacd98f88e19191312ae22cb29c4457b3a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:35:09 -0500 Subject: [PATCH 35/67] feat: List files shared with user (#56) * feat: Get files shared with user * tests: Add tests for the new endpoint --- docs/rest/get_metadata.http | 3 + docs/rest/get_metadata_shated_with_user.http | 3 + docs/rest/mark_as_ready.http | 8 ++ .../application/FilesMetaUseCases.scala | 4 + .../domain/FilesMetaRepository.scala | 2 + .../FilesMetaPostgresRepository.scala | 45 ++++++++ .../infrastructure/MetadataControllers.scala | 53 +++++++++ .../infrastructure/MetadataRoutes.scala | 8 ++ .../files_metadata/FilesTestsUtils.scala | 9 ++ .../files_metadata/GetSharedWithUser.scala | 106 ++++++++++++++++++ 10 files changed, 241 insertions(+) create mode 100644 docs/rest/get_metadata.http create mode 100644 docs/rest/get_metadata_shated_with_user.http create mode 100644 docs/rest/mark_as_ready.http create mode 100644 src/test/scala/files_metadata/GetSharedWithUser.scala diff --git a/docs/rest/get_metadata.http b/docs/rest/get_metadata.http new file mode 100644 index 0000000..db51eff --- /dev/null +++ b/docs/rest/get_metadata.http @@ -0,0 +1,3 @@ +### Get metadata for a given file + +GET http://localhost:8080/api/v1/files/metadata/{fileUUID} HTTP/1.1 \ No newline at end of file diff --git a/docs/rest/get_metadata_shated_with_user.http b/docs/rest/get_metadata_shated_with_user.http new file mode 100644 index 0000000..5739293 --- /dev/null +++ b/docs/rest/get_metadata_shated_with_user.http @@ -0,0 +1,3 @@ +### Get metadata of the files shared with the user + +GET http://localhost:8080/api/v1/files/shared_with_me/{userUUID} HTTP/1.1 \ No newline at end of file diff --git a/docs/rest/mark_as_ready.http b/docs/rest/mark_as_ready.http new file mode 100644 index 0000000..5a0e062 --- /dev/null +++ b/docs/rest/mark_as_ready.http @@ -0,0 +1,8 @@ +### Mark file as ready + +POST http://localhost:8080/api/v1/files/ready/{fileUUID} HTTP/1.1 +Content-Type: application/json + +{ + "volume": "volume_2" +} \ No newline at end of file diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index cefef30..e120ce1 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -103,4 +103,8 @@ class FilesMetaUseCases { def getArchiveMetadata( archiveUUID: UUID ): ArchivesMeta = { repository.getArchiveMeta( archiveUUID ) } + + def getFilesMetadataSharedWithUser( userUUID: UUID ): Seq[FileMeta] = { + repository.getFilesSharedWithUserMeta( userUUID ) + } } diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index da8cf2a..c1d5c82 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -21,6 +21,8 @@ trait FilesMetaRepository { def getArchiveMeta( uuid: UUID ): ArchivesMeta + def getFilesSharedWithUserMeta( userUuid: UUID ): Seq[FileMeta] + def searchFileInDirectory( ownerUuid: UUID, directoryUuid: Option[UUID], diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 4a7f170..5de93af 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -197,6 +197,51 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } + override def getFilesSharedWithUserMeta( userUuid: UUID ): Seq[FileMeta] = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + """ + |SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name + |FROM files WHERE uuid IN ( + |SELECT file_uuid FROM shared_files WHERE user_uuid = ? + |) + | """.stripMargin + ) + statement.setObject( 1, userUuid ) + + val result = statement.executeQuery() + var filesMeta: Seq[FileMeta] = Seq() + + // Parse the rows into Domain objects + while (result.next()) { + val parentUUIDString = result.getString( "parent_uuid" ) + val archiveUUIDString = result.getString( "archive_uuid" ) + + val parentUUID = + if (parentUUIDString == null) None + else Some( UUID.fromString( parentUUIDString ) ) + val archiveUUID = + if (archiveUUIDString == null) None + else Some( UUID.fromString( archiveUUIDString ) ) + + filesMeta = filesMeta :+ FileMeta( + uuid = UUID.fromString( result.getString( "uuid" ) ), + ownerUuid = UUID.fromString( result.getString( "owner_uuid" ) ), + parentUuid = parentUUID, + archiveUuid = archiveUUID, + volume = result.getString( "volume" ), + name = result.getString( "name" ) + ) + } + + filesMeta + } finally { + connection.close() + } + } + override def searchFileInDirectory( ownerUuid: UUID, directoryUuid: Option[UUID], diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 638b6b0..6422df3 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -397,4 +397,57 @@ class MetadataControllers { ) } } + + def GetSharedWithMeController( + request: cask.Request, + userUUID: String + ): cask.Response[Obj] = { + try { + val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) + if (!isUserUUIDValid) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + val filesMeta = useCases.getFilesMetadataSharedWithUser( + userUUID = UUID.fromString( userUUID ) + ) + + val responseArray = ujson.Arr.from( + filesMeta.map( fileMeta => { + val fileType = + if (fileMeta.archiveUuid.isEmpty) "directory" + else "archive" + + ujson.Obj( + "uuid" -> fileMeta.uuid.toString, + "name" -> fileMeta.name, + "fileType" -> fileType + ) + } ) + ) + + cask.Response( + ujson.Obj( + "files" -> responseArray + ), + statusCode = 200 + ) + + } catch { + case _: Exception => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "There was an error while getting the files shared with the user" + ), + statusCode = 500 + ) + } + } } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index 8e35c66..31d5774 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -50,5 +50,13 @@ case class MetadataRoutes() extends cask.Routes { controllers.GetFileMetadataController( request, fileUUID ) } + @cask.get( s"${ basePath }/shared_with_me/:userUUID" ) + def GetSharedWithMeHandler( + request: cask.Request, + userUUID: String + ): cask.Response[Obj] = { + controllers.GetSharedWithMeController( request, userUUID ) + } + initialize() } diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index 9e43077..3b15979 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -64,4 +64,13 @@ object FilesTestsUtils { s"${ GetFileMetadataTestsData.API_PREFIX }/${ fileUUID }" ) } + + def GetSharedWithUser( userUUID: String ): Response = { + `given`() + .port( 8080 ) + .when() + .get( + s"${ GetShareWithUserTestsData.API_PREFIX }/${ userUUID }" + ) + } } diff --git a/src/test/scala/files_metadata/GetSharedWithUser.scala b/src/test/scala/files_metadata/GetSharedWithUser.scala new file mode 100644 index 0000000..18ac626 --- /dev/null +++ b/src/test/scala/files_metadata/GetSharedWithUser.scala @@ -0,0 +1,106 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util.UUID + +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Before +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +object GetShareWithUserTestsData { + val API_PREFIX: String = "/api/v1/files/shared_with_me" + val OWNER_USER_UUID: UUID = UUID.randomUUID() + val OTHER_USER_UUID: UUID = UUID.randomUUID() + + var savedFileUUID: UUID = _ + var savedDirectoryUUID: UUID = _ +} + +@OrderWith( classOf[Alphanumeric] ) +class GetSharedWithUser extends JUnitSuite { + def saveAndShareFilesToObtain(): Unit = { + // Save a file and a directory + val saveFilePayload = new java.util.HashMap[String, Any]() + saveFilePayload.put( + "userUUID", + GetShareWithUserTestsData.OWNER_USER_UUID.toString + ) + saveFilePayload.put( "parentUUID", null ) + saveFilePayload.put( "hashSum", "" ) + saveFilePayload.put( "fileType", "directory" ) + saveFilePayload.put( "fileName", "Directory to share" ) + saveFilePayload.put( "fileSize", 0 ) + + val saveDirectoryResponse = FilesTestsUtils.SaveFile( saveFilePayload ) + GetShareWithUserTestsData.savedDirectoryUUID = + UUID.fromString( saveDirectoryResponse.jsonPath().get( "uuid" ) ) + + saveFilePayload.put( "fileName", "File to share" ) + saveFilePayload.put( "fileType", "archive" ) + saveFilePayload.put( "fileSize", 15 ) + saveFilePayload.put( + "hashSum", + "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" + ) + val saveFileResponse = FilesTestsUtils.SaveFile( saveFilePayload ) + GetShareWithUserTestsData.savedFileUUID = + UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) + + // Share the file and directory with the other user + val shareFilePayload = new java.util.HashMap[String, Any]() + shareFilePayload.put( + "otherUserUUID", + GetShareWithUserTestsData.OTHER_USER_UUID.toString + ) + + FilesTestsUtils.ShareFile( + GetShareWithUserTestsData.OWNER_USER_UUID.toString, + GetShareWithUserTestsData.savedDirectoryUUID.toString, + shareFilePayload + ) + FilesTestsUtils.ShareFile( + GetShareWithUserTestsData.OWNER_USER_UUID.toString, + GetShareWithUserTestsData.savedFileUUID.toString, + shareFilePayload + ) + } + + @Before + def startHttpServer(): Unit = { + FilesTestsUtils.StartHttpServer() + } + + @Test + def T1_GetSharedWithUserBadRequest(): Unit = { + saveAndShareFilesToObtain() + + val response = FilesTestsUtils.GetSharedWithUser( + "not_an_uuid" + ) + assert( response.statusCode() == 400 ) + } + + @Test + def T2_GetSharedWithUserSuccess(): Unit = { + val response = FilesTestsUtils.GetSharedWithUser( + GetShareWithUserTestsData.OTHER_USER_UUID.toString + ) + assert( response.statusCode() == 200 ) + assert( + response.jsonPath().getList( "files" ).size() == 2 + ) + } + + @Test + def T3_GetSharedWithUserEmpty(): Unit = { + val response = FilesTestsUtils.GetSharedWithUser( + UUID.randomUUID().toString + ) + assert( response.statusCode() == 200 ) + assert( + response.jsonPath().getList( "files" ).size() == 0 + ) + } +} From de696ed1d4f1c9e4a6a3c8425bf307e6555a5288 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Wed, 13 Sep 2023 17:35:22 +0000 Subject: [PATCH 36/67] chore(release): v0.7.0 [skip ci] --- CHANGELOG.md | 30 +++++++++++++++++++++++------- version.json | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d6c5a..7c0bffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,45 @@ +# [0.7.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.6.0...v0.7.0) (2023-09-13) + + +### Features + +* List files shared with user ([#56](https://github.com/hawks-atlanta/metadata-scala/issues/56)) ([4111fea](https://github.com/hawks-atlanta/metadata-scala/commit/4111feacd98f88e19191312ae22cb29c4457b3a6)) + + + # [0.6.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.5.0...v0.6.0) (2023-09-12) + ### Features -- Obtain file metadata ([#53](https://github.com/hawks-atlanta/metadata-scala/issues/53)) ([22542c6](https://github.com/hawks-atlanta/metadata-scala/commit/22542c6e66cd95bd27ec3e4f30079ea9f54bb03c)) +* Obtain file metadata ([#53](https://github.com/hawks-atlanta/metadata-scala/issues/53)) ([22542c6](https://github.com/hawks-atlanta/metadata-scala/commit/22542c6e66cd95bd27ec3e4f30079ea9f54bb03c)) + + # [0.5.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.4.0...v0.5.0) (2023-09-12) + ### Features -- Mark files as ready ([#52](https://github.com/hawks-atlanta/metadata-scala/issues/52)) ([f66a70a](https://github.com/hawks-atlanta/metadata-scala/commit/f66a70a8669be258bfdc714c45cc1f82eef16f4f)) +* Mark files as ready ([#52](https://github.com/hawks-atlanta/metadata-scala/issues/52)) ([f66a70a](https://github.com/hawks-atlanta/metadata-scala/commit/f66a70a8669be258bfdc714c45cc1f82eef16f4f)) + + # [0.4.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.3.0...v0.4.0) (2023-09-11) + ### Features -- Can read endpoint ([#49](https://github.com/hawks-atlanta/metadata-scala/issues/49)) ([e1577c2](https://github.com/hawks-atlanta/metadata-scala/commit/e1577c26ede19cf8160fc828ec4ef45bea2663ab)) +* Can read endpoint ([#49](https://github.com/hawks-atlanta/metadata-scala/issues/49)) ([e1577c2](https://github.com/hawks-atlanta/metadata-scala/commit/e1577c26ede19cf8160fc828ec4ef45bea2663ab)) + + # [0.3.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.2.1...v0.3.0) (2023-09-11) + ### Features -- Share files ([#48](https://github.com/hawks-atlanta/metadata-scala/issues/48)) ([1c2e8ea](https://github.com/hawks-atlanta/metadata-scala/commit/1c2e8ea772c7c0ae51e17ab143f7f581cace8f34)) +* Share files ([#48](https://github.com/hawks-atlanta/metadata-scala/issues/48)) ([1c2e8ea](https://github.com/hawks-atlanta/metadata-scala/commit/1c2e8ea772c7c0ae51e17ab143f7f581cace8f34)) -## [0.2.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.2.0...v0.2.1) (2023-09-11) -### Bug Fixes -- Update save metadata endpoint to avoid endpoints overlapping ([#47](https://github.com/hawks-atlanta/metadata-scala/issues/47)) ([5e2ea56](https://github.com/hawks-atlanta/metadata-scala/commit/5e2ea56fac46705a9c77a7a2c13a3379391b0800)) diff --git a/version.json b/version.json index 8284914..17aba91 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.6.0" + "version": "0.7.0" } \ No newline at end of file From 4cbb5bbfe61fd0dc1c94e0315c97b88c9d141e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Fri, 15 Sep 2023 07:29:05 -0500 Subject: [PATCH 37/67] feat: Shared with who (#57) * feat: Shared with who endpoint * docs(openapi): Update spec * tests: Add tests for the new endpoint * refactor: Refactor tests by using utils functions --- ...th_user.http => get_shared_with_user.http} | 0 docs/rest/get_shared_with_who.http | 3 + docs/spec.openapi.yaml | 6 + .../application/FilesMetaUseCases.scala | 5 + .../domain/FilesMetaRepository.scala | 2 + .../FilesMetaPostgresRepository.scala | 23 ++++ .../infrastructure/MetadataControllers.scala | 51 +++++++++ .../infrastructure/MetadataRoutes.scala | 8 ++ .../files_metadata/CanReadFileTests.scala | 80 ++++--------- .../files_metadata/FilesTestsUtils.scala | 95 ++++++++++++++-- .../files_metadata/GetFileMetadataTests.scala | 31 +----- .../files_metadata/GetSharedWithUser.scala | 34 ++---- .../GetSharedWithWhoTests.scala | 105 ++++++++++++++++++ .../SaveFileMetadataTests.scala | 39 ++----- .../scala/files_metadata/ShareFileTests.scala | 37 ++---- .../files_metadata/UpdateReadyFile.scala | 31 +----- 16 files changed, 356 insertions(+), 194 deletions(-) rename docs/rest/{get_metadata_shated_with_user.http => get_shared_with_user.http} (100%) create mode 100644 docs/rest/get_shared_with_who.http create mode 100644 src/test/scala/files_metadata/GetSharedWithWhoTests.scala diff --git a/docs/rest/get_metadata_shated_with_user.http b/docs/rest/get_shared_with_user.http similarity index 100% rename from docs/rest/get_metadata_shated_with_user.http rename to docs/rest/get_shared_with_user.http diff --git a/docs/rest/get_shared_with_who.http b/docs/rest/get_shared_with_who.http new file mode 100644 index 0000000..e7d5a1c --- /dev/null +++ b/docs/rest/get_shared_with_who.http @@ -0,0 +1,3 @@ +### Get the list of UUIDs of the users whom a file is shared with + +GET http://localhost:8080/api/v1/files/shared_with_who/{fileUUID} HTTP/1.1 \ No newline at end of file diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml index b17d850..004dfa5 100644 --- a/docs/spec.openapi.yaml +++ b/docs/spec.openapi.yaml @@ -235,6 +235,12 @@ paths: items: type: string example: "6cec2ad8-7329-47f8-8b76-7daf7036945c" + "404": + description: There is no file with the given file_uuid. + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" "500": description: Internal server error content: diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index e120ce1..7bb0805 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -107,4 +107,9 @@ class FilesMetaUseCases { def getFilesMetadataSharedWithUser( userUUID: UUID ): Seq[FileMeta] = { repository.getFilesSharedWithUserMeta( userUUID ) } + + def getUsersFileWasSharedWith( fileUUID: UUID ): Seq[UUID] = { + repository.getFileMeta( fileUUID ) + repository.getUsersFileWasSharedWith( fileUUID ) + } } diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index c1d5c82..92910ba 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -23,6 +23,8 @@ trait FilesMetaRepository { def getFilesSharedWithUserMeta( userUuid: UUID ): Seq[FileMeta] + def getUsersFileWasSharedWith( fileUuid: UUID ): Seq[UUID] + def searchFileInDirectory( ownerUuid: UUID, directoryUuid: Option[UUID], diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 5de93af..6f0196e 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -242,6 +242,29 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } + override def getUsersFileWasSharedWith( fileUuid: UUID ): Seq[UUID] = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + "SELECT user_uuid FROM shared_files WHERE file_uuid = ?" + ) + statement.setObject( 1, fileUuid ) + + val result = statement.executeQuery() + var usersUUID: Seq[UUID] = Seq() + + while (result.next()) { + usersUUID = + usersUUID :+ UUID.fromString( result.getString( "user_uuid" ) ) + } + + usersUUID + } finally { + connection.close() + } + } + override def searchFileInDirectory( ownerUuid: UUID, directoryUuid: Option[UUID], diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 6422df3..664d43c 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -450,4 +450,55 @@ class MetadataControllers { ) } } + + def GetSharedWithWhoController( + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { + try { + val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) + if (!isFileUUIDValid) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + val usersUUID = useCases.getUsersFileWasSharedWith( + fileUUID = UUID.fromString( fileUUID ) + ) + + val responseArray = ujson.Arr.from( + usersUUID.map( userUUID => userUUID.toString ) + ) + + cask.Response( + ujson.Obj( + "shared_with" -> responseArray + ), + statusCode = 200 + ) + } catch { + case e: BaseDomainException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> e.message + ), + statusCode = e.statusCode + ) + + case _: Exception => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "There was an error while getting the users with whom the file is shared" + ), + statusCode = 500 + ) + } + } } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index 31d5774..f26a707 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -58,5 +58,13 @@ case class MetadataRoutes() extends cask.Routes { controllers.GetSharedWithMeController( request, userUUID ) } + @cask.get( s"${ basePath }/shared_with_who/:fileUUID" ) + def GetSharedWithWhoHandler( + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { + controllers.GetSharedWithWhoController( request, fileUUID ) + } + initialize() } diff --git a/src/test/scala/files_metadata/CanReadFileTests.scala b/src/test/scala/files_metadata/CanReadFileTests.scala index e13dc87..b16d46b 100644 --- a/src/test/scala/files_metadata/CanReadFileTests.scala +++ b/src/test/scala/files_metadata/CanReadFileTests.scala @@ -28,83 +28,50 @@ object CanReadFileTestsData { class CanReadFileTests extends JUnitSuite { def saveAndShareFilesToCheck(): Unit = { // Save the directories - val thirdLvlDirectoryPayload = new java.util.HashMap[String, Any]() - thirdLvlDirectoryPayload.put( - "userUUID", - CanReadFileTestsData.OWNER_USER_UUID.toString + val thirdLvlDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = CanReadFileTestsData.OWNER_USER_UUID, + parentDirUUID = None ) - thirdLvlDirectoryPayload.put( "parentUUID", null ) - thirdLvlDirectoryPayload.put( "hashSum", "" ) - thirdLvlDirectoryPayload.put( "fileType", "directory" ) - thirdLvlDirectoryPayload.put( "fileName", "Third Level Directory" ) - thirdLvlDirectoryPayload.put( "fileSize", 0 ) val thirdLvlDirectoryResponse = FilesTestsUtils.SaveFile( thirdLvlDirectoryPayload ) - CanReadFileTestsData.savedThirdLevelDirectoryUUID = UUID.fromString( thirdLvlDirectoryResponse.jsonPath().get( "uuid" ) ) - val secondLvlDirectoryPayload = new java.util.HashMap[String, Any]() - secondLvlDirectoryPayload.put( - "userUUID", - CanReadFileTestsData.OWNER_USER_UUID.toString - ) - secondLvlDirectoryPayload.put( - "parentUUID", - CanReadFileTestsData.savedThirdLevelDirectoryUUID.toString + val secondLvlDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = CanReadFileTestsData.OWNER_USER_UUID, + parentDirUUID = Some( + CanReadFileTestsData.savedThirdLevelDirectoryUUID + ) ) - secondLvlDirectoryPayload.put( "hashSum", "" ) - secondLvlDirectoryPayload.put( "fileType", "directory" ) - secondLvlDirectoryPayload.put( "fileName", "Second Level Directory" ) - secondLvlDirectoryPayload.put( "fileSize", 0 ) val secondLvlDirectoryResponse = FilesTestsUtils.SaveFile( secondLvlDirectoryPayload ) - CanReadFileTestsData.savedSecondLevelDirectoryUUID = UUID.fromString( secondLvlDirectoryResponse.jsonPath().get( "uuid" ) ) - val firstLvlDirectoryPayload = new java.util.HashMap[String, Any]() - firstLvlDirectoryPayload.put( - "userUUID", - CanReadFileTestsData.OWNER_USER_UUID.toString - ) - firstLvlDirectoryPayload.put( - "parentUUID", - CanReadFileTestsData.savedSecondLevelDirectoryUUID.toString + val firstLvlDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = CanReadFileTestsData.OWNER_USER_UUID, + parentDirUUID = Some( + CanReadFileTestsData.savedSecondLevelDirectoryUUID + ) ) - firstLvlDirectoryPayload.put( "hashSum", "" ) - firstLvlDirectoryPayload.put( "fileType", "directory" ) - firstLvlDirectoryPayload.put( "fileName", "First Level Directory" ) - firstLvlDirectoryPayload.put( "fileSize", 0 ) val firstLvlDirectoryResponse = FilesTestsUtils.SaveFile( firstLvlDirectoryPayload ) - CanReadFileTestsData.savedFirstLevelDirectoryUUID = UUID.fromString( firstLvlDirectoryResponse.jsonPath().get( "uuid" ) ) // Save the first file - val saveFilePayload = new java.util.HashMap[String, Any]() - saveFilePayload.put( - "userUUID", - CanReadFileTestsData.OWNER_USER_UUID.toString - ) - saveFilePayload.put( - "parentUUID", - CanReadFileTestsData.savedFirstLevelDirectoryUUID.toString - ) - saveFilePayload.put( - "hashSum", - "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" + val saveFilePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = CanReadFileTestsData.OWNER_USER_UUID, + parentDirUUID = Some( + CanReadFileTestsData.savedFirstLevelDirectoryUUID + ) ) - saveFilePayload.put( "fileType", "archive" ) - saveFilePayload.put( "fileName", "indirectly_shared.txt" ) - saveFilePayload.put( "fileSize", 150 ) val saveFileResponse = FilesTestsUtils.SaveFile( saveFilePayload ) CanReadFileTestsData.indirectlySharedFileUUID = @@ -126,11 +93,10 @@ class CanReadFileTests extends JUnitSuite { UUID.fromString( saveFileResponse3.jsonPath().get( "uuid" ) ) // Share the third level directory - val sharePayload = new java.util.HashMap[String, Any]() - sharePayload.put( - "otherUserUUID", - CanReadFileTestsData.OTHER_USER_UUID.toString + val sharePayload = FilesTestsUtils.generateShareFilePayload( + otherUserUUID = CanReadFileTestsData.OTHER_USER_UUID ) + FilesTestsUtils.ShareFile( ownerUUID = CanReadFileTestsData.OWNER_USER_UUID.toString, fileUUID = CanReadFileTestsData.savedThirdLevelDirectoryUUID.toString, @@ -151,7 +117,6 @@ class CanReadFileTests extends JUnitSuite { } @Test - // GET /api/v1/files/can_read/:userUUID/:fileUUID Success def T1_OwnersCanReadTheirFiles(): Unit = { saveAndShareFilesToCheck() @@ -177,10 +142,10 @@ class CanReadFileTests extends JUnitSuite { .get( s"${ CanReadFileTestsData.API_PREFIX }/${ CanReadFileTestsData.OWNER_USER_UUID.toString }/${ CanReadFileTestsData.unsharedFileUUID.toString }" ) + assert( thirdFileResponse.statusCode() == 204 ) } @Test - // GET /api/v1/files/can_read/:userUUID/:fileUUID Success def T2_SharedUsersCanReadFiles(): Unit = { val directlySharedFileResponse = `given`() .port( 8080 ) @@ -200,7 +165,6 @@ class CanReadFileTests extends JUnitSuite { } @Test - // GET /api/v1/files/can_read/:userUUID/:fileUUID Forbidden def T3_UnsharedUsersCannotReadFiles(): Unit = { val unsharedFileResponse = `given`() .port( 8080 ) diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index 3b15979..1fc4402 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -1,8 +1,10 @@ package org.hawksatlanta.metadata package files_metadata +import java.security.MessageDigest import java.util import java.util.concurrent.atomic.AtomicBoolean +import java.util.UUID import io.restassured.response.Response import io.restassured.RestAssured.`given` @@ -18,6 +20,8 @@ object FilesTestsUtils { } } + // -- Save files -- + def SaveFile( payload: util.HashMap[String, Any] ): Response = { `given`() .port( 8080 ) @@ -27,6 +31,59 @@ object FilesTestsUtils { .post( s"${ SaveFileTestsData.API_PREFIX }" ) } + def generateFilePayload( + ownerUUID: UUID, + parentDirUUID: Option[UUID] + ): util.HashMap[String, Any] = { + val parentUUID: String = + if (parentDirUUID.isDefined) parentDirUUID.get.toString + else null + + val randomUUID: UUID = UUID.randomUUID() + val hash = String.format( + "%064x", + new java.math.BigInteger( + 1, + MessageDigest + .getInstance( "SHA-256" ) + .digest( randomUUID.toString.getBytes( "UTF-8" ) ) + ) + ) + + val filePayload = new util.HashMap[String, Any]() + filePayload.put( "userUUID", ownerUUID.toString ) + filePayload.put( "parentUUID", parentUUID ) + filePayload.put( "fileName", randomUUID.toString ) + filePayload.put( "fileType", "archive" ) + filePayload.put( "fileSize", 15 ) + filePayload.put( "hashSum", hash ) + + filePayload + } + + def generateDirectoryPayload( + ownerUUID: UUID, + parentDirUUID: Option[UUID] + ): util.HashMap[String, Any] = { + val parentUUID: String = + if (parentDirUUID.isDefined) parentDirUUID.get.toString + else null + + val randomUUID: UUID = UUID.randomUUID() + + val directoryPayload = new util.HashMap[String, Any]() + directoryPayload.put( "userUUID", ownerUUID.toString ) + directoryPayload.put( "parentUUID", parentUUID ) + directoryPayload.put( "fileName", randomUUID.toString ) + directoryPayload.put( "fileType", "directory" ) + directoryPayload.put( "fileSize", 0 ) + directoryPayload.put( "hashSum", "" ) + + directoryPayload + } + + // -- Share files -- + def ShareFile( ownerUUID: String, fileUUID: String, @@ -42,6 +99,34 @@ object FilesTestsUtils { ) } + def generateShareFilePayload( + otherUserUUID: UUID + ): util.HashMap[String, Any] = { + val shareFilePayload = new util.HashMap[String, Any]() + shareFilePayload.put( "otherUserUUID", otherUserUUID.toString ) + shareFilePayload + } + + def GetSharedWithUser( userUUID: String ): Response = { + `given`() + .port( 8080 ) + .when() + .get( + s"${ GetShareWithUserTestsData.API_PREFIX }/${ userUUID }" + ) + } + + def GetSharedWithWho( fileUUID: String ): Response = { + `given`() + .port( 8080 ) + .when() + .get( + s"${ GetShareWithWhoTestsData.API_PREFIX }/${ fileUUID }" + ) + } + + // -- Update files -- + def UpdateReadyFile( fileUUID: String, payload: util.HashMap[String, Any] @@ -56,6 +141,8 @@ object FilesTestsUtils { ) } + // -- Get files metadata -- + def GetFileMetadata( fileUUID: String ): Response = { `given`() .port( 8080 ) @@ -65,12 +152,4 @@ object FilesTestsUtils { ) } - def GetSharedWithUser( userUUID: String ): Response = { - `given`() - .port( 8080 ) - .when() - .get( - s"${ GetShareWithUserTestsData.API_PREFIX }/${ userUUID }" - ) - } } diff --git a/src/test/scala/files_metadata/GetFileMetadataTests.scala b/src/test/scala/files_metadata/GetFileMetadataTests.scala index 11e0a4e..072b5f1 100644 --- a/src/test/scala/files_metadata/GetFileMetadataTests.scala +++ b/src/test/scala/files_metadata/GetFileMetadataTests.scala @@ -20,35 +20,20 @@ object GetFileMetadataTestsData { @OrderWith( classOf[Alphanumeric] ) class GetFileMetadataTests extends JUnitSuite { def saveFilesToBeObtained(): Unit = { - val filePayload = new java.util.HashMap[String, Any]() - filePayload.put( - "userUUID", - GetFileMetadataTestsData.OWNER_UUID.toString + val filePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = GetFileMetadataTestsData.OWNER_UUID, + parentDirUUID = None ) - filePayload.put( "parentUUID", null ) - filePayload.put( - "hashSum", - "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" - ) - filePayload.put( "fileType", "archive" ) - filePayload.put( "fileName", "File to get metadata.txt" ) - filePayload.put( "fileSize", 15 ) val fileResponse = FilesTestsUtils.SaveFile( filePayload ) GetFileMetadataTestsData.savedFileUUID = UUID.fromString( fileResponse.body().jsonPath().getString( "uuid" ) ) - val directoryPayload = new java.util.HashMap[String, Any]() - directoryPayload.put( - "userUUID", - GetFileMetadataTestsData.OWNER_UUID.toString + val directoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = GetFileMetadataTestsData.OWNER_UUID, + parentDirUUID = None ) - directoryPayload.put( "parentUUID", null ) - directoryPayload.put( "hashSum", "" ) - directoryPayload.put( "fileType", "directory" ) - directoryPayload.put( "fileName", "Directory to get metadata" ) - directoryPayload.put( "fileSize", 0 ) val directoryResponse = FilesTestsUtils.SaveFile( directoryPayload ) GetFileMetadataTestsData.savedDirectoryUUID = UUID.fromString( @@ -77,21 +62,18 @@ class GetFileMetadataTests extends JUnitSuite { } @Test - // GET /api/v1/files/metadata/{fileUUID} Bad request def fileMetadataWithInvalidUUID(): Unit = { val response = FilesTestsUtils.GetFileMetadata( "not_an_uuid" ) assert( response.statusCode() == 400 ) } @Test - // GET /api/v1/files/metadata/{fileUUID} File not found def fileMetadataWithNonExistentUUID(): Unit = { val response = FilesTestsUtils.GetFileMetadata( UUID.randomUUID().toString ) assert( response.statusCode() == 404 ) } @Test - // GET /api/v1/files/metadata/{fileUUID} Non-ready file def fileMetadataWithNonReadyFileUUID(): Unit = { saveFilesToBeObtained() val response = FilesTestsUtils.GetFileMetadata( @@ -101,7 +83,6 @@ class GetFileMetadataTests extends JUnitSuite { } @Test - // GET /api/v1/files/metadata/{fileUUID} Success def fileMetadataWithReadyFileUUID(): Unit = { markFilesAsReady() diff --git a/src/test/scala/files_metadata/GetSharedWithUser.scala b/src/test/scala/files_metadata/GetSharedWithUser.scala index 18ac626..e4102dc 100644 --- a/src/test/scala/files_metadata/GetSharedWithUser.scala +++ b/src/test/scala/files_metadata/GetSharedWithUser.scala @@ -21,38 +21,28 @@ object GetShareWithUserTestsData { @OrderWith( classOf[Alphanumeric] ) class GetSharedWithUser extends JUnitSuite { def saveAndShareFilesToObtain(): Unit = { - // Save a file and a directory - val saveFilePayload = new java.util.HashMap[String, Any]() - saveFilePayload.put( - "userUUID", - GetShareWithUserTestsData.OWNER_USER_UUID.toString + // Save a directory + val saveDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = GetShareWithUserTestsData.OWNER_USER_UUID, + parentDirUUID = None ) - saveFilePayload.put( "parentUUID", null ) - saveFilePayload.put( "hashSum", "" ) - saveFilePayload.put( "fileType", "directory" ) - saveFilePayload.put( "fileName", "Directory to share" ) - saveFilePayload.put( "fileSize", 0 ) - val saveDirectoryResponse = FilesTestsUtils.SaveFile( saveFilePayload ) + val saveDirectoryResponse = FilesTestsUtils.SaveFile( saveDirectoryPayload ) GetShareWithUserTestsData.savedDirectoryUUID = UUID.fromString( saveDirectoryResponse.jsonPath().get( "uuid" ) ) - saveFilePayload.put( "fileName", "File to share" ) - saveFilePayload.put( "fileType", "archive" ) - saveFilePayload.put( "fileSize", 15 ) - saveFilePayload.put( - "hashSum", - "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" + // Save a file + val saveFilePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = GetShareWithUserTestsData.OWNER_USER_UUID, + parentDirUUID = None ) val saveFileResponse = FilesTestsUtils.SaveFile( saveFilePayload ) GetShareWithUserTestsData.savedFileUUID = UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) - // Share the file and directory with the other user - val shareFilePayload = new java.util.HashMap[String, Any]() - shareFilePayload.put( - "otherUserUUID", - GetShareWithUserTestsData.OTHER_USER_UUID.toString + // Share the file and the directory + val shareFilePayload = FilesTestsUtils.generateShareFilePayload( + otherUserUUID = GetShareWithUserTestsData.OTHER_USER_UUID ) FilesTestsUtils.ShareFile( diff --git a/src/test/scala/files_metadata/GetSharedWithWhoTests.scala b/src/test/scala/files_metadata/GetSharedWithWhoTests.scala new file mode 100644 index 0000000..431d16b --- /dev/null +++ b/src/test/scala/files_metadata/GetSharedWithWhoTests.scala @@ -0,0 +1,105 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util.UUID + +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Before +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +object GetShareWithWhoTestsData { + val API_PREFIX: String = "/api/v1/files/shared_with_who" + val OWNER_USER_UUID: UUID = UUID.randomUUID() + val OTHER_USER_UUID: UUID = UUID.randomUUID() + + var sharedFileUUID: UUID = _ + var unsharedFileUUID: UUID = _ +} + +@OrderWith( classOf[Alphanumeric] ) +class GetSharedWithWhoTests extends JUnitSuite { + def saveAndShareFilesToCheck(): Unit = { + // Save and share a file + val filePayload = FilesTestsUtils.generateFilePayload( + GetShareWithWhoTestsData.OWNER_USER_UUID, + None + ) + val sharePayload = FilesTestsUtils.generateShareFilePayload( + GetShareWithWhoTestsData.OTHER_USER_UUID + ) + + val saveFileResponse = FilesTestsUtils.SaveFile( filePayload ) + GetShareWithWhoTestsData.sharedFileUUID = + UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) + + FilesTestsUtils.ShareFile( + GetShareWithWhoTestsData.OWNER_USER_UUID.toString, + GetShareWithWhoTestsData.sharedFileUUID.toString, + sharePayload + ) + + // Save and don't share a file + val secondFilePayload = FilesTestsUtils.generateFilePayload( + GetShareWithWhoTestsData.OWNER_USER_UUID, + None + ) + + val saveSecondFileResponse = FilesTestsUtils.SaveFile( secondFilePayload ) + GetShareWithWhoTestsData.unsharedFileUUID = + UUID.fromString( saveSecondFileResponse.jsonPath().get( "uuid" ) ) + } + + @Before + def before(): Unit = { + FilesTestsUtils.StartHttpServer() + } + + @Test + def T1_SharedWithWhoBadRequest(): Unit = { + saveAndShareFilesToCheck() + + val response = FilesTestsUtils.GetSharedWithWho( + "not_an_uuid" + ) + + assert( response.statusCode() == 400 ) + } + + @Test + def T2_SharedWithWhoNotFound(): Unit = { + val response = FilesTestsUtils.GetSharedWithWho( + UUID.randomUUID().toString + ) + assert( response.statusCode() == 404 ) + } + + @Test + def T3_SharedWithWhoSuccess(): Unit = { + val response = FilesTestsUtils.GetSharedWithWho( + GetShareWithWhoTestsData.sharedFileUUID.toString + ) + val responseJson = response.jsonPath() + + assert( response.statusCode() == 200 ) + assert( responseJson.getList( "shared_with" ).size() == 1 ) + assert( + responseJson + .getList( "shared_with" ) + .get( 0 ) + .equals( GetShareWithWhoTestsData.OTHER_USER_UUID.toString ) + ) + } + + @Test + def T4_SharedWithWhoEmpty(): Unit = { + val response = FilesTestsUtils.GetSharedWithWho( + GetShareWithWhoTestsData.unsharedFileUUID.toString + ) + val responseJson = response.jsonPath() + + assert( response.statusCode() == 200 ) + assert( responseJson.getList( "shared_with" ).size() == 0 ) + } +} diff --git a/src/test/scala/files_metadata/SaveFileMetadataTests.scala b/src/test/scala/files_metadata/SaveFileMetadataTests.scala index b21ed4b..fe2075d 100644 --- a/src/test/scala/files_metadata/SaveFileMetadataTests.scala +++ b/src/test/scala/files_metadata/SaveFileMetadataTests.scala @@ -20,16 +20,10 @@ object SaveFileTestsData { def getFilePayloadCopy(): util.HashMap[String, Any] = { if (filePayload == null) { - filePayload = new util.HashMap[String, Any] - filePayload.put( "userUUID", USER_UUID.toString ) - filePayload.put( "parentUUID", null ) - filePayload.put( - "hashSum", - "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" + filePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = USER_UUID, + parentDirUUID = None ) - filePayload.put( "fileType", "archive" ) - filePayload.put( "fileName", "save.txt" ) - filePayload.put( "fileSize", 150 ) } filePayload.clone().asInstanceOf[util.HashMap[String, Any]] @@ -48,7 +42,6 @@ class SaveFileMetadataTests extends JUnitSuite { } @Test - // POST /api/v1/files/:user_uuid Success: Save file metadata def T1_SaveArchiveMetadataSuccess(): Unit = { val response = FilesTestsUtils.SaveFile( SaveFileTestsData.getFilePayloadCopy() @@ -65,30 +58,24 @@ class SaveFileMetadataTests extends JUnitSuite { } @Test - // POST /api/v1/files/:user_uuid Success: Save directory metadata def T2_SaveDirectoryMetadataSuccess(): Unit = { - val payload = new util.HashMap[String, Any]() - payload.put( "userUUID", SaveFileTestsData.USER_UUID.toString ) - payload.put( "parentUUID", null ) - payload.put( "hashSum", "" ) - payload.put( "fileType", "directory" ) - payload.put( "fileName", "project" ) - payload.put( "fileSize", 0 ) + val payload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = SaveFileTestsData.USER_UUID, + parentDirUUID = None + ) - val response = FilesTestsUtils.SaveFile( payload ) - val responseJSON = response.jsonPath() + val response = FilesTestsUtils.SaveFile( payload ) + val responseJSON = response.jsonPath() + val directoryUUID = responseJSON.getString( "uuid" ) assert( response.statusCode() == 201 ) assert( !responseJSON.getBoolean( "error" ) ) - - val directoryUUID = responseJSON.getString( "uuid" ) assert( CommonValidator.validateUUID( directoryUUID ) ) + SaveFileTestsData.setDirectoryUUID( UUID.fromString( directoryUUID ) ) } @Test - /* POST /api/v1/files/:user_uuid Success: Save file metadata with parent - * directory */ def T3_SaveArchiveMetadataWithParentSuccess(): Unit = { val payload = SaveFileTestsData.getFilePayloadCopy() payload.put( "parentUUID", SaveFileTestsData.savedDirectoryUUID.toString ) @@ -106,7 +93,6 @@ class SaveFileMetadataTests extends JUnitSuite { } @Test - // POST /api/v1/files/:user_uuid Conflict: File already exists def T4_SaveArchiveMetadataConflict(): Unit = { val response = FilesTestsUtils.SaveFile( SaveFileTestsData.getFilePayloadCopy() @@ -118,8 +104,6 @@ class SaveFileMetadataTests extends JUnitSuite { } @Test - /* POST /api/v1/files/:user_uuid Conflict: File in parent directory already - * exists */ def T5_SaveArchiveMetadataWithParentConflict(): Unit = { val payload = SaveFileTestsData.getFilePayloadCopy() payload.put( "parentUUID", SaveFileTestsData.savedDirectoryUUID.toString ) @@ -132,7 +116,6 @@ class SaveFileMetadataTests extends JUnitSuite { } @Test - // POST /api/v1/files/:user_uuid Not found: Parent directory does not exist def T6_SaveArchiveMetadataWithParentNotFound(): Unit = { val payload = SaveFileTestsData.getFilePayloadCopy() payload.put( "parentUUID", UUID.randomUUID().toString ) diff --git a/src/test/scala/files_metadata/ShareFileTests.scala b/src/test/scala/files_metadata/ShareFileTests.scala index e5cb4ef..3f0ba21 100644 --- a/src/test/scala/files_metadata/ShareFileTests.scala +++ b/src/test/scala/files_metadata/ShareFileTests.scala @@ -20,8 +20,9 @@ object ShareFileTestsData { def getSharePayload(): java.util.HashMap[String, Any] = { if (sharePayload == null) { - sharePayload = new java.util.HashMap[String, Any] - sharePayload.put( "otherUserUUID", OTHER_USER_UUID.toString ) + sharePayload = FilesTestsUtils.generateShareFilePayload( + otherUserUUID = OTHER_USER_UUID + ) } sharePayload.clone().asInstanceOf[java.util.HashMap[String, Any]] @@ -32,35 +33,20 @@ object ShareFileTestsData { class ShareFileTests extends JUnitSuite { def saveFilesToShare(): Unit = { // Save a file to share - val saveFilePayload = new java.util.HashMap[String, Any]() - saveFilePayload.put( - "userUUID", - ShareFileTestsData.OWNER_USER_UUID.toString + val saveFilePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = ShareFileTestsData.OWNER_USER_UUID, + parentDirUUID = None ) - saveFilePayload.put( "parentUUID", null ) - saveFilePayload.put( - "hashSum", - "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" - ) - saveFilePayload.put( "fileType", "archive" ) - saveFilePayload.put( "fileName", "share.txt" ) - saveFilePayload.put( "fileSize", 150 ) val saveFileResponse = FilesTestsUtils.SaveFile( saveFilePayload ) ShareFileTestsData.savedFileUUID = UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) // Save a directory to share - val saveDirectoryPayload = new java.util.HashMap[String, Any]() - saveDirectoryPayload.put( - "userUUID", - ShareFileTestsData.OWNER_USER_UUID.toString + val saveDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = ShareFileTestsData.OWNER_USER_UUID, + parentDirUUID = None ) - saveDirectoryPayload.put( "parentUUID", null ) - saveDirectoryPayload.put( "hashSum", "" ) - saveDirectoryPayload.put( "fileType", "directory" ) - saveDirectoryPayload.put( "fileName", "share" ) - saveDirectoryPayload.put( "fileSize", 0 ) val saveDirectoryResponse = FilesTestsUtils.SaveFile( saveDirectoryPayload ) @@ -74,7 +60,6 @@ class ShareFileTests extends JUnitSuite { } @Test - // POST /api/v1/files/share/:user_uuid/:file_uuid Bad request def T1_ShareFileBadRequest(): Unit = { saveFilesToShare() @@ -110,7 +95,6 @@ class ShareFileTests extends JUnitSuite { } @Test - // POST /api/v1/files/share/:user_uuid/:file_uuid Success: Share file def T2_ShareFileSuccess(): Unit = { // Share the file val fileResponse = FilesTestsUtils.ShareFile( @@ -131,7 +115,6 @@ class ShareFileTests extends JUnitSuite { } @Test - // POST /api/v1/files/share/:user_uuid/:file_uuid Not found def T3_ShareFileNotFound(): Unit = { val response = FilesTestsUtils.ShareFile( ownerUUID = ShareFileTestsData.OWNER_USER_UUID.toString, @@ -143,7 +126,6 @@ class ShareFileTests extends JUnitSuite { } @Test - // POST /api/v1/files/share/:user_uuid/:file_uuid Conflict def T4_ShareFileConflict(): Unit = { // Share the same file as in "T2" again val response = FilesTestsUtils.ShareFile( @@ -156,7 +138,6 @@ class ShareFileTests extends JUnitSuite { } @Test - // POST /api/v1/files/share/:user_uuid/:file_uuid Forbidden def T5_ShareFileForbidden(): Unit = { val response = FilesTestsUtils.ShareFile( ownerUUID = ShareFileTestsData.OTHER_USER_UUID.toString, diff --git a/src/test/scala/files_metadata/UpdateReadyFile.scala b/src/test/scala/files_metadata/UpdateReadyFile.scala index 4888aac..e9c8923 100644 --- a/src/test/scala/files_metadata/UpdateReadyFile.scala +++ b/src/test/scala/files_metadata/UpdateReadyFile.scala @@ -30,35 +30,20 @@ object UpdateReadyFileTestsData { @OrderWith( classOf[Alphanumeric] ) class UpdateReadyFile extends JUnitSuite { def saveFilesToBeUpdated(): Unit = { - val filePayload = new java.util.HashMap[String, Any]() - filePayload.put( - "userUUID", - UpdateReadyFileTestsData.OWNER_UUID.toString + val filePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = UpdateReadyFileTestsData.OWNER_UUID, + parentDirUUID = None ) - filePayload.put( "parentUUID", null ) - filePayload.put( - "hashSum", - "71988c4d8e0803ba4519f0b2864c1331c14a1890bf8694e251379177bfedb5c3" - ) - filePayload.put( "fileType", "archive" ) - filePayload.put( "fileName", "File to mark as ready.txt" ) - filePayload.put( "fileSize", 15 ) val fileResponse = FilesTestsUtils.SaveFile( filePayload ) UpdateReadyFileTestsData.savedFileUUID = UUID.fromString( fileResponse.body().jsonPath().getString( "uuid" ) ) - val directoryPayload = new java.util.HashMap[String, Any]() - directoryPayload.put( - "userUUID", - UpdateReadyFileTestsData.OWNER_UUID.toString + val directoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = UpdateReadyFileTestsData.OWNER_UUID, + parentDirUUID = None ) - directoryPayload.put( "parentUUID", null ) - directoryPayload.put( "hashSum", "" ) - directoryPayload.put( "fileType", "directory" ) - directoryPayload.put( "fileName", "Directory to mark as ready" ) - directoryPayload.put( "fileSize", 0 ) val directoryResponse = FilesTestsUtils.SaveFile( directoryPayload ) UpdateReadyFileTestsData.savedDirectoryUUID = UUID.fromString( @@ -71,7 +56,6 @@ class UpdateReadyFile extends JUnitSuite { } @Test - // PUT /api/v1/files/ready/{fileUUID} Bad Request def T1_UpdateReadyFileBadRequest(): Unit = { saveFilesToBeUpdated() @@ -96,7 +80,6 @@ class UpdateReadyFile extends JUnitSuite { } @Test - // PUT /api/v1/files/ready/{fileUUID} Not Found def T2_UpdateReadyFileNotFound(): Unit = { val response = FilesTestsUtils.UpdateReadyFile( UUID.randomUUID().toString, @@ -107,7 +90,6 @@ class UpdateReadyFile extends JUnitSuite { } @Test - // PUT /api/v1/files/ready/{fileUUID} Success def T3_UpdateReadyFileSuccess(): Unit = { // Update file val updateFileResponse = FilesTestsUtils.UpdateReadyFile( @@ -127,7 +109,6 @@ class UpdateReadyFile extends JUnitSuite { } @Test - // PUT /api/v1/files/read/{fileUUID} Conflict def T4_UpdateReadyFileConflict(): Unit = { // Try to mark the file as ready again val updateFileResponse = FilesTestsUtils.UpdateReadyFile( From be1765f66c010bb33fcd46ef9701d9efb7f8552c Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Fri, 15 Sep 2023 12:29:19 +0000 Subject: [PATCH 38/67] chore(release): v0.8.0 [skip ci] --- CHANGELOG.md | 18 +++++++++--------- version.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0bffe..813133d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [0.8.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.7.0...v0.8.0) (2023-09-15) + + +### Features + +* Shared with who ([#57](https://github.com/hawks-atlanta/metadata-scala/issues/57)) ([4cbb5bb](https://github.com/hawks-atlanta/metadata-scala/commit/4cbb5bbfe61fd0dc1c94e0315c97b88c9d141e3d)) + + + # [0.7.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.6.0...v0.7.0) (2023-09-13) @@ -34,12 +43,3 @@ -# [0.3.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.2.1...v0.3.0) (2023-09-11) - - -### Features - -* Share files ([#48](https://github.com/hawks-atlanta/metadata-scala/issues/48)) ([1c2e8ea](https://github.com/hawks-atlanta/metadata-scala/commit/1c2e8ea772c7c0ae51e17ab143f7f581cace8f34)) - - - diff --git a/version.json b/version.json index 17aba91..cf94925 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.7.0" + "version": "0.8.0" } \ No newline at end of file From db1f85c28a2b64000e81341ce4f880bbcc748da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Fri, 15 Sep 2023 11:02:10 -0500 Subject: [PATCH 39/67] fix: Production migrations (#61) * fix: Include MANIFEST.MF file in build * fix: Read migration files from the classpath * fix: Listen to any ip --- build.sbt | 15 ++------------- .../main/resources/migrations/V1.0.0__init.sql | 0 .../migrations/V1.1.0__can_read_procedure.sql | 0 .../scala/migrations/PostgreSQLMigration.scala | 2 +- .../shared/infrastructure/CaskHTTPRouter.scala | 3 ++- 5 files changed, 5 insertions(+), 15 deletions(-) rename db/migrations/V1__init.sql => src/main/resources/migrations/V1.0.0__init.sql (100%) rename {db => src/main/resources}/migrations/V1.1.0__can_read_procedure.sql (100%) diff --git a/build.sbt b/build.sbt index c3d73aa..59f1b8c 100644 --- a/build.sbt +++ b/build.sbt @@ -10,8 +10,8 @@ lazy val root = ( project in file( "." ) ) // Strategy to solve duplicate files in the assembly process assembly / assemblyMergeStrategy := { - case PathList( "META-INF", _* ) => MergeStrategy.discard - case _ => MergeStrategy.first + case PathList( "META-INF", "MANIFEST.MF" ) => MergeStrategy.discard + case _ => MergeStrategy.first } // Testing dependencies @@ -42,14 +42,3 @@ libraryDependencies ++= Seq( libraryDependencies ++= Seq( "com.wix" %% "accord-core" % "0.7.6" ) - -// Migration dependencies -libraryDependencies ++= Seq( - "org.flywaydb" % "flyway-core" % "9.16.0" -) - -// Database connection dependencies -libraryDependencies ++= Seq( - "com.zaxxer" % "HikariCP" % "5.0.1", - "org.postgresql" % "postgresql" % "42.5.4" -) \ No newline at end of file diff --git a/db/migrations/V1__init.sql b/src/main/resources/migrations/V1.0.0__init.sql similarity index 100% rename from db/migrations/V1__init.sql rename to src/main/resources/migrations/V1.0.0__init.sql diff --git a/db/migrations/V1.1.0__can_read_procedure.sql b/src/main/resources/migrations/V1.1.0__can_read_procedure.sql similarity index 100% rename from db/migrations/V1.1.0__can_read_procedure.sql rename to src/main/resources/migrations/V1.1.0__can_read_procedure.sql diff --git a/src/main/scala/migrations/PostgreSQLMigration.scala b/src/main/scala/migrations/PostgreSQLMigration.scala index 8bd058b..f28d894 100644 --- a/src/main/scala/migrations/PostgreSQLMigration.scala +++ b/src/main/scala/migrations/PostgreSQLMigration.scala @@ -13,7 +13,7 @@ object PostgreSQLMigration { Environment.dbUser, Environment.dbPassword ) - .locations( "filesystem:db/migrations" ) + .locations( "classpath:/migrations" ) .load() try { diff --git a/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala b/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala index 120e7c1..290af49 100644 --- a/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala +++ b/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala @@ -4,7 +4,8 @@ package shared.infrastructure import org.hawksatlanta.metadata.files_metadata.infrastructure.MetadataRoutes object CaskHTTPRouter extends cask.Main { - override def port: Int = 8080 + override def port: Int = 8080 + override def host: String = "0.0.0.0" val allRoutes = Seq( MetadataRoutes() From 47bec0e107f8a9bd3e0101e1d2822272e07da5e8 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Fri, 15 Sep 2023 16:02:22 +0000 Subject: [PATCH 40/67] chore(release): v0.8.1 [skip ci] --- CHANGELOG.md | 18 +++++++++--------- version.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 813133d..01f84f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [0.8.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.8.0...v0.8.1) (2023-09-15) + + +### Bug Fixes + +* Production migrations ([#61](https://github.com/hawks-atlanta/metadata-scala/issues/61)) ([db1f85c](https://github.com/hawks-atlanta/metadata-scala/commit/db1f85c28a2b64000e81341ce4f880bbcc748da3)) + + + # [0.8.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.7.0...v0.8.0) (2023-09-15) @@ -34,12 +43,3 @@ -# [0.4.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.3.0...v0.4.0) (2023-09-11) - - -### Features - -* Can read endpoint ([#49](https://github.com/hawks-atlanta/metadata-scala/issues/49)) ([e1577c2](https://github.com/hawks-atlanta/metadata-scala/commit/e1577c26ede19cf8160fc828ec4ef45bea2663ab)) - - - diff --git a/version.json b/version.json index cf94925..4069080 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.8.0" + "version": "0.8.1" } \ No newline at end of file From 3aab30f0ac8f1d86f55c428bed8a4861b3d4e57a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= Date: Fri, 15 Sep 2023 11:16:24 -0500 Subject: [PATCH 41/67] chore: Remove old directory --- db/migrations/V1.1.0__can_read_procedure.sql | 34 -------------------- 1 file changed, 34 deletions(-) delete mode 100644 db/migrations/V1.1.0__can_read_procedure.sql diff --git a/db/migrations/V1.1.0__can_read_procedure.sql b/db/migrations/V1.1.0__can_read_procedure.sql deleted file mode 100644 index 1b0f0f4..0000000 --- a/db/migrations/V1.1.0__can_read_procedure.sql +++ /dev/null @@ -1,34 +0,0 @@ -CREATE OR REPLACE FUNCTION can_read(user_uuid_arg UUID, file_uuid_arg UUID) - RETURNS BOOLEAN - LANGUAGE PLPGSQL - AS $$ -DECLARE - folder_parent_uuid UUID; - is_shared BOOLEAN; -BEGIN - -- Check if the file was directly shared with the user - SELECT COUNT(uuid) > 0 - INTO is_shared - FROM shared_files - WHERE - shared_files.file_uuid = file_uuid_arg AND - shared_files.user_uuid = user_uuid_arg; - - IF is_shared THEN - RETURN TRUE; - END IF; - - -- Check if the file is contained in a directory shared with the user - SELECT files.parent_uuid - INTO folder_parent_uuid - FROM files - WHERE - files.uuid = file_uuid_arg; - - IF folder_parent_uuid IS NULL THEN - RETURN FALSE; - ELSE - RETURN can_read(user_uuid_arg, folder_parent_uuid); - END IF; -END $$ -; \ No newline at end of file From 6a91d2119e034c70c3381b2475da9434d77f02b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:43:21 -0500 Subject: [PATCH 42/67] feat: Rename files (#63) * docs(openapi): Update spec * feat: Create endpoint to rename files * refactor: Create common function to handle exceptions * refactor: Rename tests files * feat: Add name field to the endpoint to get metadata * fix: Close db connections after querying * test: Add tests for the new endpoint * refactor: Add extension column (#64) --- docs/rest/save_metadata.http | 8 +- docs/spec.openapi.yaml | 132 +++++++++- .../migrations/V1.2.0__files_view.sql | 21 ++ .../application/FilesMetaUseCases.scala | 27 +- .../files_metadata/domain/ArchiveMeta.scala | 27 ++ .../files_metadata/domain/ArchivesMeta.scala | 16 -- .../domain/FileExtendedMeta.scala | 17 ++ .../domain/FilesMetaRepository.scala | 4 +- .../FilesMetaPostgresRepository.scala | 131 ++++++---- .../infrastructure/MetadataControllers.scala | 240 ++++++++---------- .../infrastructure/MetadataRoutes.scala | 9 + .../requests/CreationReqSchema.scala | 11 +- .../requests/RenameReqSchema.scala | 20 ++ ...nReadFileTests.scala => CanReadFile.scala} | 0 .../files_metadata/FilesTestsUtils.scala | 29 +++ ...eMetadataTests.scala => GetMetadata.scala} | 0 .../scala/files_metadata/RenameFile.scala | 136 ++++++++++ ...MetadataTests.scala => SaveMetadata.scala} | 0 .../{ShareFileTests.scala => ShareFile.scala} | 0 ...haredWithUser.scala => SharedWithMe.scala} | 11 + ...WithWhoTests.scala => SharedWithWho.scala} | 0 ...ateReadyFile.scala => UpdateToReady.scala} | 6 +- 22 files changed, 627 insertions(+), 218 deletions(-) create mode 100644 src/main/resources/migrations/V1.2.0__files_view.sql create mode 100644 src/main/scala/files_metadata/domain/ArchiveMeta.scala delete mode 100644 src/main/scala/files_metadata/domain/ArchivesMeta.scala create mode 100644 src/main/scala/files_metadata/domain/FileExtendedMeta.scala create mode 100644 src/main/scala/files_metadata/infrastructure/requests/RenameReqSchema.scala rename src/test/scala/files_metadata/{CanReadFileTests.scala => CanReadFile.scala} (100%) rename src/test/scala/files_metadata/{GetFileMetadataTests.scala => GetMetadata.scala} (100%) create mode 100644 src/test/scala/files_metadata/RenameFile.scala rename src/test/scala/files_metadata/{SaveFileMetadataTests.scala => SaveMetadata.scala} (100%) rename src/test/scala/files_metadata/{ShareFileTests.scala => ShareFile.scala} (100%) rename src/test/scala/files_metadata/{GetSharedWithUser.scala => SharedWithMe.scala} (88%) rename src/test/scala/files_metadata/{GetSharedWithWhoTests.scala => SharedWithWho.scala} (100%) rename src/test/scala/files_metadata/{UpdateReadyFile.scala => UpdateToReady.scala} (96%) diff --git a/docs/rest/save_metadata.http b/docs/rest/save_metadata.http index bd2bd48..da352b7 100644 --- a/docs/rest/save_metadata.http +++ b/docs/rest/save_metadata.http @@ -8,7 +8,8 @@ Content-Type: application/json "parentUUID": null, "hashSum": "fb3a2b16764328a3c90f2122cdb4e583d2b344c9499fdf9bd1f846170e05cb52", "fileType": "archive", - "fileName": "project.txt", + "fileName": "project", + "fileExtension": "txt", "fileSize": 32, } @@ -23,6 +24,7 @@ Content-Type: application/json "hashSum": "", "fileType": "directory", "fileName": "university", + "fileExtension": null, "fileSize": 0, } @@ -36,7 +38,8 @@ Content-Type: application/json "parentUUID": "92467e11-38e8-41f1-a088-0538f43811bd", "hashSum": "5a362b73f98d8a4123ba318e9c5bead3135caa33eada95e80e17290ce9bbf4be", "fileType": "archive", - "fileName": "nested.txt", + "fileName": "nested", + "fileExtension": "txt", "fileSize": 16, } @@ -51,5 +54,6 @@ Content-Type: application/json "hashSum": "", "fileType": "directory", "fileName": "nested", + "fileExtension": null, "fileSize": 0, } \ No newline at end of file diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml index 004dfa5..2221553 100644 --- a/docs/spec.openapi.yaml +++ b/docs/spec.openapi.yaml @@ -287,13 +287,13 @@ paths: schema: $ref: "#/components/schemas/error_response" - /files/share/{owner_uuid}/{file_uuid}: + /files/share/{user_uuid}/{file_uuid}: post: tags: ["File"] description: Share the given file with the given user parameters: - in: path - name: owner_uuid + name: user_uuid schema: type: string example: "658b4e63-b5ac-46a7-ac43-efb6a1415130" @@ -337,13 +337,13 @@ paths: schema: $ref: "#/components/schemas/error_response" - /files/unshare/{owner_uuid}/{file_uuid}: + /files/unshare/{user_uuid}/{file_uuid}: post: tags: ["File"] description: Unshare the given file with the given user parameters: - in: path - name: owner_uuid + name: user_uuid schema: type: string example: "658b4e63-b5ac-46a7-ac43-efb6a1415130" @@ -423,6 +423,112 @@ paths: application/json: schema: $ref: "#/components/schemas/error_response" + /files/rename/{user_uuid}/{file_uuid}: + put: + tags: ["File"] + description: Rename the given file. + parameters: + - in: path + name: user_uuid + schema: + type: string + example: "658b4e63-b5ac-46a7-ac43-efb6a1415130" + required: true + - in: path + name: file_uuid + schema: + type: string + example: "b96bdc16-8f27-44aa-9758-b4e5f13060fe" + required: true + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "renamed" + responses: + "204": + description: No content. The name of the file was updated. + "403": + description: The file is not owned by the user. + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" + "404": + description: Not found. No file with the given file_uuid was found. + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" + "409": + description: There is another file in the same folder with the same name. + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" + "500": + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" + /files/move/{user_uuid}/{file_uuid}: + put: + tags: ["File"] + description: Move the given file to the given directory. + parameters: + - in: path + name: user_uuid + schema: + type: string + example: "658b4e63-b5ac-46a7-ac43-efb6a1415130" + required: true + - in: path + name: file_uuid + schema: + type: string + example: "b96bdc16-8f27-44aa-9758-b4e5f13060fe" + required: true + requestBody: + content: + application/json: + schema: + type: object + properties: + parentUUID: + type: string + example: "5ad724f0-4091-453a-914a-c2d11d69d1e3" + responses: + "204": + description: No content. The parent of the file was updated. + "403": + description: The file is not owned by the user. + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" + "404": + description: Not found. No file with the given file_uuid was found. + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" + "409": + description: There is another file in the same folder with the same name. + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" + "500": + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/error_response" components: schemas: @@ -436,10 +542,13 @@ components: type: string enum: ["archive", "directory"] description: "Whether the file is a directory or an archive" - example: "file" + example: "archive" fileName: type: string example: "filename" + fileExtension: + type: string + example: "pdf" metadata: type: object @@ -447,6 +556,12 @@ components: archiveUUID: type: string example: "0b82495a-350b-4f4f-95ce-0119998466d4" + name: + type: string + example: "filename" + extension: + type: string + example: "pdf" volume: type: string example: "VOLUME_1" @@ -472,12 +587,15 @@ components: example: "56d50f755d5dbca915cf93779d3b51d6562e6183" fileType: type: string - enum: ["file", "directory"] + enum: ["archive", "directory"] description: "Whether the file is a directory or an archive" - example: "file" + example: "archive" fileName: type: string example: "filename" + fileExtension: + type: string + example: "pdf" fileSize: type: number description: "Size in KB" diff --git a/src/main/resources/migrations/V1.2.0__files_view.sql b/src/main/resources/migrations/V1.2.0__files_view.sql new file mode 100644 index 0000000..43a0152 --- /dev/null +++ b/src/main/resources/migrations/V1.2.0__files_view.sql @@ -0,0 +1,21 @@ +-- Add a new column to separate the extension from the name +ALTER TABLE archives ADD COLUMN extension VARCHAR(16) DEFAULT NULL; + +-- View to simplify the queries +CREATE OR REPLACE VIEW files_view AS + SELECT + files."uuid", + files."owner_uuid", + files."parent_uuid", + files."archive_uuid", + files."volume", + files."name", + archives."extension", + archives."hash_sum", + archives."size", + archives."ready", + files."is_shared", + files."created_at", + files."updated_at" + FROM files +LEFT JOIN archives ON files."archive_uuid" = archives."uuid"; \ No newline at end of file diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index 7bb0805..5296b41 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -5,6 +5,7 @@ import java.util.UUID import files_metadata.domain.ArchivesMeta import files_metadata.domain.DomainExceptions +import files_metadata.domain.FileExtendedMeta import files_metadata.domain.FileMeta import files_metadata.domain.FilesMetaRepository @@ -104,7 +105,9 @@ class FilesMetaUseCases { repository.getArchiveMeta( archiveUUID ) } - def getFilesMetadataSharedWithUser( userUUID: UUID ): Seq[FileMeta] = { + def getFilesMetadataSharedWithUser( + userUUID: UUID + ): Seq[FileExtendedMeta] = { repository.getFilesSharedWithUserMeta( userUUID ) } @@ -112,4 +115,26 @@ class FilesMetaUseCases { repository.getFileMeta( fileUUID ) repository.getUsersFileWasSharedWith( fileUUID ) } + + def renameFile( userUUID: UUID, fileUUID: UUID, newName: String ): Unit = { + val fileMeta = repository.getFileMeta( fileUUID ) + if (fileMeta.ownerUuid != userUUID) { + throw DomainExceptions.FileNotOwnedException( + "The user does not own the file" + ) + } + + val existingFileMeta = repository.searchFileInDirectory( + ownerUuid = fileMeta.ownerUuid, + directoryUuid = fileMeta.parentUuid, + fileName = newName + ) + if (existingFileMeta.isDefined) { + throw DomainExceptions.FileAlreadyExistsException( + "A file with the same name already exists in the file directory" + ) + } + + repository.updateFileName( fileUUID, newName ) + } } diff --git a/src/main/scala/files_metadata/domain/ArchiveMeta.scala b/src/main/scala/files_metadata/domain/ArchiveMeta.scala new file mode 100644 index 0000000..d83c61c --- /dev/null +++ b/src/main/scala/files_metadata/domain/ArchiveMeta.scala @@ -0,0 +1,27 @@ +package org.hawksatlanta.metadata +package files_metadata.domain + +import java.util.UUID + +case class ArchivesMeta( + uuid: UUID, + extension: String, + hashSum: String, + size: Long, + ready: Boolean +) + +object ArchivesMeta { + def createNewArchive( + extension: String, + hashSum: String, + size: Long + ): ArchivesMeta = + new ArchivesMeta( + uuid = null, + extension = extension, + hashSum = hashSum, + size = size, + ready = false + ) +} diff --git a/src/main/scala/files_metadata/domain/ArchivesMeta.scala b/src/main/scala/files_metadata/domain/ArchivesMeta.scala deleted file mode 100644 index 7397911..0000000 --- a/src/main/scala/files_metadata/domain/ArchivesMeta.scala +++ /dev/null @@ -1,16 +0,0 @@ -package org.hawksatlanta.metadata -package files_metadata.domain - -import java.util.UUID - -case class ArchivesMeta( - uuid: UUID, - hashSum: String, - size: Long, - ready: Boolean -) - -object ArchivesMeta { - def createNewArchive( hashSum: String, size: Long ): ArchivesMeta = - new ArchivesMeta( null, hashSum, size, false ) -} diff --git a/src/main/scala/files_metadata/domain/FileExtendedMeta.scala b/src/main/scala/files_metadata/domain/FileExtendedMeta.scala new file mode 100644 index 0000000..f671796 --- /dev/null +++ b/src/main/scala/files_metadata/domain/FileExtendedMeta.scala @@ -0,0 +1,17 @@ +package org.hawksatlanta.metadata +package files_metadata.domain + +import java.util.UUID + +case class FileExtendedMeta( + uuid: UUID, + ownerUuid: UUID, + parentUuid: Option[UUID], + archiveUuid: Option[UUID], + volume: String, + name: String, + extension: String, + hashSum: String, + size: Long, + ready: Boolean +) diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index 92910ba..ad297b9 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -21,7 +21,7 @@ trait FilesMetaRepository { def getArchiveMeta( uuid: UUID ): ArchivesMeta - def getFilesSharedWithUserMeta( userUuid: UUID ): Seq[FileMeta] + def getFilesSharedWithUserMeta( userUuid: UUID ): Seq[FileExtendedMeta] def getUsersFileWasSharedWith( fileUuid: UUID ): Seq[UUID] @@ -43,6 +43,8 @@ trait FilesMetaRepository { def updateFileVolume( fileUUID: UUID, volume: String ): Unit + def updateFileName( fileUUID: UUID, newName: String ): Unit + // --- Delete --- def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit } diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 6f0196e..8516a40 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -8,6 +8,7 @@ import java.util.UUID import com.zaxxer.hikari.HikariDataSource import files_metadata.domain.ArchivesMeta import files_metadata.domain.DomainExceptions +import files_metadata.domain.FileExtendedMeta import files_metadata.domain.FileMeta import files_metadata.domain.FilesMetaRepository import shared.infrastructure.PostgreSQLPool @@ -15,6 +16,14 @@ import shared.infrastructure.PostgreSQLPool class FilesMetaPostgresRepository extends FilesMetaRepository { private val pool: HikariDataSource = PostgreSQLPool.getInstance() + override def saveFileMeta( + archiveMeta: ArchivesMeta, + fileMeta: FileMeta + ): UUID = { + if (archiveMeta.hashSum.isEmpty) saveDirectory( fileMeta ) + else saveArchive( archiveMeta, fileMeta ) + } + private def saveDirectory( fileMeta: FileMeta ): UUID = { val connection: Connection = pool.getConnection() @@ -59,12 +68,13 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { // 1. Insert the archive val archiveStatemet = connection.prepareStatement( - "INSERT INTO archives (hash_sum, size, ready) VALUES (?, ?, ?) RETURNING uuid" + "INSERT INTO archives (extension, hash_sum, size, ready) VALUES (?, ?, ?, ?) RETURNING uuid" ) - archiveStatemet.setString( 1, archivesMeta.hashSum ) - archiveStatemet.setLong( 2, archivesMeta.size ) - archiveStatemet.setBoolean( 3, false ) + archiveStatemet.setString( 1, archivesMeta.extension ) + archiveStatemet.setString( 2, archivesMeta.hashSum ) + archiveStatemet.setLong( 3, archivesMeta.size ) + archiveStatemet.setBoolean( 4, false ) val archiveResult = archiveStatemet.executeQuery() var archiveUUID: Option[UUID] = None @@ -114,14 +124,6 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } - override def saveFileMeta( - archiveMeta: ArchivesMeta, - fileMeta: FileMeta - ): UUID = { - if (archiveMeta.hashSum.isEmpty) saveDirectory( fileMeta ) - else saveArchive( archiveMeta, fileMeta ) - } - override def getFilesMetaInRoot( ownerUuid: UUID ): Seq[FileMeta] = ??? override def getFilesMetaInDirectory( @@ -163,8 +165,6 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { "There is no file with the given UUID" ) } - } catch { - case exception: Exception => throw exception } finally { connection.close() } @@ -175,7 +175,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { try { val statement = connection.prepareStatement( - "SELECT uuid, hash_sum, size, ready FROM archives WHERE uuid = ?" + "SELECT uuid, extension, hash_sum, size, ready FROM archives WHERE uuid = ?" ) statement.setObject( 1, uuid ) @@ -188,6 +188,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { ArchivesMeta( uuid = UUID.fromString( result.getString( "uuid" ) ), + extension = result.getString( "extension" ), hashSum = result.getString( "hash_sum" ), size = result.getLong( "size" ), ready = result.getBoolean( "ready" ) @@ -197,22 +198,26 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } - override def getFilesSharedWithUserMeta( userUuid: UUID ): Seq[FileMeta] = { + override def getFilesSharedWithUserMeta( + userUuid: UUID + ): Seq[FileExtendedMeta] = { val connection: Connection = pool.getConnection() try { val statement = connection.prepareStatement( """ - |SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name - |FROM files WHERE uuid IN ( - |SELECT file_uuid FROM shared_files WHERE user_uuid = ? + |SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name, extension, hash_sum, size + |FROM files_view WHERE + |uuid IN ( + | SELECT file_uuid FROM shared_files WHERE user_uuid = ? |) + |AND volume IS NOT NULL | """.stripMargin ) statement.setObject( 1, userUuid ) - val result = statement.executeQuery() - var filesMeta: Seq[FileMeta] = Seq() + val result = statement.executeQuery() + var filesMeta: Seq[FileExtendedMeta] = Seq() // Parse the rows into Domain objects while (result.next()) { @@ -222,17 +227,22 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { val parentUUID = if (parentUUIDString == null) None else Some( UUID.fromString( parentUUIDString ) ) + val archiveUUID = if (archiveUUIDString == null) None else Some( UUID.fromString( archiveUUIDString ) ) - filesMeta = filesMeta :+ FileMeta( + filesMeta = filesMeta :+ FileExtendedMeta( uuid = UUID.fromString( result.getString( "uuid" ) ), ownerUuid = UUID.fromString( result.getString( "owner_uuid" ) ), parentUuid = parentUUID, archiveUuid = archiveUUID, volume = result.getString( "volume" ), - name = result.getString( "name" ) + name = result.getString( "name" ), + extension = result.getString( "extension" ), + hashSum = result.getString( "hash_sum" ), + size = result.getLong( "size" ), + ready = true ) } @@ -346,8 +356,6 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } else { false } - } catch { - case _: Exception => false } finally { connection.close() } @@ -365,8 +373,6 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { statement.setObject( 2, userUUID ) statement.executeUpdate() - } catch { - case exception: Exception => throw exception } finally { connection.close() } @@ -375,15 +381,19 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { override def canUserReadFile( userUuid: UUID, fileUuid: UUID ): Boolean = { val connection: Connection = pool.getConnection() - val statement = connection.prepareStatement( "SELECT can_read(?, ?)" ) - statement.setObject( 1, userUuid ) - statement.setObject( 2, fileUuid ) + try { + val statement = connection.prepareStatement( "SELECT can_read(?, ?)" ) + statement.setObject( 1, userUuid ) + statement.setObject( 2, fileUuid ) - val result = statement.executeQuery() - if (result.next()) { - result.getBoolean( 1 ) - } else { - false + val result = statement.executeQuery() + if (result.next()) { + result.getBoolean( 1 ) + } else { + false + } + } finally { + connection.close() } } @@ -393,13 +403,17 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { ): Unit = { val connection: Connection = pool.getConnection() - val statement = connection.prepareStatement( - "UPDATE archives SET ready = ? WHERE uuid = ?" - ) - statement.setBoolean( 1, ready ) - statement.setObject( 2, archiveUUID ) + try { + val statement = connection.prepareStatement( + "UPDATE archives SET ready = ? WHERE uuid = ?" + ) + statement.setBoolean( 1, ready ) + statement.setObject( 2, archiveUUID ) - statement.executeUpdate() + statement.executeUpdate() + } finally { + connection.close() + } } def updateFileVolume( @@ -408,13 +422,36 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { ): Unit = { val connection: Connection = pool.getConnection() - val statement = connection.prepareStatement( - "UPDATE files SET volume = ? WHERE uuid = ?" - ) - statement.setString( 1, volume ) - statement.setObject( 2, fileUUID ) + try { + val statement = connection.prepareStatement( + "UPDATE files SET volume = ? WHERE uuid = ?" + ) + statement.setString( 1, volume ) + statement.setObject( 2, fileUUID ) + + statement.executeUpdate() + } finally { + connection.close() + } + } + + override def updateFileName( + fileUUID: UUID, + newName: String + ): Unit = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + "UPDATE files SET name = ? WHERE uuid = ?" + ) + statement.setString( 1, newName ) + statement.setObject( 2, fileUUID ) - statement.executeUpdate() + statement.executeUpdate() + } finally { + connection.close() + } } override def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit = ??? diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 664d43c..87fd6bb 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -12,6 +12,7 @@ import files_metadata.domain.FileMeta import files_metadata.domain.FilesMetaRepository import files_metadata.infrastructure.requests.CreationReqSchema import files_metadata.infrastructure.requests.MarkAsReadyReqSchema +import files_metadata.infrastructure.requests.RenameReqSchema import files_metadata.infrastructure.requests.ShareReqSchema import shared.infrastructure.CommonValidator import ujson.Obj @@ -27,6 +28,37 @@ class MetadataControllers { useCases = new FilesMetaUseCases( repository ) } + private def _handleException( exception: Exception ): cask.Response[Obj] = { + exception match { + case _: upickle.core.AbortException | _: ujson.IncompleteParseException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Unable to decode JSON body" + ), + statusCode = 400 + ) + + case e: BaseDomainException => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> e.message + ), + statusCode = e.statusCode + ) + + case _: Exception => + cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "There was an error" + ), + statusCode = 500 + ) + } + } + def SaveMetadataController( request: cask.Request ): cask.Response[Obj] = { @@ -68,6 +100,7 @@ class MetadataControllers { // Save the metadata val receivedArchiveMeta = ArchivesMeta.createNewArchive( + decoded.fileExtension, decoded.hashSum, decoded.fileSize ) @@ -94,33 +127,7 @@ class MetadataControllers { statusCode = 201 ) } catch { - case _: upickle.core.AbortException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "Unable to decode JSON body" - ), - statusCode = 400 - ) - - case e: BaseDomainException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> e.message - ), - statusCode = e.statusCode - ) - - case _: Exception => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "There was an error while saving the metadata" - ), - statusCode = 500 - ) - + case e: Exception => _handleException( e ) } } @@ -163,32 +170,7 @@ class MetadataControllers { statusCode = 204 ) } catch { - case _: upickle.core.AbortException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "Unable to decode JSON body" - ), - statusCode = 400 - ) - - case e: BaseDomainException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> e.message - ), - statusCode = e.statusCode - ) - - case _: Exception => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "There was an error while sharing the file" - ), - statusCode = 500 - ) + case e: Exception => _handleException( e ) } } @@ -227,23 +209,7 @@ class MetadataControllers { cask.Response( None, statusCode = 204 ) } } catch { - case e: BaseDomainException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> e.message - ), - statusCode = e.statusCode - ) - - case _: Exception => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "There was an error while checking if the user can read the file" - ), - statusCode = 500 - ) + case e: Exception => _handleException( e ) } } @@ -281,6 +247,8 @@ class MetadataControllers { cask.Response( ujson.Obj( "archiveUUID" -> ujson.Null, // Needs to be a "custom" null value + "name" -> fileMeta.name, + "extension" -> ujson.Null, "volume" -> fileMeta.volume, "size" -> 0, "hashSum" -> "" @@ -296,6 +264,8 @@ class MetadataControllers { cask.Response( ujson.Obj( "archiveUUID" -> fileMeta.archiveUuid.get.toString, + "name" -> fileMeta.name, + "extension" -> archivesMeta.extension, "volume" -> fileMeta.volume, "size" -> archivesMeta.size, "hashSum" -> archivesMeta.hashSum @@ -304,23 +274,7 @@ class MetadataControllers { ) } } catch { - case e: BaseDomainException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> e.message - ), - statusCode = e.statusCode - ) - - case _: Exception => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "There was an error while getting the file metadata" - ), - statusCode = 500 - ) + case e: Exception => _handleException( e ) } } @@ -369,32 +323,7 @@ class MetadataControllers { statusCode = 204 ) } catch { - case _: upickle.core.AbortException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "Unable to decode JSON body" - ), - statusCode = 400 - ) - - case e: BaseDomainException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> e.message - ), - statusCode = e.statusCode - ) - - case e: Exception => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "There was an error while marking the file as ready" - ), - statusCode = 500 - ) + case e: Exception => _handleException( e ) } } @@ -420,15 +349,21 @@ class MetadataControllers { val responseArray = ujson.Arr.from( filesMeta.map( fileMeta => { - val fileType = - if (fileMeta.archiveUuid.isEmpty) "directory" - else "archive" - - ujson.Obj( - "uuid" -> fileMeta.uuid.toString, - "name" -> fileMeta.name, - "fileType" -> fileType - ) + if (fileMeta.archiveUuid.isEmpty) { + ujson.Obj( + "uuid" -> fileMeta.uuid.toString, + "fileType" -> "directory", + "name" -> fileMeta.name, + "extension" -> ujson.Null + ) + } else { + ujson.Obj( + "uuid" -> fileMeta.uuid.toString, + "fileType" -> "archive", + "name" -> fileMeta.name, + "extension" -> fileMeta.extension + ) + } } ) ) @@ -440,14 +375,7 @@ class MetadataControllers { ) } catch { - case _: Exception => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "There was an error while getting the files shared with the user" - ), - statusCode = 500 - ) + case e: Exception => _handleException( e ) } } @@ -482,23 +410,59 @@ class MetadataControllers { statusCode = 200 ) } catch { - case e: BaseDomainException => - cask.Response( + case e: Exception => _handleException( e ) + } + } + + def RenameFileController( + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { + try { + val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) + val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) + if (!isFileUUIDValid || !isUserUUIDValid) { + return cask.Response( ujson.Obj( "error" -> true, - "message" -> e.message + "message" -> "Fields validation failed" ), - statusCode = e.statusCode + statusCode = 400 ) + } - case _: Exception => - cask.Response( + val decoded: RenameReqSchema = read[RenameReqSchema]( + request.text() + ) + + val validationRule: Validator[RenameReqSchema] = + RenameReqSchema.schemaValidator + val validationResult = validate[RenameReqSchema]( decoded )( + validationRule + ) + if (validationResult.isFailure) { + return cask.Response( ujson.Obj( "error" -> true, - "message" -> "There was an error while getting the users with whom the file is shared" + "message" -> "Fields validation failed" ), - statusCode = 500 + statusCode = 400 ) + } + + useCases.renameFile( + fileUUID = UUID.fromString( fileUUID ), + userUUID = UUID.fromString( userUUID ), + newName = decoded.name + ) + + cask.Response( + None, + statusCode = 204 + ) + } catch { + case e: Exception => _handleException( e ) } } } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index f26a707..ef9ff7b 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -66,5 +66,14 @@ case class MetadataRoutes() extends cask.Routes { controllers.GetSharedWithWhoController( request, fileUUID ) } + @cask.put( s"${ basePath }/rename/:userUUID/:fileUUID" ) + def RenameFileHandler( + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { + controllers.RenameFileController( request, userUUID, fileUUID ) + } + initialize() } diff --git a/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala b/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala index 013b4c0..a1e72b2 100644 --- a/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala +++ b/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala @@ -12,6 +12,7 @@ case class CreationReqSchema( hashSum: String, fileType: String, fileName: String, + fileExtension: String, fileSize: Long ) @@ -31,7 +32,7 @@ object CreationReqSchema { ) request.parentUUID .is( aNull ) // Can be empty if it's in the root directory - .or( // Otherwise should be a valid UUID + .or( request.parentUUID should matchRegex( CommonValidator.uuidRegex ) @@ -39,6 +40,13 @@ object CreationReqSchema { request.hashSum.has( size == 64 ) // SHA-256 request.fileType.is( equalTo( "archive" ) ) request.fileName.is( notEmpty ) + request.fileExtension + .is( aNull ) // Can't be able to recognize MIME type + .or( + request.fileExtension + .is( notEmpty ) + .and( request.fileExtension.has( size <= 16 ) ) + ) request.fileName.has( size <= 128 ) request.fileSize should be > 0L } @@ -61,6 +69,7 @@ object CreationReqSchema { request.fileType.is( equalTo( "directory" ) ) request.fileName.is( notEmpty ) request.fileName.has( size <= 128 ) + request.fileExtension.is( aNull ) request.fileSize.is( equalTo( 0L ) ) // File size is 0 for a directory } } diff --git a/src/main/scala/files_metadata/infrastructure/requests/RenameReqSchema.scala b/src/main/scala/files_metadata/infrastructure/requests/RenameReqSchema.scala new file mode 100644 index 0000000..89e447c --- /dev/null +++ b/src/main/scala/files_metadata/infrastructure/requests/RenameReqSchema.scala @@ -0,0 +1,20 @@ +package org.hawksatlanta.metadata +package files_metadata.infrastructure.requests + +import com.wix.accord.dsl._ +import com.wix.accord.Validator + +case class RenameReqSchema( + name: String +) + +object RenameReqSchema { + import upickle.default._ + implicit def rw: ReadWriter[RenameReqSchema] = + macroRW[RenameReqSchema] + + val schemaValidator: Validator[RenameReqSchema] = + validator[RenameReqSchema] { req => + req.name.is( notEmpty ) + } +} diff --git a/src/test/scala/files_metadata/CanReadFileTests.scala b/src/test/scala/files_metadata/CanReadFile.scala similarity index 100% rename from src/test/scala/files_metadata/CanReadFileTests.scala rename to src/test/scala/files_metadata/CanReadFile.scala diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index 1fc4402..fe08e61 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -54,6 +54,7 @@ object FilesTestsUtils { filePayload.put( "userUUID", ownerUUID.toString ) filePayload.put( "parentUUID", parentUUID ) filePayload.put( "fileName", randomUUID.toString ) + filePayload.put( "fileExtension", "txt" ) filePayload.put( "fileType", "archive" ) filePayload.put( "fileSize", 15 ) filePayload.put( "hashSum", hash ) @@ -75,6 +76,7 @@ object FilesTestsUtils { directoryPayload.put( "userUUID", ownerUUID.toString ) directoryPayload.put( "parentUUID", parentUUID ) directoryPayload.put( "fileName", randomUUID.toString ) + directoryPayload.put( "fileExtension", null ) directoryPayload.put( "fileType", "directory" ) directoryPayload.put( "fileSize", 0 ) directoryPayload.put( "hashSum", "" ) @@ -141,6 +143,33 @@ object FilesTestsUtils { ) } + def generateReadyFilePayload(): util.HashMap[String, Any] = { + val readyFilePayload = new util.HashMap[String, Any]() + readyFilePayload.put( "volume", "volume_x" ) + readyFilePayload + } + + def UpdateFileName( + userUUID: String, + fileUUID: String, + payload: util.HashMap[String, Any] + ): Response = { + `given`() + .port( 8080 ) + .contentType( "application/json" ) + .body( payload ) + .when() + .put( + s"${ RenameFileTestsData.API_PREFIX }/${ userUUID }/${ fileUUID }" + ) + } + + def generateRenameFilePayload(): util.HashMap[String, Any] = { + val renameFilePayload = new util.HashMap[String, Any]() + renameFilePayload.put( "name", UUID.randomUUID().toString ) + renameFilePayload + } + // -- Get files metadata -- def GetFileMetadata( fileUUID: String ): Response = { diff --git a/src/test/scala/files_metadata/GetFileMetadataTests.scala b/src/test/scala/files_metadata/GetMetadata.scala similarity index 100% rename from src/test/scala/files_metadata/GetFileMetadataTests.scala rename to src/test/scala/files_metadata/GetMetadata.scala diff --git a/src/test/scala/files_metadata/RenameFile.scala b/src/test/scala/files_metadata/RenameFile.scala new file mode 100644 index 0000000..5bbc9f1 --- /dev/null +++ b/src/test/scala/files_metadata/RenameFile.scala @@ -0,0 +1,136 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util.UUID + +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Before +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +object RenameFileTestsData { + val API_PREFIX: String = "/api/v1/files/rename" + val USER_UUID: UUID = UUID.randomUUID() + + var savedFileUUID: UUID = _ + var updatedName: String = _ +} + +@OrderWith( classOf[Alphanumeric] ) +class RenameFileTests extends JUnitSuite { + def saveFileToRename(): Unit = { + val saveFilePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = RenameFileTestsData.USER_UUID, + parentDirUUID = None + ) + + val saveFileResponse = FilesTestsUtils.SaveFile( saveFilePayload ) + RenameFileTestsData.savedFileUUID = + UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) + + FilesTestsUtils.UpdateReadyFile( + fileUUID = RenameFileTestsData.savedFileUUID.toString, + payload = FilesTestsUtils.generateReadyFilePayload() + ) + } + + @Before + def startHttpServer(): Unit = { + FilesTestsUtils.StartHttpServer() + } + + @Test + def T1_RenameFileBadRequest(): Unit = { + saveFileToRename() + + // -- Bad userUUID + val renamePayload = FilesTestsUtils.generateRenameFilePayload() + val badUserUUIDResponse = FilesTestsUtils.UpdateFileName( + userUUID = "not_an_uuid", + fileUUID = RenameFileTestsData.savedFileUUID.toString, + payload = renamePayload + ) + assert( badUserUUIDResponse.statusCode() == 400 ) + + // -- Bad fileUUID + val badFileUUIDResponse = FilesTestsUtils.UpdateFileName( + userUUID = RenameFileTestsData.USER_UUID.toString, + fileUUID = "not_an_uuid", + payload = renamePayload + ) + assert( badFileUUIDResponse.statusCode() == 400 ) + + // -- Bad payload + renamePayload.put( "name", "" ) + val badPayloadResponse = FilesTestsUtils.UpdateFileName( + userUUID = RenameFileTestsData.USER_UUID.toString, + fileUUID = RenameFileTestsData.savedFileUUID.toString, + payload = renamePayload + ) + assert( badPayloadResponse.statusCode() == 400 ) + } + + @Test + def T2_RenameFile(): Unit = { + val renamePayload = FilesTestsUtils.generateRenameFilePayload() + val renameResponse = FilesTestsUtils.UpdateFileName( + userUUID = RenameFileTestsData.USER_UUID.toString, + fileUUID = RenameFileTestsData.savedFileUUID.toString, + payload = renamePayload + ) + assert( renameResponse.statusCode() == 204 ) + RenameFileTestsData.updatedName = renamePayload.get( "name" ).toString + + val getMetadataResponse = FilesTestsUtils.GetFileMetadata( + fileUUID = RenameFileTestsData.savedFileUUID.toString + ) + val getMetadataJson = getMetadataResponse.jsonPath() + assert( + getMetadataJson.getString( "name" ) == RenameFileTestsData.updatedName + ) + } + + @Test + def T3_RenameFileNotFound(): Unit = { + val renamePayload = FilesTestsUtils.generateRenameFilePayload() + val renameResponse = FilesTestsUtils.UpdateFileName( + userUUID = RenameFileTestsData.USER_UUID.toString, + fileUUID = UUID.randomUUID().toString, + payload = renamePayload + ) + assert( renameResponse.statusCode() == 404 ) + } + + @Test + def T4_RenameFileConflict(): Unit = { + // Save another file + val saveFilePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = RenameFileTestsData.USER_UUID, + parentDirUUID = None + ) + FilesTestsUtils.SaveFile( saveFilePayload ) + + // Try to rename the file with the same name + val renamePayload = FilesTestsUtils.generateRenameFilePayload() + renamePayload.put( "name", saveFilePayload.get( "fileName" ).toString ) + + val renameResponse = FilesTestsUtils.UpdateFileName( + userUUID = RenameFileTestsData.USER_UUID.toString, + fileUUID = RenameFileTestsData.savedFileUUID.toString, + payload = renamePayload + ) + assert( renameResponse.statusCode() == 409 ) + } + + @Test + def T5_RenameFileForbidden(): Unit = { + val renamePayload = FilesTestsUtils.generateRenameFilePayload() + val renameResponse = FilesTestsUtils.UpdateFileName( + userUUID = UUID.randomUUID().toString, + fileUUID = RenameFileTestsData.savedFileUUID.toString, + payload = renamePayload + ) + assert( renameResponse.statusCode() == 403 ) + } +} diff --git a/src/test/scala/files_metadata/SaveFileMetadataTests.scala b/src/test/scala/files_metadata/SaveMetadata.scala similarity index 100% rename from src/test/scala/files_metadata/SaveFileMetadataTests.scala rename to src/test/scala/files_metadata/SaveMetadata.scala diff --git a/src/test/scala/files_metadata/ShareFileTests.scala b/src/test/scala/files_metadata/ShareFile.scala similarity index 100% rename from src/test/scala/files_metadata/ShareFileTests.scala rename to src/test/scala/files_metadata/ShareFile.scala diff --git a/src/test/scala/files_metadata/GetSharedWithUser.scala b/src/test/scala/files_metadata/SharedWithMe.scala similarity index 88% rename from src/test/scala/files_metadata/GetSharedWithUser.scala rename to src/test/scala/files_metadata/SharedWithMe.scala index e4102dc..5ed027d 100644 --- a/src/test/scala/files_metadata/GetSharedWithUser.scala +++ b/src/test/scala/files_metadata/SharedWithMe.scala @@ -40,6 +40,17 @@ class GetSharedWithUser extends JUnitSuite { GetShareWithUserTestsData.savedFileUUID = UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) + // Mark the file and the directory as ready + FilesTestsUtils.UpdateReadyFile( + GetShareWithUserTestsData.savedDirectoryUUID.toString, + FilesTestsUtils.generateReadyFilePayload() + ) + + FilesTestsUtils.UpdateReadyFile( + GetShareWithUserTestsData.savedFileUUID.toString, + FilesTestsUtils.generateReadyFilePayload() + ) + // Share the file and the directory val shareFilePayload = FilesTestsUtils.generateShareFilePayload( otherUserUUID = GetShareWithUserTestsData.OTHER_USER_UUID diff --git a/src/test/scala/files_metadata/GetSharedWithWhoTests.scala b/src/test/scala/files_metadata/SharedWithWho.scala similarity index 100% rename from src/test/scala/files_metadata/GetSharedWithWhoTests.scala rename to src/test/scala/files_metadata/SharedWithWho.scala diff --git a/src/test/scala/files_metadata/UpdateReadyFile.scala b/src/test/scala/files_metadata/UpdateToReady.scala similarity index 96% rename from src/test/scala/files_metadata/UpdateReadyFile.scala rename to src/test/scala/files_metadata/UpdateToReady.scala index e9c8923..d56bb8e 100644 --- a/src/test/scala/files_metadata/UpdateReadyFile.scala +++ b/src/test/scala/files_metadata/UpdateToReady.scala @@ -18,11 +18,7 @@ object UpdateReadyFileTestsData { var savedDirectoryUUID: UUID = _ def getPayloadCopy(): java.util.HashMap[String, Any] = { - if (payload == null) { - payload = new java.util.HashMap[String, Any]() - payload.put( "volume", "volume_x" ) - } - + if (payload == null) payload = FilesTestsUtils.generateReadyFilePayload() payload.clone().asInstanceOf[java.util.HashMap[String, Any]] } } From 0d73ad3491e57e1e9be584d76c693c879525b21c Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Mon, 18 Sep 2023 18:48:51 +0000 Subject: [PATCH 43/67] chore(release): v0.9.0 [skip ci] --- CHANGELOG.md | 30 +++++++++++++++++++++++------- version.json | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05af56f..ca61021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,45 @@ +# [0.9.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.8.1...v0.9.0) (2023-09-18) + + +### Features + +* Rename files ([#63](https://github.com/hawks-atlanta/metadata-scala/issues/63)) ([6a91d21](https://github.com/hawks-atlanta/metadata-scala/commit/6a91d2119e034c70c3381b2475da9434d77f02b7)), closes [#64](https://github.com/hawks-atlanta/metadata-scala/issues/64) + + + ## [0.8.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.8.0...v0.8.1) (2023-09-15) + ### Bug Fixes -- Production migrations ([#61](https://github.com/hawks-atlanta/metadata-scala/issues/61)) ([db1f85c](https://github.com/hawks-atlanta/metadata-scala/commit/db1f85c28a2b64000e81341ce4f880bbcc748da3)) +* Production migrations ([#61](https://github.com/hawks-atlanta/metadata-scala/issues/61)) ([db1f85c](https://github.com/hawks-atlanta/metadata-scala/commit/db1f85c28a2b64000e81341ce4f880bbcc748da3)) + + # [0.8.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.7.0...v0.8.0) (2023-09-15) + ### Features -- Shared with who ([#57](https://github.com/hawks-atlanta/metadata-scala/issues/57)) ([4cbb5bb](https://github.com/hawks-atlanta/metadata-scala/commit/4cbb5bbfe61fd0dc1c94e0315c97b88c9d141e3d)) +* Shared with who ([#57](https://github.com/hawks-atlanta/metadata-scala/issues/57)) ([4cbb5bb](https://github.com/hawks-atlanta/metadata-scala/commit/4cbb5bbfe61fd0dc1c94e0315c97b88c9d141e3d)) + + # [0.7.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.6.0...v0.7.0) (2023-09-13) + ### Features -- List files shared with user ([#56](https://github.com/hawks-atlanta/metadata-scala/issues/56)) ([4111fea](https://github.com/hawks-atlanta/metadata-scala/commit/4111feacd98f88e19191312ae22cb29c4457b3a6)) +* List files shared with user ([#56](https://github.com/hawks-atlanta/metadata-scala/issues/56)) ([4111fea](https://github.com/hawks-atlanta/metadata-scala/commit/4111feacd98f88e19191312ae22cb29c4457b3a6)) + + # [0.6.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.5.0...v0.6.0) (2023-09-12) + ### Features -- Obtain file metadata ([#53](https://github.com/hawks-atlanta/metadata-scala/issues/53)) ([22542c6](https://github.com/hawks-atlanta/metadata-scala/commit/22542c6e66cd95bd27ec3e4f30079ea9f54bb03c)) +* Obtain file metadata ([#53](https://github.com/hawks-atlanta/metadata-scala/issues/53)) ([22542c6](https://github.com/hawks-atlanta/metadata-scala/commit/22542c6e66cd95bd27ec3e4f30079ea9f54bb03c)) -# [0.5.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.4.0...v0.5.0) (2023-09-12) -### Features -- Mark files as ready ([#52](https://github.com/hawks-atlanta/metadata-scala/issues/52)) ([f66a70a](https://github.com/hawks-atlanta/metadata-scala/commit/f66a70a8669be258bfdc714c45cc1f82eef16f4f)) diff --git a/version.json b/version.json index 4069080..d9f1c3f 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.8.1" + "version": "0.9.0" } \ No newline at end of file From 21f49327aac77adf494a18a1fd12576feb801ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:05:49 -0500 Subject: [PATCH 44/67] refactor: remove hashsum column (#66) * refactor: Drop hash sum column from database --- docs/rest/save_metadata.http | 4 --- docs/spec.openapi.yaml | 7 ------ .../migrations/V1.2.0__files_view.sql | 4 ++- .../application/FilesMetaUseCases.scala | 21 +++++++++++++--- .../files_metadata/domain/ArchiveMeta.scala | 7 ++---- .../domain/FileExtendedMeta.scala | 1 - .../domain/FilesMetaRepository.scala | 4 ++- .../FilesMetaPostgresRepository.scala | 25 ++++++------------- .../infrastructure/MetadataControllers.scala | 21 ++++++++++------ .../requests/CreationReqSchema.scala | 3 --- .../files_metadata/FilesTestsUtils.scala | 2 -- 11 files changed, 46 insertions(+), 53 deletions(-) diff --git a/docs/rest/save_metadata.http b/docs/rest/save_metadata.http index da352b7..70471ca 100644 --- a/docs/rest/save_metadata.http +++ b/docs/rest/save_metadata.http @@ -6,7 +6,6 @@ Content-Type: application/json { "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", "parentUUID": null, - "hashSum": "fb3a2b16764328a3c90f2122cdb4e583d2b344c9499fdf9bd1f846170e05cb52", "fileType": "archive", "fileName": "project", "fileExtension": "txt", @@ -21,7 +20,6 @@ Content-Type: application/json { "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", "parentUUID": null, - "hashSum": "", "fileType": "directory", "fileName": "university", "fileExtension": null, @@ -36,7 +34,6 @@ Content-Type: application/json { "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", "parentUUID": "92467e11-38e8-41f1-a088-0538f43811bd", - "hashSum": "5a362b73f98d8a4123ba318e9c5bead3135caa33eada95e80e17290ce9bbf4be", "fileType": "archive", "fileName": "nested", "fileExtension": "txt", @@ -51,7 +48,6 @@ Content-Type: application/json { "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", "parentUUID": "92467e11-38e8-41f1-a088-0538f43811bd", - "hashSum": "", "fileType": "directory", "fileName": "nested", "fileExtension": null, diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml index 2221553..c14edc6 100644 --- a/docs/spec.openapi.yaml +++ b/docs/spec.openapi.yaml @@ -565,9 +565,6 @@ components: volume: type: string example: "VOLUME_1" - hashSum: - type: string - example: "56d50f755d5dbca915cf93779d3b51d6562e6183" size: type: number example: 3072 @@ -581,10 +578,6 @@ components: parentUUID: type: string example: "5ad724f0-4091-453a-914a-c2d11d69d1e3" - hashSum: - type: string - description: "SHA256 hash of the file" - example: "56d50f755d5dbca915cf93779d3b51d6562e6183" fileType: type: string enum: ["archive", "directory"] diff --git a/src/main/resources/migrations/V1.2.0__files_view.sql b/src/main/resources/migrations/V1.2.0__files_view.sql index 43a0152..d95e397 100644 --- a/src/main/resources/migrations/V1.2.0__files_view.sql +++ b/src/main/resources/migrations/V1.2.0__files_view.sql @@ -1,6 +1,9 @@ -- Add a new column to separate the extension from the name ALTER TABLE archives ADD COLUMN extension VARCHAR(16) DEFAULT NULL; +-- Remove the hash-sum column from the files table +ALTER TABLE archives DROP COLUMN hash_sum; + -- View to simplify the queries CREATE OR REPLACE VIEW files_view AS SELECT @@ -11,7 +14,6 @@ CREATE OR REPLACE VIEW files_view AS files."volume", files."name", archives."extension", - archives."hash_sum", archives."size", archives."ready", files."is_shared", diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index 5296b41..01fc8d8 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -17,8 +17,9 @@ class FilesMetaUseCases { this.repository = repository } - def saveMetadata( archiveMeta: ArchivesMeta, fileMeta: FileMeta ): UUID = { - // Check if the file already exists + private def ensureFileCanBeCreated( + fileMeta: FileMeta + ): Unit = { val existingFileMeta = repository.searchFileInDirectory( ownerUuid = fileMeta.ownerUuid, directoryUuid = fileMeta.parentUuid, @@ -37,9 +38,21 @@ class FilesMetaUseCases { uuid = fileMeta.parentUuid.get ) } + } + + def saveArchiveMetadata( + archiveMeta: ArchivesMeta, + fileMeta: FileMeta + ): UUID = { + ensureFileCanBeCreated( fileMeta = fileMeta ) + repository.saveArchiveMeta( archiveMeta, fileMeta ) + } - // Save the metadata - repository.saveFileMeta( archiveMeta, fileMeta ) + def saveDirectoryMetadata( + fileMeta: FileMeta + ): UUID = { + ensureFileCanBeCreated( fileMeta = fileMeta ) + repository.saveDirectoryMeta( fileMeta ) } def shareFile( diff --git a/src/main/scala/files_metadata/domain/ArchiveMeta.scala b/src/main/scala/files_metadata/domain/ArchiveMeta.scala index d83c61c..a686e2a 100644 --- a/src/main/scala/files_metadata/domain/ArchiveMeta.scala +++ b/src/main/scala/files_metadata/domain/ArchiveMeta.scala @@ -6,7 +6,6 @@ import java.util.UUID case class ArchivesMeta( uuid: UUID, extension: String, - hashSum: String, size: Long, ready: Boolean ) @@ -14,14 +13,12 @@ case class ArchivesMeta( object ArchivesMeta { def createNewArchive( extension: String, - hashSum: String, size: Long ): ArchivesMeta = new ArchivesMeta( uuid = null, + ready = false, extension = extension, - hashSum = hashSum, - size = size, - ready = false + size = size ) } diff --git a/src/main/scala/files_metadata/domain/FileExtendedMeta.scala b/src/main/scala/files_metadata/domain/FileExtendedMeta.scala index f671796..f8e72f8 100644 --- a/src/main/scala/files_metadata/domain/FileExtendedMeta.scala +++ b/src/main/scala/files_metadata/domain/FileExtendedMeta.scala @@ -11,7 +11,6 @@ case class FileExtendedMeta( volume: String, name: String, extension: String, - hashSum: String, size: Long, ready: Boolean ) diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index ad297b9..0b35fcf 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -5,7 +5,9 @@ import java.util.UUID trait FilesMetaRepository { // --- Create --- - def saveFileMeta( archiveMeta: ArchivesMeta, fileMeta: FileMeta ): UUID + def saveArchiveMeta( archiveMeta: ArchivesMeta, fileMeta: FileMeta ): UUID + + def saveDirectoryMeta( fileMeta: FileMeta ): UUID def shareFile( fileUUID: UUID, userUUID: UUID ): Unit diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 8516a40..2a5e0de 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -16,15 +16,7 @@ import shared.infrastructure.PostgreSQLPool class FilesMetaPostgresRepository extends FilesMetaRepository { private val pool: HikariDataSource = PostgreSQLPool.getInstance() - override def saveFileMeta( - archiveMeta: ArchivesMeta, - fileMeta: FileMeta - ): UUID = { - if (archiveMeta.hashSum.isEmpty) saveDirectory( fileMeta ) - else saveArchive( archiveMeta, fileMeta ) - } - - private def saveDirectory( fileMeta: FileMeta ): UUID = { + override def saveDirectoryMeta( fileMeta: FileMeta ): UUID = { val connection: Connection = pool.getConnection() try { @@ -56,7 +48,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } - private def saveArchive( + override def saveArchiveMeta( archivesMeta: ArchivesMeta, fileMeta: FileMeta ): UUID = { @@ -68,13 +60,12 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { // 1. Insert the archive val archiveStatemet = connection.prepareStatement( - "INSERT INTO archives (extension, hash_sum, size, ready) VALUES (?, ?, ?, ?) RETURNING uuid" + "INSERT INTO archives (extension, size, ready) VALUES (?, ?, ?) RETURNING uuid" ) archiveStatemet.setString( 1, archivesMeta.extension ) - archiveStatemet.setString( 2, archivesMeta.hashSum ) - archiveStatemet.setLong( 3, archivesMeta.size ) - archiveStatemet.setBoolean( 4, false ) + archiveStatemet.setLong( 2, archivesMeta.size ) + archiveStatemet.setBoolean( 3, false ) val archiveResult = archiveStatemet.executeQuery() var archiveUUID: Option[UUID] = None @@ -175,7 +166,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { try { val statement = connection.prepareStatement( - "SELECT uuid, extension, hash_sum, size, ready FROM archives WHERE uuid = ?" + "SELECT uuid, extension, size, ready FROM archives WHERE uuid = ?" ) statement.setObject( 1, uuid ) @@ -189,7 +180,6 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { ArchivesMeta( uuid = UUID.fromString( result.getString( "uuid" ) ), extension = result.getString( "extension" ), - hashSum = result.getString( "hash_sum" ), size = result.getLong( "size" ), ready = result.getBoolean( "ready" ) ) @@ -206,7 +196,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { try { val statement = connection.prepareStatement( """ - |SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name, extension, hash_sum, size + |SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name, extension, size |FROM files_view WHERE |uuid IN ( | SELECT file_uuid FROM shared_files WHERE user_uuid = ? @@ -240,7 +230,6 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { volume = result.getString( "volume" ), name = result.getString( "name" ), extension = result.getString( "extension" ), - hashSum = result.getString( "hash_sum" ), size = result.getLong( "size" ), ready = true ) diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 87fd6bb..9d6ac7d 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -101,7 +101,6 @@ class MetadataControllers { // Save the metadata val receivedArchiveMeta = ArchivesMeta.createNewArchive( decoded.fileExtension, - decoded.hashSum, decoded.fileSize ) @@ -116,8 +115,18 @@ class MetadataControllers { ) // Save the metadata - val savedUUID = - useCases.saveMetadata( receivedArchiveMeta, receivedFileMeta ) + var savedUUID: UUID = null + if (decoded.fileType == "archive") { + savedUUID = useCases.saveArchiveMetadata( + archiveMeta = receivedArchiveMeta, + fileMeta = receivedFileMeta + ) + } else { + savedUUID = useCases.saveDirectoryMetadata( + fileMeta = receivedFileMeta + ) + } + cask.Response( ujson.Obj( "error" -> false, @@ -250,8 +259,7 @@ class MetadataControllers { "name" -> fileMeta.name, "extension" -> ujson.Null, "volume" -> fileMeta.volume, - "size" -> 0, - "hashSum" -> "" + "size" -> 0 ), statusCode = 200 ) @@ -267,8 +275,7 @@ class MetadataControllers { "name" -> fileMeta.name, "extension" -> archivesMeta.extension, "volume" -> fileMeta.volume, - "size" -> archivesMeta.size, - "hashSum" -> archivesMeta.hashSum + "size" -> archivesMeta.size ), statusCode = 200 ) diff --git a/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala b/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala index a1e72b2..9c4a995 100644 --- a/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala +++ b/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala @@ -9,7 +9,6 @@ import shared.infrastructure.CommonValidator case class CreationReqSchema( userUUID: String, parentUUID: String, - hashSum: String, fileType: String, fileName: String, fileExtension: String, @@ -37,7 +36,6 @@ object CreationReqSchema { CommonValidator.uuidRegex ) ) - request.hashSum.has( size == 64 ) // SHA-256 request.fileType.is( equalTo( "archive" ) ) request.fileName.is( notEmpty ) request.fileExtension @@ -65,7 +63,6 @@ object CreationReqSchema { CommonValidator.uuidRegex ) ) - request.hashSum.is( empty ) // HashSum is not needed for a directory request.fileType.is( equalTo( "directory" ) ) request.fileName.is( notEmpty ) request.fileName.has( size <= 128 ) diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index fe08e61..ec22477 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -57,7 +57,6 @@ object FilesTestsUtils { filePayload.put( "fileExtension", "txt" ) filePayload.put( "fileType", "archive" ) filePayload.put( "fileSize", 15 ) - filePayload.put( "hashSum", hash ) filePayload } @@ -79,7 +78,6 @@ object FilesTestsUtils { directoryPayload.put( "fileExtension", null ) directoryPayload.put( "fileType", "directory" ) directoryPayload.put( "fileSize", 0 ) - directoryPayload.put( "hashSum", "" ) directoryPayload } From 06842463a6c24c2b38569991bfb8cb6c5caf15e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Thu, 21 Sep 2023 09:13:30 -0500 Subject: [PATCH 45/67] feat: Move file (#67) --- docs/rest/move_file.http | 17 ++ docs/rest/rename_file.http | 8 + .../application/FilesMetaUseCases.scala | 50 +++++ .../domain/DomainExceptions.scala | 3 + .../domain/FilesMetaRepository.scala | 2 + .../FilesMetaPostgresRepository.scala | 21 ++- .../infrastructure/MetadataControllers.scala | 57 ++++++ .../infrastructure/MetadataRoutes.scala | 9 + .../requests/MoveReqSchema.scala | 23 +++ .../files_metadata/FilesTestsUtils.scala | 31 +++- src/test/scala/files_metadata/MoveFile.scala | 173 ++++++++++++++++++ 11 files changed, 383 insertions(+), 11 deletions(-) create mode 100644 docs/rest/move_file.http create mode 100644 docs/rest/rename_file.http create mode 100644 src/main/scala/files_metadata/infrastructure/requests/MoveReqSchema.scala create mode 100644 src/test/scala/files_metadata/MoveFile.scala diff --git a/docs/rest/move_file.http b/docs/rest/move_file.http new file mode 100644 index 0000000..a447fcf --- /dev/null +++ b/docs/rest/move_file.http @@ -0,0 +1,17 @@ +### Move file to a new parent directory + +PUT http://localhost:8080/api/v1/files/move/{userUUID}/{fileUUUID} HTTP/1.1 +Content-Type: application/json + +{ + "parentUUID": "{parentUUID}" +} + +### Move file to root + +PUT http://localhost:8080/api/v1/files/move/{userUUID}/{fileUUUID} HTTP/1.1 +Content-Type: application/json + +{ + "parentUUID": null +} \ No newline at end of file diff --git a/docs/rest/rename_file.http b/docs/rest/rename_file.http new file mode 100644 index 0000000..71a13ea --- /dev/null +++ b/docs/rest/rename_file.http @@ -0,0 +1,8 @@ +### Rename file + +PUT http://localhost:8080/api/v1/files/rename/{userUUID}/{fileUUUID} HTTP/1.1 +Content-Type: application/json + +{ + "name": "{name}" +} diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index 01fc8d8..045011c 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -150,4 +150,54 @@ class FilesMetaUseCases { repository.updateFileName( fileUUID, newName ) } + + def moveFile( + userUUID: UUID, + fileUUID: UUID, + newParentUUID: Option[UUID] + ): Unit = { + // Check the file exists + val fileMeta = repository.getFileMeta( fileUUID ) + + // Check the user owns the file + if (fileMeta.ownerUuid != userUUID) { + throw DomainExceptions.FileNotOwnedException( + "The user does not own the file" + ) + } + + // Check the current parent is not the same as the new parent + if (fileMeta.parentUuid.orNull == newParentUUID.orNull) { + throw DomainExceptions.FileAlreadyExistsException( + "The file is already in the given directory" + ) + } + + if (newParentUUID.isDefined) { + // Check the parent exists + val newParentMeta = repository.getFileMeta( newParentUUID.get ) + + // Check the new parent is a directory + val parentIsDirectory = newParentMeta.archiveUuid.isEmpty + if (!parentIsDirectory) { + throw DomainExceptions.ParentIsNotADirectoryException( + "The new parent is not a directory" + ) + } + } + + // Check there is no file with the same name in the new parent + val existingFileMeta = repository.searchFileInDirectory( + ownerUuid = fileMeta.ownerUuid, + directoryUuid = newParentUUID, + fileName = fileMeta.name + ) + if (existingFileMeta.isDefined) { + throw DomainExceptions.FileAlreadyExistsException( + "A file with the same name already exists in the file directory" + ) + } + + repository.updateFileParent( fileUUID, newParentUUID ) + } } diff --git a/src/main/scala/files_metadata/domain/DomainExceptions.scala b/src/main/scala/files_metadata/domain/DomainExceptions.scala index 037aaef..e18367b 100644 --- a/src/main/scala/files_metadata/domain/DomainExceptions.scala +++ b/src/main/scala/files_metadata/domain/DomainExceptions.scala @@ -36,4 +36,7 @@ object DomainExceptions { case class FileAlreadyMarkedAsReadyException( override val message: String ) extends BaseDomainException( message, 409 ) + + case class ParentIsNotADirectoryException( override val message: String ) + extends BaseDomainException( message, 400 ) } diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index 0b35fcf..fabc3ed 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -47,6 +47,8 @@ trait FilesMetaRepository { def updateFileName( fileUUID: UUID, newName: String ): Unit + def updateFileParent( fileUUID: UUID, parentUUID: Option[UUID] ): Unit + // --- Delete --- def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit } diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 2a5e0de..4e7fa40 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -153,7 +153,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { ) } else { throw DomainExceptions.FileNotFoundException( - "There is no file with the given UUID" + s"There is no file with the ${ uuid.toString } UUID" ) } } finally { @@ -443,5 +443,24 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } + override def updateFileParent( + fileUUID: UUID, + parentUUID: Option[UUID] + ): Unit = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + "UPDATE files SET parent_uuid = ? WHERE uuid = ?" + ) + statement.setObject( 1, parentUUID.orNull ) + statement.setObject( 2, fileUUID ) + + statement.executeUpdate() + } finally { + connection.close() + } + } + override def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit = ??? } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 9d6ac7d..af80a9a 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -12,6 +12,7 @@ import files_metadata.domain.FileMeta import files_metadata.domain.FilesMetaRepository import files_metadata.infrastructure.requests.CreationReqSchema import files_metadata.infrastructure.requests.MarkAsReadyReqSchema +import files_metadata.infrastructure.requests.MoveReqSchema import files_metadata.infrastructure.requests.RenameReqSchema import files_metadata.infrastructure.requests.ShareReqSchema import shared.infrastructure.CommonValidator @@ -472,4 +473,60 @@ class MetadataControllers { case e: Exception => _handleException( e ) } } + + def MoveFileController( + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { + try { + val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) + val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) + if (!isFileUUIDValid || !isUserUUIDValid) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + val decoded: MoveReqSchema = read[MoveReqSchema]( + request.text() + ) + + val validationRule: Validator[MoveReqSchema] = + MoveReqSchema.schemaValidator + val validationResult = validate[MoveReqSchema]( decoded )( + validationRule + ) + if (validationResult.isFailure) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + val parentUUID = + if (decoded.parentUUID == null) None + else Some( UUID.fromString( decoded.parentUUID ) ) + + useCases.moveFile( + userUUID = UUID.fromString( userUUID ), + fileUUID = UUID.fromString( fileUUID ), + newParentUUID = parentUUID + ) + + cask.Response( + None, + statusCode = 204 + ) + } catch { + case e: Exception => _handleException( e ) + } + } } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index ef9ff7b..912a661 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -75,5 +75,14 @@ case class MetadataRoutes() extends cask.Routes { controllers.RenameFileController( request, userUUID, fileUUID ) } + @cask.put( s"${ basePath }/move/:userUUID/:fileUUID" ) + def MoveFileHandler( + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { + controllers.MoveFileController( request, userUUID, fileUUID ) + } + initialize() } diff --git a/src/main/scala/files_metadata/infrastructure/requests/MoveReqSchema.scala b/src/main/scala/files_metadata/infrastructure/requests/MoveReqSchema.scala new file mode 100644 index 0000000..401473d --- /dev/null +++ b/src/main/scala/files_metadata/infrastructure/requests/MoveReqSchema.scala @@ -0,0 +1,23 @@ +package org.hawksatlanta.metadata +package files_metadata.infrastructure.requests + +import com.wix.accord.dsl._ +import com.wix.accord.Validator +import org.hawksatlanta.metadata.shared.infrastructure.CommonValidator + +case class MoveReqSchema( + parentUUID: String +) + +object MoveReqSchema { + import upickle.default._ + implicit def rw: ReadWriter[MoveReqSchema] = + macroRW[MoveReqSchema] + + val schemaValidator: Validator[MoveReqSchema] = + validator[MoveReqSchema] { req => + req.parentUUID + .is( aNull ) + .or( req.parentUUID.should( matchRegex( CommonValidator.uuidRegex ) ) ) + } +} diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index ec22477..875b021 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -1,7 +1,6 @@ package org.hawksatlanta.metadata package files_metadata -import java.security.MessageDigest import java.util import java.util.concurrent.atomic.AtomicBoolean import java.util.UUID @@ -40,15 +39,6 @@ object FilesTestsUtils { else null val randomUUID: UUID = UUID.randomUUID() - val hash = String.format( - "%064x", - new java.math.BigInteger( - 1, - MessageDigest - .getInstance( "SHA-256" ) - .digest( randomUUID.toString.getBytes( "UTF-8" ) ) - ) - ) val filePayload = new util.HashMap[String, Any]() filePayload.put( "userUUID", ownerUUID.toString ) @@ -168,6 +158,27 @@ object FilesTestsUtils { renameFilePayload } + def MoveFile( + userUUID: String, + fileUUID: String, + payload: util.HashMap[String, Any] + ): Response = { + `given`() + .port( 8080 ) + .contentType( "application/json" ) + .body( payload ) + .when() + .put( + s"${ MoveFileTestsData.API_PREFIX }/${ userUUID }/${ fileUUID }" + ) + } + + def generateMoveFilePayload( parentUUID: UUID ): util.HashMap[String, Any] = { + val moveFilePayload = new util.HashMap[String, Any]() + moveFilePayload.put( "parentUUID", parentUUID.toString ) + moveFilePayload + } + // -- Get files metadata -- def GetFileMetadata( fileUUID: String ): Response = { diff --git a/src/test/scala/files_metadata/MoveFile.scala b/src/test/scala/files_metadata/MoveFile.scala new file mode 100644 index 0000000..1142f68 --- /dev/null +++ b/src/test/scala/files_metadata/MoveFile.scala @@ -0,0 +1,173 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util.UUID + +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Before +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +object MoveFileTestsData { + val API_PREFIX: String = "/api/v1/files/move" + val USER_UUID: UUID = UUID.randomUUID() + + var savedDirectoryUUID: UUID = _ + var savedFileUUID: UUID = _ + var secondSavedFileUUID: UUID = _ +} + +@OrderWith( classOf[Alphanumeric] ) +class MoveFile extends JUnitSuite { + def saveFileToMove(): Unit = { + // Save a file + val saveFilePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = MoveFileTestsData.USER_UUID, + parentDirUUID = None + ) + + val saveFileResponse = FilesTestsUtils.SaveFile( saveFilePayload ) + MoveFileTestsData.savedFileUUID = + UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) + + // Save a directory + val saveDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = MoveFileTestsData.USER_UUID, + parentDirUUID = None + ) + + val saveDirectoryResponse = FilesTestsUtils.SaveFile( saveDirectoryPayload ) + MoveFileTestsData.savedDirectoryUUID = + UUID.fromString( saveDirectoryResponse.jsonPath().get( "uuid" ) ) + + // Save a second file + val saveSecondFilePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = MoveFileTestsData.USER_UUID, + parentDirUUID = None + ) + + val saveSecondFileResponse = + FilesTestsUtils.SaveFile( saveSecondFilePayload ) + MoveFileTestsData.secondSavedFileUUID = + UUID.fromString( saveSecondFileResponse.jsonPath().get( "uuid" ) ) + } + + @Before + def startHttpServer(): Unit = { + FilesTestsUtils.StartHttpServer() + } + + @Test + def T1_MoveFileBadRequest(): Unit = { + saveFileToMove() + + // -- Bad userUUID + val movePayload = FilesTestsUtils.generateMoveFilePayload( + parentUUID = MoveFileTestsData.savedDirectoryUUID + ) + val badUserUUIDResponse = FilesTestsUtils.MoveFile( + userUUID = "not_an_uuid", + fileUUID = MoveFileTestsData.savedFileUUID.toString, + payload = movePayload + ) + assert( badUserUUIDResponse.statusCode() == 400 ) + + // -- Bad fileUUID + val badFileUUIDResponse = FilesTestsUtils.MoveFile( + userUUID = MoveFileTestsData.USER_UUID.toString, + fileUUID = "not_an_uuid", + payload = movePayload + ) + assert( badFileUUIDResponse.statusCode() == 400 ) + + // -- Bad parentUUID + movePayload.put( "parentUUID", "not_an_uuid" ) + val badParentUUIDResponse = FilesTestsUtils.MoveFile( + userUUID = MoveFileTestsData.USER_UUID.toString, + fileUUID = MoveFileTestsData.savedFileUUID.toString, + payload = movePayload + ) + assert( badParentUUIDResponse.statusCode() == 400 ) + } + + @Test + def T2_MoveFileNotFound(): Unit = { + // File not found + val movePayload = FilesTestsUtils.generateMoveFilePayload( + parentUUID = MoveFileTestsData.savedDirectoryUUID + ) + val notFoundResponse = FilesTestsUtils.MoveFile( + userUUID = MoveFileTestsData.USER_UUID.toString, + fileUUID = UUID.randomUUID().toString, + payload = movePayload + ) + assert( notFoundResponse.statusCode() == 404 ) + + // Parent directory not found + movePayload.put( "parentUUID", UUID.randomUUID().toString ) + val parentNotFoundResponse = FilesTestsUtils.MoveFile( + userUUID = MoveFileTestsData.USER_UUID.toString, + fileUUID = MoveFileTestsData.savedFileUUID.toString, + payload = movePayload + ) + assert( parentNotFoundResponse.statusCode() == 404 ) + } + + @Test + def T3_MoveFileParentIsNotADirectory(): Unit = { + val movePayload = FilesTestsUtils.generateMoveFilePayload( + parentUUID = MoveFileTestsData.secondSavedFileUUID + ) + val parentNotADirectoryResponse = FilesTestsUtils.MoveFile( + userUUID = MoveFileTestsData.USER_UUID.toString, + fileUUID = MoveFileTestsData.savedFileUUID.toString, + payload = movePayload + ) + assert( parentNotADirectoryResponse.statusCode() == 400 ) + } + + @Test + def T4_MoveFileForbidden(): Unit = { + val movePayload = FilesTestsUtils.generateMoveFilePayload( + parentUUID = MoveFileTestsData.savedDirectoryUUID + ) + val forbiddenResponse = FilesTestsUtils.MoveFile( + userUUID = UUID.randomUUID().toString, + fileUUID = MoveFileTestsData.savedFileUUID.toString, + payload = movePayload + ) + assert( forbiddenResponse.statusCode() == 403 ) + } + + @Test + def T5_MoveFile(): Unit = { + // Move the file to the saved directory + val movePayload = FilesTestsUtils.generateMoveFilePayload( + parentUUID = MoveFileTestsData.savedDirectoryUUID + ) + val moveResponse = FilesTestsUtils.MoveFile( + userUUID = MoveFileTestsData.USER_UUID.toString, + fileUUID = MoveFileTestsData.savedFileUUID.toString, + payload = movePayload + ) + assert( moveResponse.statusCode() == 204 ) + + // Try to move the file again + val secondMoveResponse = FilesTestsUtils.MoveFile( + userUUID = MoveFileTestsData.USER_UUID.toString, + fileUUID = MoveFileTestsData.savedFileUUID.toString, + payload = movePayload + ) + assert( secondMoveResponse.statusCode() == 409 ) + + // Move the file to root + movePayload.put( "parentUUID", null ) + val moveFileToRootResponse = FilesTestsUtils.MoveFile( + userUUID = MoveFileTestsData.USER_UUID.toString, + fileUUID = MoveFileTestsData.savedFileUUID.toString, + payload = movePayload + ) + assert( moveFileToRootResponse.statusCode() == 204 ) + } +} From cb497ef9087f6014d5f16eed02a5e0657bb755aa Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Thu, 21 Sep 2023 14:13:46 +0000 Subject: [PATCH 46/67] chore(release): v0.10.0 [skip ci] --- CHANGELOG.md | 18 +++++++++--------- version.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca61021..64cc5cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [0.10.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.9.0...v0.10.0) (2023-09-21) + + +### Features + +* Move file ([#67](https://github.com/hawks-atlanta/metadata-scala/issues/67)) ([0684246](https://github.com/hawks-atlanta/metadata-scala/commit/06842463a6c24c2b38569991bfb8cb6c5caf15e6)) + + + # [0.9.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.8.1...v0.9.0) (2023-09-18) @@ -34,12 +43,3 @@ -# [0.6.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.5.0...v0.6.0) (2023-09-12) - - -### Features - -* Obtain file metadata ([#53](https://github.com/hawks-atlanta/metadata-scala/issues/53)) ([22542c6](https://github.com/hawks-atlanta/metadata-scala/commit/22542c6e66cd95bd27ec3e4f30079ea9f54bb03c)) - - - diff --git a/version.json b/version.json index d9f1c3f..eab3c25 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.9.0" + "version": "0.10.0" } \ No newline at end of file From 7f63347667d0095616e8d09641d264e888f44ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Tue, 26 Sep 2023 06:46:37 -0500 Subject: [PATCH 47/67] fix: Update is_shared column when a file is shared (#70) * fix: Use a transaction to update the is_shared column * refactor: Add is_shared column to get metadata endpoint response * chore: Update docker-compose images versions * fix: Include is_shared column in select statements --- docker-compose.yaml | 4 +- .../files_metadata/domain/FileMeta.scala | 6 ++- .../FilesMetaPostgresRepository.scala | 39 ++++++++++++++----- .../infrastructure/MetadataControllers.scala | 6 ++- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 921158a..bb2e4de 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3.1' services: postgres-db: - image: postgres:latest + image: postgres:alpine3.18 container_name: postgres-db restart: on-failure ports: @@ -15,7 +15,7 @@ services: - ./volumes/postgres:/var/lib/postgresql/data postgres-admin: - image: dpage/pgadmin4:snapshot + image: dpage/pgadmin4:7.6 container_name: postgres-admin ports: - "5050:80" diff --git a/src/main/scala/files_metadata/domain/FileMeta.scala b/src/main/scala/files_metadata/domain/FileMeta.scala index 927e5be..08d2846 100644 --- a/src/main/scala/files_metadata/domain/FileMeta.scala +++ b/src/main/scala/files_metadata/domain/FileMeta.scala @@ -9,7 +9,8 @@ case class FileMeta( parentUuid: Option[UUID], archiveUuid: Option[UUID], volume: String, - name: String + name: String, + isShared: Boolean ) object FileMeta { @@ -24,7 +25,8 @@ object FileMeta { parentUuid = parentUuid, archiveUuid = null, volume = null, - name = name + name = name, + isShared = false ) } } diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 4e7fa40..f60f9d8 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -127,7 +127,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { try { val statement = connection.prepareStatement( - "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name FROM files WHERE uuid = ?" + "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name, is_shared FROM files WHERE uuid = ?" ) statement.setObject( 1, uuid ) @@ -149,7 +149,8 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { parentUuid = parentUUID, archiveUuid = archiveUUID, volume = result.getString( "volume" ), - name = result.getString( "name" ) + name = result.getString( "name" ), + isShared = result.getBoolean( "is_shared" ) ) } else { throw DomainExceptions.FileNotFoundException( @@ -276,14 +277,26 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { if (directoryUuid.isEmpty) { statement = connection.prepareStatement( - "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name FROM files WHERE owner_uuid = ? AND parent_uuid IS NULL AND name = ? Limit 1" + """ + |SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name, is_shared + |FROM files WHERE owner_uuid = ? + |AND parent_uuid IS NULL + |AND name = ? + |Limit 1 + |""".stripMargin ) statement.setObject( 1, ownerUuid ) statement.setString( 2, fileName ) } else { statement = connection.prepareStatement( - "SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name FROM files WHERE owner_uuid = ? AND parent_uuid = ? AND name = ? Limit 1" + """ + |SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name, is_shared FROM files + |WHERE owner_uuid = ? + |AND parent_uuid = ? + |AND name = ? + |Limit 1 + |""".stripMargin ) statement.setObject( 1, ownerUuid ) @@ -311,7 +324,8 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { parentUuid = parentUUID, archiveUuid = archiveUUID, volume = result.getString( "volume" ), - name = result.getString( "name" ) + name = result.getString( "name" ), + isShared = result.getBoolean( "is_shared" ) ) ) } else { @@ -352,16 +366,23 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { override def shareFile( fileUUID: UUID, userUUID: UUID ): Unit = { val connection: Connection = pool.getConnection() + connection.setAutoCommit( false ) try { - val statement = connection.prepareStatement( + val shareStatement = connection.prepareStatement( "INSERT INTO shared_files (file_uuid, user_uuid) VALUES (?, ?)" ) + shareStatement.setObject( 1, fileUUID ) + shareStatement.setObject( 2, userUUID ) + shareStatement.executeUpdate() - statement.setObject( 1, fileUUID ) - statement.setObject( 2, userUUID ) + val updateStatement = connection.prepareStatement( + "UPDATE files SET is_shared = true WHERE uuid = ?" + ) + updateStatement.setObject( 1, fileUUID ) + updateStatement.executeUpdate() - statement.executeUpdate() + connection.commit() } finally { connection.close() } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index af80a9a..e852e66 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -260,7 +260,8 @@ class MetadataControllers { "name" -> fileMeta.name, "extension" -> ujson.Null, "volume" -> fileMeta.volume, - "size" -> 0 + "size" -> 0, + "is_shared" -> fileMeta.isShared ), statusCode = 200 ) @@ -276,7 +277,8 @@ class MetadataControllers { "name" -> fileMeta.name, "extension" -> archivesMeta.extension, "volume" -> fileMeta.volume, - "size" -> archivesMeta.size + "size" -> archivesMeta.size, + "is_shared" -> fileMeta.isShared ), statusCode = 200 ) From 2eaa043fb7cf143ca6164fec257a168072380069 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Tue, 26 Sep 2023 11:46:54 +0000 Subject: [PATCH 48/67] chore(release): v0.10.1 [skip ci] --- CHANGELOG.md | 18 +++++++++--------- version.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64cc5cb..982d551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [0.10.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.0...v0.10.1) (2023-09-26) + + +### Bug Fixes + +* Update is_shared column when a file is shared ([#70](https://github.com/hawks-atlanta/metadata-scala/issues/70)) ([7f63347](https://github.com/hawks-atlanta/metadata-scala/commit/7f63347667d0095616e8d09641d264e888f44ffb)) + + + # [0.10.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.9.0...v0.10.0) (2023-09-21) @@ -34,12 +43,3 @@ -# [0.7.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.6.0...v0.7.0) (2023-09-13) - - -### Features - -* List files shared with user ([#56](https://github.com/hawks-atlanta/metadata-scala/issues/56)) ([4111fea](https://github.com/hawks-atlanta/metadata-scala/commit/4111feacd98f88e19191312ae22cb29c4457b3a6)) - - - diff --git a/version.json b/version.json index eab3c25..7ae2759 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.10.0" + "version": "0.10.1" } \ No newline at end of file From 712fa398339a3df74f4136ccd2d432cb961ec118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= Date: Tue, 26 Sep 2023 09:20:20 -0500 Subject: [PATCH 49/67] chore: Fix merge conflicts --- .../infrastructure/MetadataControllers.scala | 166 ++++-------------- .../files_metadata/FilesTestsUtils.scala | 5 - .../files_metadata/GetSharedWithUser.scala | 96 ---------- .../GetSharedWithWhoTests.scala | 105 ----------- 4 files changed, 31 insertions(+), 341 deletions(-) delete mode 100644 src/test/scala/files_metadata/GetSharedWithUser.scala delete mode 100644 src/test/scala/files_metadata/GetSharedWithWhoTests.scala diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index a887a83..f258ed7 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -61,8 +61,8 @@ class MetadataControllers { } def SaveMetadataController( - request: cask.Request - ): cask.Response[Obj] = { + request: cask.Request + ): cask.Response[Obj] = { try { // Decode the JSON payload val decoded: CreationReqSchema = read[CreationReqSchema]( @@ -142,10 +142,10 @@ class MetadataControllers { } def ShareFileController( - request: cask.Request, - ownerUUID: String, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + ownerUUID: String, + fileUUID: String + ): cask.Response[Obj] = { try { val decoded: ShareReqSchema = read[ShareReqSchema]( request.text() @@ -185,10 +185,10 @@ class MetadataControllers { } def CanReadFileController( - request: cask.Request, - userUUID: String, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { try { val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) @@ -224,9 +224,9 @@ class MetadataControllers { } def GetFileMetadataController( - request: cask.Request, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { try { val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) if (!isFileUUIDValid) { @@ -289,9 +289,9 @@ class MetadataControllers { } def MarkFileAsReadyController( - request: cask.Request, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { try { val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) if (!isFileUUIDValid) { @@ -338,9 +338,9 @@ class MetadataControllers { } def GetSharedWithMeController( - request: cask.Request, - userUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + userUUID: String + ): cask.Response[Obj] = { try { val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) if (!isUserUUIDValid) { @@ -390,9 +390,9 @@ class MetadataControllers { } def GetSharedWithWhoController( - request: cask.Request, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { try { val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) if (!isFileUUIDValid) { @@ -425,10 +425,10 @@ class MetadataControllers { } def RenameFileController( - request: cask.Request, - userUUID: String, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { try { val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) @@ -477,10 +477,10 @@ class MetadataControllers { } def MoveFileController( - request: cask.Request, - userUUID: String, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { try { val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) @@ -531,108 +531,4 @@ class MetadataControllers { case e: Exception => _handleException( e ) } } - - def GetSharedWithMeController( - request: cask.Request, - userUUID: String - ): cask.Response[Obj] = { - try { - val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) - if (!isUserUUIDValid) { - return cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "Fields validation failed" - ), - statusCode = 400 - ) - } - - val filesMeta = useCases.getFilesMetadataSharedWithUser( - userUUID = UUID.fromString( userUUID ) - ) - - val responseArray = ujson.Arr.from( - filesMeta.map( fileMeta => { - val fileType = - if (fileMeta.archiveUuid.isEmpty) "directory" - else "archive" - - ujson.Obj( - "uuid" -> fileMeta.uuid.toString, - "name" -> fileMeta.name, - "fileType" -> fileType - ) - } ) - ) - - cask.Response( - ujson.Obj( - "files" -> responseArray - ), - statusCode = 200 - ) - - } catch { - case _: Exception => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "There was an error while getting the files shared with the user" - ), - statusCode = 500 - ) - } - } - - def GetSharedWithWhoController( - request: cask.Request, - fileUUID: String - ): cask.Response[Obj] = { - try { - val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) - if (!isFileUUIDValid) { - return cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "Fields validation failed" - ), - statusCode = 400 - ) - } - - val usersUUID = useCases.getUsersFileWasSharedWith( - fileUUID = UUID.fromString( fileUUID ) - ) - - val responseArray = ujson.Arr.from( - usersUUID.map( userUUID => userUUID.toString ) - ) - - cask.Response( - ujson.Obj( - "shared_with" -> responseArray - ), - statusCode = 200 - ) - } catch { - case e: BaseDomainException => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> e.message - ), - statusCode = e.statusCode - ) - - case _: Exception => - cask.Response( - ujson.Obj( - "error" -> true, - "message" -> "There was an error while getting the users with whom the file is shared" - ), - statusCode = 500 - ) - } - } -} +} \ No newline at end of file diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index 06a145e..f8fb334 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -1,11 +1,9 @@ package org.hawksatlanta.metadata package files_metadata -import java.security.MessageDigest import java.util import java.util.concurrent.atomic.AtomicBoolean import java.util.UUID -import java.util.UUID import io.restassured.response.Response import io.restassured.RestAssured.`given` @@ -22,9 +20,6 @@ object FilesTestsUtils { } // -- Save files -- - - // -- Save files -- - def SaveFile( payload: util.HashMap[String, Any] ): Response = { `given`() .port( 8080 ) diff --git a/src/test/scala/files_metadata/GetSharedWithUser.scala b/src/test/scala/files_metadata/GetSharedWithUser.scala deleted file mode 100644 index e4102dc..0000000 --- a/src/test/scala/files_metadata/GetSharedWithUser.scala +++ /dev/null @@ -1,96 +0,0 @@ -package org.hawksatlanta.metadata -package files_metadata - -import java.util.UUID - -import org.junit.runner.manipulation.Alphanumeric -import org.junit.runner.OrderWith -import org.junit.Before -import org.junit.Test -import org.scalatestplus.junit.JUnitSuite - -object GetShareWithUserTestsData { - val API_PREFIX: String = "/api/v1/files/shared_with_me" - val OWNER_USER_UUID: UUID = UUID.randomUUID() - val OTHER_USER_UUID: UUID = UUID.randomUUID() - - var savedFileUUID: UUID = _ - var savedDirectoryUUID: UUID = _ -} - -@OrderWith( classOf[Alphanumeric] ) -class GetSharedWithUser extends JUnitSuite { - def saveAndShareFilesToObtain(): Unit = { - // Save a directory - val saveDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( - ownerUUID = GetShareWithUserTestsData.OWNER_USER_UUID, - parentDirUUID = None - ) - - val saveDirectoryResponse = FilesTestsUtils.SaveFile( saveDirectoryPayload ) - GetShareWithUserTestsData.savedDirectoryUUID = - UUID.fromString( saveDirectoryResponse.jsonPath().get( "uuid" ) ) - - // Save a file - val saveFilePayload = FilesTestsUtils.generateFilePayload( - ownerUUID = GetShareWithUserTestsData.OWNER_USER_UUID, - parentDirUUID = None - ) - val saveFileResponse = FilesTestsUtils.SaveFile( saveFilePayload ) - GetShareWithUserTestsData.savedFileUUID = - UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) - - // Share the file and the directory - val shareFilePayload = FilesTestsUtils.generateShareFilePayload( - otherUserUUID = GetShareWithUserTestsData.OTHER_USER_UUID - ) - - FilesTestsUtils.ShareFile( - GetShareWithUserTestsData.OWNER_USER_UUID.toString, - GetShareWithUserTestsData.savedDirectoryUUID.toString, - shareFilePayload - ) - FilesTestsUtils.ShareFile( - GetShareWithUserTestsData.OWNER_USER_UUID.toString, - GetShareWithUserTestsData.savedFileUUID.toString, - shareFilePayload - ) - } - - @Before - def startHttpServer(): Unit = { - FilesTestsUtils.StartHttpServer() - } - - @Test - def T1_GetSharedWithUserBadRequest(): Unit = { - saveAndShareFilesToObtain() - - val response = FilesTestsUtils.GetSharedWithUser( - "not_an_uuid" - ) - assert( response.statusCode() == 400 ) - } - - @Test - def T2_GetSharedWithUserSuccess(): Unit = { - val response = FilesTestsUtils.GetSharedWithUser( - GetShareWithUserTestsData.OTHER_USER_UUID.toString - ) - assert( response.statusCode() == 200 ) - assert( - response.jsonPath().getList( "files" ).size() == 2 - ) - } - - @Test - def T3_GetSharedWithUserEmpty(): Unit = { - val response = FilesTestsUtils.GetSharedWithUser( - UUID.randomUUID().toString - ) - assert( response.statusCode() == 200 ) - assert( - response.jsonPath().getList( "files" ).size() == 0 - ) - } -} diff --git a/src/test/scala/files_metadata/GetSharedWithWhoTests.scala b/src/test/scala/files_metadata/GetSharedWithWhoTests.scala deleted file mode 100644 index 431d16b..0000000 --- a/src/test/scala/files_metadata/GetSharedWithWhoTests.scala +++ /dev/null @@ -1,105 +0,0 @@ -package org.hawksatlanta.metadata -package files_metadata - -import java.util.UUID - -import org.junit.runner.manipulation.Alphanumeric -import org.junit.runner.OrderWith -import org.junit.Before -import org.junit.Test -import org.scalatestplus.junit.JUnitSuite - -object GetShareWithWhoTestsData { - val API_PREFIX: String = "/api/v1/files/shared_with_who" - val OWNER_USER_UUID: UUID = UUID.randomUUID() - val OTHER_USER_UUID: UUID = UUID.randomUUID() - - var sharedFileUUID: UUID = _ - var unsharedFileUUID: UUID = _ -} - -@OrderWith( classOf[Alphanumeric] ) -class GetSharedWithWhoTests extends JUnitSuite { - def saveAndShareFilesToCheck(): Unit = { - // Save and share a file - val filePayload = FilesTestsUtils.generateFilePayload( - GetShareWithWhoTestsData.OWNER_USER_UUID, - None - ) - val sharePayload = FilesTestsUtils.generateShareFilePayload( - GetShareWithWhoTestsData.OTHER_USER_UUID - ) - - val saveFileResponse = FilesTestsUtils.SaveFile( filePayload ) - GetShareWithWhoTestsData.sharedFileUUID = - UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) - - FilesTestsUtils.ShareFile( - GetShareWithWhoTestsData.OWNER_USER_UUID.toString, - GetShareWithWhoTestsData.sharedFileUUID.toString, - sharePayload - ) - - // Save and don't share a file - val secondFilePayload = FilesTestsUtils.generateFilePayload( - GetShareWithWhoTestsData.OWNER_USER_UUID, - None - ) - - val saveSecondFileResponse = FilesTestsUtils.SaveFile( secondFilePayload ) - GetShareWithWhoTestsData.unsharedFileUUID = - UUID.fromString( saveSecondFileResponse.jsonPath().get( "uuid" ) ) - } - - @Before - def before(): Unit = { - FilesTestsUtils.StartHttpServer() - } - - @Test - def T1_SharedWithWhoBadRequest(): Unit = { - saveAndShareFilesToCheck() - - val response = FilesTestsUtils.GetSharedWithWho( - "not_an_uuid" - ) - - assert( response.statusCode() == 400 ) - } - - @Test - def T2_SharedWithWhoNotFound(): Unit = { - val response = FilesTestsUtils.GetSharedWithWho( - UUID.randomUUID().toString - ) - assert( response.statusCode() == 404 ) - } - - @Test - def T3_SharedWithWhoSuccess(): Unit = { - val response = FilesTestsUtils.GetSharedWithWho( - GetShareWithWhoTestsData.sharedFileUUID.toString - ) - val responseJson = response.jsonPath() - - assert( response.statusCode() == 200 ) - assert( responseJson.getList( "shared_with" ).size() == 1 ) - assert( - responseJson - .getList( "shared_with" ) - .get( 0 ) - .equals( GetShareWithWhoTestsData.OTHER_USER_UUID.toString ) - ) - } - - @Test - def T4_SharedWithWhoEmpty(): Unit = { - val response = FilesTestsUtils.GetSharedWithWho( - GetShareWithWhoTestsData.unsharedFileUUID.toString - ) - val responseJson = response.jsonPath() - - assert( response.statusCode() == 200 ) - assert( responseJson.getList( "shared_with" ).size() == 0 ) - } -} From fa5467f86bda9312a6dad474bbdd3f5360a875c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Fri, 29 Sep 2023 06:25:57 -0500 Subject: [PATCH 50/67] fix: Ignore ready state validations for directories (#73) * fix: Skip directories ready check * fix: Use transaction to update volume and ready columns * fix: Update query to obtain files shared with an user * test: Update tests --- .../application/FilesMetaUseCases.scala | 21 +++--- .../domain/FilesMetaRepository.scala | 4 +- .../FilesMetaPostgresRepository.scala | 42 +++++------ .../infrastructure/MetadataControllers.scala | 74 ++++++++++--------- .../scala/files_metadata/GetMetadata.scala | 13 +--- .../scala/files_metadata/SharedWithMe.scala | 7 +- .../scala/files_metadata/UpdateToReady.scala | 10 +-- 7 files changed, 73 insertions(+), 98 deletions(-) diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index 045011c..4dfb104 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -92,22 +92,23 @@ class FilesMetaUseCases { def updateSavedFile( fileUUID: UUID, volume: String ): Unit = { val fileMetadata = repository.getFileMeta( fileUUID ) - if (fileMetadata.volume != null) { + // Skip if the file is a directory + val fileIsDirectory = fileMetadata.archiveUuid.isEmpty + if (fileIsDirectory) { throw DomainExceptions.FileAlreadyMarkedAsReadyException( - "The file was already marked as ready" + "Directories cannot be marked as ready" ) } - // If the file is an archive, update the archive status - if (fileMetadata.archiveUuid.isDefined) { - val archiveMetadata = - repository.getArchiveMeta( fileMetadata.archiveUuid.get ) - - repository.updateArchiveStatus( archiveMetadata.uuid, ready = true ) + // Skip if the file is already marked as ready + if (fileMetadata.volume != null) { + throw DomainExceptions.FileAlreadyMarkedAsReadyException( + "The file was already marked as ready" + ) } - // Update the file volume - repository.updateFileVolume( fileUUID, volume ) + // Update the status and volume + repository.updateArchiveToReady( fileMetadata, volume ) } def getFileMetadata( fileUUID: UUID ): FileMeta = { diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index fabc3ed..72009cc 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -41,9 +41,7 @@ trait FilesMetaRepository { def canUserReadFile( userUuid: UUID, fileUuid: UUID ): Boolean // --- Update --- - def updateArchiveStatus( archiveUUID: UUID, ready: Boolean ): Unit - - def updateFileVolume( fileUUID: UUID, volume: String ): Unit + def updateArchiveToReady( file: FileMeta, volume: String ): Unit def updateFileName( fileUUID: UUID, newName: String ): Unit diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index f60f9d8..2e0becf 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -202,7 +202,10 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { |uuid IN ( | SELECT file_uuid FROM shared_files WHERE user_uuid = ? |) - |AND volume IS NOT NULL + |AND ( + | archive_uuid is NULL + | OR volume IS NOT NULL + |) | """.stripMargin ) statement.setObject( 1, userUuid ) @@ -407,39 +410,28 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } - override def updateArchiveStatus( - archiveUUID: UUID, - ready: Boolean + override def updateArchiveToReady( + file: FileMeta, + volume: String ): Unit = { val connection: Connection = pool.getConnection() + connection.setAutoCommit( false ) try { - val statement = connection.prepareStatement( - "UPDATE archives SET ready = ? WHERE uuid = ?" + val updateArchiveStatement = connection.prepareStatement( + "UPDATE archives SET ready = true WHERE uuid = ?" ) - statement.setBoolean( 1, ready ) - statement.setObject( 2, archiveUUID ) - - statement.executeUpdate() - } finally { - connection.close() - } - } + updateArchiveStatement.setObject( 1, file.archiveUuid.get ) + updateArchiveStatement.executeUpdate() - def updateFileVolume( - fileUUID: UUID, - volume: String - ): Unit = { - val connection: Connection = pool.getConnection() - - try { - val statement = connection.prepareStatement( + val updateFileStatement = connection.prepareStatement( "UPDATE files SET volume = ? WHERE uuid = ?" ) - statement.setString( 1, volume ) - statement.setObject( 2, fileUUID ) + updateFileStatement.setString( 1, volume ) + updateFileStatement.setObject( 2, file.uuid ) + updateFileStatement.executeUpdate() - statement.executeUpdate() + connection.commit() } finally { connection.close() } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index f258ed7..1f758c7 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -61,8 +61,8 @@ class MetadataControllers { } def SaveMetadataController( - request: cask.Request - ): cask.Response[Obj] = { + request: cask.Request + ): cask.Response[Obj] = { try { // Decode the JSON payload val decoded: CreationReqSchema = read[CreationReqSchema]( @@ -142,10 +142,10 @@ class MetadataControllers { } def ShareFileController( - request: cask.Request, - ownerUUID: String, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + ownerUUID: String, + fileUUID: String + ): cask.Response[Obj] = { try { val decoded: ShareReqSchema = read[ShareReqSchema]( request.text() @@ -185,10 +185,10 @@ class MetadataControllers { } def CanReadFileController( - request: cask.Request, - userUUID: String, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { try { val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) @@ -224,9 +224,9 @@ class MetadataControllers { } def GetFileMetadataController( - request: cask.Request, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { try { val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) if (!isFileUUIDValid) { @@ -243,7 +243,9 @@ class MetadataControllers { fileUUID = UUID.fromString( fileUUID ) ) - if (fileMeta.volume == null) { + val isFile = fileMeta.archiveUuid.isDefined + val notSavedYet = fileMeta.volume == null + if (isFile && notSavedYet) { return cask.Response( ujson.Obj( "message" -> "The file is not ready yet" @@ -256,12 +258,12 @@ class MetadataControllers { // Directories metadata cask.Response( ujson.Obj( - "archiveUUID" -> ujson.Null, // Needs to be a "custom" null value "name" -> fileMeta.name, + "is_shared" -> fileMeta.isShared, + "archiveUUID" -> ujson.Null, "extension" -> ujson.Null, - "volume" -> fileMeta.volume, - "size" -> 0, - "is_shared" -> fileMeta.isShared + "volume" -> ujson.Null, + "size" -> 0 ), statusCode = 200 ) @@ -289,9 +291,9 @@ class MetadataControllers { } def MarkFileAsReadyController( - request: cask.Request, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { try { val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) if (!isFileUUIDValid) { @@ -338,9 +340,9 @@ class MetadataControllers { } def GetSharedWithMeController( - request: cask.Request, - userUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + userUUID: String + ): cask.Response[Obj] = { try { val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) if (!isUserUUIDValid) { @@ -390,9 +392,9 @@ class MetadataControllers { } def GetSharedWithWhoController( - request: cask.Request, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + fileUUID: String + ): cask.Response[Obj] = { try { val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) if (!isFileUUIDValid) { @@ -425,10 +427,10 @@ class MetadataControllers { } def RenameFileController( - request: cask.Request, - userUUID: String, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { try { val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) @@ -477,10 +479,10 @@ class MetadataControllers { } def MoveFileController( - request: cask.Request, - userUUID: String, - fileUUID: String - ): cask.Response[Obj] = { + request: cask.Request, + userUUID: String, + fileUUID: String + ): cask.Response[Obj] = { try { val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) @@ -531,4 +533,4 @@ class MetadataControllers { case e: Exception => _handleException( e ) } } -} \ No newline at end of file +} diff --git a/src/test/scala/files_metadata/GetMetadata.scala b/src/test/scala/files_metadata/GetMetadata.scala index 072b5f1..e42fc15 100644 --- a/src/test/scala/files_metadata/GetMetadata.scala +++ b/src/test/scala/files_metadata/GetMetadata.scala @@ -12,7 +12,7 @@ import org.scalatestplus.junit.JUnitSuite object GetFileMetadataTestsData { val API_PREFIX: String = "/api/v1/files/metadata" val OWNER_UUID: UUID = UUID.randomUUID() - val VOLUME_NAME: String = "volume_x" + val VOLUME_NAME: String = "1" var savedFileUUID: UUID = _ var savedDirectoryUUID: UUID = _ @@ -41,7 +41,7 @@ class GetFileMetadataTests extends JUnitSuite { ) } - def markFilesAsReady(): Unit = { + def markFileAsReady(): Unit = { val updatePayload = new java.util.HashMap[String, Any]() updatePayload.put( "volume", GetFileMetadataTestsData.VOLUME_NAME ) @@ -49,11 +49,6 @@ class GetFileMetadataTests extends JUnitSuite { GetFileMetadataTestsData.savedFileUUID.toString, updatePayload ) - - FilesTestsUtils.UpdateReadyFile( - GetFileMetadataTestsData.savedDirectoryUUID.toString, - updatePayload - ) } @Before @@ -84,7 +79,7 @@ class GetFileMetadataTests extends JUnitSuite { @Test def fileMetadataWithReadyFileUUID(): Unit = { - markFilesAsReady() + markFileAsReady() // Get the file val response = FilesTestsUtils.GetFileMetadata( @@ -108,7 +103,7 @@ class GetFileMetadataTests extends JUnitSuite { directoryResponse .body() .jsonPath() - .getString( "volume" ) == GetFileMetadataTestsData.VOLUME_NAME + .getString( "volume" ) == null ) assert( directoryResponse diff --git a/src/test/scala/files_metadata/SharedWithMe.scala b/src/test/scala/files_metadata/SharedWithMe.scala index 5ed027d..bac25b6 100644 --- a/src/test/scala/files_metadata/SharedWithMe.scala +++ b/src/test/scala/files_metadata/SharedWithMe.scala @@ -40,12 +40,7 @@ class GetSharedWithUser extends JUnitSuite { GetShareWithUserTestsData.savedFileUUID = UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) - // Mark the file and the directory as ready - FilesTestsUtils.UpdateReadyFile( - GetShareWithUserTestsData.savedDirectoryUUID.toString, - FilesTestsUtils.generateReadyFilePayload() - ) - + // Mark the file as ready FilesTestsUtils.UpdateReadyFile( GetShareWithUserTestsData.savedFileUUID.toString, FilesTestsUtils.generateReadyFilePayload() diff --git a/src/test/scala/files_metadata/UpdateToReady.scala b/src/test/scala/files_metadata/UpdateToReady.scala index d56bb8e..3bbbeb4 100644 --- a/src/test/scala/files_metadata/UpdateToReady.scala +++ b/src/test/scala/files_metadata/UpdateToReady.scala @@ -101,7 +101,7 @@ class UpdateReadyFile extends JUnitSuite { UpdateReadyFileTestsData.getPayloadCopy() ) - assert( updateDirectoryResponse.statusCode() == 204 ) + assert( updateDirectoryResponse.statusCode() == 409 ) } @Test @@ -113,13 +113,5 @@ class UpdateReadyFile extends JUnitSuite { ) assert( updateFileResponse.statusCode() == 409 ) - - // Try to mark the directory as ready again - val updateDirectoryResponse = FilesTestsUtils.UpdateReadyFile( - UpdateReadyFileTestsData.savedDirectoryUUID.toString, - UpdateReadyFileTestsData.getPayloadCopy() - ) - - assert( updateDirectoryResponse.statusCode() == 409 ) } } From 053743bb6504ccdbbe9e084efdda160aa869527a Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Fri, 29 Sep 2023 11:29:02 +0000 Subject: [PATCH 51/67] chore(release): v0.10.2 [skip ci] --- CHANGELOG.md | 30 +++++++++++++++++++++++------- version.json | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb64ee..aa7bb32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,45 @@ +## [0.10.2](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.1...v0.10.2) (2023-09-29) + + +### Bug Fixes + +* Ignore ready state validations for directories ([#73](https://github.com/hawks-atlanta/metadata-scala/issues/73)) ([fa5467f](https://github.com/hawks-atlanta/metadata-scala/commit/fa5467f86bda9312a6dad474bbdd3f5360a875c9)) + + + ## [0.10.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.0...v0.10.1) (2023-09-26) + ### Bug Fixes -- Update is_shared column when a file is shared ([#70](https://github.com/hawks-atlanta/metadata-scala/issues/70)) ([7f63347](https://github.com/hawks-atlanta/metadata-scala/commit/7f63347667d0095616e8d09641d264e888f44ffb)) +* Update is_shared column when a file is shared ([#70](https://github.com/hawks-atlanta/metadata-scala/issues/70)) ([7f63347](https://github.com/hawks-atlanta/metadata-scala/commit/7f63347667d0095616e8d09641d264e888f44ffb)) + + # [0.10.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.9.0...v0.10.0) (2023-09-21) + ### Features -- Move file ([#67](https://github.com/hawks-atlanta/metadata-scala/issues/67)) ([0684246](https://github.com/hawks-atlanta/metadata-scala/commit/06842463a6c24c2b38569991bfb8cb6c5caf15e6)) +* Move file ([#67](https://github.com/hawks-atlanta/metadata-scala/issues/67)) ([0684246](https://github.com/hawks-atlanta/metadata-scala/commit/06842463a6c24c2b38569991bfb8cb6c5caf15e6)) + + # [0.9.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.8.1...v0.9.0) (2023-09-18) + ### Features -- Rename files ([#63](https://github.com/hawks-atlanta/metadata-scala/issues/63)) ([6a91d21](https://github.com/hawks-atlanta/metadata-scala/commit/6a91d2119e034c70c3381b2475da9434d77f02b7)), closes [#64](https://github.com/hawks-atlanta/metadata-scala/issues/64) +* Rename files ([#63](https://github.com/hawks-atlanta/metadata-scala/issues/63)) ([6a91d21](https://github.com/hawks-atlanta/metadata-scala/commit/6a91d2119e034c70c3381b2475da9434d77f02b7)), closes [#64](https://github.com/hawks-atlanta/metadata-scala/issues/64) + + ## [0.8.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.8.0...v0.8.1) (2023-09-15) + ### Bug Fixes -- Production migrations ([#61](https://github.com/hawks-atlanta/metadata-scala/issues/61)) ([db1f85c](https://github.com/hawks-atlanta/metadata-scala/commit/db1f85c28a2b64000e81341ce4f880bbcc748da3)) +* Production migrations ([#61](https://github.com/hawks-atlanta/metadata-scala/issues/61)) ([db1f85c](https://github.com/hawks-atlanta/metadata-scala/commit/db1f85c28a2b64000e81341ce4f880bbcc748da3)) -# [0.8.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.7.0...v0.8.0) (2023-09-15) -### Features -- Shared with who ([#57](https://github.com/hawks-atlanta/metadata-scala/issues/57)) ([4cbb5bb](https://github.com/hawks-atlanta/metadata-scala/commit/4cbb5bbfe61fd0dc1c94e0315c97b88c9d141e3d)) diff --git a/version.json b/version.json index 7ae2759..2294f0c 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.10.1" + "version": "0.10.2" } \ No newline at end of file From 9ae4d0da4f1fb0720de33715f89686286e6597ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Sun, 1 Oct 2023 07:17:59 -0500 Subject: [PATCH 52/67] fix: Parse null-able extension field (#75) --- .../infrastructure/MetadataControllers.scala | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 1f758c7..9208679 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -60,6 +60,13 @@ class MetadataControllers { } } + private def parseNullableValueToJSON( value: Any ): ujson.Value = { + value match { + case v: ujson.Value => v + case _ => ujson.Null + } + } + def SaveMetadataController( request: cask.Request ): cask.Response[Obj] = { @@ -277,7 +284,7 @@ class MetadataControllers { ujson.Obj( "archiveUUID" -> fileMeta.archiveUuid.get.toString, "name" -> fileMeta.name, - "extension" -> archivesMeta.extension, + "extension" -> parseNullableValueToJSON( archivesMeta.extension ), "volume" -> fileMeta.volume, "size" -> archivesMeta.size, "is_shared" -> fileMeta.isShared @@ -373,7 +380,7 @@ class MetadataControllers { "uuid" -> fileMeta.uuid.toString, "fileType" -> "archive", "name" -> fileMeta.name, - "extension" -> fileMeta.extension + "extension" -> parseNullableValueToJSON( fileMeta.extension ) ) } } ) From d5c3ba6be85e24aedc6136030e6e6efde4e7a2fb Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Sun, 1 Oct 2023 12:19:57 +0000 Subject: [PATCH 53/67] chore(release): v0.10.3 [skip ci] --- CHANGELOG.md | 18 +++++++++--------- version.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7bb32..39a9b50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [0.10.3](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.2...v0.10.3) (2023-10-01) + + +### Bug Fixes + +* Parse null-able extension field ([#75](https://github.com/hawks-atlanta/metadata-scala/issues/75)) ([9ae4d0d](https://github.com/hawks-atlanta/metadata-scala/commit/9ae4d0da4f1fb0720de33715f89686286e6597ef)) + + + ## [0.10.2](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.1...v0.10.2) (2023-09-29) @@ -34,12 +43,3 @@ -## [0.8.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.8.0...v0.8.1) (2023-09-15) - - -### Bug Fixes - -* Production migrations ([#61](https://github.com/hawks-atlanta/metadata-scala/issues/61)) ([db1f85c](https://github.com/hawks-atlanta/metadata-scala/commit/db1f85c28a2b64000e81341ce4f880bbcc748da3)) - - - diff --git a/version.json b/version.json index 2294f0c..9cf86e1 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.10.2" + "version": "0.10.3" } \ No newline at end of file From f825a844532ef928227305c5ed30407736df746b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= Date: Sun, 1 Oct 2023 07:53:55 -0500 Subject: [PATCH 54/67] refactor: Use same filename in ArchiveMeta class to fix warning --- .../files_metadata/application/FilesMetaUseCases.scala | 6 +++--- src/main/scala/files_metadata/domain/ArchiveMeta.scala | 8 ++++---- .../files_metadata/domain/FilesMetaRepository.scala | 8 ++------ .../infrastructure/FilesMetaPostgresRepository.scala | 10 +++++----- .../infrastructure/MetadataControllers.scala | 6 +++--- 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index 4dfb104..28db3e0 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -3,7 +3,7 @@ package files_metadata.application import java.util.UUID -import files_metadata.domain.ArchivesMeta +import files_metadata.domain.ArchiveMeta import files_metadata.domain.DomainExceptions import files_metadata.domain.FileExtendedMeta import files_metadata.domain.FileMeta @@ -41,7 +41,7 @@ class FilesMetaUseCases { } def saveArchiveMetadata( - archiveMeta: ArchivesMeta, + archiveMeta: ArchiveMeta, fileMeta: FileMeta ): UUID = { ensureFileCanBeCreated( fileMeta = fileMeta ) @@ -115,7 +115,7 @@ class FilesMetaUseCases { repository.getFileMeta( fileUUID ) } - def getArchiveMetadata( archiveUUID: UUID ): ArchivesMeta = { + def getArchiveMetadata( archiveUUID: UUID ): ArchiveMeta = { repository.getArchiveMeta( archiveUUID ) } diff --git a/src/main/scala/files_metadata/domain/ArchiveMeta.scala b/src/main/scala/files_metadata/domain/ArchiveMeta.scala index a686e2a..1bd9d47 100644 --- a/src/main/scala/files_metadata/domain/ArchiveMeta.scala +++ b/src/main/scala/files_metadata/domain/ArchiveMeta.scala @@ -3,19 +3,19 @@ package files_metadata.domain import java.util.UUID -case class ArchivesMeta( +case class ArchiveMeta( uuid: UUID, extension: String, size: Long, ready: Boolean ) -object ArchivesMeta { +object ArchiveMeta { def createNewArchive( extension: String, size: Long - ): ArchivesMeta = - new ArchivesMeta( + ): ArchiveMeta = + new ArchiveMeta( uuid = null, ready = false, extension = extension, diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index b658c16..5ed7f6c 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -5,7 +5,7 @@ import java.util.UUID trait FilesMetaRepository { // --- Create --- - def saveArchiveMeta( archiveMeta: ArchivesMeta, fileMeta: FileMeta ): UUID + def saveArchiveMeta( archiveMeta: ArchiveMeta, fileMeta: FileMeta ): UUID def saveDirectoryMeta( fileMeta: FileMeta ): UUID @@ -21,7 +21,7 @@ trait FilesMetaRepository { def getFileMeta( uuid: UUID ): FileMeta - def getArchiveMeta( uuid: UUID ): ArchivesMeta + def getArchiveMeta( uuid: UUID ): ArchiveMeta def getFilesSharedWithUserMeta( userUuid: UUID ): Seq[FileExtendedMeta] @@ -47,10 +47,6 @@ trait FilesMetaRepository { def updateFileParent( fileUUID: UUID, parentUUID: Option[UUID] ): Unit - def updateFileName( fileUUID: UUID, newName: String ): Unit - - def updateFileParent( fileUUID: UUID, parentUUID: Option[UUID] ): Unit - // --- Delete --- def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit } diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index c867372..32b7922 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -6,7 +6,7 @@ import java.sql.PreparedStatement import java.util.UUID import com.zaxxer.hikari.HikariDataSource -import files_metadata.domain.ArchivesMeta +import files_metadata.domain.ArchiveMeta import files_metadata.domain.DomainExceptions import files_metadata.domain.FileExtendedMeta import files_metadata.domain.FileMeta @@ -49,7 +49,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } override def saveArchiveMeta( - archivesMeta: ArchivesMeta, + archivesMeta: ArchiveMeta, fileMeta: FileMeta ): UUID = { val connection: Connection = pool.getConnection() @@ -162,7 +162,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } - override def getArchiveMeta( uuid: UUID ): ArchivesMeta = { + override def getArchiveMeta( uuid: UUID ): ArchiveMeta = { val connection: Connection = pool.getConnection() try { @@ -178,7 +178,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { ) } - ArchivesMeta( + ArchiveMeta( uuid = UUID.fromString( result.getString( "uuid" ) ), extension = result.getString( "extension" ), size = result.getLong( "size" ), @@ -476,4 +476,4 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } override def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit = ??? -} \ No newline at end of file +} diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 40234dc..5422160 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -6,7 +6,7 @@ import java.util.UUID import com.wix.accord.validate import com.wix.accord.Validator import files_metadata.application.FilesMetaUseCases -import files_metadata.domain.ArchivesMeta +import files_metadata.domain.ArchiveMeta import files_metadata.domain.BaseDomainException import files_metadata.domain.FileMeta import files_metadata.domain.FilesMetaRepository @@ -107,7 +107,7 @@ class MetadataControllers { } // Save the metadata - val receivedArchiveMeta = ArchivesMeta.createNewArchive( + val receivedArchiveMeta = ArchiveMeta.createNewArchive( decoded.fileExtension, decoded.fileSize ) @@ -540,4 +540,4 @@ class MetadataControllers { case e: Exception => _handleException( e ) } } -} \ No newline at end of file +} From 26914939f0e4cfc7580d97687901f20833474ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= Date: Sun, 1 Oct 2023 07:54:45 -0500 Subject: [PATCH 55/67] ci: Run tests when a pull request is created to main --- .github/workflows/testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 37aeaa2..74e448a 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -2,7 +2,7 @@ name: Test on: pull_request: - branches: ["dev"] + branches: [ "dev","main" ] jobs: build: From 0286714f27d108d89f58e79bc0d6523ac0d9177e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= Date: Sun, 1 Oct 2023 07:57:25 -0500 Subject: [PATCH 56/67] style: Format file --- src/test/scala/files_metadata/GetMetadata.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/files_metadata/GetMetadata.scala b/src/test/scala/files_metadata/GetMetadata.scala index 876dc46..e42fc15 100644 --- a/src/test/scala/files_metadata/GetMetadata.scala +++ b/src/test/scala/files_metadata/GetMetadata.scala @@ -112,4 +112,4 @@ class GetFileMetadataTests extends JUnitSuite { .getString( "archiveUUID" ) == null ) } -} \ No newline at end of file +} From 34f60cb3bc0baa4e8b5851c5b1a7487f19da8290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= Date: Sun, 1 Oct 2023 08:00:02 -0500 Subject: [PATCH 57/67] style: Format file --- src/test/scala/files_metadata/SharedWithMe.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/files_metadata/SharedWithMe.scala b/src/test/scala/files_metadata/SharedWithMe.scala index 3182589..bac25b6 100644 --- a/src/test/scala/files_metadata/SharedWithMe.scala +++ b/src/test/scala/files_metadata/SharedWithMe.scala @@ -99,4 +99,4 @@ class GetSharedWithUser extends JUnitSuite { response.jsonPath().getList( "files" ).size() == 0 ) } -} \ No newline at end of file +} From 30dbcff56fd1f4dca2005ee65521bd939d98bb87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:55:59 -0500 Subject: [PATCH 58/67] fix: Update controllers error handler (#80) * refactor: Print log when non-domain exceptions are caught * fix: Caught ujson parse exception as a bad request error --- .../infrastructure/MetadataControllers.scala | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 5422160..2bae1a6 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -1,6 +1,7 @@ package org.hawksatlanta.metadata package files_metadata.infrastructure +import java.util.Date import java.util.UUID import com.wix.accord.validate @@ -31,7 +32,8 @@ class MetadataControllers { private def _handleException( exception: Exception ): cask.Response[Obj] = { exception match { - case _: upickle.core.AbortException | _: ujson.IncompleteParseException => + case _: upickle.core.AbortException | _: ujson.IncompleteParseException | + _: ujson.ParseException => cask.Response( ujson.Obj( "error" -> true, @@ -49,7 +51,14 @@ class MetadataControllers { statusCode = e.statusCode ) - case _: Exception => + case e: Exception => + // Log the error + val currentDate = new Date() + println( + s"[${ currentDate.toString }] The following error was caught: $e" + ) + + // Send a response cask.Response( ujson.Obj( "error" -> true, From 30032cd1c481f7c7b3ea9ef1287863af285b23b5 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Tue, 3 Oct 2023 19:59:44 +0000 Subject: [PATCH 59/67] chore(release): v0.10.4 [skip ci] --- CHANGELOG.md | 15 +++++++++------ version.json | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a110b..294e1bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [0.10.4](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.3...v0.10.4) (2023-10-03) + + +### Bug Fixes + +* Update controllers error handler ([#80](https://github.com/hawks-atlanta/metadata-scala/issues/80)) ([30dbcff](https://github.com/hawks-atlanta/metadata-scala/commit/30dbcff56fd1f4dca2005ee65521bd939d98bb87)) + + + ## [0.10.3](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.2...v0.10.3) (2023-10-01) @@ -34,9 +43,3 @@ -# [0.9.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.8.1...v0.9.0) (2023-09-18) - - -### Features - -* Rename files ([#63](https://github.com/hawks-atlanta/metadata-scala/issues/63)) ([6a91d21](https://github.com/hawks-atlanta/metadata-scala/commit/6a91d2119e034c70c3381b2475da9434d77f02b7)), closes [#64](https://github.com/hawks-atlanta/metadata-scala/issues/64) diff --git a/version.json b/version.json index 9cf86e1..fe98e6f 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.10.3" + "version": "0.10.4" } \ No newline at end of file From 75f15598787454d9e259d3de16b64126ce217da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:16:31 -0500 Subject: [PATCH 60/67] feat: List files (#78) * feat: Add `isShared` field to `FileExtendedMeta` entity * refactor: Add domain exception to throw when an user cannot read a file * feat: Add new method to the repository to list files * feat: Create endpoint to list files * fix: Fix query to list directories to include nested directories * test: Add tests to the endpoint to list directories * refactor: Remove redundant if statements * docs(http): Add http examples for the new endpoint * docs(openapi): Update parent uuid parameter in the openapi spec * fix: Update function to parse nullable strings to ujson value --- docs/rest/list_files.http | 7 + docs/spec.openapi.yaml | 6 +- .../application/FilesMetaUseCases.scala | 27 ++ .../domain/DomainExceptions.scala | 3 + .../domain/FileExtendedMeta.scala | 3 +- .../domain/FilesMetaRepository.scala | 6 +- .../FilesMetaPostgresRepository.scala | 128 +++++++++- .../infrastructure/MetadataControllers.scala | 86 ++++++- .../infrastructure/MetadataRoutes.scala | 8 + .../files_metadata/FilesTestsUtils.scala | 20 ++ .../ListFilesInDirectories.scala | 230 ++++++++++++++++++ 11 files changed, 503 insertions(+), 21 deletions(-) create mode 100644 docs/rest/list_files.http create mode 100644 src/test/scala/files_metadata/ListFilesInDirectories.scala diff --git a/docs/rest/list_files.http b/docs/rest/list_files.http new file mode 100644 index 0000000..f303077 --- /dev/null +++ b/docs/rest/list_files.http @@ -0,0 +1,7 @@ +### List files in root directory + +GET http://localhost:8080/api/v1/files/list/{userUUID} + +### List files in subdirectory + +GET http://localhost:8080/api/v1/files/list/{userUUID}?parentUUID={parentUUID} \ No newline at end of file diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml index c14edc6..83d9ead 100644 --- a/docs/spec.openapi.yaml +++ b/docs/spec.openapi.yaml @@ -16,7 +16,7 @@ paths: /files/list/{user_uuid}: get: tags: ["File"] - description: List files in the given directory. List the user's root directory by default when the parent_uuid query parameter is not provided. + description: List files in the given directory. List the user's root directory by default when the parentUUID query parameter is not provided. parameters: - in: path name: user_uuid @@ -25,7 +25,7 @@ paths: example: "658b4e63-b5ac-46a7-ac43-efb6a1415130" required: true - in: query - name: parent_uuid + name: parentUUID schema: type: string example: "5ad724f0-4091-453a-914a-c2d11d69d1e3" @@ -46,7 +46,7 @@ paths: schema: $ref: "#/components/schemas/error_response" "404": - description: Not found. No directory with the given parent_uuid was found. + description: Not found. No directory with the given parentUUID was found. content: application/json: schema: diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index 28db3e0..d565ba2 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -40,6 +40,33 @@ class FilesMetaUseCases { } } + def listFiles( + userUUID: UUID, + parentUUID: Option[UUID] + ): Seq[FileExtendedMeta] = { + if (parentUUID.isDefined) { + // Check the parent exists + val parentMeta = repository.getFileMeta( parentUUID.get ) + + // Check the parent is a directory + val parentIsDirectory = parentMeta.archiveUuid.isEmpty + if (!parentIsDirectory) { + throw DomainExceptions.ParentIsNotADirectoryException( + "The parent is not a directory" + ) + } + + // Check the user has access to the parent + if (!canReadFile( userUUID, parentUUID.get )) { + throw DomainExceptions.CannotReadFileException( + "You do not have access to the parent directory" + ) + } + } + + repository.getFilesMetaInDirectory( userUUID, parentUUID ) + } + def saveArchiveMetadata( archiveMeta: ArchiveMeta, fileMeta: FileMeta diff --git a/src/main/scala/files_metadata/domain/DomainExceptions.scala b/src/main/scala/files_metadata/domain/DomainExceptions.scala index e18367b..21912e8 100644 --- a/src/main/scala/files_metadata/domain/DomainExceptions.scala +++ b/src/main/scala/files_metadata/domain/DomainExceptions.scala @@ -25,6 +25,9 @@ object DomainExceptions { case class FileNotOwnedException( override val message: String ) extends BaseDomainException( message, 403 ) + case class CannotReadFileException( override val message: String ) + extends BaseDomainException( message, 403 ) + case class ArchiveNotSavedException( override val message: String ) extends BaseDomainException( message, 500 ) diff --git a/src/main/scala/files_metadata/domain/FileExtendedMeta.scala b/src/main/scala/files_metadata/domain/FileExtendedMeta.scala index f8e72f8..a204d52 100644 --- a/src/main/scala/files_metadata/domain/FileExtendedMeta.scala +++ b/src/main/scala/files_metadata/domain/FileExtendedMeta.scala @@ -12,5 +12,6 @@ case class FileExtendedMeta( name: String, extension: String, size: Long, - ready: Boolean + isReady: Boolean, + isShared: Boolean ) diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index 5ed7f6c..b0bc88e 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -12,12 +12,10 @@ trait FilesMetaRepository { def shareFile( fileUUID: UUID, userUUID: UUID ): Unit // --- Read --- - def getFilesMetaInRoot( ownerUuid: UUID ): Seq[FileMeta] - def getFilesMetaInDirectory( ownerUuid: UUID, - directoryUuid: UUID - ): Seq[FileMeta] + directoryUuid: Option[UUID] + ): Seq[FileExtendedMeta] def getFileMeta( uuid: UUID ): FileMeta diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 32b7922..5d7169f 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -115,12 +115,125 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } - override def getFilesMetaInRoot( ownerUuid: UUID ): Seq[FileMeta] = ??? - override def getFilesMetaInDirectory( ownerUuid: UUID, + directoryUuid: Option[UUID] + ): Seq[FileExtendedMeta] = { + try { + if (directoryUuid.isEmpty) { + getFilesInRoot( ownerUuid ) + } else { + getFilesInParentDirectory( directoryUuid.get ) + } + } catch { + case exception: Exception => throw exception + } + } + + def getFilesInRoot( + ownerUuid: UUID + ): Seq[FileExtendedMeta] = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + """ + |SELECT uuid, owner_uuid, archive_uuid, volume, name, extension, size, is_shared + |FROM files_view WHERE + |owner_uuid = ? + |AND parent_uuid IS NULL + |AND ( + | ready = true + | OR archive_uuid IS NULL + |) + |""".stripMargin + ) + statement.setObject( 1, ownerUuid ) + + val result = statement.executeQuery() + var filesMeta: Seq[FileExtendedMeta] = Seq() + + // Parse the rows into Domain objects + while (result.next()) { + val archiveUUIDString = result.getString( "archive_uuid" ) + + val parentUUID = None + + val archiveUUID = + if (archiveUUIDString == null) None + else Some( UUID.fromString( archiveUUIDString ) ) + + filesMeta = filesMeta :+ FileExtendedMeta( + uuid = UUID.fromString( result.getString( "uuid" ) ), + ownerUuid = UUID.fromString( result.getString( "owner_uuid" ) ), + parentUuid = parentUUID, + archiveUuid = archiveUUID, + volume = result.getString( "volume" ), + name = result.getString( "name" ), + extension = result.getString( "extension" ), + size = result.getLong( "size" ), + isReady = true, + isShared = result.getBoolean( "is_shared" ) + ) + } + + filesMeta + } finally { + connection.close() + } + } + + def getFilesInParentDirectory( directoryUuid: UUID - ): Seq[FileMeta] = ??? + ) = { + val connection: Connection = pool.getConnection() + + try { + val statement = connection.prepareStatement( + """ + |SELECT uuid, owner_uuid, archive_uuid, volume, name, extension, size, is_shared + |FROM files_view WHERE + |parent_uuid = ? + |AND ( + | ready = true + | OR archive_uuid IS NULL + | ) + |""".stripMargin + ) + statement.setObject( 1, directoryUuid ) + + val result = statement.executeQuery() + var filesMeta: Seq[FileExtendedMeta] = Seq() + + // Parse the rows into Domain objects + while (result.next()) { + val archiveUUIDString = result.getString( "archive_uuid" ) + + val parentUUID = Some( directoryUuid ) + + val archiveUUID = + if (archiveUUIDString == null) None + else Some( UUID.fromString( archiveUUIDString ) ) + + filesMeta = filesMeta :+ FileExtendedMeta( + uuid = UUID.fromString( result.getString( "uuid" ) ), + ownerUuid = UUID.fromString( result.getString( "owner_uuid" ) ), + parentUuid = parentUUID, + archiveUuid = archiveUUID, + volume = result.getString( "volume" ), + name = result.getString( "name" ), + extension = result.getString( "extension" ), + size = result.getLong( "size" ), + isReady = true, + isShared = result.getBoolean( "is_shared" ) + ) + } + + filesMeta + } finally { + connection.close() + } + } override def getFileMeta( uuid: UUID ): FileMeta = { val connection: Connection = pool.getConnection() @@ -197,14 +310,14 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { try { val statement = connection.prepareStatement( """ - |SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name, extension, size + |SELECT uuid, owner_uuid, parent_uuid, archive_uuid, volume, name, extension, size, is_shared |FROM files_view WHERE |uuid IN ( | SELECT file_uuid FROM shared_files WHERE user_uuid = ? |) |AND ( - | archive_uuid is NULL - | OR volume IS NOT NULL + | ready = true + | OR archive_uuid IS NULL |) | """.stripMargin ) @@ -235,7 +348,8 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { name = result.getString( "name" ), extension = result.getString( "extension" ), size = result.getLong( "size" ), - ready = true + isReady = true, + isShared = result.getBoolean( "is_shared" ) ) } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 2bae1a6..52166ee 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -69,10 +69,84 @@ class MetadataControllers { } } - private def parseNullableValueToJSON( value: Any ): ujson.Value = { - value match { - case v: ujson.Value => v - case _ => ujson.Null + private def parseNullableStringToJSON( value: String ): ujson.Value = { + if (value == null) ujson.Null + else ujson.Str( value ) + } + + def ListFilesController( + userUUID: String, + parentUUID: Option[String] + ): cask.Response[Obj] = { + try { + // Validate the parent UUID if it's given + if (parentUUID.isDefined) { + val isParentUUIDValid = CommonValidator.validateUUID( parentUUID.get ) + if (!isParentUUIDValid) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Directory UUID is not valid" + ), + statusCode = 400 + ) + } + } + + // Validate the user UUID + val isUserUUIDValid = CommonValidator.validateUUID( userUUID ) + if (!isUserUUIDValid) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "User UUID is not valid" + ), + statusCode = 400 + ) + } + + // Get the files metadata + val parsedParentUUID: Option[UUID] = + if (parentUUID.isDefined) Some( UUID.fromString( parentUUID.get ) ) + else None + + val filesMeta = useCases.listFiles( + userUUID = UUID.fromString( userUUID ), + parentUUID = parsedParentUUID + ) + + // Parse the response + val responseArray = ujson.Arr.from( + filesMeta.map( fileMeta => { + val isDirectory = fileMeta.archiveUuid.isEmpty + if (isDirectory) { + ujson.Obj( + "uuid" -> fileMeta.uuid.toString, + "fileType" -> "directory", + "name" -> fileMeta.name, + "extension" -> ujson.Null, + "isShared" -> fileMeta.isShared + ) + } else { + ujson.Obj( + "uuid" -> fileMeta.uuid.toString, + "fileType" -> "archive", + "name" -> fileMeta.name, + "extension" -> parseNullableStringToJSON( fileMeta.extension ), + "isShared" -> fileMeta.isShared + ) + } + } ) + ) + + cask.Response( + ujson.Obj( + "files" -> responseArray + ), + statusCode = 200 + ) + } catch { + case e: Exception => _handleException( e ) } } @@ -293,7 +367,7 @@ class MetadataControllers { ujson.Obj( "archiveUUID" -> fileMeta.archiveUuid.get.toString, "name" -> fileMeta.name, - "extension" -> parseNullableValueToJSON( archivesMeta.extension ), + "extension" -> parseNullableStringToJSON( archivesMeta.extension ), "volume" -> fileMeta.volume, "size" -> archivesMeta.size, "is_shared" -> fileMeta.isShared @@ -389,7 +463,7 @@ class MetadataControllers { "uuid" -> fileMeta.uuid.toString, "fileType" -> "archive", "name" -> fileMeta.name, - "extension" -> parseNullableValueToJSON( fileMeta.extension ) + "extension" -> parseNullableStringToJSON( fileMeta.extension ) ) } } ) diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index 912a661..7e7acff 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -9,6 +9,14 @@ case class MetadataRoutes() extends cask.Routes { private val controllers = new MetadataControllers() controllers._init() + @cask.get( s"${ basePath }/list/:userUUID" ) + def ListMetadataHandler( + userUUID: String, + parentUUID: Option[String] = None + ): cask.Response[Obj] = { + controllers.ListFilesController( userUUID, parentUUID ) + } + @cask.post( s"${ basePath }" ) def SaveMetadataHandler( request: cask.Request diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index f8fb334..9ae3377 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -19,6 +19,26 @@ object FilesTestsUtils { } } + // -- List files --- + def ListFilesInRootDirectory( userUUID: String ): Response = { + `given`() + .port( 8080 ) + .when() + .get( s"${ ListFilesInDirectoresTestsData.API_PREFIX }/${ userUUID }" ) + } + + def ListFilesInDirectory( + userUUID: String, + directoryUUID: String + ): Response = { + `given`() + .port( 8080 ) + .when() + .get( + s"${ ListFilesInDirectoresTestsData.API_PREFIX }/${ userUUID }?parentUUID=${ directoryUUID }" + ) + } + // -- Save files -- def SaveFile( payload: util.HashMap[String, Any] ): Response = { `given`() diff --git a/src/test/scala/files_metadata/ListFilesInDirectories.scala b/src/test/scala/files_metadata/ListFilesInDirectories.scala new file mode 100644 index 0000000..2e158e1 --- /dev/null +++ b/src/test/scala/files_metadata/ListFilesInDirectories.scala @@ -0,0 +1,230 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util.UUID + +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Before +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +object ListFilesInDirectoresTestsData { + val API_PREFIX: String = "/api/v1/files/list" + val OWNER_USER_UUID: UUID = UUID.randomUUID() + val OTHER_USER_UUID: UUID = UUID.randomUUID() + + var savedFirstLevelDirectoryUUID: UUID = _ + var savedSecondLevelDirectoryUUID: UUID = _ + + var fileSavedInRootDirectoryUUID: UUID = _ + var fileSavedInFirstLevelDirectoryUUID: UUID = _ +} + +@OrderWith( classOf[Alphanumeric] ) +class ListFilesInDirectories extends JUnitSuite { + def SaveAndShareFilesToList(): Unit = { + // 1. Save the directories + val firstLvlDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID, + parentDirUUID = None + ) + + val firstLvlDirectoryResponse = + FilesTestsUtils.SaveFile( firstLvlDirectoryPayload ) + ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID = + UUID.fromString( + firstLvlDirectoryResponse.jsonPath().get( "uuid" ) + ) + + val secondLvlDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID, + parentDirUUID = Some( + ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID + ) + ) + + val secondLvlDirectoryResponse = + FilesTestsUtils.SaveFile( secondLvlDirectoryPayload ) + ListFilesInDirectoresTestsData.savedSecondLevelDirectoryUUID = + UUID.fromString( + secondLvlDirectoryResponse.jsonPath().get( "uuid" ) + ) + + // 2. Save the files + val fileSavedInRootDirectoryPayload = FilesTestsUtils.generateFilePayload( + ownerUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID, + parentDirUUID = None + ) + + val fileSavedInRootDirectoryResponse = + FilesTestsUtils.SaveFile( fileSavedInRootDirectoryPayload ) + ListFilesInDirectoresTestsData.fileSavedInRootDirectoryUUID = + UUID.fromString( + fileSavedInRootDirectoryResponse.jsonPath().get( "uuid" ) + ) + + val fileSavedInFirstLevelDirectoryPayload = + FilesTestsUtils.generateFilePayload( + ownerUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID, + parentDirUUID = Some( + ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID + ) + ) + + val fileSavedInFirstLevelDirectoryResponse = + FilesTestsUtils.SaveFile( fileSavedInFirstLevelDirectoryPayload ) + ListFilesInDirectoresTestsData.fileSavedInFirstLevelDirectoryUUID = + UUID.fromString( + fileSavedInFirstLevelDirectoryResponse.jsonPath().get( "uuid" ) + ) + + // 3. Share the first level directory with the other user + val shareFirstLevelDirectoryPayload = + FilesTestsUtils.generateShareFilePayload( + otherUserUUID = ListFilesInDirectoresTestsData.OTHER_USER_UUID + ) + FilesTestsUtils.ShareFile( + ownerUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + fileUUID = + ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID.toString, + payload = shareFirstLevelDirectoryPayload + ) + } + + @Before + def startHttpServer(): Unit = { + FilesTestsUtils.StartHttpServer() + } + + @Test + def T1_ParametersAreValidated(): Unit = { + // 1. userUUID is validated + val response = FilesTestsUtils.ListFilesInRootDirectory( + userUUID = "not_an_uuid" + ) + + assert( response.statusCode() == 400 ) + + // 2. parentUUID is validated + val response2 = FilesTestsUtils.ListFilesInDirectory( + userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + directoryUUID = "not_an_uuid" + ) + + assert( response2.statusCode() == 400 ) + } + + @Test + def T2_CanListFilesInRootDirectory(): Unit = { + SaveAndShareFilesToList() + + /* 1. Should be able to see the directory since the file was not marked as + * ready yet */ + val response = FilesTestsUtils.ListFilesInRootDirectory( + userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString + ) + + assert( response.statusCode() == 200 ) + assert( + response.jsonPath().getList( "files" ).size() == 1 + ) + + // Mark the file as ready + FilesTestsUtils.UpdateReadyFile( + fileUUID = + ListFilesInDirectoresTestsData.fileSavedInRootDirectoryUUID.toString, + payload = FilesTestsUtils.generateReadyFilePayload() + ) + + // 2. Should be able to see the file and the directory + val response2 = FilesTestsUtils.ListFilesInRootDirectory( + userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString + ) + + assert( response2.statusCode() == 200 ) + assert( + response2.jsonPath().getList( "files" ).size() == 2 + ) + } + + @Test + def T3_CanListFilesInDirectory(): Unit = { + /* 1. Should be able to see the nested directory since the nested file was + * not marked as ready yet */ + val response = FilesTestsUtils.ListFilesInDirectory( + userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + directoryUUID = + ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID.toString + ) + + assert( response.statusCode() == 200 ) + assert( + response.jsonPath().getList( "files" ).size() == 1 + ) + + // Mark the file as ready + FilesTestsUtils.UpdateReadyFile( + fileUUID = + ListFilesInDirectoresTestsData.fileSavedInFirstLevelDirectoryUUID.toString, + payload = FilesTestsUtils.generateReadyFilePayload() + ) + + // 2. Should be able to see the file and the directory + val response2 = FilesTestsUtils.ListFilesInDirectory( + userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + directoryUUID = + ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID.toString + ) + + assert( response2.statusCode() == 200 ) + assert( + response2.jsonPath().getList( "files" ).size() == 2 + ) + } + + @Test + def T4_UsersWithAccessToDirectoryCanListFiles(): Unit = { + val response = FilesTestsUtils.ListFilesInDirectory( + userUUID = ListFilesInDirectoresTestsData.OTHER_USER_UUID.toString, + directoryUUID = + ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID.toString + ) + + assert( response.statusCode() == 200 ) + assert( + response.jsonPath().getList( "files" ).size() == 2 + ) + } + + @Test + def T5_UsersWithoutAccessToDirectoryCannotListFiles(): Unit = { + val response = FilesTestsUtils.ListFilesInDirectory( + userUUID = UUID.randomUUID().toString, + directoryUUID = + ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID.toString + ) + + assert( response.statusCode() == 403 ) + } + + @Test + def T6_ParentDirectoryMustExist(): Unit = { + // 1. Given a non-existing parent directory, should return 404 + val response = FilesTestsUtils.ListFilesInDirectory( + userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + directoryUUID = UUID.randomUUID().toString + ) + + assert( response.statusCode() == 404 ) + + // 2. Given a file as parent directory, should return 400 + val response2 = FilesTestsUtils.ListFilesInDirectory( + userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + directoryUUID = + ListFilesInDirectoresTestsData.fileSavedInRootDirectoryUUID.toString + ) + + assert( response2.statusCode() == 400 ) + } +} From af127dea68e8d266d2775fa6e612bfa433c1e625 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Tue, 3 Oct 2023 21:16:50 +0000 Subject: [PATCH 61/67] chore(release): v0.11.0 [skip ci] --- CHANGELOG.md | 18 +++++++++--------- version.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 294e1bd..27d1122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [0.11.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.4...v0.11.0) (2023-10-03) + + +### Features + +* List files ([#78](https://github.com/hawks-atlanta/metadata-scala/issues/78)) ([75f1559](https://github.com/hawks-atlanta/metadata-scala/commit/75f15598787454d9e259d3de16b64126ce217da5)) + + + ## [0.10.4](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.3...v0.10.4) (2023-10-03) @@ -34,12 +43,3 @@ -# [0.10.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.9.0...v0.10.0) (2023-09-21) - - -### Features - -* Move file ([#67](https://github.com/hawks-atlanta/metadata-scala/issues/67)) ([0684246](https://github.com/hawks-atlanta/metadata-scala/commit/06842463a6c24c2b38569991bfb8cb6c5caf15e6)) - - - diff --git a/version.json b/version.json index fe98e6f..2cd0808 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.10.4" + "version": "0.11.0" } \ No newline at end of file From 3e27c2d32c23d4ef96ed42fa545999ecfdf71aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Wed, 4 Oct 2023 18:40:52 -0500 Subject: [PATCH 62/67] refactor: Add logs and fix Scala warnings (#83) * refactor(logs): Create basic stdout logger * refactor: Remove unused request object in get methods * style: Fix typos and remove unneeded enclosing blocks --- .../FilesMetaPostgresRepository.scala | 38 ++++---- .../infrastructure/MetadataControllers.scala | 13 +-- .../infrastructure/MetadataRoutes.scala | 88 ++++++++++++++----- .../requests/MoveReqSchema.scala | 2 +- .../infrastructure/CaskHTTPRouter.scala | 2 +- .../shared/infrastructure/StdoutLogger.scala | 29 ++++++ .../files_metadata/FilesTestsUtils.scala | 18 ++-- .../ListFilesInDirectories.scala | 58 ++++++------ 8 files changed, 154 insertions(+), 94 deletions(-) create mode 100644 src/main/scala/shared/infrastructure/StdoutLogger.scala diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index 5d7169f..eca1096 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -20,15 +20,15 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { val connection: Connection = pool.getConnection() try { - val statemet = connection.prepareStatement( + val statement = connection.prepareStatement( "INSERT INTO files (owner_uuid, parent_uuid, name) VALUES (?, ?, ?) RETURNING uuid" ) - statemet.setObject( 1, fileMeta.ownerUuid ) - statemet.setObject( 2, fileMeta.parentUuid.orNull ) - statemet.setString( 3, fileMeta.name ) + statement.setObject( 1, fileMeta.ownerUuid ) + statement.setObject( 2, fileMeta.parentUuid.orNull ) + statement.setString( 3, fileMeta.name ) - val result = statemet.executeQuery() + val result = statement.executeQuery() var insertedUUID: UUID = null if (result.next()) { @@ -55,19 +55,19 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { val connection: Connection = pool.getConnection() try { - // Start a transancion + // Start a transaction connection.setAutoCommit( false ) // 1. Insert the archive - val archiveStatemet = connection.prepareStatement( + val archiveStatement = connection.prepareStatement( "INSERT INTO archives (extension, size, ready) VALUES (?, ?, ?) RETURNING uuid" ) - archiveStatemet.setString( 1, archivesMeta.extension ) - archiveStatemet.setLong( 2, archivesMeta.size ) - archiveStatemet.setBoolean( 3, false ) + archiveStatement.setString( 1, archivesMeta.extension ) + archiveStatement.setLong( 2, archivesMeta.size ) + archiveStatement.setBoolean( 3, false ) - val archiveResult = archiveStatemet.executeQuery() + val archiveResult = archiveStatement.executeQuery() var archiveUUID: Option[UUID] = None if (archiveResult.next()) { @@ -82,16 +82,16 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } // 2. Insert the file - val fileStatemet = connection.prepareStatement( + val fileStatement = connection.prepareStatement( "INSERT INTO files (owner_uuid, parent_uuid, archive_uuid, name) VALUES (?, ?, ?, ?) RETURNING uuid" ) - fileStatemet.setObject( 1, fileMeta.ownerUuid ) - fileStatemet.setObject( 2, fileMeta.parentUuid.orNull ) - fileStatemet.setObject( 3, archiveUUID.get ) - fileStatemet.setString( 4, fileMeta.name ) + fileStatement.setObject( 1, fileMeta.ownerUuid ) + fileStatement.setObject( 2, fileMeta.parentUuid.orNull ) + fileStatement.setObject( 3, archiveUUID.get ) + fileStatement.setString( 4, fileMeta.name ) - val fileResult = fileStatemet.executeQuery() + val fileResult = fileStatement.executeQuery() var fileUUID: UUID = null if (fileResult.next()) { @@ -130,7 +130,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } - def getFilesInRoot( + private def getFilesInRoot( ownerUuid: UUID ): Seq[FileExtendedMeta] = { val connection: Connection = pool.getConnection() @@ -183,7 +183,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } - def getFilesInParentDirectory( + private def getFilesInParentDirectory( directoryUuid: UUID ) = { val connection: Connection = pool.getConnection() diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 52166ee..8611b77 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -1,7 +1,6 @@ package org.hawksatlanta.metadata package files_metadata.infrastructure -import java.util.Date import java.util.UUID import com.wix.accord.validate @@ -17,6 +16,7 @@ import files_metadata.infrastructure.requests.MoveReqSchema import files_metadata.infrastructure.requests.RenameReqSchema import files_metadata.infrastructure.requests.ShareReqSchema import shared.infrastructure.CommonValidator +import shared.infrastructure.StdoutLogger import ujson.Obj import upickle.default.read @@ -52,13 +52,8 @@ class MetadataControllers { ) case e: Exception => - // Log the error - val currentDate = new Date() - println( - s"[${ currentDate.toString }] The following error was caught: $e" - ) + StdoutLogger.logCaughtException( e ) - // Send a response cask.Response( ujson.Obj( "error" -> true, @@ -275,7 +270,6 @@ class MetadataControllers { } def CanReadFileController( - request: cask.Request, userUUID: String, fileUUID: String ): cask.Response[Obj] = { @@ -314,7 +308,6 @@ class MetadataControllers { } def GetFileMetadataController( - request: cask.Request, fileUUID: String ): cask.Response[Obj] = { try { @@ -430,7 +423,6 @@ class MetadataControllers { } def GetSharedWithMeController( - request: cask.Request, userUUID: String ): cask.Response[Obj] = { try { @@ -482,7 +474,6 @@ class MetadataControllers { } def GetSharedWithWhoController( - request: cask.Request, fileUUID: String ): cask.Response[Obj] = { try { diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index 7e7acff..a692552 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -1,6 +1,7 @@ package org.hawksatlanta.metadata package files_metadata.infrastructure +import shared.infrastructure.StdoutLogger import ujson.Obj case class MetadataRoutes() extends cask.Routes { @@ -9,87 +10,126 @@ case class MetadataRoutes() extends cask.Routes { private val controllers = new MetadataControllers() controllers._init() - @cask.get( s"${ basePath }/list/:userUUID" ) + private val listMetadataEndpoint = s"$basePath/list/:userUUID" + @cask.get( listMetadataEndpoint ) def ListMetadataHandler( userUUID: String, parentUUID: Option[String] = None ): cask.Response[Obj] = { - controllers.ListFilesController( userUUID, parentUUID ) + StdoutLogger.logAndReturnEndpointResponse( + listMetadataEndpoint, + controllers.ListFilesController( userUUID, parentUUID ) + ) } - @cask.post( s"${ basePath }" ) + private val saveMetadataEndpoint = s"$basePath" + @cask.post( saveMetadataEndpoint ) def SaveMetadataHandler( request: cask.Request ): cask.Response[Obj] = { - controllers.SaveMetadataController( request ) + StdoutLogger.logAndReturnEndpointResponse( + saveMetadataEndpoint, + controllers.SaveMetadataController( request ) + ) } - @cask.post( s"${ basePath }/share/:ownerUUID/:fileUUID" ) + private val shareMetadataEndpoint = + s"$basePath/share/:ownerUUID/:fileUUID" + @cask.post( shareMetadataEndpoint ) def ShareMetadataHandler( request: cask.Request, ownerUUID: String, fileUUID: String ): cask.Response[Obj] = { - controllers.ShareFileController( request, ownerUUID, fileUUID ) + StdoutLogger.logAndReturnEndpointResponse( + shareMetadataEndpoint, + controllers.ShareFileController( request, ownerUUID, fileUUID ) + ) } - @cask.get( s"${ basePath }/can_read/:userUUID/:fileUUID" ) + private val canReadMetadataEndpoint = + s"$basePath/can_read/:userUUID/:fileUUID" + @cask.get( canReadMetadataEndpoint ) def CanReadMetadataHandler( - request: cask.Request, userUUID: String, fileUUID: String ): cask.Response[Obj] = { - controllers.CanReadFileController( request, userUUID, fileUUID ) + StdoutLogger.logAndReturnEndpointResponse( + canReadMetadataEndpoint, + controllers.CanReadFileController( userUUID, fileUUID ) + ) } - @cask.put( s"${ basePath }/ready/:fileUUID" ) + private val markFileAsReadyEndpoint = s"$basePath/ready/:fileUUID" + @cask.put( markFileAsReadyEndpoint ) def ReadyMetadataHandler( request: cask.Request, fileUUID: String ): cask.Response[Obj] = { - controllers.MarkFileAsReadyController( request, fileUUID ) + StdoutLogger.logAndReturnEndpointResponse( + markFileAsReadyEndpoint, + controllers.MarkFileAsReadyController( request, fileUUID ) + ) } - @cask.get( s"${ basePath }/metadata/:fileUUID" ) + private val getMetadataEndpoint = s"$basePath/metadata/:fileUUID" + @cask.get( getMetadataEndpoint ) def GetFileMetadataHandler( - request: cask.Request, fileUUID: String ): cask.Response[Obj] = { - controllers.GetFileMetadataController( request, fileUUID ) + StdoutLogger.logAndReturnEndpointResponse( + getMetadataEndpoint, + controllers.GetFileMetadataController( fileUUID ) + ) } - @cask.get( s"${ basePath }/shared_with_me/:userUUID" ) + private val getFilesSharedWithUserEndpoint = + s"$basePath/shared_with_me/:userUUID" + @cask.get( getFilesSharedWithUserEndpoint ) def GetSharedWithMeHandler( - request: cask.Request, userUUID: String ): cask.Response[Obj] = { - controllers.GetSharedWithMeController( request, userUUID ) + StdoutLogger.logAndReturnEndpointResponse( + getFilesSharedWithUserEndpoint, + controllers.GetSharedWithMeController( userUUID ) + ) } - @cask.get( s"${ basePath }/shared_with_who/:fileUUID" ) + private val getSharedWithWhoEndpoint = s"$basePath/shared_with_who/:fileUUID" + @cask.get( getSharedWithWhoEndpoint ) def GetSharedWithWhoHandler( - request: cask.Request, fileUUID: String ): cask.Response[Obj] = { - controllers.GetSharedWithWhoController( request, fileUUID ) + StdoutLogger.logAndReturnEndpointResponse( + getSharedWithWhoEndpoint, + controllers.GetSharedWithWhoController( fileUUID ) + ) } - @cask.put( s"${ basePath }/rename/:userUUID/:fileUUID" ) + private val renameFileEndpoint = s"$basePath/rename/:userUUID/:fileUUID" + @cask.put( renameFileEndpoint ) def RenameFileHandler( request: cask.Request, userUUID: String, fileUUID: String ): cask.Response[Obj] = { - controllers.RenameFileController( request, userUUID, fileUUID ) + StdoutLogger.logAndReturnEndpointResponse( + renameFileEndpoint, + controllers.RenameFileController( request, userUUID, fileUUID ) + ) } - @cask.put( s"${ basePath }/move/:userUUID/:fileUUID" ) + private val moveFileEndpoint = s"$basePath/move/:userUUID/:fileUUID" + @cask.put( moveFileEndpoint ) def MoveFileHandler( request: cask.Request, userUUID: String, fileUUID: String ): cask.Response[Obj] = { - controllers.MoveFileController( request, userUUID, fileUUID ) + StdoutLogger.logAndReturnEndpointResponse( + moveFileEndpoint, + controllers.MoveFileController( request, userUUID, fileUUID ) + ) } initialize() diff --git a/src/main/scala/files_metadata/infrastructure/requests/MoveReqSchema.scala b/src/main/scala/files_metadata/infrastructure/requests/MoveReqSchema.scala index 401473d..986d50c 100644 --- a/src/main/scala/files_metadata/infrastructure/requests/MoveReqSchema.scala +++ b/src/main/scala/files_metadata/infrastructure/requests/MoveReqSchema.scala @@ -3,7 +3,7 @@ package files_metadata.infrastructure.requests import com.wix.accord.dsl._ import com.wix.accord.Validator -import org.hawksatlanta.metadata.shared.infrastructure.CommonValidator +import shared.infrastructure.CommonValidator case class MoveReqSchema( parentUUID: String diff --git a/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala b/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala index 290af49..565727d 100644 --- a/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala +++ b/src/main/scala/shared/infrastructure/CaskHTTPRouter.scala @@ -1,7 +1,7 @@ package org.hawksatlanta.metadata package shared.infrastructure -import org.hawksatlanta.metadata.files_metadata.infrastructure.MetadataRoutes +import files_metadata.infrastructure.MetadataRoutes object CaskHTTPRouter extends cask.Main { override def port: Int = 8080 diff --git a/src/main/scala/shared/infrastructure/StdoutLogger.scala b/src/main/scala/shared/infrastructure/StdoutLogger.scala new file mode 100644 index 0000000..28b13f4 --- /dev/null +++ b/src/main/scala/shared/infrastructure/StdoutLogger.scala @@ -0,0 +1,29 @@ +package org.hawksatlanta.metadata +package shared.infrastructure +import java.util.Date + +import ujson.Obj + +object StdoutLogger { + def logAndReturnEndpointResponse( + endpoint: String, + response: cask.Response[Obj] + ): cask.Response[Obj] = { + val currentDate = new Date() + + val logMessage = + s"[$currentDate] [Response] $endpoint => ${ response.statusCode }" + println( logMessage ) + + response + } + + def logCaughtException( + exception: Throwable + ): Unit = { + val currentDate = new Date() + + val logMessage = s"[$currentDate] [Error] => $exception" + println( logMessage ) + } +} diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index 9ae3377..81e92ca 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -24,7 +24,7 @@ object FilesTestsUtils { `given`() .port( 8080 ) .when() - .get( s"${ ListFilesInDirectoresTestsData.API_PREFIX }/${ userUUID }" ) + .get( s"${ ListFilesInDirectoriesTestsData.API_PREFIX }/$userUUID" ) } def ListFilesInDirectory( @@ -35,7 +35,7 @@ object FilesTestsUtils { .port( 8080 ) .when() .get( - s"${ ListFilesInDirectoresTestsData.API_PREFIX }/${ userUUID }?parentUUID=${ directoryUUID }" + s"${ ListFilesInDirectoriesTestsData.API_PREFIX }/$userUUID?parentUUID=$directoryUUID" ) } @@ -104,7 +104,7 @@ object FilesTestsUtils { .body( payload ) .when() .post( - s"${ ShareFileTestsData.API_PREFIX }/${ ownerUUID }/${ fileUUID }" + s"${ ShareFileTestsData.API_PREFIX }/$ownerUUID/$fileUUID" ) } @@ -121,7 +121,7 @@ object FilesTestsUtils { .port( 8080 ) .when() .get( - s"${ GetShareWithUserTestsData.API_PREFIX }/${ userUUID }" + s"${ GetShareWithUserTestsData.API_PREFIX }/$userUUID" ) } @@ -130,7 +130,7 @@ object FilesTestsUtils { .port( 8080 ) .when() .get( - s"${ GetShareWithWhoTestsData.API_PREFIX }/${ fileUUID }" + s"${ GetShareWithWhoTestsData.API_PREFIX }/$fileUUID" ) } @@ -146,7 +146,7 @@ object FilesTestsUtils { .body( payload ) .when() .put( - s"${ UpdateReadyFileTestsData.API_PREFIX }/${ fileUUID }" + s"${ UpdateReadyFileTestsData.API_PREFIX }/$fileUUID" ) } @@ -167,7 +167,7 @@ object FilesTestsUtils { .body( payload ) .when() .put( - s"${ RenameFileTestsData.API_PREFIX }/${ userUUID }/${ fileUUID }" + s"${ RenameFileTestsData.API_PREFIX }/$userUUID/$fileUUID" ) } @@ -188,7 +188,7 @@ object FilesTestsUtils { .body( payload ) .when() .put( - s"${ MoveFileTestsData.API_PREFIX }/${ userUUID }/${ fileUUID }" + s"${ MoveFileTestsData.API_PREFIX }/$userUUID/$fileUUID" ) } @@ -205,7 +205,7 @@ object FilesTestsUtils { .port( 8080 ) .when() .get( - s"${ GetFileMetadataTestsData.API_PREFIX }/${ fileUUID }" + s"${ GetFileMetadataTestsData.API_PREFIX }/$fileUUID" ) } diff --git a/src/test/scala/files_metadata/ListFilesInDirectories.scala b/src/test/scala/files_metadata/ListFilesInDirectories.scala index 2e158e1..1b22541 100644 --- a/src/test/scala/files_metadata/ListFilesInDirectories.scala +++ b/src/test/scala/files_metadata/ListFilesInDirectories.scala @@ -9,7 +9,7 @@ import org.junit.Before import org.junit.Test import org.scalatestplus.junit.JUnitSuite -object ListFilesInDirectoresTestsData { +object ListFilesInDirectoriesTestsData { val API_PREFIX: String = "/api/v1/files/list" val OWNER_USER_UUID: UUID = UUID.randomUUID() val OTHER_USER_UUID: UUID = UUID.randomUUID() @@ -26,55 +26,55 @@ class ListFilesInDirectories extends JUnitSuite { def SaveAndShareFilesToList(): Unit = { // 1. Save the directories val firstLvlDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( - ownerUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID, + ownerUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID, parentDirUUID = None ) val firstLvlDirectoryResponse = FilesTestsUtils.SaveFile( firstLvlDirectoryPayload ) - ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID = + ListFilesInDirectoriesTestsData.savedFirstLevelDirectoryUUID = UUID.fromString( firstLvlDirectoryResponse.jsonPath().get( "uuid" ) ) val secondLvlDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( - ownerUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID, + ownerUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID, parentDirUUID = Some( - ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID + ListFilesInDirectoriesTestsData.savedFirstLevelDirectoryUUID ) ) val secondLvlDirectoryResponse = FilesTestsUtils.SaveFile( secondLvlDirectoryPayload ) - ListFilesInDirectoresTestsData.savedSecondLevelDirectoryUUID = + ListFilesInDirectoriesTestsData.savedSecondLevelDirectoryUUID = UUID.fromString( secondLvlDirectoryResponse.jsonPath().get( "uuid" ) ) // 2. Save the files val fileSavedInRootDirectoryPayload = FilesTestsUtils.generateFilePayload( - ownerUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID, + ownerUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID, parentDirUUID = None ) val fileSavedInRootDirectoryResponse = FilesTestsUtils.SaveFile( fileSavedInRootDirectoryPayload ) - ListFilesInDirectoresTestsData.fileSavedInRootDirectoryUUID = + ListFilesInDirectoriesTestsData.fileSavedInRootDirectoryUUID = UUID.fromString( fileSavedInRootDirectoryResponse.jsonPath().get( "uuid" ) ) val fileSavedInFirstLevelDirectoryPayload = FilesTestsUtils.generateFilePayload( - ownerUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID, + ownerUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID, parentDirUUID = Some( - ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID + ListFilesInDirectoriesTestsData.savedFirstLevelDirectoryUUID ) ) val fileSavedInFirstLevelDirectoryResponse = FilesTestsUtils.SaveFile( fileSavedInFirstLevelDirectoryPayload ) - ListFilesInDirectoresTestsData.fileSavedInFirstLevelDirectoryUUID = + ListFilesInDirectoriesTestsData.fileSavedInFirstLevelDirectoryUUID = UUID.fromString( fileSavedInFirstLevelDirectoryResponse.jsonPath().get( "uuid" ) ) @@ -82,12 +82,12 @@ class ListFilesInDirectories extends JUnitSuite { // 3. Share the first level directory with the other user val shareFirstLevelDirectoryPayload = FilesTestsUtils.generateShareFilePayload( - otherUserUUID = ListFilesInDirectoresTestsData.OTHER_USER_UUID + otherUserUUID = ListFilesInDirectoriesTestsData.OTHER_USER_UUID ) FilesTestsUtils.ShareFile( - ownerUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + ownerUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID.toString, fileUUID = - ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID.toString, + ListFilesInDirectoriesTestsData.savedFirstLevelDirectoryUUID.toString, payload = shareFirstLevelDirectoryPayload ) } @@ -108,7 +108,7 @@ class ListFilesInDirectories extends JUnitSuite { // 2. parentUUID is validated val response2 = FilesTestsUtils.ListFilesInDirectory( - userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + userUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID.toString, directoryUUID = "not_an_uuid" ) @@ -122,7 +122,7 @@ class ListFilesInDirectories extends JUnitSuite { /* 1. Should be able to see the directory since the file was not marked as * ready yet */ val response = FilesTestsUtils.ListFilesInRootDirectory( - userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString + userUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID.toString ) assert( response.statusCode() == 200 ) @@ -133,13 +133,13 @@ class ListFilesInDirectories extends JUnitSuite { // Mark the file as ready FilesTestsUtils.UpdateReadyFile( fileUUID = - ListFilesInDirectoresTestsData.fileSavedInRootDirectoryUUID.toString, + ListFilesInDirectoriesTestsData.fileSavedInRootDirectoryUUID.toString, payload = FilesTestsUtils.generateReadyFilePayload() ) // 2. Should be able to see the file and the directory val response2 = FilesTestsUtils.ListFilesInRootDirectory( - userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString + userUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID.toString ) assert( response2.statusCode() == 200 ) @@ -153,9 +153,9 @@ class ListFilesInDirectories extends JUnitSuite { /* 1. Should be able to see the nested directory since the nested file was * not marked as ready yet */ val response = FilesTestsUtils.ListFilesInDirectory( - userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + userUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID.toString, directoryUUID = - ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID.toString + ListFilesInDirectoriesTestsData.savedFirstLevelDirectoryUUID.toString ) assert( response.statusCode() == 200 ) @@ -166,15 +166,15 @@ class ListFilesInDirectories extends JUnitSuite { // Mark the file as ready FilesTestsUtils.UpdateReadyFile( fileUUID = - ListFilesInDirectoresTestsData.fileSavedInFirstLevelDirectoryUUID.toString, + ListFilesInDirectoriesTestsData.fileSavedInFirstLevelDirectoryUUID.toString, payload = FilesTestsUtils.generateReadyFilePayload() ) // 2. Should be able to see the file and the directory val response2 = FilesTestsUtils.ListFilesInDirectory( - userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + userUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID.toString, directoryUUID = - ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID.toString + ListFilesInDirectoriesTestsData.savedFirstLevelDirectoryUUID.toString ) assert( response2.statusCode() == 200 ) @@ -186,9 +186,9 @@ class ListFilesInDirectories extends JUnitSuite { @Test def T4_UsersWithAccessToDirectoryCanListFiles(): Unit = { val response = FilesTestsUtils.ListFilesInDirectory( - userUUID = ListFilesInDirectoresTestsData.OTHER_USER_UUID.toString, + userUUID = ListFilesInDirectoriesTestsData.OTHER_USER_UUID.toString, directoryUUID = - ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID.toString + ListFilesInDirectoriesTestsData.savedFirstLevelDirectoryUUID.toString ) assert( response.statusCode() == 200 ) @@ -202,7 +202,7 @@ class ListFilesInDirectories extends JUnitSuite { val response = FilesTestsUtils.ListFilesInDirectory( userUUID = UUID.randomUUID().toString, directoryUUID = - ListFilesInDirectoresTestsData.savedFirstLevelDirectoryUUID.toString + ListFilesInDirectoriesTestsData.savedFirstLevelDirectoryUUID.toString ) assert( response.statusCode() == 403 ) @@ -212,7 +212,7 @@ class ListFilesInDirectories extends JUnitSuite { def T6_ParentDirectoryMustExist(): Unit = { // 1. Given a non-existing parent directory, should return 404 val response = FilesTestsUtils.ListFilesInDirectory( - userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + userUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID.toString, directoryUUID = UUID.randomUUID().toString ) @@ -220,9 +220,9 @@ class ListFilesInDirectories extends JUnitSuite { // 2. Given a file as parent directory, should return 400 val response2 = FilesTestsUtils.ListFilesInDirectory( - userUUID = ListFilesInDirectoresTestsData.OWNER_USER_UUID.toString, + userUUID = ListFilesInDirectoriesTestsData.OWNER_USER_UUID.toString, directoryUUID = - ListFilesInDirectoresTestsData.fileSavedInRootDirectoryUUID.toString + ListFilesInDirectoriesTestsData.fileSavedInRootDirectoryUUID.toString ) assert( response2.statusCode() == 400 ) From b25818c89036883c1582a5a0bad2c09be9965968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Thu, 12 Oct 2023 08:55:40 -0500 Subject: [PATCH 63/67] docs(http): Add bruno collection (#84) * docs(http): Add bruno collection --- docs/bruno/bruno.json | 5 ++ docs/bruno/can-read/can-read.bru | 11 ++++ docs/bruno/create-file/file-in-folder.bru | 26 +++++++++ docs/bruno/create-file/file-in-root.bru | 26 +++++++++ docs/bruno/create-file/file-no-extension.bru | 26 +++++++++ docs/bruno/create-file/folder-in-folder.bru | 26 +++++++++ docs/bruno/create-file/folder-in-root.bru | 26 +++++++++ docs/bruno/environments/development.bru | 3 + docs/bruno/get-metadata/get-metadata.bru | 11 ++++ docs/bruno/list-files/files-in-folder.bru | 15 +++++ docs/bruno/list-files/files-in-root.bru | 11 ++++ docs/bruno/mark-as-ready/mark-as-ready.bru | 21 +++++++ docs/bruno/move-file/move-file.bru | 21 +++++++ docs/bruno/rename-file/rename-file.bru | 21 +++++++ docs/bruno/share-file/share-file.bru | 21 +++++++ docs/bruno/shared-with-me/shared-with-me.bru | 11 ++++ .../bruno/shared-with-who/shared-with-who.bru | 11 ++++ docs/rest/can_read.http | 3 - docs/rest/get_metadata.http | 3 - docs/rest/get_shared_with_user.http | 3 - docs/rest/get_shared_with_who.http | 3 - docs/rest/list_files.http | 7 --- docs/rest/mark_as_ready.http | 8 --- docs/rest/move_file.http | 17 ------ docs/rest/rename_file.http | 8 --- docs/rest/save_metadata.http | 55 ------------------- docs/rest/share_file.http | 8 --- 27 files changed, 292 insertions(+), 115 deletions(-) create mode 100644 docs/bruno/bruno.json create mode 100644 docs/bruno/can-read/can-read.bru create mode 100644 docs/bruno/create-file/file-in-folder.bru create mode 100644 docs/bruno/create-file/file-in-root.bru create mode 100644 docs/bruno/create-file/file-no-extension.bru create mode 100644 docs/bruno/create-file/folder-in-folder.bru create mode 100644 docs/bruno/create-file/folder-in-root.bru create mode 100644 docs/bruno/environments/development.bru create mode 100644 docs/bruno/get-metadata/get-metadata.bru create mode 100644 docs/bruno/list-files/files-in-folder.bru create mode 100644 docs/bruno/list-files/files-in-root.bru create mode 100644 docs/bruno/mark-as-ready/mark-as-ready.bru create mode 100644 docs/bruno/move-file/move-file.bru create mode 100644 docs/bruno/rename-file/rename-file.bru create mode 100644 docs/bruno/share-file/share-file.bru create mode 100644 docs/bruno/shared-with-me/shared-with-me.bru create mode 100644 docs/bruno/shared-with-who/shared-with-who.bru delete mode 100644 docs/rest/can_read.http delete mode 100644 docs/rest/get_metadata.http delete mode 100644 docs/rest/get_shared_with_user.http delete mode 100644 docs/rest/get_shared_with_who.http delete mode 100644 docs/rest/list_files.http delete mode 100644 docs/rest/mark_as_ready.http delete mode 100644 docs/rest/move_file.http delete mode 100644 docs/rest/rename_file.http delete mode 100644 docs/rest/save_metadata.http delete mode 100644 docs/rest/share_file.http diff --git a/docs/bruno/bruno.json b/docs/bruno/bruno.json new file mode 100644 index 0000000..8488913 --- /dev/null +++ b/docs/bruno/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "bruno", + "type": "collection" +} \ No newline at end of file diff --git a/docs/bruno/can-read/can-read.bru b/docs/bruno/can-read/can-read.bru new file mode 100644 index 0000000..a822bf4 --- /dev/null +++ b/docs/bruno/can-read/can-read.bru @@ -0,0 +1,11 @@ +meta { + name: can-read + type: http + seq: 1 +} + +get { + url: {{BASE_URL}}/files/can_read/e1139bb6-d291-4170-8fb0-94dd787fa84b/22a7c6ca-8e57-46d2-9977-43dcbfcac760 + body: none + auth: none +} diff --git a/docs/bruno/create-file/file-in-folder.bru b/docs/bruno/create-file/file-in-folder.bru new file mode 100644 index 0000000..abfb798 --- /dev/null +++ b/docs/bruno/create-file/file-in-folder.bru @@ -0,0 +1,26 @@ +meta { + name: file-in-folder + type: http + seq: 8 +} + +post { + url: {{BASE_URL}}/files + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "userUUID": "5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39", + "parentUUID": "32693db3-cc27-470b-b54e-442e7184b3fd", + "fileType": "archive", + "fileName": "distributed-systems-notes", + "fileExtension": "md", + "fileSize": 24 + } +} diff --git a/docs/bruno/create-file/file-in-root.bru b/docs/bruno/create-file/file-in-root.bru new file mode 100644 index 0000000..c8ff329 --- /dev/null +++ b/docs/bruno/create-file/file-in-root.bru @@ -0,0 +1,26 @@ +meta { + name: file-in-root + type: http + seq: 10 +} + +post { + url: {{BASE_URL}}/files + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "userUUID": "5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39", + "parentUUID": null, + "fileType": "archive", + "fileName": "tasks", + "fileExtension": "md", + "fileSize": 15 + } +} diff --git a/docs/bruno/create-file/file-no-extension.bru b/docs/bruno/create-file/file-no-extension.bru new file mode 100644 index 0000000..e26e23d --- /dev/null +++ b/docs/bruno/create-file/file-no-extension.bru @@ -0,0 +1,26 @@ +meta { + name: file-no-extension + type: http + seq: 7 +} + +post { + url: {{BASE_URL}}/files + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "userUUID": "5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39", + "parentUUID": null, + "fileType": "archive", + "fileName": "rare", + "fileExtension": null, + "fileSize": 15 + } +} diff --git a/docs/bruno/create-file/folder-in-folder.bru b/docs/bruno/create-file/folder-in-folder.bru new file mode 100644 index 0000000..c8287cf --- /dev/null +++ b/docs/bruno/create-file/folder-in-folder.bru @@ -0,0 +1,26 @@ +meta { + name: folder-in-folder + type: http + seq: 6 +} + +post { + url: {{BASE_URL}}/files + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "userUUID": "5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39", + "parentUUID": "034bc5d4-1334-4f15-b986-31ea14ee03d9", + "fileType": "directory", + "fileName": "nested", + "fileExtension": null, + "fileSize": 0 + } +} diff --git a/docs/bruno/create-file/folder-in-root.bru b/docs/bruno/create-file/folder-in-root.bru new file mode 100644 index 0000000..220c531 --- /dev/null +++ b/docs/bruno/create-file/folder-in-root.bru @@ -0,0 +1,26 @@ +meta { + name: folder-in-root + type: http + seq: 9 +} + +post { + url: {{BASE_URL}}/files + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "userUUID": "5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39", + "parentUUID": null, + "fileType": "directory", + "fileName": "university", + "fileExtension": null, + "fileSize": 0 + } +} diff --git a/docs/bruno/environments/development.bru b/docs/bruno/environments/development.bru new file mode 100644 index 0000000..deb7f69 --- /dev/null +++ b/docs/bruno/environments/development.bru @@ -0,0 +1,3 @@ +vars { + BASE_URL: http://localhost:8080/api/v1 +} diff --git a/docs/bruno/get-metadata/get-metadata.bru b/docs/bruno/get-metadata/get-metadata.bru new file mode 100644 index 0000000..f4db4ed --- /dev/null +++ b/docs/bruno/get-metadata/get-metadata.bru @@ -0,0 +1,11 @@ +meta { + name: get-metadata + type: http + seq: 2 +} + +get { + url: {{BASE_URL}}/files/metadata/70bd47dc-13e2-4ab0-a16c-8f4871cdd980 + body: none + auth: none +} diff --git a/docs/bruno/list-files/files-in-folder.bru b/docs/bruno/list-files/files-in-folder.bru new file mode 100644 index 0000000..aa7b819 --- /dev/null +++ b/docs/bruno/list-files/files-in-folder.bru @@ -0,0 +1,15 @@ +meta { + name: files-in-folder + type: http + seq: 3 +} + +get { + url: {{BASE_URL}}/files/list/5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39?parentUUID=034bc5d4-1334-4f15-b986-31ea14ee03d9 + body: none + auth: none +} + +query { + parentUUID: 034bc5d4-1334-4f15-b986-31ea14ee03d9 +} diff --git a/docs/bruno/list-files/files-in-root.bru b/docs/bruno/list-files/files-in-root.bru new file mode 100644 index 0000000..3bd6b14 --- /dev/null +++ b/docs/bruno/list-files/files-in-root.bru @@ -0,0 +1,11 @@ +meta { + name: files-in-root + type: http + seq: 4 +} + +get { + url: {{BASE_URL}}/files/list/5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39 + body: none + auth: none +} diff --git a/docs/bruno/mark-as-ready/mark-as-ready.bru b/docs/bruno/mark-as-ready/mark-as-ready.bru new file mode 100644 index 0000000..efe7ab4 --- /dev/null +++ b/docs/bruno/mark-as-ready/mark-as-ready.bru @@ -0,0 +1,21 @@ +meta { + name: mark-as-ready + type: http + seq: 2 +} + +put { + url: {{BASE_URL}}/files/ready/70bd47dc-13e2-4ab0-a16c-8f4871cdd980 + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "volume": "1" + } +} diff --git a/docs/bruno/move-file/move-file.bru b/docs/bruno/move-file/move-file.bru new file mode 100644 index 0000000..511e99d --- /dev/null +++ b/docs/bruno/move-file/move-file.bru @@ -0,0 +1,21 @@ +meta { + name: move-file + type: http + seq: 2 +} + +put { + url: {{BASE_URL}}/files/move/5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39/0cbd00a1-fc6c-43b3-96d6-9bb52b739290 + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "parentUUID": "bc421da0-684c-497f-bfd6-224305c04655" + } +} diff --git a/docs/bruno/rename-file/rename-file.bru b/docs/bruno/rename-file/rename-file.bru new file mode 100644 index 0000000..ee3c929 --- /dev/null +++ b/docs/bruno/rename-file/rename-file.bru @@ -0,0 +1,21 @@ +meta { + name: rename-file + type: http + seq: 2 +} + +put { + url: {{BASE_URL}}/files/rename/5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39/89c2ff2e-836a-4a40-85da-d52cd91450bb + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "name": "renamed" + } +} diff --git a/docs/bruno/share-file/share-file.bru b/docs/bruno/share-file/share-file.bru new file mode 100644 index 0000000..23e59f4 --- /dev/null +++ b/docs/bruno/share-file/share-file.bru @@ -0,0 +1,21 @@ +meta { + name: share-file + type: http + seq: 1 +} + +post { + url: {{BASE_URL}}/files/share/5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39/e237038f-32e5-4b50-bc95-5ab88537ec5d + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "otherUserUUID": 'e1139bb6-d291-4170-8fb0-94dd787fa84b' + } +} diff --git a/docs/bruno/shared-with-me/shared-with-me.bru b/docs/bruno/shared-with-me/shared-with-me.bru new file mode 100644 index 0000000..bd077c6 --- /dev/null +++ b/docs/bruno/shared-with-me/shared-with-me.bru @@ -0,0 +1,11 @@ +meta { + name: shared-with-me + type: http + seq: 2 +} + +get { + url: {{BASE_URL}}/files/shared_with_me/e1139bb6-d291-4170-8fb0-94dd787fa84b + body: none + auth: none +} diff --git a/docs/bruno/shared-with-who/shared-with-who.bru b/docs/bruno/shared-with-who/shared-with-who.bru new file mode 100644 index 0000000..5997c33 --- /dev/null +++ b/docs/bruno/shared-with-who/shared-with-who.bru @@ -0,0 +1,11 @@ +meta { + name: shared-with-who + type: http + seq: 2 +} + +get { + url: {{BASE_URL}}/files/shared_with_who/22a7c6ca-8e57-46d2-9977-43dcbfcac760 + body: none + auth: none +} diff --git a/docs/rest/can_read.http b/docs/rest/can_read.http deleted file mode 100644 index a9e4d50..0000000 --- a/docs/rest/can_read.http +++ /dev/null @@ -1,3 +0,0 @@ -### Check if an user can read a file (archive or directory) - -GET http://localhost:8080/api/v1/files/can_read/{userUUID}/{fileUUID} HTTP/1.1 diff --git a/docs/rest/get_metadata.http b/docs/rest/get_metadata.http deleted file mode 100644 index db51eff..0000000 --- a/docs/rest/get_metadata.http +++ /dev/null @@ -1,3 +0,0 @@ -### Get metadata for a given file - -GET http://localhost:8080/api/v1/files/metadata/{fileUUID} HTTP/1.1 \ No newline at end of file diff --git a/docs/rest/get_shared_with_user.http b/docs/rest/get_shared_with_user.http deleted file mode 100644 index 5739293..0000000 --- a/docs/rest/get_shared_with_user.http +++ /dev/null @@ -1,3 +0,0 @@ -### Get metadata of the files shared with the user - -GET http://localhost:8080/api/v1/files/shared_with_me/{userUUID} HTTP/1.1 \ No newline at end of file diff --git a/docs/rest/get_shared_with_who.http b/docs/rest/get_shared_with_who.http deleted file mode 100644 index e7d5a1c..0000000 --- a/docs/rest/get_shared_with_who.http +++ /dev/null @@ -1,3 +0,0 @@ -### Get the list of UUIDs of the users whom a file is shared with - -GET http://localhost:8080/api/v1/files/shared_with_who/{fileUUID} HTTP/1.1 \ No newline at end of file diff --git a/docs/rest/list_files.http b/docs/rest/list_files.http deleted file mode 100644 index f303077..0000000 --- a/docs/rest/list_files.http +++ /dev/null @@ -1,7 +0,0 @@ -### List files in root directory - -GET http://localhost:8080/api/v1/files/list/{userUUID} - -### List files in subdirectory - -GET http://localhost:8080/api/v1/files/list/{userUUID}?parentUUID={parentUUID} \ No newline at end of file diff --git a/docs/rest/mark_as_ready.http b/docs/rest/mark_as_ready.http deleted file mode 100644 index 5a0e062..0000000 --- a/docs/rest/mark_as_ready.http +++ /dev/null @@ -1,8 +0,0 @@ -### Mark file as ready - -POST http://localhost:8080/api/v1/files/ready/{fileUUID} HTTP/1.1 -Content-Type: application/json - -{ - "volume": "volume_2" -} \ No newline at end of file diff --git a/docs/rest/move_file.http b/docs/rest/move_file.http deleted file mode 100644 index a447fcf..0000000 --- a/docs/rest/move_file.http +++ /dev/null @@ -1,17 +0,0 @@ -### Move file to a new parent directory - -PUT http://localhost:8080/api/v1/files/move/{userUUID}/{fileUUUID} HTTP/1.1 -Content-Type: application/json - -{ - "parentUUID": "{parentUUID}" -} - -### Move file to root - -PUT http://localhost:8080/api/v1/files/move/{userUUID}/{fileUUUID} HTTP/1.1 -Content-Type: application/json - -{ - "parentUUID": null -} \ No newline at end of file diff --git a/docs/rest/rename_file.http b/docs/rest/rename_file.http deleted file mode 100644 index 71a13ea..0000000 --- a/docs/rest/rename_file.http +++ /dev/null @@ -1,8 +0,0 @@ -### Rename file - -PUT http://localhost:8080/api/v1/files/rename/{userUUID}/{fileUUUID} HTTP/1.1 -Content-Type: application/json - -{ - "name": "{name}" -} diff --git a/docs/rest/save_metadata.http b/docs/rest/save_metadata.http deleted file mode 100644 index 70471ca..0000000 --- a/docs/rest/save_metadata.http +++ /dev/null @@ -1,55 +0,0 @@ -### Save metadata for an archive file stored in the root directory - -POST http://localhost:8080/api/v1/files/ HTTP/1.1 -Content-Type: application/json - -{ - "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", - "parentUUID": null, - "fileType": "archive", - "fileName": "project", - "fileExtension": "txt", - "fileSize": 32, -} - -### Save metadata for a directory stored in the root directory - -POST http://localhost:8080/api/v1/files/ HTTP/1.1 -Content-Type: application/json - -{ - "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", - "parentUUID": null, - "fileType": "directory", - "fileName": "university", - "fileExtension": null, - "fileSize": 0, -} - -### Save metadata for an archive file stored in a parent directory - -POST http://localhost:8080/api/v1/files/ HTTP/1.1 -Content-Type: application/json - -{ - "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", - "parentUUID": "92467e11-38e8-41f1-a088-0538f43811bd", - "fileType": "archive", - "fileName": "nested", - "fileExtension": "txt", - "fileSize": 16, -} - -### Save metadata for a directory stored in a parent directory - -POST http://localhost:8080/api/v1/files/ HTTP/1.1 -Content-Type: application/json - -{ - "userUUID": "9ae0ee6f-0644-46c8-b364-ee36c9f9bd81", - "parentUUID": "92467e11-38e8-41f1-a088-0538f43811bd", - "fileType": "directory", - "fileName": "nested", - "fileExtension": null, - "fileSize": 0, -} \ No newline at end of file diff --git a/docs/rest/share_file.http b/docs/rest/share_file.http deleted file mode 100644 index c3daa18..0000000 --- a/docs/rest/share_file.http +++ /dev/null @@ -1,8 +0,0 @@ -### Share a file (archive or directory) - -POST http://localhost:8080/api/v1/files/share/{ownerUUID}/{fileUUID} HTTP/1.1 -Content-Type: application/json - -{ - "otherUserUUID": "928f7a86-a091-4199-b767-58a0e1147b72" -} \ No newline at end of file From 8d46afa5721c3ef0d021e504b5e8f8645b06b49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Andr=C3=A9s=20Chaparro=20Quintero?= <62714297+PedroChaparro@users.noreply.github.com> Date: Sat, 14 Oct 2023 13:43:02 -0500 Subject: [PATCH 64/67] fix: Include files size (#86) * docs(openapi): Update spec * fix: Include files size when listing files * fix: Use `int` instead of `long` to store files size --- docs/spec.openapi.yaml | 40 ++++++++++------- .../files_metadata/domain/ArchiveMeta.scala | 4 +- .../domain/FileExtendedMeta.scala | 2 +- .../FilesMetaPostgresRepository.scala | 8 ++-- .../infrastructure/MetadataControllers.scala | 44 +++++++++++-------- .../requests/CreationReqSchema.scala | 6 +-- 6 files changed, 61 insertions(+), 43 deletions(-) diff --git a/docs/spec.openapi.yaml b/docs/spec.openapi.yaml index 83d9ead..2b70858 100644 --- a/docs/spec.openapi.yaml +++ b/docs/spec.openapi.yaml @@ -15,7 +15,7 @@ tags: paths: /files/list/{user_uuid}: get: - tags: ["File"] + tags: [ "File" ] description: List files in the given directory. List the user's root directory by default when the parentUUID query parameter is not provided. parameters: - in: path @@ -38,7 +38,14 @@ paths: schema: type: array items: - $ref: "#/components/schemas/file" + type: object + allOf: + - $ref: "#/components/schemas/file" + - type: object + properties: + isShared: + type: boolean + example: false "403": description: Forbidden. The directory is not owned by the user. content: @@ -60,7 +67,7 @@ paths: /files: post: - tags: ["File"] + tags: [ "File" ] description: Save the metadata for a new file or directory. requestBody: content: @@ -105,7 +112,7 @@ paths: /files/shared_with_me/{user_uuid}: get: - tags: ["File"] + tags: [ "File" ] description: List the files shared with the given user. parameters: - in: path @@ -132,7 +139,7 @@ paths: /files/can_read/{user_uuid}/{file_uuid}: get: - tags: ["File"] + tags: [ "File" ] description: Check if the given user can read the given file. parameters: - in: path @@ -171,7 +178,7 @@ paths: /files/metadata/{file_uuid}: get: - tags: ["File"] + tags: [ "File" ] description: Get the metadata of the given file. This endpoint is suposed to only be used by the gateway service to obtain the location (files/volume/archive_uuid) of the file. parameters: - in: path @@ -214,7 +221,7 @@ paths: /files/shared_with_who/{file_uuid}: get: - tags: ["File"] + tags: [ "File" ] parameters: - in: path name: file_uuid @@ -250,7 +257,7 @@ paths: /files/{user_uuid}/{file_uuid}: delete: - tags: ["File"] + tags: [ "File" ] description: Delete the metadata of the given file. parameters: - in: path @@ -289,7 +296,7 @@ paths: /files/share/{user_uuid}/{file_uuid}: post: - tags: ["File"] + tags: [ "File" ] description: Share the given file with the given user parameters: - in: path @@ -339,7 +346,7 @@ paths: /files/unshare/{user_uuid}/{file_uuid}: post: - tags: ["File"] + tags: [ "File" ] description: Unshare the given file with the given user parameters: - in: path @@ -383,7 +390,7 @@ paths: /files/ready/{file_uuid}: put: - tags: ["File"] + tags: [ "File" ] description: Mark the given file as ready (Stored in the filesystem). parameters: - in: path @@ -425,7 +432,7 @@ paths: $ref: "#/components/schemas/error_response" /files/rename/{user_uuid}/{file_uuid}: put: - tags: ["File"] + tags: [ "File" ] description: Rename the given file. parameters: - in: path @@ -478,7 +485,7 @@ paths: $ref: "#/components/schemas/error_response" /files/move/{user_uuid}/{file_uuid}: put: - tags: ["File"] + tags: [ "File" ] description: Move the given file to the given directory. parameters: - in: path @@ -540,7 +547,7 @@ components: example: "b96bdc16-8f27-44aa-9758-b4e5f13060fe" fileType: type: string - enum: ["archive", "directory"] + enum: [ "archive", "directory" ] description: "Whether the file is a directory or an archive" example: "archive" fileName: @@ -549,6 +556,9 @@ components: fileExtension: type: string example: "pdf" + fileSize: + type: number + example: 1024 metadata: type: object @@ -580,7 +590,7 @@ components: example: "5ad724f0-4091-453a-914a-c2d11d69d1e3" fileType: type: string - enum: ["archive", "directory"] + enum: [ "archive", "directory" ] description: "Whether the file is a directory or an archive" example: "archive" fileName: diff --git a/src/main/scala/files_metadata/domain/ArchiveMeta.scala b/src/main/scala/files_metadata/domain/ArchiveMeta.scala index 1bd9d47..f3ff017 100644 --- a/src/main/scala/files_metadata/domain/ArchiveMeta.scala +++ b/src/main/scala/files_metadata/domain/ArchiveMeta.scala @@ -6,14 +6,14 @@ import java.util.UUID case class ArchiveMeta( uuid: UUID, extension: String, - size: Long, + size: Int, ready: Boolean ) object ArchiveMeta { def createNewArchive( extension: String, - size: Long + size: Int ): ArchiveMeta = new ArchiveMeta( uuid = null, diff --git a/src/main/scala/files_metadata/domain/FileExtendedMeta.scala b/src/main/scala/files_metadata/domain/FileExtendedMeta.scala index a204d52..78b4b9d 100644 --- a/src/main/scala/files_metadata/domain/FileExtendedMeta.scala +++ b/src/main/scala/files_metadata/domain/FileExtendedMeta.scala @@ -11,7 +11,7 @@ case class FileExtendedMeta( volume: String, name: String, extension: String, - size: Long, + size: Int, isReady: Boolean, isShared: Boolean ) diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index eca1096..dda7d48 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -171,7 +171,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { volume = result.getString( "volume" ), name = result.getString( "name" ), extension = result.getString( "extension" ), - size = result.getLong( "size" ), + size = result.getInt( "size" ), isReady = true, isShared = result.getBoolean( "is_shared" ) ) @@ -223,7 +223,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { volume = result.getString( "volume" ), name = result.getString( "name" ), extension = result.getString( "extension" ), - size = result.getLong( "size" ), + size = result.getInt( "size" ), isReady = true, isShared = result.getBoolean( "is_shared" ) ) @@ -294,7 +294,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { ArchiveMeta( uuid = UUID.fromString( result.getString( "uuid" ) ), extension = result.getString( "extension" ), - size = result.getLong( "size" ), + size = result.getInt( "size" ), ready = result.getBoolean( "ready" ) ) } finally { @@ -347,7 +347,7 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { volume = result.getString( "volume" ), name = result.getString( "name" ), extension = result.getString( "extension" ), - size = result.getLong( "size" ), + size = result.getInt( "size" ), isReady = true, isShared = result.getBoolean( "is_shared" ) ) diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 8611b77..4dc5106 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -116,19 +116,23 @@ class MetadataControllers { val isDirectory = fileMeta.archiveUuid.isEmpty if (isDirectory) { ujson.Obj( - "uuid" -> fileMeta.uuid.toString, - "fileType" -> "directory", - "name" -> fileMeta.name, - "extension" -> ujson.Null, - "isShared" -> fileMeta.isShared + "uuid" -> fileMeta.uuid.toString, + "fileType" -> "directory", + "fileName" -> fileMeta.name, + "fileExtension" -> ujson.Null, + "fileSize" -> 0, + "isShared" -> fileMeta.isShared ) } else { ujson.Obj( - "uuid" -> fileMeta.uuid.toString, - "fileType" -> "archive", - "name" -> fileMeta.name, - "extension" -> parseNullableStringToJSON( fileMeta.extension ), - "isShared" -> fileMeta.isShared + "uuid" -> fileMeta.uuid.toString, + "fileType" -> "archive", + "fileName" -> fileMeta.name, + "fileExtension" -> parseNullableStringToJSON( + fileMeta.extension + ), + "fileSize" -> fileMeta.size, + "isShared" -> fileMeta.isShared ) } } ) @@ -445,17 +449,21 @@ class MetadataControllers { filesMeta.map( fileMeta => { if (fileMeta.archiveUuid.isEmpty) { ujson.Obj( - "uuid" -> fileMeta.uuid.toString, - "fileType" -> "directory", - "name" -> fileMeta.name, - "extension" -> ujson.Null + "uuid" -> fileMeta.uuid.toString, + "fileType" -> "directory", + "fileName" -> fileMeta.name, + "fileExtension" -> ujson.Null, + "fileSize" -> 0 ) } else { ujson.Obj( - "uuid" -> fileMeta.uuid.toString, - "fileType" -> "archive", - "name" -> fileMeta.name, - "extension" -> parseNullableStringToJSON( fileMeta.extension ) + "uuid" -> fileMeta.uuid.toString, + "fileType" -> "archive", + "fileName" -> fileMeta.name, + "fileExtension" -> parseNullableStringToJSON( + fileMeta.extension + ), + "fileSize" -> fileMeta.size ) } } ) diff --git a/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala b/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala index 9c4a995..6a775d6 100644 --- a/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala +++ b/src/main/scala/files_metadata/infrastructure/requests/CreationReqSchema.scala @@ -12,7 +12,7 @@ case class CreationReqSchema( fileType: String, fileName: String, fileExtension: String, - fileSize: Long + fileSize: Int ) object CreationReqSchema { @@ -46,7 +46,7 @@ object CreationReqSchema { .and( request.fileExtension.has( size <= 16 ) ) ) request.fileName.has( size <= 128 ) - request.fileSize should be > 0L + request.fileSize should be > 0 } } @@ -67,7 +67,7 @@ object CreationReqSchema { request.fileName.is( notEmpty ) request.fileName.has( size <= 128 ) request.fileExtension.is( aNull ) - request.fileSize.is( equalTo( 0L ) ) // File size is 0 for a directory + request.fileSize.is( equalTo( 0 ) ) // File size is 0 for a directory } } } From 609b8075433f0bb3b78fcbc7b522f3ecceb489e2 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Sat, 14 Oct 2023 18:43:18 +0000 Subject: [PATCH 65/67] chore(release): v0.11.1 [skip ci] --- CHANGELOG.md | 18 +++++++++--------- version.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d1122..cb7f086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [0.11.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.11.0...v0.11.1) (2023-10-14) + + +### Bug Fixes + +* Include files size ([#86](https://github.com/hawks-atlanta/metadata-scala/issues/86)) ([8d46afa](https://github.com/hawks-atlanta/metadata-scala/commit/8d46afa5721c3ef0d021e504b5e8f8645b06b49a)) + + + # [0.11.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.4...v0.11.0) (2023-10-03) @@ -34,12 +43,3 @@ -## [0.10.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.0...v0.10.1) (2023-09-26) - - -### Bug Fixes - -* Update is_shared column when a file is shared ([#70](https://github.com/hawks-atlanta/metadata-scala/issues/70)) ([7f63347](https://github.com/hawks-atlanta/metadata-scala/commit/7f63347667d0095616e8d09641d264e888f44ffb)) - - - diff --git a/version.json b/version.json index 2cd0808..9e20280 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.11.0" + "version": "0.11.1" } \ No newline at end of file From be58b4ad112ebe141f88781e8ea7d04713f1bd7d Mon Sep 17 00:00:00 2001 From: Sergio Daniel Baron Cabrera <47455237+sdbaronc@users.noreply.github.com> Date: Sun, 22 Oct 2023 16:37:17 -0500 Subject: [PATCH 66/67] feat: Unshare file (#89) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add unshare file method to the repository * feat: add endpoint unshare file * fix: repair path route unshare metadata * feat: add utils unshare and generate Unshare * feat: add test unshare * refactor: add format code * refactor: repair messages exeptions * docs(http): Update bruno collection --------- Co-authored-by: Pedro Andrés Chaparro Quintero --- .../bruno/shared-with-who/shared-with-who.bru | 2 +- docs/bruno/unshare-file/unshare-file.bru | 17 ++ .../application/FilesMetaUseCases.scala | 27 ++++ .../domain/FilesMetaRepository.scala | 1 + .../FilesMetaPostgresRepository.scala | 31 ++++ .../infrastructure/MetadataControllers.scala | 43 +++++ .../infrastructure/MetadataRoutes.scala | 15 ++ .../files_metadata/FilesTestsUtils.scala | 23 +++ .../scala/files_metadata/UnshareFile.scala | 150 ++++++++++++++++++ 9 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 docs/bruno/unshare-file/unshare-file.bru create mode 100644 src/test/scala/files_metadata/UnshareFile.scala diff --git a/docs/bruno/shared-with-who/shared-with-who.bru b/docs/bruno/shared-with-who/shared-with-who.bru index 5997c33..79a412f 100644 --- a/docs/bruno/shared-with-who/shared-with-who.bru +++ b/docs/bruno/shared-with-who/shared-with-who.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{BASE_URL}}/files/shared_with_who/22a7c6ca-8e57-46d2-9977-43dcbfcac760 + url: {{BASE_URL}}/files/shared_with_who/76c5a635-08c4-46c9-89ef-89c120e2e55f body: none auth: none } diff --git a/docs/bruno/unshare-file/unshare-file.bru b/docs/bruno/unshare-file/unshare-file.bru new file mode 100644 index 0000000..d16dbac --- /dev/null +++ b/docs/bruno/unshare-file/unshare-file.bru @@ -0,0 +1,17 @@ +meta { + name: unshare-file + type: http + seq: 1 +} + +post { + url: {{BASE_URL}}/files/unshare/5c8e5e9d-7d82-450f-9fe3-43fc73aa5a39/76c5a635-08c4-46c9-89ef-89c120e2e55f + body: json + auth: none +} + +body:json { + { + "otherUserUUID": "e1139bb6-d291-4170-8fb0-94dd787fa84b" + } +} diff --git a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala index d565ba2..d921238 100644 --- a/src/main/scala/files_metadata/application/FilesMetaUseCases.scala +++ b/src/main/scala/files_metadata/application/FilesMetaUseCases.scala @@ -228,4 +228,31 @@ class FilesMetaUseCases { repository.updateFileParent( fileUUID, newParentUUID ) } + + def unShareFile( + ownerUUID: UUID, + fileUUID: UUID, + otherUserUUID: UUID + ): Unit = { + val fileMeta = repository.getFileMeta( fileUUID ) + + if (fileMeta.ownerUuid != ownerUUID) { + throw DomainExceptions.FileNotOwnedException( + "You don't own the file" + ) + } + if (ownerUUID == otherUserUUID) { + throw DomainExceptions.FileNotOwnedException( + "You cannot un-share a file with yourself" + ) + } + + if (!repository.isFileDirectlySharedWithUser( fileUUID, otherUserUUID )) { + throw DomainExceptions.FileAlreadySharedException( + "The file is not shared with the given user" + ) + } + + repository.unShareFile( fileUUID, otherUserUUID ) + } } diff --git a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala index b0bc88e..a1a35e0 100644 --- a/src/main/scala/files_metadata/domain/FilesMetaRepository.scala +++ b/src/main/scala/files_metadata/domain/FilesMetaRepository.scala @@ -44,6 +44,7 @@ trait FilesMetaRepository { def updateFileName( fileUUID: UUID, newName: String ): Unit def updateFileParent( fileUUID: UUID, parentUUID: Option[UUID] ): Unit + def unShareFile( fileUUID: UUID, userUUID: UUID ): Unit // --- Delete --- def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit diff --git a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala index dda7d48..c82290c 100644 --- a/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala +++ b/src/main/scala/files_metadata/infrastructure/FilesMetaPostgresRepository.scala @@ -589,5 +589,36 @@ class FilesMetaPostgresRepository extends FilesMetaRepository { } } + override def unShareFile( fileUUID: UUID, userUUID: UUID ): Unit = { + val connection: Connection = pool.getConnection() + connection.setAutoCommit( false ) + + try { + val shareStatement = connection.prepareStatement( + "DELETE FROM shared_files WHERE file_uuid =? AND user_uuid =?" + ) + shareStatement.setObject( 1, fileUUID ) + shareStatement.setObject( 2, userUUID ) + shareStatement.executeUpdate() + + val checkSharedStatement = connection.prepareStatement( + "SELECT * FROM shared_files WHERE file_uuid = ?" + ) + checkSharedStatement.setObject( 1, fileUUID ) + val resultSet = checkSharedStatement.executeQuery() + if (!resultSet.next()) { + val updateStatement = connection.prepareStatement( + "UPDATE files SET is_shared = false WHERE uuid = ?" + ) + updateStatement.setObject( 1, fileUUID ) + updateStatement.executeUpdate() + } + + connection.commit() + } finally { + connection.close() + } + } + override def deleteFileMeta( ownerUuid: UUID, uuid: UUID ): Unit = ??? } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala index 4dc5106..ee755cc 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataControllers.scala @@ -622,4 +622,47 @@ class MetadataControllers { case e: Exception => _handleException( e ) } } + + def UnShareFileController( + request: cask.Request, + ownerUUID: String, + fileUUID: String + ): cask.Response[Obj] = { + try { + val decoded: ShareReqSchema = read[ShareReqSchema]( + request.text() + ) + + val isOwnerUUIDValid = CommonValidator.validateUUID( ownerUUID ) + val isFileUUIDValid = CommonValidator.validateUUID( fileUUID ) + + val validationRule: Validator[ShareReqSchema] = + ShareReqSchema.shareSchemaValidator + val validationResult = validate[ShareReqSchema]( decoded )( + validationRule + ) + if (!isOwnerUUIDValid || !isFileUUIDValid || validationResult.isFailure) { + return cask.Response( + ujson.Obj( + "error" -> true, + "message" -> "Fields validation failed" + ), + statusCode = 400 + ) + } + + useCases.unShareFile( + ownerUUID = UUID.fromString( ownerUUID ), + fileUUID = UUID.fromString( fileUUID ), + otherUserUUID = UUID.fromString( decoded.otherUserUUID ) + ) + + cask.Response( + None, + statusCode = 204 + ) + } catch { + case e: Exception => _handleException( e ) + } + } } diff --git a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala index a692552..68b822f 100644 --- a/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala +++ b/src/main/scala/files_metadata/infrastructure/MetadataRoutes.scala @@ -132,5 +132,20 @@ case class MetadataRoutes() extends cask.Routes { ) } + private val unShareMetadataEndpoint = + s"$basePath/unshare/:ownerUUID/:fileUUID" + + @cask.post( unShareMetadataEndpoint ) + def UnShareMetadataHandler( + request: cask.Request, + ownerUUID: String, + fileUUID: String + ): cask.Response[Obj] = { + StdoutLogger.logAndReturnEndpointResponse( + unShareMetadataEndpoint, + controllers.UnShareFileController( request, ownerUUID, fileUUID ) + ) + } + initialize() } diff --git a/src/test/scala/files_metadata/FilesTestsUtils.scala b/src/test/scala/files_metadata/FilesTestsUtils.scala index 81e92ca..35dddba 100644 --- a/src/test/scala/files_metadata/FilesTestsUtils.scala +++ b/src/test/scala/files_metadata/FilesTestsUtils.scala @@ -108,6 +108,21 @@ object FilesTestsUtils { ) } + def UnShareFile( + ownerUUID: String, + fileUUID: String, + payload: util.HashMap[String, Any] + ): Response = { + `given`() + .port( 8080 ) + .contentType( "application/json" ) + .body( payload ) + .when() + .post( + s"${ UnShareFileTestsData.API_PREFIX }/$ownerUUID/$fileUUID" + ) + } + def generateShareFilePayload( otherUserUUID: UUID ): util.HashMap[String, Any] = { @@ -116,6 +131,14 @@ object FilesTestsUtils { shareFilePayload } + def generateUnshareFilePayload( + otherUserUUID: UUID + ): util.HashMap[String, Any] = { + val unShareFilePayload = new util.HashMap[String, Any]() + unShareFilePayload.put( "otherUserUUID", otherUserUUID.toString ) + unShareFilePayload + } + def GetSharedWithUser( userUUID: String ): Response = { `given`() .port( 8080 ) diff --git a/src/test/scala/files_metadata/UnshareFile.scala b/src/test/scala/files_metadata/UnshareFile.scala new file mode 100644 index 0000000..fde7abc --- /dev/null +++ b/src/test/scala/files_metadata/UnshareFile.scala @@ -0,0 +1,150 @@ +package org.hawksatlanta.metadata +package files_metadata + +import java.util.UUID + +import org.junit.runner.manipulation.Alphanumeric +import org.junit.runner.OrderWith +import org.junit.Before +import org.junit.Test +import org.scalatestplus.junit.JUnitSuite + +object UnShareFileTestsData { + val API_PREFIX: String = "/api/v1/files/unshare" + val OWNER_USER_UUID: UUID = UUID.randomUUID() + val OTHER_USER_UUID: UUID = UUID.randomUUID() + + private var unsharePayload: java.util.HashMap[String, Any] = _ + var savedDirectoryUUID: UUID = _ + var savedFileUUID: UUID = _ + var sharedFileUUID: UUID = _ + var sharedDirectoryUUID: UUID = _ + + def getUnsharePayload(): java.util.HashMap[String, Any] = { + if (unsharePayload == null) { + unsharePayload = FilesTestsUtils.generateUnshareFilePayload( + otherUserUUID = OTHER_USER_UUID + ) + } + + unsharePayload.clone().asInstanceOf[java.util.HashMap[String, Any]] + } +} + +@OrderWith( classOf[Alphanumeric] ) +class UnShareFileTests extends JUnitSuite { + def saveFilesAndShareToUnhare(): Unit = { + // Save a file to share + val saveFilePayload = FilesTestsUtils.generateFilePayload( + ownerUUID = UnShareFileTestsData.OWNER_USER_UUID, + parentDirUUID = None + ) + + val saveFileResponse = FilesTestsUtils.SaveFile( saveFilePayload ) + UnShareFileTestsData.savedFileUUID = + UUID.fromString( saveFileResponse.jsonPath().get( "uuid" ) ) + + // Save a directory to share + val saveDirectoryPayload = FilesTestsUtils.generateDirectoryPayload( + ownerUUID = UnShareFileTestsData.OWNER_USER_UUID, + parentDirUUID = None + ) + + val saveDirectoryResponse = + FilesTestsUtils.SaveFile( saveDirectoryPayload ) + UnShareFileTestsData.savedDirectoryUUID = + UUID.fromString( saveDirectoryResponse.jsonPath().get( "uuid" ) ) + + val ShareFilePayload = FilesTestsUtils.generateShareFilePayload( + otherUserUUID = UnShareFileTestsData.OTHER_USER_UUID + ) + val ShareFileResponse = FilesTestsUtils.ShareFile( + ownerUUID = UnShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = UnShareFileTestsData.savedFileUUID.toString, + payload = ShareFilePayload + ) + val ShareDirectoryPayload = FilesTestsUtils.generateShareFilePayload( + otherUserUUID = UnShareFileTestsData.OTHER_USER_UUID + ) + val ShareDirectoryResponse = FilesTestsUtils.ShareFile( + ownerUUID = UnShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = UnShareFileTestsData.savedDirectoryUUID.toString, + payload = ShareDirectoryPayload + ) + + } + + @Before + def startHttpServer(): Unit = { + FilesTestsUtils.StartHttpServer() + } + @Test + def T1_UnShareFileBadRequest(): Unit = { + saveFilesAndShareToUnhare() + // 1. Bad other user + val requestBody = ShareFileTestsData.getSharePayload() + requestBody.put( "otherUserUUID", "Not an UUID" ) + val response = FilesTestsUtils.UnShareFile( + ownerUUID = UnShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = UnShareFileTestsData.savedFileUUID.toString, + payload = requestBody + ) + assert( response.statusCode() == 400 ) + assert( response.jsonPath().getBoolean( "error" ) ) + // 2. Bad ownerUserUUID + val response2 = FilesTestsUtils.UnShareFile( + ownerUUID = "Not an UUID", + fileUUID = UnShareFileTestsData.savedFileUUID.toString, + payload = UnShareFileTestsData.getUnsharePayload() + ) + assert( response2.statusCode() == 400 ) + assert( response2.jsonPath().getBoolean( "error" ) ) + // 3. Bad fileUUID + val response3 = FilesTestsUtils.UnShareFile( + ownerUUID = UnShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = "Not an UUID", + payload = UnShareFileTestsData.getUnsharePayload() + ) + assert( response3.statusCode() == 400 ) + assert( response3.jsonPath().getBoolean( "error" ) ) + } + @Test + def T2_UnshareSuccess(): Unit = { + // Unshare the File + val fileResponse = FilesTestsUtils.UnShareFile( + ownerUUID = UnShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = UnShareFileTestsData.savedFileUUID.toString, + payload = UnShareFileTestsData.getUnsharePayload() + ) + assert( fileResponse.statusCode() == 204 ) + // Unshare the File + val fileResponse2 = FilesTestsUtils.UnShareFile( + ownerUUID = UnShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = UnShareFileTestsData.savedDirectoryUUID.toString, + payload = UnShareFileTestsData.getUnsharePayload() + ) + assert( fileResponse2.statusCode() == 204 ) + } + + @Test + def T3_UnshareFileNotFound(): Unit = { + val response = FilesTestsUtils.UnShareFile( + ownerUUID = UnShareFileTestsData.OWNER_USER_UUID.toString, + fileUUID = UUID.randomUUID().toString, + payload = UnShareFileTestsData.getUnsharePayload() + ) + assert( response.statusCode() == 404 ) + assert( response.jsonPath().getBoolean( "error" ) ) + } + + @Test + def T4_ShareFileForbidden(): Unit = { + val response = FilesTestsUtils.UnShareFile( + ownerUUID = UnShareFileTestsData.OTHER_USER_UUID.toString, + fileUUID = UnShareFileTestsData.savedFileUUID.toString, + payload = UnShareFileTestsData.getUnsharePayload() + ) + assert( response.statusCode() == 403 ) + assert( response.jsonPath().getBoolean( "error" ) ) + } +} From 9a6129fce2a5d2e7c460e85de13dadf5d64fb7e3 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Sun, 22 Oct 2023 21:39:35 +0000 Subject: [PATCH 67/67] chore(release): v0.12.0 [skip ci] --- CHANGELOG.md | 18 +++++++++--------- version.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7f086..7525955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [0.12.0](https://github.com/hawks-atlanta/metadata-scala/compare/v0.11.1...v0.12.0) (2023-10-22) + + +### Features + +* Unshare file ([#89](https://github.com/hawks-atlanta/metadata-scala/issues/89)) ([be58b4a](https://github.com/hawks-atlanta/metadata-scala/commit/be58b4ad112ebe141f88781e8ea7d04713f1bd7d)) + + + ## [0.11.1](https://github.com/hawks-atlanta/metadata-scala/compare/v0.11.0...v0.11.1) (2023-10-14) @@ -34,12 +43,3 @@ -## [0.10.2](https://github.com/hawks-atlanta/metadata-scala/compare/v0.10.1...v0.10.2) (2023-09-29) - - -### Bug Fixes - -* Ignore ready state validations for directories ([#73](https://github.com/hawks-atlanta/metadata-scala/issues/73)) ([fa5467f](https://github.com/hawks-atlanta/metadata-scala/commit/fa5467f86bda9312a6dad474bbdd3f5360a875c9)) - - - diff --git a/version.json b/version.json index 9e20280..239eb2a 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "0.11.1" + "version": "0.12.0" } \ No newline at end of file