diff --git a/.github/ISSUE_TEMPLATE/issue-.md b/.github/ISSUE_TEMPLATE/issue-.md deleted file mode 100644 index 60f04d42d..000000000 --- a/.github/ISSUE_TEMPLATE/issue-.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: 'Issue ' -about: Describe this issue purpose here. - ---- - -## Issue Name - -### Summary - -### Steps to Reproduce - -### Current Behaviour - -### Expected Behaviour - -### Extra Details - -Here you should include details about the system (if it is unique) and possible information about a fix (feel free to link to code where relevant). Screenshots/GIFs are also fine here. diff --git a/.github/openapi/python-config.json b/.github/openapi/python-config.json new file mode 100644 index 000000000..709747cf2 --- /dev/null +++ b/.github/openapi/python-config.json @@ -0,0 +1,5 @@ +{ + "generatorName": "python", + "packageName": "scicat-sdk-py", + "projectName": "scicat-sdk-py" +} diff --git a/.github/openapi/python-pydantic-v1-config.json b/.github/openapi/python-pydantic-v1-config.json new file mode 100644 index 000000000..ef10d66f2 --- /dev/null +++ b/.github/openapi/python-pydantic-v1-config.json @@ -0,0 +1,5 @@ +{ + "generatorName": "python-pydantic-v1", + "packageName": "scicat-sdk-pydantic", + "projectName": "scicat-sdk-pydantic" +} diff --git a/.github/openapi/typescript-angular-config.json b/.github/openapi/typescript-angular-config.json new file mode 100644 index 000000000..fc279bbcf --- /dev/null +++ b/.github/openapi/typescript-angular-config.json @@ -0,0 +1,6 @@ +{ + "generatorName": "typescript-angular", + "npmName": "@scicatproject/scicat-sdk-ts", + "ngVersion": "16.2.12", + "withInterfaces": true +} diff --git a/.github/pr-title-checker-config.json b/.github/pr-title-checker-config.json new file mode 100644 index 000000000..893159467 --- /dev/null +++ b/.github/pr-title-checker-config.json @@ -0,0 +1,15 @@ +{ + "LABEL": { + "name": "" + }, + "CHECKS": { + "NOTE": "You can test the regex here: https://regex101.com/r/nDeps5/1", + "regexp": "(^(?build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|BREAKING CHANGE)(?\\([\\w\\s*-]+\\))?(?!?): (?([a-z]).+[^.|\\s])$)", + "regexpFlags": "gm" + }, + "MESSAGES": { + "success": "All OK", + "failure": "PR title not following the semantic-release guidelines. Please check https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines for more information.", + "notice": "" + } +} diff --git a/.github/workflows/auto-merge-dependabot.yaml b/.github/workflows/auto-merge-dependabot.yml similarity index 96% rename from .github/workflows/auto-merge-dependabot.yaml rename to .github/workflows/auto-merge-dependabot.yml index 5ceef930d..c9fc4b674 100644 --- a/.github/workflows/auto-merge-dependabot.yaml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -15,7 +15,7 @@ jobs: ## Extract information about the dependencies being updated by a Dependabot-generated PR - name: Dependabot metadata id: dependabot-metadata - uses: dependabot/fetch-metadata@v2.1.0 + uses: dependabot/fetch-metadata@v2.2.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yml similarity index 90% rename from .github/workflows/build-release.yaml rename to .github/workflows/build-release.yml index 35865481b..c6e51b436 100644 --- a/.github/workflows/build-release.yaml +++ b/.github/workflows/build-release.yml @@ -33,8 +33,9 @@ jobs: type=ref,event=tag - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . + platforms: linux/amd64,linux/arm64/v8 push: true tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 018ee476f..627b603d2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,7 +32,7 @@ jobs: tags: type=sha,format=long,prefix= # adds : tag to outputs.tags - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64/v8 diff --git a/.github/workflows/github-tag-and-release.yml b/.github/workflows/github-tag-and-release.yml deleted file mode 100644 index 8512df323..000000000 --- a/.github/workflows/github-tag-and-release.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Bump version -on: - push: - branches: - - release - -env: - NODE_VERSION: 18.x - RELEASE_BRANCH: release - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - ## Commit message examples for Release type (patch|minor|major) can be found: - ## https://github.com/mathieudutour/github-tag-action - - name: Bump version and push tag - id: tag_version - uses: mathieudutour/github-tag-action@v6.2 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - release_branches: ${{ env.RELEASE_BRANCH }} - - - name: Create a GitHub release - uses: ncipollo/release-action@v1 - with: - tag: ${{ steps.tag_version.outputs.new_tag }} - name: Release ${{ steps.tag_version.outputs.new_tag }} - body: ${{ steps.tag_version.outputs.changelog }} - - ## The setup-qemu-action simplifies the setup of QEMU for cross-platform builds - ## https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install Node.js dependencies - run: npm ci - - ## The metadata-action dynamically generates and manages metadata for Docker images, - ## like tags and labels, based on the provided inputs and workflow context. - ## https://github.com/docker/metadata-action - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/scicatproject/backend-next - tags: | - type=raw,value=stable - type=raw,value=${{ steps.tag_version.outputs.new_tag }} - type=semver,pattern={{version}} - type=raw,value={{date 'YYYY_MM'}},prefix=r_ - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml new file mode 100644 index 000000000..07be7fd15 --- /dev/null +++ b/.github/workflows/pr-title-checker.yml @@ -0,0 +1,22 @@ +name: PR Title Checker + +on: + pull_request: + branches: + - master + types: + - opened + - edited + - synchronize + - labeled + - unlabeled + +jobs: + check-pr-title: + runs-on: ubuntu-latest + steps: + - uses: thehanimo/pr-title-checker@v1.4.1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + pass_on_octokit_error: false + configuration_path: .github/pr-title-checker-config.json #(optional. defaults to .github/pr-title-checker-config.json) diff --git a/.github/workflows/release-and-publish-sdk.yml b/.github/workflows/release-and-publish-sdk.yml new file mode 100644 index 000000000..980cbe8d6 --- /dev/null +++ b/.github/workflows/release-and-publish-sdk.yml @@ -0,0 +1,242 @@ +name: Bump release version, build-push image and publish SDK + +on: + push: + branches: + - release + +env: + NODE_VERSION: 20.x + PYTHON_VERSION: 3.x + RELEASE_BRANCH: release + +jobs: + build-release: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + outputs: + new_tag: ${{ steps.without_v.outputs.tag }} + changelog: ${{ steps.tag_version.outputs.changelog }} + + steps: + - uses: actions/checkout@v4 + + ## Commit message examples for Release type (patch|minor|major) can be found: + ## https://github.com/mathieudutour/github-tag-action + - name: Bump version and push tag + id: tag_version + uses: mathieudutour/github-tag-action@v6.2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + release_branches: ${{ env.RELEASE_BRANCH }} + + - name: Strip 'v' from the tag + id: without_v + run: | + TAG=${{ steps.tag_version.outputs.new_tag }} + WITHOUT_V=${TAG#v} + echo "tag=$WITHOUT_V" >> $GITHUB_OUTPUT + + - name: Create a GitHub release + uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.tag_version.outputs.new_tag }} + name: Release ${{ steps.tag_version.outputs.new_tag }} + body: ${{ steps.tag_version.outputs.changelog }} + + ## The setup-qemu-action simplifies the setup of QEMU for cross-platform builds + ## https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install Node.js dependencies + run: npm ci + + ## The metadata-action dynamically generates and manages metadata for Docker images, + ## like tags and labels, based on the provided inputs and workflow context. + ## https://github.com/docker/metadata-action + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/scicatproject/backend-next + tags: | + type=raw,value=stable + type=raw,value=${{ steps.tag_version.outputs.new_tag }} + type=semver,pattern={{version}} + type=raw,value={{date 'YYYY_MM'}},prefix=r_ + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64/v8 + push: true + tags: ${{ steps.meta.outputs.tags }} + + start-backend-export-swagger: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{env.NODE_VERSION}} + + - name: Pull and Run MongoDB + run: | + docker pull mongo:latest + docker run -d --name mongo-container -p 27017:27017 mongo:latest + + - name: Install Backend and wait for it to be ready + env: + MONGODB_URI: "mongodb://localhost:27017/scicat" + JWT_SECRET: thisIsTheJwtSecret + run: | + npm install -g wait-on && npm install + npm run start & wait-on http://localhost:3000/api/v3/health --timeout 200000 + + - name: Download the Swagger schema + run: curl -o ./swagger-schema.json http://localhost:3000/explorer-json + + - uses: actions/upload-artifact@v4 + with: + name: swagger-schema-${{ github.sha }} + path: ./swagger-schema.json + + generate-upload-sdk: + runs-on: ubuntu-latest + needs: + - build-release + - start-backend-export-swagger + strategy: + matrix: + generator: [python, python-pydantic-v1, typescript-angular] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: swagger-schema-${{ github.sha }} + path: . + + - name: Generate Client + uses: openapi-generators/openapitools-generator-action@v1 + with: + generator: ${{ matrix.generator }} + openapi-file: ./swagger-schema.json + config-file: .github/openapi/${{ matrix.generator }}-config.json + command-args: | + --git-repo-id scicat-backend-next \ + --git-user-id SciCatProject \ + -o ./sdk/${{ matrix.generator }} $( + if [ "${{ matrix.generator }}" == "typescript-angular" ]; then + echo "--additional-properties=npmVersion=${{ needs.build-release.outputs.new_tag}}"; + elif [ "${{ matrix.generator }}" == "python" ]; then + echo "--additional-properties=packageVersion=${{ needs.build-release.outputs.new_tag}}"; + elif [ "${{ matrix.generator }}" == "python-pydantic-v1" ]; then + echo "--additional-properties=packageVersion=${{ needs.build-release.outputs.new_tag}}"; + fi + ) + + - uses: actions/upload-artifact@v4 + with: + name: sdk-${{ matrix.generator }}-${{ github.sha }} + path: ./sdk + + npm-publish: + needs: generate-upload-sdk + runs-on: ubuntu-latest + environment: + name: npm-sdk-package + url: https://www.npmjs.com/package/@scicatproject/scicat-sdk-ts + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: "https://registry.npmjs.org/" + + - name: Download TypeScript Angular SDK Artifact + uses: actions/download-artifact@v4 + with: + name: sdk-typescript-angular-${{github.sha}} + path: ./sdk + + - name: Publish package + run: | + npm install + npm run build + npm publish --access public + working-directory: ./sdk/typescript-angular/ + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + pypi-publish: + needs: generate-upload-sdk + runs-on: ubuntu-latest + strategy: + matrix: + sdk_type: [python, python-pydantic-v1] + environment: + name: ${{ matrix.sdk_type }}-sdk-package + url: ${{ matrix.sdk_type == 'python' && 'https://pypi.org/project/scicat-sdk-py' || 'https://pypi.org/project/scicat-sdk-pydantic' }} + permissions: + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Download Python SDK Artifact + uses: actions/download-artifact@v4 + with: + name: sdk-${{ matrix.sdk_type }}-${{github.sha}} + path: ./sdk + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + working-directory: ./sdk/${{ matrix.sdk_type }}/ + + - name: Build package + run: | + python setup.py sdist bdist_wheel + working-directory: ./sdk/${{ matrix.sdk_type }}/ + + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: ./sdk/${{ matrix.sdk_type }}/dist/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a977c611..a757094f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: Test env: - NODE_VERSION: 18.x + NODE_VERSION: 20.x on: pull_request: @@ -130,7 +130,7 @@ jobs: - name: API tests env: ELASTICSEARCH_ENABLED: ${{ matrix.elasticsearch_enabled }} - MONGODB_URI: mongodb://localhost:27017/scicat + MONGODB_URI: mongodb://localhost:27017/scicat-test-db EXPRESS_SESSION_SECRET: a_scicat_secret JWT_SECRET: a_scicat_secret PORT: 3000 @@ -173,5 +173,5 @@ jobs: run: | cp CI/ESS/docker-compose.api.yaml docker-compose.yaml cp functionalAccounts.json.test functionalAccounts.json - docker-compose up --build -d + docker compose up --build -d npm run test:api diff --git a/.github/workflows/upload-sdk-artifact.yml b/.github/workflows/upload-sdk-artifact.yml new file mode 100644 index 000000000..a33c50a09 --- /dev/null +++ b/.github/workflows/upload-sdk-artifact.yml @@ -0,0 +1,82 @@ +name: Generate and upload latest SDK artifacts + +on: + push: + branches: + - master + +env: + NODE_VERSION: 20.x + SDK_VERSION: latest + +jobs: + start-backend-and-upload-swagger-schema: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{env.NODE_VERSION}} + + - name: Pull MongoDB Image + run: | + docker pull mongo:latest + docker run -d --name mongo-container -p 27017:27017 mongo:latest + + - name: Install Backend and wait for it to be ready + env: + MONGODB_URI: "mongodb://localhost:27017/scicat" + JWT_SECRET: thisIsTheJwtSecret + run: | + npm install -g wait-on && npm install + npm run start & wait-on http://localhost:3000/api/v3/health --timeout 200000 + + - name: Download the Swagger schema + run: curl -o ./swagger-schema.json http://localhost:3000/explorer-json + + - uses: actions/upload-artifact@v4 + with: + name: swagger-schema-${{ github.sha }} + path: ./swagger-schema.json + + generate-and-upload-sdk: + runs-on: ubuntu-latest + needs: + - start-backend-and-upload-swagger-schema + strategy: + matrix: + generator: [python, typescript-angular, python-pydantic-v1] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: swagger-schema-${{ github.sha }} + path: . + + - name: Generate Client + uses: openapi-generators/openapitools-generator-action@v1 + with: + generator: ${{ matrix.generator }} + openapi-file: ./swagger-schema.json + config-file: .github/openapi/${{ matrix.generator }}-config.json + command-args: | + -o ./sdk/${{ matrix.generator }} $( + if [ "${{ matrix.generator }}" == "typescript-angular" ]; then + echo "--additional-properties=npmVersion=${{env.SDK_VERSION}}"; + elif [ "${{ matrix.generator }}" == "python" ]; then + echo "--additional-properties=packageVersion=${{env.SDK_VERSION}}"; + elif [ "${{ matrix.generator }}" == "python-pydantic-v1" ]; then + echo "--additional-properties=packageVersion=${{env.SDK_VERSION}}"; + fi + ) + + - uses: actions/upload-artifact@v4 + with: + name: sdk-${{ matrix.generator }}-${{ github.sha }} + path: ./sdk diff --git a/Dockerfile b/Dockerfile index a426258ba..e56c0c88d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine AS dev +FROM node:20-alpine AS dev # Prepare app directory WORKDIR /home/node/app @@ -6,17 +6,15 @@ COPY . . # Set up local user RUN mkdir /home/node/app/dist -RUN npm install -g npm@9.8.1 # Install dependencies RUN npm install glob rimraf RUN npm install -FROM node:18-alpine AS builder +FROM node:20-alpine AS builder # Prepare app directory -RUN npm install -g npm@9.8.1 WORKDIR /usr/src/app # Set up local user @@ -29,7 +27,7 @@ RUN npm run build # Remove development dependencies RUN npm prune --production -FROM node:18-alpine +FROM node:20-alpine # Prepare app directory WORKDIR /home/node/app diff --git a/README.md b/README.md index 1a150b78f..b1ffe56a3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Test](https://github.com/SciCatProject/scicat-backend-next/actions/workflows/test.yml/badge.svg)](https://github.com/SciCatProject/scicat-backend-next/actions/workflows/test.yml) [![Deploy](https://github.com/SciCatProject/scicat-backend-next/actions/workflows/deploy.yml/badge.svg)](https://github.com/SciCatProject/scicat-backend-next/actions/workflows/deploy.yml) +[![Generate and upload latest SDK artifacts](https://github.com/SciCatProject/scicat-backend-next/actions/workflows/upload-sdk-artifact.yml/badge.svg?branch=master)](https://github.com/SciCatProject/scicat-backend-next/actions/workflows/upload-sdk-artifact.yml) [![DeepScan grade](https://deepscan.io/api/teams/8394/projects/19251/branches/494247/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=8394&pid=19251&bid=494247) [![Known Vulnerabilities](https://snyk.io/test/github/SciCatProject/scicat-backend-next/master/badge.svg?targetFile=package.json)](https://snyk.io/test/github/SciCatProject/scicat-backend-next/master?targetFile=package.json) @@ -22,6 +23,16 @@ The repo for backend v3.x is archived and read-only If you have any questions, please feel free to contact any member of the development team, or the SciCat team at ESS. +## Contributing + +If you're planning to contribute to this project by adding new functionality, we encourage you to discuss it first in the Discussions tab. This helps ensure that your proposed changes align with the project's goals and prevents duplicate efforts. Here's how you can initiate a discussion: + +1. Go to the [Discussions tab](https://github.com/SciCatProject/scicat-backend-next/discussions). +2. Start a new discussion thread outlining your proposed changes. +3. Wait for feedback and consensus before proceeding with creating a pull request. + +Thank you for your interest in contributing to our project! + ## Get started 1. `git clone https://github.com/SciCatProject/scicat-backend-next.git` @@ -165,6 +176,7 @@ Valid environment variables for the .env file. See [.env.example](/.env.example) | `ES_FIELDS_LIMIT` | number | | The total number of fields in an index. | 1000 | | `ES_REFRESH` | string | | If set to `wait_for`, Elasticsearch will wait till data is inserted into the specified index before returning a response. | false | | `LOGGERS_CONFIG_FILE` | string | | The file name for loggers configuration, located in the project root directory. | "loggers.json" | +| `SWAGGER_PATH` | string | Yes | swaggerPath is the path where the swagger UI will be available| "explorer"| ## Migrating from the old SciCat Backend @@ -208,45 +220,6 @@ Following are the post that I found useful working on the migration: - https://docs.nestjs.com/openapi/types-and-parameters - https://docs.nestjs.com/openapi/decorators -## New Release Version Bump Workflow - -### Workflow Overview - -Scicat Backend controls new releases with the `GitHub-tag-and-release` GitHub Action. This workflow is triggered by push events to the release branch. It automates the version bumping and release processes based on semantic commit messages. Full documentation of the action package can be found on [github-tag-action](https://github.com/marketplace/actions/github-tag) - -The image below shows visualized workflow. -![image](https://github.com/SciCatProject/scicat-backend-next/assets/78078898/0f3c5386-4a16-4ed1-a2ee-d71ef6f34e99) - -### Workflow Trigger Condition - -> [!Caution] -> Any push to the `release` branch initiates the workflow. - -### Versioning and Release Strategy - -**Semantic Versioning:** - -- The version is automatically bumped according to the semantic PR titles, using the [semantic-release](https://github.com/semantic-release/semantic-release) conventions: - - - `fix:` prefixed titles generate a patch release. - - `feat:` prefixed titles generate a minor release. - - `BREAKING CHANGE:` in the commit message triggers a major release. - -**Auto-generated Release Notes:** - -The release log is automatically populated with all commit messages since the last tag, providing a detailed changelog for the release. By default semantic-release uses [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines). - -In order to generate detailed changelog for the release, the following type for the `commit message`/`PR title` should be used: - -- feat: message - A new feature -- fix: message - A bug fix -- docs: message - Documentation only changes -- style: message - Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) -- refactor: message - A code change that neither fixes a bug nor adds a feature -- perf: message - A code change that improves performance -- test: message - Adding missing or correcting existing tests -- chore: message - Changes to the build process or auxiliary tools and libraries such as documentation generation - ## License This project is licensed under the GPL License - see the [LICENSE](LICENSE) file for details diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..f7bb2b150 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,18 @@ +# Security Policy + +## Supported Versions + +Only the latest version is supported. + +## Reporting a Vulnerability + +If you believe you have found a security vulnerability in SciCat, please +- ✅ report it to us by creating a [security advisory](https://github.com/SciCatProject/scicat-backend-next/security/advisories/new). +- ❌ do not report security vulnerabilities through public GitHub issues, discussions, or pull requests, etc. + +Please include as much information as you can to help us better understand and resolve the issue. +We work on fixing the issues [privately](https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/collaborating-in-a-temporary-private-fork-to-resolve-a-repository-security-vulnerability). + +## Disclosure + +We use GitHub [security advisories](https://github.com/SciCatProject/scicat-backend-next/security/advisories) to disclose fixed vulnerabilities. diff --git a/docs/developer-guide/contributing/pr-guidelines.md b/docs/developer-guide/contributing/pr-guidelines.md new file mode 100644 index 000000000..c91f31e8d --- /dev/null +++ b/docs/developer-guide/contributing/pr-guidelines.md @@ -0,0 +1,37 @@ +# Pull Request Guidelines + +When creating a pull request (PR) for this repository, it's important to follow the guidelines for PR titles and PR template to ensure consistency and clarity. Proper PR titles help maintain a clear and understandable project history and facilitate better release notes. + +## PR Title Format + +All PR titles must adhere to the conventional commits format. This format helps in automating the release process and generating changelogs. The format is as follows: + +``` +(): +``` + +type: The type of change being made. It must be one of the following: + +- `feat:` A new feature +- `fix:` A bug fix +- `ci:` Continuous integration-related changes. +- `docs:` Documentation only changes +- `style:` Changes that do not affect the meaning of the code (white -space, formatting, missing semi -colons, etc) +- `refactor:` A code change that neither fixes a bug nor adds a feature +- `revert`: Reverts a previous commit. +- `perf:` A code change that improves performance +- `test:` Adding missing or correcting existing tests +- `chore:` Changes to the build process or auxiliary tools and libraries such as documentation generation +- `build:` Changes that affect the build system or external dependencies. +- `BREAKING CHANGE:` A change that introduces a breaking API change. + +scope: An `optional` description of the section of the codebase affected by the changes (e.g., api, ui, docs). This is enclosed in parentheses. + +description: A brief summary of the changes made. + +## Examples + +- feat(api): add new endpoint for data retrieval +- fix(ui): correct button alignment on mobile devices +- docs: update contributing guidelines +- BREAKING CHANGE: refactor authentication middleware to use new library diff --git a/docs/developer-guide/release-guidelines.md b/docs/developer-guide/release-guidelines.md new file mode 100644 index 000000000..fb07eaee0 --- /dev/null +++ b/docs/developer-guide/release-guidelines.md @@ -0,0 +1,27 @@ +# New Release Version Bump Workflow + +## Workflow Overview + +Scicat Backend controls new releases with the `GitHub-tag-and-release` GitHub Action. This workflow is triggered by push events to the release branch. It automates the version bumping and release processes based on semantic commit messages. Full documentation of the action package can be found on [github-tag-action](https://github.com/marketplace/actions/github-tag) + +The image below shows visualized workflow. +![image](https://github.com/SciCatProject/scicat-backend-next/assets/78078898/0f3c5386-4a16-4ed1-a2ee-d71ef6f34e99) + +## Workflow Trigger Condition + +> [!Caution] +> Any push to the `release` branch initiates the workflow. + +## Versioning and Release Strategy + +**Semantic Versioning:** + +- The version is automatically bumped according to the semantic PR titles, using the [semantic-release](https://github.com/semantic-release/semantic-release) conventions: + + - `fix:` prefixed titles generate a patch release. + - `feat:` prefixed titles generate a minor release. + - `BREAKING CHANGE:` in the commit message triggers a major release. + +**Auto-generated Release Notes:** + +The release log is automatically populated with all commit messages since the last tag, providing a detailed changelog for the release. By default semantic-release uses [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines). diff --git a/migrations/20240920111733-dataset-unified-schema.js b/migrations/20240920111733-dataset-unified-schema.js new file mode 100644 index 000000000..598d6db6c --- /dev/null +++ b/migrations/20240920111733-dataset-unified-schema.js @@ -0,0 +1,47 @@ +module.exports = { + async up(db, client) { + // TODO write your migration here. + // See https://github.com/seppevs/migrate-mongo/#creating-a-new-migration-script + // Example: + // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: true}}); + await db.collection("Dataset").updateMany({}, [ + { + $set: { + proposalIds: ["$proposalId"], + instrumentIds: ["$instrumentId"], + sampleIds: ["$sampleId"], + }, + }, + ]); + await db.collection("Dataset").updateMany({ type: "derived" }, [ + { + $set: { + principalInvestigator: "$investigator", + }, + }, + ]); + }, + + async down(db, client) { + // TODO write the statements to rollback your migration (if possible) + // Example: + // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}}); + + await db.collection("Dataset").updateMany({}, [ + { + $set: { + proposalId: "$proposalIds[0]", + instrumentId: "$instrumentId[0]", + sampleId: "$sampleId[0]", + }, + }, + ]); + await db.collection("Dataset").updateMany({ type: "derived" }, [ + { + $set: { + investigator: "$principalInvestigator", + }, + }, + ]); + }, +}; diff --git a/package-lock.json b/package-lock.json index 9c26d183b..42c8412f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "BSD-3-Clause", "dependencies": { "@casl/ability": "^6.3.2", - "@elastic/elasticsearch": "^8.9.0", + "@elastic/elasticsearch": "^8.15.0", "@nestjs-modules/mailer": "^2.0.2", "@nestjs/axios": "^3.0.0", "@nestjs/common": "^10.3.8", @@ -34,7 +34,7 @@ "handlebars": "^4.7.7", "lodash": "^4.17.21", "luxon": "^3.2.1", - "mathjs": "^12.0.0", + "mathjs": "^13.0.0", "migrate-mongo": "^11.0.0", "mongoose": "^8.4.0", "node-fetch": "^3.3.0", @@ -45,13 +45,13 @@ "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", "reflect-metadata": "^0.2.2", - "rimraf": "^5.0.0", + "rimraf": "^6.0.1", "rxjs": "^7.5.7", "swagger-ui-express": "^5.0.0", - "uuid": "^9.0.0" + "uuid": "^10.0.0" }, "devDependencies": { - "@faker-js/faker": "^8.0.1", + "@faker-js/faker": "^9.0.0", "@nestjs/cli": "^10.0.5", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.3.8", @@ -63,18 +63,18 @@ "@types/lodash": "^4.14.180", "@types/luxon": "^3.1.0", "@types/mocha": "^10.0.0", - "@types/node": "^20.1.0", + "@types/node": "^22.0.0", "@types/node-fetch": "^2.6.2", "@types/nodemailer": "^6.4.4", "@types/passport-jwt": "^4.0.0", "@types/passport-local": "^1.0.34", "@types/supertest": "^6.0.1", - "@types/uuid": "^9.0.0", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.3.0", "@typescript-eslint/parser": "^7.3.0", "chai": "^5.0.0", "chai-http": "^4.3.6", - "concurrently": "^8.0.1", + "concurrently": "^9.0.0", "eslint": "^8.46.0", "eslint-config-loopback": "^13.1.0", "eslint-config-prettier": "^9.0.0", @@ -89,7 +89,7 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.0", "typescript": "^4.8.3", - "wait-on": "^7.0.1" + "wait-on": "^8.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -128,15 +128,15 @@ } }, "node_modules/@angular-devkit/core": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.1.2.tgz", - "integrity": "sha512-ku+/W/HMCBacSWFppenr9y6Lx8mDuTuQvn1IkTyBLiJOpWnzgVbx9kHDeaDchGa1PwLlJUBBrv27t3qgJOIDPw==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.8.tgz", + "integrity": "sha512-Q8q0voCGudbdCgJ7lXdnyaxKHbNQBARH68zPQV72WT8NWy+Gw/tys870i6L58NWbBaCJEUcIj/kb6KoakSRu+Q==", "dev": true, "dependencies": { "ajv": "8.12.0", "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "picomatch": "3.0.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", "rxjs": "7.8.1", "source-map": "0.7.4" }, @@ -155,26 +155,26 @@ } }, "node_modules/@angular-devkit/core/node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@angular-devkit/schematics": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.1.2.tgz", - "integrity": "sha512-8S9RuM8olFN/gwN+mjbuF1CwHX61f0i59EGXz9tXLnKRUTjsRR+8vVMTAmX0dvVAT5fJTG/T69X+HX7FeumdqA==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.8.tgz", + "integrity": "sha512-QRVEYpIfgkprNHc916JlPuNbLzOgrm9DZalHasnLUz4P6g7pR21olb8YCyM2OTJjombNhya9ZpckcADU5Qyvlg==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.1.2", - "jsonc-parser": "3.2.0", - "magic-string": "0.30.5", + "@angular-devkit/core": "17.3.8", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", "ora": "5.4.1", "rxjs": "7.8.1" }, @@ -185,15 +185,15 @@ } }, "node_modules/@angular-devkit/schematics-cli": { - "version": "17.1.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.1.2.tgz", - "integrity": "sha512-bvXykYzSST05qFdlgIzUguNOb3z0hCa8HaTwtqdmQo9aFPf+P+/AC56I64t1iTchMjQtf3JrBQhYM25gUdcGbg==", + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.8.tgz", + "integrity": "sha512-TjmiwWJarX7oqvNiRAroQ5/LeKUatxBOCNEuKXO/PV8e7pn/Hr/BqfFm+UcYrQoFdZplmtNAfqmbqgVziKvCpA==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.1.2", - "@angular-devkit/schematics": "17.1.2", + "@angular-devkit/core": "17.3.8", + "@angular-devkit/schematics": "17.3.8", "ansi-colors": "4.1.3", - "inquirer": "9.2.12", + "inquirer": "9.2.15", "symbol-observable": "4.0.0", "yargs-parser": "21.1.1" }, @@ -227,47 +227,19 @@ "node": ">= 12" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/schematics-cli/node_modules/figures": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", - "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^5.0.0", - "is-unicode-supported": "^1.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { - "version": "9.2.12", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", - "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", "dev": true, "dependencies": { - "@ljharb/through": "^2.3.11", + "@ljharb/through": "^2.3.12", "ansi-escapes": "^4.3.2", "chalk": "^5.3.0", "cli-cursor": "^3.1.0", "cli-width": "^4.1.0", "external-editor": "^3.1.0", - "figures": "^5.0.0", + "figures": "^3.2.0", "lodash": "^4.17.21", "mute-stream": "1.0.0", "ora": "^5.4.1", @@ -278,19 +250,7 @@ "wrap-ansi": "^6.2.0" }, "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/@angular-devkit/schematics-cli/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { @@ -882,9 +842,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", - "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz", + "integrity": "sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1169,11 +1129,11 @@ } }, "node_modules/@elastic/elasticsearch": { - "version": "8.13.1", - "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.13.1.tgz", - "integrity": "sha512-2G4Vu6OHw4+XTrp7AGIcOEezpPEoVrWg2JTK1v/exEKSLYquZkUdd+m4yOL3/UZ6bTj7hmXwrmYzW76BnLCkJQ==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.15.0.tgz", + "integrity": "sha512-mG90EMdTDoT6GFSdqpUAhWK9LGuiJo6tOWqs0Usd/t15mPQDj7ZqHXfCBqNkASZpwPZpbAYVjd57S6nbUBINCg==", "dependencies": { - "@elastic/transport": "~8.4.1", + "@elastic/transport": "^8.7.0", "tslib": "^2.4.0" }, "engines": { @@ -1181,19 +1141,20 @@ } }, "node_modules/@elastic/transport": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.4.1.tgz", - "integrity": "sha512-/SXVuVnuU5b4dq8OFY4izG+dmGla185PcoqgK6+AJMpmOeY1QYVNbWtCwvSvoAANN5D/wV+EBU8+x7Vf9EphbA==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.7.0.tgz", + "integrity": "sha512-IqXT7a8DZPJtqP2qmX1I2QKmxYyN27kvSW4g6pInESE1SuGwZDp2FxHJ6W2kwmYOJwQdAt+2aWwzXO5jHo9l4A==", "dependencies": { + "@opentelemetry/api": "1.x", "debug": "^4.3.4", "hpagent": "^1.0.0", "ms": "^2.1.3", "secure-json-parse": "^2.4.0", "tslib": "^2.4.0", - "undici": "^5.22.1" + "undici": "^6.12.0" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@elastic/transport/node_modules/ms": { @@ -1280,9 +1241,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", - "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz", + "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==", "dev": true, "funding": [ { @@ -1291,16 +1252,8 @@ } ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0", - "npm": ">=6.14.13" - } - }, - "node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", - "engines": { - "node": ">=14" + "node": ">=18.0.0", + "npm": ">=9.0.0" } }, "node_modules/@hapi/hoek": { @@ -1929,12 +1882,12 @@ } }, "node_modules/@ljharb/through": { - "version": "2.3.12", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz", - "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==", + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.5" + "call-bind": "^1.0.7" }, "engines": { "node": ">= 0.4" @@ -2039,9 +1992,9 @@ } }, "node_modules/@microsoft/tsdoc": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", - "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", + "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==" }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.5", @@ -2129,9 +2082,9 @@ } }, "node_modules/@nestjs/axios": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz", - "integrity": "sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.3.tgz", + "integrity": "sha512-h6TCn3yJwD6OKqqqfmtRS5Zo4E46Ip2n+gK1sqwzNBC+qxQ9xpCu+ODVRFur6V3alHSCSBxb3nNtt73VEdluyA==", "peerDependencies": { "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "axios": "^1.3.1", @@ -2139,32 +2092,29 @@ } }, "node_modules/@nestjs/cli": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", - "integrity": "sha512-aWmD1GLluWrbuC4a1Iz/XBk5p74Uj6nIVZj6Ov03JbTfgtWqGFLtXuMetvzMiHxfrHehx/myt2iKAPRhKdZvTg==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.5.tgz", + "integrity": "sha512-FP7Rh13u8aJbHe+zZ7hM0CC4785g9Pw4lz4r2TTgRtf0zTxSWMkJaPEwyjX8SK9oWK2GsYxl+fKpwVZNbmnj9A==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.1.2", - "@angular-devkit/schematics": "17.1.2", - "@angular-devkit/schematics-cli": "17.1.2", + "@angular-devkit/core": "17.3.8", + "@angular-devkit/schematics": "17.3.8", + "@angular-devkit/schematics-cli": "17.3.8", "@nestjs/schematics": "^10.0.1", "chalk": "4.1.2", "chokidar": "3.6.0", - "cli-table3": "0.6.3", + "cli-table3": "0.6.5", "commander": "4.1.1", "fork-ts-checker-webpack-plugin": "9.0.2", - "glob": "10.3.10", + "glob": "10.4.2", "inquirer": "8.2.6", "node-emoji": "1.11.0", "ora": "5.4.1", - "rimraf": "4.4.1", - "shelljs": "0.8.5", - "source-map-support": "0.5.21", "tree-kill": "1.2.2", "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.3.3", - "webpack": "5.90.1", + "webpack": "5.94.0", "webpack-node-externals": "3.0.0" }, "bin": { @@ -2174,7 +2124,7 @@ "node": ">= 16.14" }, "peerDependencies": { - "@swc/cli": "^0.1.62 || ^0.3.0", + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0", "@swc/core": "^1.3.62" }, "peerDependenciesMeta": { @@ -2186,15 +2136,6 @@ } } }, - "node_modules/@nestjs/cli/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@nestjs/cli/node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2219,66 +2160,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/@nestjs/cli/node_modules/rimraf": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz", - "integrity": "sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==", - "dev": true, - "dependencies": { - "glob": "^9.2.0" - }, - "bin": { - "rimraf": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@nestjs/cli/node_modules/rimraf/node_modules/glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "minimatch": "^8.0.2", - "minipass": "^4.2.4", - "path-scurry": "^1.6.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@nestjs/cli/node_modules/rimraf/node_modules/minimatch": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", - "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@nestjs/cli/node_modules/rimraf/node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@nestjs/cli/node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -2293,12 +2174,12 @@ } }, "node_modules/@nestjs/common": { - "version": "10.3.8", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.8.tgz", - "integrity": "sha512-P+vPEIvqx2e+fonsYVlFXKvoChyJ8Tq+lfpqdVFqblovHbFr3kZ/nYX0cPs+XuW6bnRT8tz0SSR9XBGU43kJhw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", + "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==", "dependencies": { "iterare": "1.2.1", - "tslib": "2.6.2", + "tslib": "2.6.3", "uid": "2.0.2" }, "funding": { @@ -2321,14 +2202,13 @@ } }, "node_modules/@nestjs/config": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.2.tgz", - "integrity": "sha512-vGICPOui5vE6kPz1iwQ7oCnp3qWgqxldPmBQ9onkVoKlBtyc83KJCr7CjuVtf4OdovMAVcux1d8Q6jglU2ZphA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", + "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", "dependencies": { "dotenv": "16.4.5", "dotenv-expand": "10.0.0", - "lodash": "4.17.21", - "uuid": "9.0.1" + "lodash": "4.17.21" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -2336,16 +2216,16 @@ } }, "node_modules/@nestjs/core": { - "version": "10.3.8", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.8.tgz", - "integrity": "sha512-AxF4tpYLDNn5Wfb3C4bNaaHJ4pREH5FJrSisR2A5zkYpQFORFs0Tc36lOFPMwBTy8Iv2wUwWLUVc5ftBnxEv4w==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", + "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "3.2.0", - "tslib": "2.6.2", + "tslib": "2.6.3", "uid": "2.0.2" }, "funding": { @@ -2426,9 +2306,9 @@ } }, "node_modules/@nestjs/mongoose": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-10.0.6.tgz", - "integrity": "sha512-J8jFgSvCDEKMXU57QAIdXIlWQsOFWK+x0PM1KI/0zHe3/4JrAtFGTFD08hRX3IHk+WJT9g/XQIpMSNM7/10Jlg==", + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-10.0.10.tgz", + "integrity": "sha512-3Ff60ock8nwlAJC823TG91Qy+Qc6av+ddIb6n6wlFsTK0akDF/aTcagX8cF8uI8mWxCWjEwEsgv99vo6p0yJ+w==", "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -2446,15 +2326,15 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.3.8", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.8.tgz", - "integrity": "sha512-sifLoxgEJvAgbim1UuW6wyScMfkS9SVQRH+lN33N/9ZvZSjO6NSDLOe+wxqsnZkia+QrjFC0qy0ITRAsggfqbg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", + "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", "express": "4.19.2", "multer": "1.4.4-lts.1", - "tslib": "2.6.2" + "tslib": "2.6.3" }, "funding": { "type": "opencollective", @@ -2466,15 +2346,15 @@ } }, "node_modules/@nestjs/schematics": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.1.tgz", - "integrity": "sha512-o4lfCnEeIkfJhGBbLZxTuVWcGuqDCFwg5OrvpgRUBM7vI/vONvKKiB5riVNpO+JqXoH0I42NNeDb0m4V5RREig==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.4.tgz", + "integrity": "sha512-QpY8ez9cTvXXPr3/KBrtSgXQHMSV6BkOUYy2c2TTe6cBqriEdGnCYqGl8cnfrQl3632q3lveQPaZ/c127dHsEw==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.1.2", - "@angular-devkit/schematics": "17.1.2", + "@angular-devkit/core": "17.3.8", + "@angular-devkit/schematics": "17.3.8", "comment-json": "4.2.3", - "jsonc-parser": "3.2.1", + "jsonc-parser": "3.3.1", "pluralize": "8.0.0" }, "peerDependencies": { @@ -2482,22 +2362,22 @@ } }, "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true }, "node_modules/@nestjs/swagger": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.1.tgz", - "integrity": "sha512-LUC4mr+5oAleEC/a2j8pNRh1S5xhKXJ1Gal5ZdRjt9XebQgbngXCdW7JTA9WOEcwGtFZN9EnKYdquzH971LZfw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.0.tgz", + "integrity": "sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g==", "dependencies": { - "@microsoft/tsdoc": "^0.14.2", + "@microsoft/tsdoc": "^0.15.0", "@nestjs/mapped-types": "2.0.5", "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "3.2.0", - "swagger-ui-dist": "5.11.2" + "swagger-ui-dist": "5.17.14" }, "peerDependencies": { "@fastify/static": "^6.0.0 || ^7.0.0", @@ -2589,12 +2469,12 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.3.8", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.8.tgz", - "integrity": "sha512-hpX9das2TdFTKQ4/2ojhjI6YgXtCfXRKui3A4Qaj54VVzc5+mtK502Jj18Vzji98o9MVS6skmYu+S/UvW3U6Fw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz", + "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==", "dev": true, "dependencies": { - "tslib": "2.6.2" + "tslib": "2.6.3" }, "funding": { "type": "opencollective", @@ -2711,6 +2591,14 @@ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", "optional": true }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2720,19 +2608,11 @@ "node": ">=14" } }, - "node_modules/@pkgr/utils": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", - "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "fast-glob": "^3.3.0", - "is-glob": "^4.0.3", - "open": "^9.1.0", - "picocolors": "^1.0.0", - "tslib": "^2.6.0" - }, "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, @@ -2740,24 +2620,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@pkgr/utils/node_modules/open": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", - "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", - "dev": true, - "dependencies": { - "default-browser": "^4.0.0", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -2772,9 +2634,9 @@ } }, "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "dev": true, "dependencies": { "@hapi/hoek": "^9.0.0" @@ -2938,9 +2800,9 @@ } }, "node_modules/@types/chai": { - "version": "4.3.16", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.16.tgz", - "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==", + "version": "4.3.19", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.19.tgz", + "integrity": "sha512-2hHHvQBVE2FiSK4eN0Br6snX9MtolHaTo/batnLjlGRhoQzlCL61iVpxoqO7SfFyOw+P/pwv+0zNHzKoGWz9Cw==", "dev": true }, "node_modules/@types/connect": { @@ -2969,21 +2831,13 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -3095,9 +2949,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, "node_modules/@types/luxon": { @@ -3134,17 +2988,17 @@ "optional": true }, "node_modules/@types/mocha": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", - "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz", + "integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==", "dev": true }, "node_modules/@types/node": { - "version": "20.12.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", - "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-fetch": { @@ -3293,9 +3147,9 @@ } }, "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "dev": true }, "node_modules/@types/validator": { @@ -3332,16 +3186,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.10.0.tgz", - "integrity": "sha512-PzCr+a/KAef5ZawX7nbyNwBDtM1HdLIT53aSA2DDlxmxMngZ43O8SIePOeX8H5S+FHXeI6t97mTt/dDdzY4Fyw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.10.0", - "@typescript-eslint/type-utils": "7.10.0", - "@typescript-eslint/utils": "7.10.0", - "@typescript-eslint/visitor-keys": "7.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3365,15 +3219,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.10.0.tgz", - "integrity": "sha512-2EjZMA0LUW5V5tGQiaa2Gys+nKdfrn2xiTIBLR4fxmPmVSvgPcKNW+AE/ln9k0A4zDUti0J/GZXMDupQoI+e1w==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.10.0", - "@typescript-eslint/types": "7.10.0", - "@typescript-eslint/typescript-estree": "7.10.0", - "@typescript-eslint/visitor-keys": "7.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "engines": { @@ -3393,13 +3247,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", - "integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.10.0", - "@typescript-eslint/visitor-keys": "7.10.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3410,13 +3264,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.10.0.tgz", - "integrity": "sha512-D7tS4WDkJWrVkuzgm90qYw9RdgBcrWmbbRkrLA4d7Pg3w0ttVGDsvYGV19SH8gPR5L7OtcN5J1hTtyenO9xE9g==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.10.0", - "@typescript-eslint/utils": "7.10.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3437,9 +3291,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", - "integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3450,13 +3304,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", - "integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.10.0", - "@typescript-eslint/visitor-keys": "7.10.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3487,9 +3341,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -3502,15 +3356,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", - "integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.10.0", - "@typescript-eslint/types": "7.10.0", - "@typescript-eslint/typescript-estree": "7.10.0" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3524,12 +3378,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", - "integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3605,9 +3459,9 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", @@ -3627,9 +3481,9 @@ "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { @@ -3650,15 +3504,15 @@ "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { @@ -3686,28 +3540,28 @@ "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -3715,24 +3569,24 @@ } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -3741,12 +3595,12 @@ } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, @@ -3824,10 +3678,10 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "peerDependencies": { "acorn": "^8" @@ -4165,11 +4019,11 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -4345,15 +4199,6 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -4507,18 +4352,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.44" - }, - "engines": { - "node": ">= 5.10.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4529,12 +4362,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "devOptional": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4652,36 +4485,6 @@ "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" }, - "node_modules/bundle-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", - "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", - "dev": true, - "dependencies": { - "run-applescript": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bundle-name/node_modules/run-applescript": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", - "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -4702,13 +4505,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5062,9 +4870,9 @@ } }, "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dependencies": { "string-width": "^4.2.0" }, @@ -5265,17 +5073,15 @@ } }, "node_modules/concurrently": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", - "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.0.0.tgz", + "integrity": "sha512-iAxbsDeUkn8E/4+QalT7T3WvlyTfmsoez+19lbbcsxZdOEMfBukd8LA30KYez2UR5xkKFzbcqXIZy5RisCbaxw==", "dev": true, "dependencies": { "chalk": "^4.1.2", - "date-fns": "^2.30.0", "lodash": "^4.17.21", "rxjs": "^7.8.1", "shell-quote": "^1.8.1", - "spawn-command": "0.0.2", "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" @@ -5285,7 +5091,7 @@ "concurrently": "dist/bin/concurrently.js" }, "engines": { - "node": "^14.13.0 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" @@ -5545,9 +5351,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -5616,156 +5422,6 @@ "node": ">=0.10.0" } }, - "node_modules/default-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", - "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", - "dev": true, - "dependencies": { - "bundle-name": "^3.0.0", - "default-browser-id": "^3.0.0", - "execa": "^7.1.1", - "titleize": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", - "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", - "dev": true, - "dependencies": { - "bplist-parser": "^0.2.0", - "untildify": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/default-browser/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/default-browser/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/default-browser/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -5779,28 +5435,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "engines": { - "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/delayed-stream": { @@ -5876,9 +5523,9 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { "node": ">=0.3.1" @@ -6156,9 +5803,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -6189,6 +5836,17 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -6378,13 +6036,13 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -6929,9 +6587,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "devOptional": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -7199,9 +6857,9 @@ } }, "node_modules/fraction.js": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.4.tgz", - "integrity": "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "engines": { "node": "*" }, @@ -7351,11 +7009,11 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.3.tgz", - "integrity": "sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "es-errors": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", @@ -7402,21 +7060,23 @@ } }, "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "devOptional": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -7444,14 +7104,34 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/glob/node_modules/jackspeak": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.1.tgz", + "integrity": "sha512-U23pQPDnmYybVkYjObcuYMk43VRlMLLqLI+RdZy8s8WV8WsxO9SnqSroKaluuvcNOdCAlauKszDwd+umbot5Mg==", + "devOptional": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "devOptional": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -7577,11 +7257,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7926,15 +7606,6 @@ "node": ">=12.0.0" } }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/ip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", @@ -7993,7 +7664,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "devOptional": true, + "optional": true, "bin": { "is-docker": "cli.js" }, @@ -8064,39 +7735,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container/node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -8207,7 +7845,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "devOptional": true, + "optional": true, "dependencies": { "is-docker": "^2.0.0" }, @@ -9081,22 +8719,22 @@ } }, "node_modules/joi": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", - "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "dev": true, "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "node_modules/jose": { - "version": "4.15.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", - "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -9285,9 +8923,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, "node_modules/jsonfile": { @@ -9701,17 +9339,17 @@ } }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "engines": { "node": ">=12" } }, "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -9831,19 +9469,19 @@ } }, "node_modules/mathjs": { - "version": "12.4.2", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.2.tgz", - "integrity": "sha512-lW14EzwAFgbNN7AZikxplmhs7wiXDhMphBOGCA3KS6T29ECEkHJsBtbEW5cnCz7sXtl4nDyvTdR+DqVsZyiiEw==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-13.1.1.tgz", + "integrity": "sha512-duaSAy7m4F+QtP1Dyv8MX2XuxcqpNDDlGly0SdVTCqpAmwdOFWilDdQKbLdo9RfD6IDNMOdo9tIsEaTXkconlQ==", "dependencies": { - "@babel/runtime": "^7.24.4", + "@babel/runtime": "^7.25.4", "complex.js": "^2.1.1", "decimal.js": "^10.4.3", "escape-latex": "^1.2.0", - "fraction.js": "4.3.4", + "fraction.js": "^4.3.7", "javascript-natural-sort": "^0.7.1", "seedrandom": "^3.0.5", "tiny-emitter": "^2.1.0", - "typed-function": "^4.1.1" + "typed-function": "^4.2.1" }, "bin": { "mathjs": "bin/cli.js" @@ -10014,9 +9652,9 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "engines": { "node": ">=16 || 14 >=14.17" } @@ -10510,31 +10148,31 @@ } }, "node_modules/mocha": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", - "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", + "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -10544,15 +10182,6 @@ "node": ">= 14.0.0" } }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/mocha/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -10584,9 +10213,9 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -10661,18 +10290,18 @@ } }, "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "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/mongodb": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.6.2.tgz", - "integrity": "sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.0.tgz", + "integrity": "sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==", "dependencies": { "@mongodb-js/saslprep": "^1.1.5", "bson": "^6.7.0", @@ -10755,13 +10384,13 @@ } }, "node_modules/mongoose": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.4.0.tgz", - "integrity": "sha512-fgqRMwVEP1qgRYfh+tUe2YBBFnPO35FIg2lfFH+w9IhRGg1/ataWGIqvf/MjwM29cZ60D5vSnqtN2b8Qp0sOZA==", + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.2.tgz", + "integrity": "sha512-ErbDVvuUzUfyQpXvJ6sXznmZDICD8r6wIsa0VKjJtB6/LZncqwUn5Um040G1BaNo6L3Jz+xItLSwT0wZmSmUaQ==", "dependencies": { "bson": "^6.7.0", "kareem": "2.6.3", - "mongodb": "6.6.2", + "mongodb": "6.8.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -10966,9 +10595,9 @@ "dev": true }, "node_modules/nodemailer": { - "version": "6.9.13", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", - "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", + "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", "engines": { "node": ">=6.0.0" } @@ -11128,11 +10757,11 @@ } }, "node_modules/openid-client": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", - "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", + "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", "dependencies": { - "jose": "^4.15.5", + "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -11307,6 +10936,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, "node_modules/param-case": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", @@ -11481,15 +11115,15 @@ "devOptional": true }, "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -11658,9 +11292,9 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -11732,6 +11366,19 @@ "node": ">=14" } }, + "node_modules/preview-email/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -12057,18 +11704,6 @@ "node": ">=8.10.0" } }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "dev": true, - "dependencies": { - "resolve": "^1.1.6" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -12206,17 +11841,102 @@ } }, "node_modules/rimraf": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", - "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dependencies": { - "glob": "^10.3.7" + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" }, "bin": { "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=14.18" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/jackspeak": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -12560,9 +12280,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -12588,15 +12308,16 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -12635,43 +12356,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", - "dev": true, - "dependencies": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/shelljs/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -12737,15 +12421,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/sinon/node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -12832,12 +12507,6 @@ "memory-pager": "^1.0.2" } }, - "node_modules/spawn-command": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", - "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", - "dev": true - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -13146,14 +12815,14 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.11.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", - "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==" + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==" }, "node_modules/swagger-ui-express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", - "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", "dependencies": { "swagger-ui-dist": ">=5.0.0" }, @@ -13180,12 +12849,12 @@ "dev": true }, "node_modules/synckit": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.6.tgz", - "integrity": "sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, "dependencies": { - "@pkgr/utils": "^2.4.2", + "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" }, "engines": { @@ -13312,15 +12981,6 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -13384,18 +13044,6 @@ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, - "node_modules/titleize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", - "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tlds": { "version": "1.240.0", "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.240.0.tgz", @@ -13686,9 +13334,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/type-check": { "version": "0.4.0", @@ -13735,11 +13383,11 @@ } }, "node_modules/typed-function": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.1.tgz", - "integrity": "sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/typedarray": { @@ -13810,20 +13458,17 @@ } }, "node_modules/undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, + "version": "6.19.7", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.7.tgz", + "integrity": "sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==", "engines": { - "node": ">=14.0" + "node": ">=18.17" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz", + "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==" }, "node_modules/universalify": { "version": "2.0.0", @@ -13841,15 +13486,6 @@ "node": ">= 0.8" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -13918,9 +13554,9 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -14053,13 +13689,13 @@ } }, "node_modules/wait-on": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", - "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.0.tgz", + "integrity": "sha512-fNE5SXinLr2Bt7cJvjvLg2PcXfqznlqRvtE3f8AqYdRZ9BhE+XpsCp1mwQbRoO7s1q7uhAuCw0Ro3mG/KdZjEw==", "dev": true, "dependencies": { - "axios": "^1.6.1", - "joi": "^17.11.0", + "axios": "^1.7.4", + "joi": "^17.13.3", "lodash": "^4.17.21", "minimist": "^1.2.8", "rxjs": "^7.8.1" @@ -14081,9 +13717,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -14288,26 +13924,25 @@ } }, "node_modules/webpack": { - "version": "5.90.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz", - "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", @@ -14315,7 +13950,7 @@ "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.0", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -14457,9 +14092,9 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "node_modules/wrap-ansi": { @@ -14517,9 +14152,9 @@ "dev": true }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "engines": { "node": ">=8.3.0" diff --git a/package.json b/package.json index 65b1ff50f..2be6c26a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scicat-backend-next", - "version": "4.0.0", + "version": "4.5.0", "description": "scicat-backend-next", "author": "", "private": true, @@ -26,12 +26,12 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:api": "npm run test:api:jest --maxWorkers=50% && concurrently -k -s first \"wait-on http://localhost:3000/explorer/ && npm run test:api:mocha\" \"npm run start\"", "test:api:jest": "jest --config ./test/config/jest-e2e.json --maxWorkers=50%", - "test:api:mocha": "mocha --config ./test/config/.mocharc.json -r chai/register-should.js ", + "test:api:mocha": "mocha --config ./test/config/.mocharc.json -r chai/register-should.js", "prepare:local": "docker-compose -f CI/E2E/docker-compose-local.yaml --env-file CI/E2E/.env.elastic-search up -d && cp functionalAccounts.json.test functionalAccounts.json" }, "dependencies": { "@casl/ability": "^6.3.2", - "@elastic/elasticsearch": "^8.9.0", + "@elastic/elasticsearch": "^8.15.0", "@nestjs-modules/mailer": "^2.0.2", "@nestjs/axios": "^3.0.0", "@nestjs/common": "^10.3.8", @@ -55,7 +55,7 @@ "handlebars": "^4.7.7", "lodash": "^4.17.21", "luxon": "^3.2.1", - "mathjs": "^12.0.0", + "mathjs": "^13.0.0", "migrate-mongo": "^11.0.0", "mongoose": "^8.4.0", "node-fetch": "^3.3.0", @@ -66,10 +66,10 @@ "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", "reflect-metadata": "^0.2.2", - "rimraf": "^5.0.0", + "rimraf": "^6.0.1", "rxjs": "^7.5.7", "swagger-ui-express": "^5.0.0", - "uuid": "^9.0.0" + "uuid": "^10.0.0" }, "overrides": { "pac-resolver": { @@ -77,7 +77,7 @@ } }, "devDependencies": { - "@faker-js/faker": "^8.0.1", + "@faker-js/faker": "^9.0.0", "@nestjs/cli": "^10.0.5", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.3.8", @@ -89,18 +89,18 @@ "@types/lodash": "^4.14.180", "@types/luxon": "^3.1.0", "@types/mocha": "^10.0.0", - "@types/node": "^20.1.0", + "@types/node": "^22.0.0", "@types/node-fetch": "^2.6.2", "@types/nodemailer": "^6.4.4", "@types/passport-jwt": "^4.0.0", "@types/passport-local": "^1.0.34", "@types/supertest": "^6.0.1", - "@types/uuid": "^9.0.0", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.3.0", "@typescript-eslint/parser": "^7.3.0", "chai": "^5.0.0", "chai-http": "^4.3.6", - "concurrently": "^8.0.1", + "concurrently": "^9.0.0", "eslint": "^8.46.0", "eslint-config-loopback": "^13.1.0", "eslint-config-prettier": "^9.0.0", @@ -115,9 +115,12 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.0", "typescript": "^4.8.3", - "wait-on": "^7.0.1" + "wait-on": "^8.0.0" }, "jest": { + "setupFilesAfterEnv": [ + "../test/config/jest.setup.js" + ], "moduleFileExtensions": [ "js", "json", diff --git a/src/attachments/schemas/attachment.schema.ts b/src/attachments/schemas/attachment.schema.ts index e23d13d97..d7203fe78 100644 --- a/src/attachments/schemas/attachment.schema.ts +++ b/src/attachments/schemas/attachment.schema.ts @@ -43,7 +43,7 @@ export class Attachment extends OwnableClass { caption: string; @ApiProperty({ type: String, required: false }) - @Prop({ type: String, ref: "Dataset", required: false }) + @Prop({ type: String, ref: "Dataset", index: true, required: false }) datasetId: string; @ApiProperty({ type: String, required: false }) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 3ee2933ae..4d77c2461 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -10,6 +10,7 @@ import { flattenObject, parseBoolean } from "src/common/utils"; import { Issuer } from "openid-client"; import { ReturnedAuthLoginDto } from "./dto/returnedLogin.dto"; import { ReturnedUserDto } from "src/users/dto/returned-user.dto"; +import { CreateUserSettingsDto } from "src/users/dto/create-user-settings.dto"; @Injectable() export class AuthService { @@ -43,6 +44,7 @@ export class AuthService { async login(user: Omit): Promise { const expiresIn = this.configService.get("jwt.expiresIn"); const accessToken = this.jwtService.sign(user, { expiresIn }); + await this.postLoginTasks(user); return { access_token: accessToken, id: accessToken, @@ -122,4 +124,30 @@ export class AuthService { return { logout: "successful" }; } + /** + * postLoginTasks: Executes additional tasks after user login. + * + * - Checks if the user has userSettings record. + * - If user has no userSetting, it creates default userSetting for the user. + * @param user - The logged-in user (without password). + */ + async postLoginTasks(user: Omit) { + if (!user) return; + + const userId = user._id; + + const userSettings = await this.usersService.findByIdUserSettings(userId); + + if (!userSettings) { + Logger.log( + `Adding default settings to user ${user.username} with userId: ${user._id}`, + "postLoginTasks", + ); + const createUserSettingsDto: CreateUserSettingsDto = { + userId, + externalSettings: {}, + }; + await this.usersService.createUserSettings(userId, createUserSettingsDto); + } + } } diff --git a/src/casl/casl-ability.factory.ts b/src/casl/casl-ability.factory.ts index 0861325c3..628beabf4 100644 --- a/src/casl/casl-ability.factory.ts +++ b/src/casl/casl-ability.factory.ts @@ -1,11 +1,12 @@ import { - Ability, AbilityBuilder, - AbilityClass, ExtractSubjectType, InferSubjects, + MongoAbility, + MongoQuery, + createMongoAbility, } from "@casl/ability"; -import { Injectable } from "@nestjs/common"; +import { Injectable, InternalServerErrorException } from "@nestjs/common"; import { Attachment } from "src/attachments/schemas/attachment.schema"; import { JWTUser } from "src/auth/interfaces/jwt-user.interface"; // import { Role } from "src/auth/role.enum"; @@ -46,45 +47,49 @@ type Subjects = | typeof ElasticSearchActions > | "all"; +type PossibleAbilities = [Action, Subjects]; +type Conditions = MongoQuery; -export type AppAbility = Ability<[Action, Subjects]>; +export type AppAbility = MongoAbility; @Injectable() export class CaslAbilityFactory { - createForUser(user: JWTUser) { - const { can, cannot, build } = new AbilityBuilder< - Ability<[Action, Subjects]> - >(Ability as AbilityClass); - - // // admin groups - // const stringAdminGroups = process.env.ADMIN_GROUPS || ""; - // const adminGroups: string[] = stringAdminGroups - // ? stringAdminGroups.split(",").map((v) => v.trim()) - // : []; - // // delete groups - // const stringDeleteGroups = process.env.DELETE_GROUPS || ""; - // const deleteGroups: string[] = stringDeleteGroups - // ? stringDeleteGroups.split(",").map((v) => v.trim()) - // : []; - // // create dataset groups - // const stringCreateDatasetGroups = - // process.env.CREATE_DATASET_GROUPS || "all"; - // const createDatasetGroups: string[] = stringCreateDatasetGroups - // .split(",") - // .map((v) => v.trim()); - - /* - / Set permissions for different type of users for the following subsystems: - / - Datasets (https://scicatproject.github.io/documentation/Development/v4.x/backend/authorization/authorization_datasets.html) - / - OrigDatablocks (https://scicatproject.github.io/documentation/Development/v4.x/backend/authorization/authorization_origdatablocks.html) - */ + private endpointAccessors: { + [endpoint: string]: (user: JWTUser) => AppAbility; + } = { + datasets: this.datasetEndpointAccess, + "elastic-search": this.elasticSearchEndpointAccess, + jobs: this.jobsEndpointAccess, + instruments: this.instrumentEndpointAccess, + logbooks: this.logbookEndpointAccess, + origdatablocks: this.origDatablockEndpointAccess, + policies: this.policyEndpointAccess, + proposals: this.proposalsEndpointAccess, + publisheddata: this.publishedDataEndpointAccess, + samples: this.samplesEndpointAccess, + users: this.userEndpointAccess, + }; + + endpointAccess(endpoint: string, user: JWTUser) { + const accessFunction = this.endpointAccessors[endpoint]; + if (!accessFunction) { + throw new InternalServerErrorException( + `No endpoint access policies defined for subject: ${endpoint}`, + ); + } + return accessFunction.call(this, user); + } + + datasetEndpointAccess(user: JWTUser) { + const { can, cannot, build } = new AbilityBuilder( + createMongoAbility, + ); + if (!user) { /** /* unauthenticated users **/ - // ------------------------------------- - // datasets endpoint authorization cannot(Action.DatasetCreate, DatasetClass); can(Action.DatasetRead, DatasetClass); cannot(Action.DatasetUpdate, DatasetClass); @@ -103,47 +108,735 @@ export class CaslAbilityFactory { cannot(Action.DatasetDatablockUpdate, DatasetClass); // - cannot(Action.DatasetLogbookRead, DatasetClass); - // ------------------------------------- - // datasets data instance authorization - can(Action.DatasetReadManyPublic, DatasetClass); - can(Action.DatasetReadOnePublic, DatasetClass, { - isPublished: true, - }); - // - - can(Action.DatasetAttachmentReadPublic, DatasetClass, { - isPublished: true, - }); - // - - can(Action.DatasetOrigdatablockReadPublic, DatasetClass, { - isPublished: true, - }); - // - - can(Action.DatasetDatablockReadPublic, DatasetClass, { - isPublished: true, - }); + } else { + if ( + user.currentGroups.some((g) => configuration().deleteGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in DELETE_GROUPS + */ - // ------------------------------------- - // origdatablock - // ------------------------------------- - // endpoint authorization - can(Action.OrigdatablockRead, OrigDatablock); - cannot(Action.OrigdatablockCreate, OrigDatablock); - cannot(Action.OrigdatablockUpdate, OrigDatablock); - // ------------------------------------- - // data instance authorization - can(Action.OrigdatablockReadManyAccess, OrigDatablock); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - isPublished: true, - }); + can(Action.DatasetDelete, DatasetClass); + // - + can(Action.DatasetOrigdatablockDelete, DatasetClass); + // - + can(Action.DatasetDatablockDelete, DatasetClass); + } else { + /* + / user that does not belong to any of the group listed in DELETE_GROUPS + */ - cannot(Action.UserReadOwn, User); - cannot(Action.UserCreateOwn, User); - cannot(Action.UserUpdateOwn, User); - cannot(Action.UserDeleteOwn, User); - cannot(Action.UserReadAny, User); - cannot(Action.UserCreateAny, User); - cannot(Action.UserUpdateAny, User); - cannot(Action.UserDeleteAny, User); + cannot(Action.DatasetDelete, DatasetClass); + // - + cannot(Action.DatasetOrigdatablockDelete, DatasetClass); + // - + cannot(Action.DatasetDatablockDelete, DatasetClass); + } + + if ( + user.currentGroups.some((g) => configuration().adminGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in ADMIN_GROUPS + */ + + can(Action.DatasetCreate, DatasetClass); + can(Action.DatasetRead, DatasetClass); + can(Action.DatasetUpdate, DatasetClass); + // - + can(Action.DatasetAttachmentCreate, DatasetClass); + can(Action.DatasetAttachmentRead, DatasetClass); + can(Action.DatasetAttachmentUpdate, DatasetClass); + can(Action.DatasetAttachmentDelete, DatasetClass); + // - + can(Action.DatasetOrigdatablockCreate, DatasetClass); + can(Action.DatasetOrigdatablockRead, DatasetClass); + can(Action.DatasetOrigdatablockUpdate, DatasetClass); + // - + can(Action.DatasetDatablockCreate, DatasetClass); + can(Action.DatasetDatablockRead, DatasetClass); + can(Action.DatasetDatablockUpdate, DatasetClass); + // - + can(Action.DatasetLogbookRead, DatasetClass); + } else if ( + user.currentGroups.some((g) => + configuration().createDatasetPrivilegedGroups.includes(g), + ) + ) { + /** + /* users belonging to CREATE_DATASET_PRIVILEGED_GROUPS + **/ + + can(Action.DatasetCreate, DatasetClass); + can(Action.DatasetRead, DatasetClass); + can(Action.DatasetUpdate, DatasetClass); + // - + can(Action.DatasetAttachmentCreate, DatasetClass); + can(Action.DatasetAttachmentRead, DatasetClass); + can(Action.DatasetAttachmentUpdate, DatasetClass); + can(Action.DatasetAttachmentDelete, DatasetClass); + // - + can(Action.DatasetOrigdatablockCreate, DatasetClass); + can(Action.DatasetOrigdatablockRead, DatasetClass); + can(Action.DatasetOrigdatablockUpdate, DatasetClass); + // - + can(Action.DatasetDatablockCreate, DatasetClass); + can(Action.DatasetDatablockRead, DatasetClass); + can(Action.DatasetDatablockUpdate, DatasetClass); + // - + can(Action.DatasetLogbookRead, DatasetClass); + } else if ( + user.currentGroups.some((g) => + configuration().createDatasetWithPidGroups.includes(g), + ) || + configuration().createDatasetWithPidGroups.includes("#all") + ) { + /** + /* users belonging to CREATE_DATASET_WITH_PID_GROUPS + **/ + + can(Action.DatasetCreate, DatasetClass); + can(Action.DatasetRead, DatasetClass); + can(Action.DatasetUpdate, DatasetClass); + // - + can(Action.DatasetAttachmentCreate, DatasetClass); + can(Action.DatasetAttachmentRead, DatasetClass); + can(Action.DatasetAttachmentUpdate, DatasetClass); + can(Action.DatasetAttachmentDelete, DatasetClass); + // - + can(Action.DatasetOrigdatablockCreate, DatasetClass); + can(Action.DatasetOrigdatablockRead, DatasetClass); + can(Action.DatasetOrigdatablockUpdate, DatasetClass); + // - + can(Action.DatasetDatablockCreate, DatasetClass); + can(Action.DatasetDatablockRead, DatasetClass); + can(Action.DatasetDatablockUpdate, DatasetClass); + // - + can(Action.DatasetLogbookRead, DatasetClass); + } else if ( + user.currentGroups.some((g) => + configuration().createDatasetGroups.includes(g), + ) || + configuration().createDatasetGroups.includes("#all") + ) { + /** + /* users belonging to CREATE_DATASET_GROUPS + **/ + + can(Action.DatasetCreate, DatasetClass); + can(Action.DatasetRead, DatasetClass); + can(Action.DatasetUpdate, DatasetClass); + // - + can(Action.DatasetAttachmentCreate, DatasetClass); + can(Action.DatasetAttachmentRead, DatasetClass); + can(Action.DatasetAttachmentUpdate, DatasetClass); + can(Action.DatasetAttachmentDelete, DatasetClass); + // - + can(Action.DatasetOrigdatablockCreate, DatasetClass); + can(Action.DatasetOrigdatablockRead, DatasetClass); + can(Action.DatasetOrigdatablockUpdate, DatasetClass); + // - + can(Action.DatasetDatablockCreate, DatasetClass); + can(Action.DatasetDatablockRead, DatasetClass); + can(Action.DatasetDatablockUpdate, DatasetClass); + // - + can(Action.DatasetLogbookRead, DatasetClass); + } else if (user) { + /** + /* authenticated users + **/ + + cannot(Action.DatasetCreate, DatasetClass); + can(Action.DatasetRead, DatasetClass); + cannot(Action.DatasetUpdate, DatasetClass); + // - + cannot(Action.DatasetAttachmentCreate, DatasetClass); + can(Action.DatasetAttachmentRead, DatasetClass); + cannot(Action.DatasetAttachmentUpdate, DatasetClass); + cannot(Action.DatasetAttachmentDelete, DatasetClass); + // - + cannot(Action.DatasetOrigdatablockCreate, DatasetClass); + can(Action.DatasetOrigdatablockRead, DatasetClass); + cannot(Action.DatasetOrigdatablockUpdate, DatasetClass); + // - + cannot(Action.DatasetDatablockCreate, DatasetClass); + can(Action.DatasetDatablockRead, DatasetClass); + cannot(Action.DatasetDatablockUpdate, DatasetClass); + // - + can(Action.DatasetLogbookRead, DatasetClass); + } + } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + elasticSearchEndpointAccess(user: JWTUser) { + const { can, build } = new AbilityBuilder( + createMongoAbility, + ); + + if ( + user && + user.currentGroups.some((g) => configuration().adminGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in ADMIN_GROUPS + */ + can(Action.Manage, ElasticSearchActions); + } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + instrumentEndpointAccess(user: JWTUser) { + const { can, cannot, build } = new AbilityBuilder( + createMongoAbility, + ); + + if (!user) { + can(Action.InstrumentRead, Instrument); + cannot(Action.InstrumentCreate, Instrument); + cannot(Action.InstrumentUpdate, Instrument); + cannot(Action.InstrumentDelete, Instrument); + } else { + if ( + user.currentGroups.some((g) => configuration().deleteGroups.includes(g)) + ) { + /* + * user that belongs to any of the group listed in DELETE_GROUPS + */ + + can(Action.InstrumentDelete, Instrument); + } else { + cannot(Action.InstrumentDelete, Instrument); + } + + if ( + user.currentGroups.some((g) => configuration().adminGroups.includes(g)) + ) { + /** + * authenticated users belonging to any of the group listed in ADMIN_GROUPS + */ + + can(Action.InstrumentRead, Instrument); + can(Action.InstrumentCreate, Instrument); + can(Action.InstrumentUpdate, Instrument); + } else { + can(Action.InstrumentRead, Instrument); + cannot(Action.InstrumentCreate, Instrument); + cannot(Action.InstrumentUpdate, Instrument); + } + } + + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + jobsEndpointAccess(user: JWTUser) { + const { can, cannot, build } = new AbilityBuilder( + createMongoAbility, + ); + if (!user) { + /** + * unauthenticated users + */ + + cannot(Action.JobsRead, JobClass); + cannot(Action.JobsCreate, JobClass); + cannot(Action.JobsUpdate, JobClass); + } else if ( + user.currentGroups.some((g) => configuration().adminGroups.includes(g)) + ) { + /** + * authenticated users belonging to any of the group listed in ADMIN_GROUPS + */ + + can(Action.JobsRead, JobClass); + can(Action.JobsCreate, JobClass); + can(Action.JobsUpdate, JobClass); + } else if ( + user.currentGroups.some((g) => + configuration().createJobGroups.includes(g), + ) + ) { + /** + * authenticated users belonging to any of the group listed in CREATE_JOBS_GROUPS + */ + + can(Action.JobsRead, JobClass); + can(Action.JobsCreate, JobClass); + can(Action.JobsUpdate, JobClass); + } else if ( + user.currentGroups.some((g) => + configuration().updateJobGroups.includes(g), + ) + ) { + /** + * authenticated users belonging to any of the group listed in UPDATE_JOBS_GROUPS + */ + + cannot(Action.JobsRead, JobClass); + cannot(Action.JobsCreate, JobClass); + can(Action.JobsUpdate, JobClass); + } else if (user) { + /** + * authenticated users + */ + + can(Action.JobsRead, JobClass); + cannot(Action.JobsCreate, JobClass); + cannot(Action.JobsUpdate, JobClass); + } + + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + logbookEndpointAccess(user: JWTUser) { + const { can, build } = new AbilityBuilder( + createMongoAbility, + ); + + if (user) { + /* + / authenticated user + */ + can(Action.Read, Logbook); + } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + origDatablockEndpointAccess(user: JWTUser) { + const { can, cannot, build } = new AbilityBuilder( + createMongoAbility, + ); + if ( + user.currentGroups.some((g) => configuration().deleteGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in DELETE_GROUPS + */ + + can(Action.OrigdatablockDelete, OrigDatablock); + } else { + /* + / user that does not belong to any of the group listed in DELETE_GROUPS + */ + + cannot(Action.OrigdatablockDelete, OrigDatablock); + } + + if ( + user.currentGroups.some((g) => configuration().adminGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in ADMIN_GROUPS + */ + + can(Action.OrigdatablockRead, OrigDatablock); + can(Action.OrigdatablockCreate, OrigDatablock); + can(Action.OrigdatablockUpdate, OrigDatablock); + } else if ( + user.currentGroups.some((g) => + configuration().createDatasetPrivilegedGroups.includes(g), + ) + ) { + /** + /* users belonging to CREATE_DATASET_PRIVILEGED_GROUPS + **/ + + can(Action.OrigdatablockRead, OrigDatablock); + can(Action.OrigdatablockCreate, OrigDatablock); + can(Action.OrigdatablockUpdate, OrigDatablock); + } else if ( + user.currentGroups.some((g) => + configuration().createDatasetWithPidGroups.includes(g), + ) || + configuration().createDatasetWithPidGroups.includes("#all") + ) { + /** + /* users belonging to CREATE_DATASET_WITH_PID_GROUPS + **/ + + can(Action.OrigdatablockRead, OrigDatablock); + can(Action.OrigdatablockCreate, OrigDatablock); + can(Action.OrigdatablockUpdate, OrigDatablock); + } else if ( + user.currentGroups.some((g) => + configuration().createDatasetGroups.includes(g), + ) || + configuration().createDatasetGroups.includes("#all") + ) { + /** + /* users belonging to CREATE_DATASET_GROUPS + **/ + + can(Action.OrigdatablockRead, OrigDatablock); + can(Action.OrigdatablockCreate, OrigDatablock); + can(Action.OrigdatablockUpdate, OrigDatablock); + } else if (user) { + /** + /* authenticated users + **/ + + can(Action.OrigdatablockRead, OrigDatablock); + cannot(Action.OrigdatablockCreate, OrigDatablock); + cannot(Action.OrigdatablockUpdate, OrigDatablock); + } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + policyEndpointAccess(user: JWTUser) { + const { can, build } = new AbilityBuilder( + createMongoAbility, + ); + if ( + user && + user.currentGroups.some((g) => configuration().deleteGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in DELETE_GROUPS + */ + can(Action.Delete, Policy); + } else if ( + user && + user.currentGroups.some((g) => configuration().adminGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in ADMIN_GROUPS + */ + + can(Action.Update, Policy); + can(Action.Read, Policy); + can(Action.Create, Policy); + } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + proposalsEndpointAccess(user: JWTUser) { + const { can, cannot, build } = new AbilityBuilder( + createMongoAbility, + ); + if (!user) { + /** + * unauthenticated users + */ + + can(Action.ProposalsRead, ProposalClass); + cannot(Action.ProposalsCreate, ProposalClass); + cannot(Action.ProposalsUpdate, ProposalClass); + cannot(Action.ProposalsDelete, ProposalClass); + can(Action.ProposalsAttachmentRead, ProposalClass); + cannot(Action.ProposalsAttachmentCreate, ProposalClass); + cannot(Action.ProposalsAttachmentUpdate, ProposalClass); + cannot(Action.ProposalsAttachmentDelete, ProposalClass); + } else if ( + user.currentGroups.some((g) => configuration().deleteGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in DELETE_GROUPS + */ + + can(Action.ProposalsDelete, ProposalClass); + } else if ( + user.currentGroups.some((g) => configuration().adminGroups.includes(g)) + ) { + /** + * authenticated users belonging to any of the group listed in ADMIN_GROUPS + */ + + can(Action.ProposalsRead, ProposalClass); + can(Action.ProposalsCreate, ProposalClass); + can(Action.ProposalsUpdate, ProposalClass); + cannot(Action.ProposalsDelete, ProposalClass); + can(Action.ProposalsAttachmentRead, ProposalClass); + can(Action.ProposalsAttachmentCreate, ProposalClass); + can(Action.ProposalsAttachmentUpdate, ProposalClass); + can(Action.ProposalsAttachmentDelete, ProposalClass); + } else if ( + user.currentGroups.some((g) => { + return configuration().proposalGroups.includes(g); + }) + ) { + /** + * authenticated users belonging to any of the group listed in PROPOSAL_GROUPS + */ + + can(Action.ProposalsRead, ProposalClass); + can(Action.ProposalsCreate, ProposalClass); + can(Action.ProposalsUpdate, ProposalClass); + cannot(Action.ProposalsDelete, ProposalClass); + can(Action.ProposalsAttachmentRead, ProposalClass); + can(Action.ProposalsAttachmentCreate, ProposalClass); + can(Action.ProposalsAttachmentUpdate, ProposalClass); + can(Action.ProposalsAttachmentDelete, ProposalClass); + cannot(Action.ProposalsDatasetRead, ProposalClass); + } else if (user) { + /** + * authenticated users + */ + + can(Action.ProposalsRead, ProposalClass); + cannot(Action.ProposalsCreate, ProposalClass); + cannot(Action.ProposalsUpdate, ProposalClass); + cannot(Action.ProposalsDelete, ProposalClass); + can(Action.ProposalsAttachmentRead, ProposalClass); + cannot(Action.ProposalsAttachmentCreate, ProposalClass); + cannot(Action.ProposalsAttachmentUpdate, ProposalClass); + cannot(Action.ProposalsAttachmentDelete, ProposalClass); + can(Action.ProposalsDatasetRead, ProposalClass); + } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + publishedDataEndpointAccess(user: JWTUser) { + const { can, build } = new AbilityBuilder( + createMongoAbility, + ); + if (user) { + can(Action.Read, PublishedData); + can(Action.Update, PublishedData); + can(Action.Create, PublishedData); + } + + if ( + user && + user.currentGroups.some((g) => configuration().deleteGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in DELETE_GROUPS + */ + can(Action.Delete, PublishedData); + } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + samplesEndpointAccess(user: JWTUser) { + const { can, cannot, build } = new AbilityBuilder( + createMongoAbility, + ); + + if (!user) { + // ------------------------------------- + // unauthenticated users + // ------------------------------------- + + can(Action.SampleRead, SampleClass); + cannot(Action.SampleCreate, SampleClass); + cannot(Action.SampleUpdate, SampleClass); + cannot(Action.SampleDelete, SampleClass); + can(Action.SampleAttachmentRead, SampleClass); + cannot(Action.SampleAttachmentCreate, SampleClass); + cannot(Action.SampleAttachmentUpdate, SampleClass); + cannot(Action.SampleAttachmentDelete, SampleClass); + cannot(Action.SampleDatasetRead, SampleClass); + } else { + // ------------------------------------- + // authenticated users + // ------------------------------------- + + if ( + user.currentGroups.some((g) => configuration().deleteGroups.includes(g)) + ) { + // ------------------------------------- + // users that belong to any of the group listed in DELETE_GROUPS + // ------------------------------------- + + can(Action.SampleDelete, SampleClass); + can(Action.SampleAttachmentDelete, SampleClass); + } else { + // ------------------------------------- + // users that do not belong to any of the group listed in DELETE_GROUPS + // ------------------------------------- + + cannot(Action.SampleDelete, SampleClass); + } + + if ( + user.currentGroups.some((g) => configuration().adminGroups.includes(g)) + ) { + // ------------------------------------- + // users belonging to any of the group listed in ADMIN_GROUPS + // ------------------------------------- + + can(Action.SampleRead, SampleClass); + can(Action.SampleCreate, SampleClass); + can(Action.SampleUpdate, SampleClass); + can(Action.SampleAttachmentRead, SampleClass); + can(Action.SampleAttachmentCreate, SampleClass); + can(Action.SampleAttachmentUpdate, SampleClass); + can(Action.SampleAttachmentDelete, SampleClass); + can(Action.SampleDatasetRead, SampleClass); + } else if ( + user.currentGroups.some((g) => + configuration().samplePrivilegedGroups.includes(g), + ) + ) { + // ------------------------------------- + // users belonging to any of the group listed in SAMPLE_GROUPS + // + + can(Action.SampleRead, SampleClass); + can(Action.SampleCreate, SampleClass); + can(Action.SampleUpdate, SampleClass); + can(Action.SampleAttachmentRead, SampleClass); + can(Action.SampleAttachmentCreate, SampleClass); + can(Action.SampleAttachmentUpdate, SampleClass); + can(Action.SampleAttachmentDelete, SampleClass); + can(Action.SampleDatasetRead, SampleClass); + } else if ( + user.currentGroups.some((g) => + configuration().sampleGroups.includes(g), + ) || + configuration().sampleGroups.includes("#all") + ) { + // ------------------------------------- + // users belonging to any of the group listed in SAMPLE_GROUPS + // + + can(Action.SampleRead, SampleClass); + can(Action.SampleCreate, SampleClass); + can(Action.SampleUpdate, SampleClass); + can(Action.SampleAttachmentRead, SampleClass); + can(Action.SampleAttachmentCreate, SampleClass); + can(Action.SampleAttachmentUpdate, SampleClass); + can(Action.SampleAttachmentDelete, SampleClass); + can(Action.SampleDatasetRead, SampleClass); + } else { + // ------------------------------------- + // users with no elevated permissions + // ------------------------------------- + + can(Action.SampleRead, SampleClass); + cannot(Action.SampleCreate, SampleClass); + cannot(Action.SampleUpdate, SampleClass); + can(Action.SampleAttachmentRead, SampleClass); + cannot(Action.SampleAttachmentCreate, SampleClass); + cannot(Action.SampleAttachmentUpdate, SampleClass); + if ( + !user.currentGroups.some((g) => + configuration().deleteGroups.includes(g), + ) + ) { + cannot(Action.SampleAttachmentDelete, SampleClass); + } + } + } + + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + userEndpointAccess(user: JWTUser) { + const { can, cannot, build } = new AbilityBuilder( + createMongoAbility, + ); + + if (!user) { + /** + /* unauthenticated users + **/ + + cannot(Action.UserReadOwn, User); + cannot(Action.UserCreateOwn, User); + cannot(Action.UserUpdateOwn, User); + cannot(Action.UserDeleteOwn, User); + cannot(Action.UserReadAny, User); + cannot(Action.UserCreateAny, User); + cannot(Action.UserUpdateAny, User); + cannot(Action.UserDeleteAny, User); + } else { + if ( + user.currentGroups.some((g) => configuration().adminGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in ADMIN_GROUPS + */ + + // can(Action.ReadAll, UserIdentity); NOT used? + + // ------------------------------------- + // user endpoint, including useridentity + can(Action.UserReadAny, User); + can(Action.UserReadOwn, User); + can(Action.UserCreateAny, User); + can(Action.UserUpdateAny, User); + can(Action.UserDeleteAny, User); + can(Action.UserCreateJwt, User); + + // ------------------------------------- + } else if (user) { + /** + /* authenticated users + **/ + cannot(Action.UserReadAny, User); + cannot(Action.UserCreateAny, User); + cannot(Action.UserUpdateAny, User); + cannot(Action.UserDeleteAny, User); + cannot(Action.UserCreateJwt, User); + } + can(Action.UserReadOwn, User, { _id: user._id }); + can(Action.UserCreateOwn, User, { _id: user._id }); + can(Action.UserUpdateOwn, User, { _id: user._id }); + can(Action.UserDeleteOwn, User, { _id: user._id }); + } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + datasetInstanceAccess(user: JWTUser) { + const { can, build } = new AbilityBuilder( + createMongoAbility, + ); + + if (!user) { + /** + /* unauthenticated users + **/ + + can(Action.DatasetReadManyPublic, DatasetClass); + can(Action.DatasetReadOnePublic, DatasetClass, { + isPublished: true, + }); + // - + can(Action.DatasetAttachmentReadPublic, DatasetClass, { + isPublished: true, + }); + // - + can(Action.DatasetOrigdatablockReadPublic, DatasetClass, { + isPublished: true, + }); + // - + can(Action.DatasetDatablockReadPublic, DatasetClass, { + isPublished: true, + }); } else { if ( user.currentGroups.some((g) => configuration().deleteGroups.includes(g)) @@ -152,54 +845,11 @@ export class CaslAbilityFactory { / user that belongs to any of the group listed in DELETE_GROUPS */ - // ------------------------------------- - // datasets - // ------------------------------------- - // endpoint authorization - can(Action.DatasetDelete, DatasetClass); - // - - can(Action.DatasetOrigdatablockDelete, DatasetClass); - // - - can(Action.DatasetDatablockDelete, DatasetClass); - // ------------------------------------- - // data instance authorization can(Action.DatasetDeleteAny, DatasetClass); // - can(Action.DatasetOrigdatablockDeleteAny, DatasetClass); // - can(Action.DatasetDatablockDeleteAny, DatasetClass); - - // ------------------------------------- - // origdatablock - // ------------------------------------- - // endpoint authorization - can(Action.OrigdatablockDelete, OrigDatablock); - // ------------------------------------- - // data instance authorization - can(Action.OrigdatablockDeleteAny, OrigDatablock); - - can(Action.Delete, PublishedData); - can(Action.Delete, Policy); - } else { - /* - / user that does not belong to any of the group listed in DELETE_GROUPS - */ - - // ------------------------------------- - // datasets - // ------------------------------------- - // endpoint authorization - cannot(Action.DatasetDelete, DatasetClass); - // - - cannot(Action.DatasetOrigdatablockDelete, DatasetClass); - // - - cannot(Action.DatasetDatablockDelete, DatasetClass); - - // ------------------------------------- - // origdatablock - // ------------------------------------- - // endpoint authorization - cannot(Action.OrigdatablockDelete, OrigDatablock); } if ( @@ -209,41 +859,6 @@ export class CaslAbilityFactory { / user that belongs to any of the group listed in ADMIN_GROUPS */ - // this tests should be all removed, once we are done with authorization review - //can(Action.ListAll, DatasetClass); - // can(Action.ListAll, ProposalClass); - can(Action.ReadAll, UserIdentity); - - // ------------------------------------- - // elasticsearch - // ------------------------------------- - // endpoint authorization - can(Action.Manage, ElasticSearchActions); - - // ------------------------------------- - // datasets - // ------------------------------------- - // endpoint authorization - can(Action.DatasetCreate, DatasetClass); - can(Action.DatasetRead, DatasetClass); - can(Action.DatasetUpdate, DatasetClass); - // - - can(Action.DatasetAttachmentCreate, DatasetClass); - can(Action.DatasetAttachmentRead, DatasetClass); - can(Action.DatasetAttachmentUpdate, DatasetClass); - can(Action.DatasetAttachmentDelete, DatasetClass); - // - - can(Action.DatasetOrigdatablockCreate, DatasetClass); - can(Action.DatasetOrigdatablockRead, DatasetClass); - can(Action.DatasetOrigdatablockUpdate, DatasetClass); - // - - can(Action.DatasetDatablockCreate, DatasetClass); - can(Action.DatasetDatablockRead, DatasetClass); - can(Action.DatasetDatablockUpdate, DatasetClass); - // - - can(Action.DatasetLogbookRead, DatasetClass); - // ------------------------------------- - // data instance authorization can(Action.DatasetCreateAny, DatasetClass); can(Action.DatasetReadAny, DatasetClass); can(Action.DatasetUpdateAny, DatasetClass); @@ -262,34 +877,6 @@ export class CaslAbilityFactory { can(Action.DatasetDatablockUpdateAny, DatasetClass); // ------------------------------------- can(Action.DatasetLogbookReadAny, DatasetClass); - - // ------------------------------------- - // origdatablock - // ------------------------------------- - // endpoint authorization - can(Action.OrigdatablockRead, OrigDatablock); - can(Action.OrigdatablockCreate, OrigDatablock); - can(Action.OrigdatablockUpdate, OrigDatablock); - // ------------------------------------- - // data instance authorization - can(Action.OrigdatablockReadAny, OrigDatablock); - can(Action.OrigdatablockCreateAny, OrigDatablock); - can(Action.OrigdatablockUpdateAny, OrigDatablock); - - // ------------------------------------- - // user endpoint, including useridentity - can(Action.UserReadAny, User); - can(Action.UserReadOwn, User); - can(Action.UserCreateAny, User); - can(Action.UserUpdateAny, User); - can(Action.UserDeleteAny, User); - can(Action.UserCreateJwt, User); - - // ------------------------------------- - // policies - can(Action.Update, Policy); - can(Action.Read, Policy); - can(Action.Create, Policy); } else if ( user.currentGroups.some((g) => configuration().createDatasetPrivilegedGroups.includes(g), @@ -299,30 +886,6 @@ export class CaslAbilityFactory { /* users belonging to CREATE_DATASET_PRIVILEGED_GROUPS **/ - // ------------------------------------- - // datasets - // ------------------------------------- - // endpoint authorization - can(Action.DatasetCreate, DatasetClass); - can(Action.DatasetRead, DatasetClass); - can(Action.DatasetUpdate, DatasetClass); - // - - can(Action.DatasetAttachmentCreate, DatasetClass); - can(Action.DatasetAttachmentRead, DatasetClass); - can(Action.DatasetAttachmentUpdate, DatasetClass); - can(Action.DatasetAttachmentDelete, DatasetClass); - // - - can(Action.DatasetOrigdatablockCreate, DatasetClass); - can(Action.DatasetOrigdatablockRead, DatasetClass); - can(Action.DatasetOrigdatablockUpdate, DatasetClass); - // - - can(Action.DatasetDatablockCreate, DatasetClass); - can(Action.DatasetDatablockRead, DatasetClass); - can(Action.DatasetDatablockUpdate, DatasetClass); - // - - can(Action.DatasetLogbookRead, DatasetClass); - // ------------------------------------- - // data instance authorization can(Action.DatasetCreateAny, DatasetClass); can(Action.DatasetReadManyAccess, DatasetClass); can(Action.DatasetReadOneAccess, DatasetClass, { @@ -386,30 +949,6 @@ export class CaslAbilityFactory { can(Action.DatasetLogbookReadOwner, DatasetClass, { ownerGroup: { $in: user.currentGroups }, }); - - // ------------------------------------- - // origdatablock - // ------------------------------------- - // endpoint authorization - can(Action.OrigdatablockRead, OrigDatablock); - can(Action.OrigdatablockCreate, OrigDatablock); - can(Action.OrigdatablockUpdate, OrigDatablock); - // ------------------------------------- - // data instance authorization - can(Action.OrigdatablockCreateAny, OrigDatablock); - can(Action.OrigdatablockReadManyAccess, OrigDatablock); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - ownerGroup: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - accessGroups: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - ownerGroup: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockUpdateOwner, OrigDatablock, { - ownerGroup: { $in: user.currentGroups }, - }); } else if ( user.currentGroups.some((g) => configuration().createDatasetWithPidGroups.includes(g), @@ -420,28 +959,6 @@ export class CaslAbilityFactory { /* users belonging to CREATE_DATASET_WITH_PID_GROUPS **/ - // ------------------------------------- - // datasets endpoint authorization - can(Action.DatasetCreate, DatasetClass); - can(Action.DatasetRead, DatasetClass); - can(Action.DatasetUpdate, DatasetClass); - // - - can(Action.DatasetAttachmentCreate, DatasetClass); - can(Action.DatasetAttachmentRead, DatasetClass); - can(Action.DatasetAttachmentUpdate, DatasetClass); - can(Action.DatasetAttachmentDelete, DatasetClass); - // - - can(Action.DatasetOrigdatablockCreate, DatasetClass); - can(Action.DatasetOrigdatablockRead, DatasetClass); - can(Action.DatasetOrigdatablockUpdate, DatasetClass); - // - - can(Action.DatasetDatablockCreate, DatasetClass); - can(Action.DatasetDatablockRead, DatasetClass); - can(Action.DatasetDatablockUpdate, DatasetClass); - // - - can(Action.DatasetLogbookRead, DatasetClass); - // ------------------------------------- - // datasets data instance authorization can(Action.DatasetCreateOwnerWithPid, DatasetClass, { ownerGroup: { $in: user.currentGroups }, }); @@ -513,32 +1030,6 @@ export class CaslAbilityFactory { can(Action.DatasetLogbookReadOwner, DatasetClass, { ownerGroup: { $in: user.currentGroups }, }); - - // ------------------------------------- - // origdatablock - // ------------------------------------- - // endpoint authorization - can(Action.OrigdatablockRead, OrigDatablock); - can(Action.OrigdatablockCreate, OrigDatablock); - can(Action.OrigdatablockUpdate, OrigDatablock); - // ------------------------------------- - // data instance authorization - can(Action.OrigdatablockCreateOwner, OrigDatablock, { - ownerGroup: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockReadManyAccess, OrigDatablock); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - ownerGroup: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - accessGroups: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - isPublished: true, - }); - can(Action.OrigdatablockUpdateOwner, OrigDatablock, { - ownerGroup: { $in: user.currentGroups }, - }); } else if ( user.currentGroups.some((g) => configuration().createDatasetGroups.includes(g), @@ -549,28 +1040,6 @@ export class CaslAbilityFactory { /* users belonging to CREATE_DATASET_GROUPS **/ - // ------------------------------------- - // datasets endpoint authorization - can(Action.DatasetCreate, DatasetClass); - can(Action.DatasetRead, DatasetClass); - can(Action.DatasetUpdate, DatasetClass); - // - - can(Action.DatasetAttachmentCreate, DatasetClass); - can(Action.DatasetAttachmentRead, DatasetClass); - can(Action.DatasetAttachmentUpdate, DatasetClass); - can(Action.DatasetAttachmentDelete, DatasetClass); - // - - can(Action.DatasetOrigdatablockCreate, DatasetClass); - can(Action.DatasetOrigdatablockRead, DatasetClass); - can(Action.DatasetOrigdatablockUpdate, DatasetClass); - // - - can(Action.DatasetDatablockCreate, DatasetClass); - can(Action.DatasetDatablockRead, DatasetClass); - can(Action.DatasetDatablockUpdate, DatasetClass); - // - - can(Action.DatasetLogbookRead, DatasetClass); - // ------------------------------------- - // datasets data instance authorization can(Action.DatasetCreateOwnerNoPid, DatasetClass, { ownerGroup: { $in: user.currentGroups }, pid: { $eq: "" }, @@ -644,59 +1113,11 @@ export class CaslAbilityFactory { can(Action.DatasetLogbookReadOwner, DatasetClass, { ownerGroup: { $in: user.currentGroups }, }); - - // ------------------------------------- - // origdatablock - // ------------------------------------- - // endpoint authorization - can(Action.OrigdatablockRead, OrigDatablock); - can(Action.OrigdatablockCreate, OrigDatablock); - can(Action.OrigdatablockUpdate, OrigDatablock); - // ------------------------------------- - // data instance authorization - can(Action.OrigdatablockCreateOwner, OrigDatablock, { - ownerGroup: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockReadManyAccess, OrigDatablock); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - ownerGroup: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - accessGroups: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - isPublished: true, - }); - can(Action.OrigdatablockUpdateOwner, OrigDatablock, { - ownerGroup: { $in: user.currentGroups }, - }); } else if (user) { /** /* authenticated users **/ - // ------------------------------------- - // datasets endpoint authorization - cannot(Action.DatasetCreate, DatasetClass); - can(Action.DatasetRead, DatasetClass); - cannot(Action.DatasetUpdate, DatasetClass); - // - - cannot(Action.DatasetAttachmentCreate, DatasetClass); - can(Action.DatasetAttachmentRead, DatasetClass); - cannot(Action.DatasetAttachmentUpdate, DatasetClass); - cannot(Action.DatasetAttachmentDelete, DatasetClass); - // - - cannot(Action.DatasetOrigdatablockCreate, DatasetClass); - can(Action.DatasetOrigdatablockRead, DatasetClass); - cannot(Action.DatasetOrigdatablockUpdate, DatasetClass); - // - - cannot(Action.DatasetDatablockCreate, DatasetClass); - can(Action.DatasetDatablockRead, DatasetClass); - cannot(Action.DatasetDatablockUpdate, DatasetClass); - // - - can(Action.DatasetLogbookRead, DatasetClass); - // ------------------------------------- - // datasets data instance authorization can(Action.DatasetReadManyAccess, DatasetClass); can(Action.DatasetReadOneAccess, DatasetClass, { ownerGroup: { $in: user.currentGroups }, @@ -741,70 +1162,140 @@ export class CaslAbilityFactory { can(Action.DatasetLogbookReadOwner, DatasetClass, { ownerGroup: { $in: user.currentGroups }, }); - - // ------------------------------------- - // origdatablock - // ------------------------------------- - // endpoint authorization - can(Action.OrigdatablockRead, OrigDatablock); - cannot(Action.OrigdatablockCreate, OrigDatablock); - cannot(Action.OrigdatablockUpdate, OrigDatablock); - // ------------------------------------- - // data instance authorization - can(Action.OrigdatablockReadManyAccess, OrigDatablock); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - ownerGroup: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - accessGroups: { $in: user.currentGroups }, - }); - can(Action.OrigdatablockReadOneAccess, OrigDatablock, { - isPublished: true, - }); - cannot(Action.UserReadAny, User); - cannot(Action.UserCreateAny, User); - cannot(Action.UserUpdateAny, User); - cannot(Action.UserDeleteAny, User); - cannot(Action.UserCreateJwt, User); } - can(Action.UserReadOwn, User, { _id: user._id }); - can(Action.UserCreateOwn, User, { _id: user._id }); - can(Action.UserUpdateOwn, User, { _id: user._id }); - can(Action.UserDeleteOwn, User, { _id: user._id }); } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + origDatablockInstanceAccess(user: JWTUser) { + const { can, build } = new AbilityBuilder( + createMongoAbility, + ); + if ( + user.currentGroups.some((g) => configuration().deleteGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in DELETE_GROUPS + */ + + can(Action.OrigdatablockDeleteAny, OrigDatablock); + } + if ( + user.currentGroups.some((g) => configuration().adminGroups.includes(g)) + ) { + /* + / user that belongs to any of the group listed in ADMIN_GROUPS + */ + + can(Action.OrigdatablockReadAny, OrigDatablock); + can(Action.OrigdatablockCreateAny, OrigDatablock); + can(Action.OrigdatablockUpdateAny, OrigDatablock); + } else if ( + user.currentGroups.some((g) => + configuration().createDatasetPrivilegedGroups.includes(g), + ) + ) { + /** + /* users belonging to CREATE_DATASET_PRIVILEGED_GROUPS + **/ + can(Action.OrigdatablockCreateAny, OrigDatablock); + can(Action.OrigdatablockReadManyAccess, OrigDatablock); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + ownerGroup: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + accessGroups: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + ownerGroup: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockUpdateOwner, OrigDatablock, { + ownerGroup: { $in: user.currentGroups }, + }); + } else if ( + user.currentGroups.some((g) => + configuration().createDatasetWithPidGroups.includes(g), + ) || + configuration().createDatasetWithPidGroups.includes("#all") + ) { + /** + /* users belonging to CREATE_DATASET_WITH_PID_GROUPS + **/ - // ************************************ - // JOBS AUTHORIZATION - // ************************************ + can(Action.OrigdatablockCreateOwner, OrigDatablock, { + ownerGroup: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockReadManyAccess, OrigDatablock); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + ownerGroup: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + accessGroups: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + isPublished: true, + }); + can(Action.OrigdatablockUpdateOwner, OrigDatablock, { + ownerGroup: { $in: user.currentGroups }, + }); + } else if ( + user.currentGroups.some((g) => + configuration().createDatasetGroups.includes(g), + ) || + configuration().createDatasetGroups.includes("#all") + ) { + /** + /* users belonging to CREATE_DATASET_GROUPS + **/ - if (!user) { + can(Action.OrigdatablockCreateOwner, OrigDatablock, { + ownerGroup: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockReadManyAccess, OrigDatablock); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + ownerGroup: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + accessGroups: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + isPublished: true, + }); + can(Action.OrigdatablockUpdateOwner, OrigDatablock, { + ownerGroup: { $in: user.currentGroups }, + }); + } else if (user) { /** - * unauthenticated users - */ + /* authenticated users + **/ - // ------------------------------------- - // jobs - // ------------------------------------- - // endpoint authorization - cannot(Action.JobsRead, JobClass); - cannot(Action.JobsCreate, JobClass); - cannot(Action.JobsUpdate, JobClass); - } else if ( + can(Action.OrigdatablockReadManyAccess, OrigDatablock); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + ownerGroup: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + accessGroups: { $in: user.currentGroups }, + }); + can(Action.OrigdatablockReadOneAccess, OrigDatablock, { + isPublished: true, + }); + } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + jobsInstanceAccess(user: JWTUser) { + const { can, build } = new AbilityBuilder( + createMongoAbility, + ); + if ( user.currentGroups.some((g) => configuration().adminGroups.includes(g)) ) { - /** - * authenticated users belonging to any of the group listed in ADMIN_GROUPS - */ - - // ------------------------------------- - // jobs - // ------------------------------------- - // endpoint authorization - can(Action.JobsRead, JobClass); - can(Action.JobsCreate, JobClass); - can(Action.JobsUpdate, JobClass); - // ------------------------------------- - // data instance authorization can(Action.JobsReadAny, JobClass); can(Action.JobsCreateAny, JobClass); can(Action.JobsUpdateAny, JobClass); @@ -817,15 +1308,6 @@ export class CaslAbilityFactory { * authenticated users belonging to any of the group listed in CREATE_JOBS_GROUPS */ - // ------------------------------------- - // jobs - // ------------------------------------- - // endpoint authorization - can(Action.JobsRead, JobClass); - can(Action.JobsCreate, JobClass); - can(Action.JobsUpdate, JobClass); - // ------------------------------------- - // data instance authorization can(Action.JobsCreateAny, JobClass, { ownerGroup: { $in: user.currentGroups }, }); @@ -844,15 +1326,6 @@ export class CaslAbilityFactory { * authenticated users belonging to any of the group listed in UPDATE_JOBS_GROUPS */ - // ------------------------------------- - // jobs - // ------------------------------------- - // endpoint authorization - cannot(Action.JobsRead, JobClass); - cannot(Action.JobsCreate, JobClass); - can(Action.JobsUpdate, JobClass); - // ------------------------------------- - // data instance authorization can(Action.JobsUpdateAny, JobClass, { ownerGroup: { $in: user.currentGroups }, }); @@ -861,44 +1334,26 @@ export class CaslAbilityFactory { * authenticated users */ - // ------------------------------------- - // jobs - // ------------------------------------- - // endpoint authorization - can(Action.JobsRead, JobClass); - cannot(Action.JobsCreate, JobClass); - cannot(Action.JobsUpdate, JobClass); - // ------------------------------------- - // data instance authorization can(Action.JobsReadAccess, JobClass, { ownerGroup: { $in: user.currentGroups }, }); } - // ************************************ - // PROPOSALS AUTHORIZATION - // ************************************ + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + proposalsInstanceAccess(user: JWTUser) { + const { can, cannot, build } = new AbilityBuilder( + createMongoAbility, + ); if (!user) { /** * unauthenticated users */ - // ------------------------------------- - // proposals - // ------------------------------------- - // endpoint authorization - can(Action.ProposalsRead, ProposalClass); - cannot(Action.ProposalsCreate, ProposalClass); - cannot(Action.ProposalsUpdate, ProposalClass); - cannot(Action.ProposalsDelete, ProposalClass); - can(Action.ProposalsAttachmentRead, ProposalClass); - cannot(Action.ProposalsAttachmentCreate, ProposalClass); - cannot(Action.ProposalsAttachmentUpdate, ProposalClass); - cannot(Action.ProposalsAttachmentDelete, ProposalClass); - - // ------------------------------------- - // data instance authorization can(Action.ProposalsReadManyPublic, ProposalClass); can(Action.ProposalsReadOnePublic, ProposalClass, { isPublished: true, @@ -912,16 +1367,6 @@ export class CaslAbilityFactory { /* / user that belongs to any of the group listed in DELETE_GROUPS */ - - // ------------------------------------- - // proposals - // ------------------------------------- - // endpoint authorization - can(Action.ProposalsDelete, ProposalClass); - - // ------------------------------------- - // data instance authorization - can(Action.ProposalsDeleteAny, ProposalClass); } else if ( user.currentGroups.some((g) => configuration().adminGroups.includes(g)) @@ -930,20 +1375,6 @@ export class CaslAbilityFactory { * authenticated users belonging to any of the group listed in ADMIN_GROUPS */ - // ------------------------------------- - // proposals - // ------------------------------------- - // endpoint authorization - can(Action.ProposalsRead, ProposalClass); - can(Action.ProposalsCreate, ProposalClass); - can(Action.ProposalsUpdate, ProposalClass); - cannot(Action.ProposalsDelete, ProposalClass); - can(Action.ProposalsAttachmentRead, ProposalClass); - can(Action.ProposalsAttachmentCreate, ProposalClass); - can(Action.ProposalsAttachmentUpdate, ProposalClass); - can(Action.ProposalsAttachmentDelete, ProposalClass); - // ------------------------------------- - // data instance authorization can(Action.ProposalsReadAny, ProposalClass); can(Action.ProposalsCreateAny, ProposalClass); can(Action.ProposalsUpdateAny, ProposalClass); @@ -961,22 +1392,6 @@ export class CaslAbilityFactory { * authenticated users belonging to any of the group listed in PROPOSAL_GROUPS */ - // ------------------------------------- - // proposals - // ------------------------------------- - // endpoint authorization - - can(Action.ProposalsRead, ProposalClass); - can(Action.ProposalsCreate, ProposalClass); - can(Action.ProposalsUpdate, ProposalClass); - cannot(Action.ProposalsDelete, ProposalClass); - can(Action.ProposalsAttachmentRead, ProposalClass); - can(Action.ProposalsAttachmentCreate, ProposalClass); - can(Action.ProposalsAttachmentUpdate, ProposalClass); - can(Action.ProposalsAttachmentDelete, ProposalClass); - cannot(Action.ProposalsDatasetRead, ProposalClass); - // ------------------------------------- - // data instance authorization can(Action.ProposalsCreateAny, ProposalClass); can(Action.ProposalsReadManyAccess, ProposalClass); can(Action.ProposalsReadOneAccess, ProposalClass, { @@ -1010,21 +1425,6 @@ export class CaslAbilityFactory { * authenticated users */ - // ------------------------------------- - // proposals - // ------------------------------------- - // endpoint authorization - can(Action.ProposalsRead, ProposalClass); - cannot(Action.ProposalsCreate, ProposalClass); - cannot(Action.ProposalsUpdate, ProposalClass); - cannot(Action.ProposalsDelete, ProposalClass); - can(Action.ProposalsAttachmentRead, ProposalClass); - cannot(Action.ProposalsAttachmentCreate, ProposalClass); - cannot(Action.ProposalsAttachmentUpdate, ProposalClass); - cannot(Action.ProposalsAttachmentDelete, ProposalClass); - can(Action.ProposalsDatasetRead, ProposalClass); - // ------------------------------------- - // data instance authorization can(Action.ProposalsReadManyAccess, ProposalClass); can(Action.ProposalsReadOneAccess, ProposalClass, { ownerGroup: { $in: user.currentGroups }, @@ -1046,29 +1446,22 @@ export class CaslAbilityFactory { isPublished: true, }); } + return build({ + detectSubjectType: (item) => + item.constructor as ExtractSubjectType, + }); + } + + samplesInstanceAccess(user: JWTUser) { + const { can, cannot, build } = new AbilityBuilder( + createMongoAbility, + ); - // ************************************ - // SAMPLES AUTHORIZATION - // ************************************ if (!user) { // ------------------------------------- // unauthenticated users // ------------------------------------- - // ------------------------------------- - // endpoint authorization - can(Action.SampleRead, SampleClass); - cannot(Action.SampleCreate, SampleClass); - cannot(Action.SampleUpdate, SampleClass); - cannot(Action.SampleDelete, SampleClass); - can(Action.SampleAttachmentRead, SampleClass); - cannot(Action.SampleAttachmentCreate, SampleClass); - cannot(Action.SampleAttachmentUpdate, SampleClass); - cannot(Action.SampleAttachmentDelete, SampleClass); - cannot(Action.SampleDatasetRead, SampleClass); - - // ------------------------------------- - // data instance authorization can(Action.SampleReadManyPublic, SampleClass); can(Action.SampleReadOnePublic, SampleClass, { isPublished: true, @@ -1088,13 +1481,6 @@ export class CaslAbilityFactory { // users that belong to any of the group listed in DELETE_GROUPS // ------------------------------------- - // ------------------------------------- - // endpoint authorization - can(Action.SampleDelete, SampleClass); - can(Action.SampleAttachmentDelete, SampleClass); - - // ------------------------------------- - // data instance authorization can(Action.SampleDeleteAny, SampleClass); can(Action.SampleAttachmentDeleteAny, SampleClass); } else { @@ -1102,12 +1488,6 @@ export class CaslAbilityFactory { // users that do not belong to any of the group listed in DELETE_GROUPS // ------------------------------------- - // ------------------------------------- - // endpoint authorization - cannot(Action.SampleDelete, SampleClass); - - // ------------------------------------- - // data instance authorization cannot(Action.SampleDeleteAny, SampleClass); cannot(Action.SampleDeleteOwner, SampleClass); } @@ -1119,19 +1499,6 @@ export class CaslAbilityFactory { // users belonging to any of the group listed in ADMIN_GROUPS // ------------------------------------- - // ------------------------------------- - // endpoint authorization - can(Action.SampleRead, SampleClass); - can(Action.SampleCreate, SampleClass); - can(Action.SampleUpdate, SampleClass); - can(Action.SampleAttachmentRead, SampleClass); - can(Action.SampleAttachmentCreate, SampleClass); - can(Action.SampleAttachmentUpdate, SampleClass); - can(Action.SampleAttachmentDelete, SampleClass); - can(Action.SampleDatasetRead, SampleClass); - - // ------------------------------------- - // data instance authorization can(Action.SampleReadAny, SampleClass); can(Action.SampleCreateAny, SampleClass); can(Action.SampleUpdateAny, SampleClass); @@ -1148,19 +1515,6 @@ export class CaslAbilityFactory { // users belonging to any of the group listed in SAMPLE_GROUPS // - // ------------------------------------- - // endpoint authorization - can(Action.SampleRead, SampleClass); - can(Action.SampleCreate, SampleClass); - can(Action.SampleUpdate, SampleClass); - can(Action.SampleAttachmentRead, SampleClass); - can(Action.SampleAttachmentCreate, SampleClass); - can(Action.SampleAttachmentUpdate, SampleClass); - can(Action.SampleAttachmentDelete, SampleClass); - can(Action.SampleDatasetRead, SampleClass); - - // ------------------------------------- - // data instance authorization can(Action.SampleCreateAny, SampleClass); can(Action.SampleUpdateOwner, SampleClass, { ownerGroup: { $in: user.currentGroups }, @@ -1201,19 +1555,6 @@ export class CaslAbilityFactory { // users belonging to any of the group listed in SAMPLE_GROUPS // - // ------------------------------------- - // endpoint authorization - can(Action.SampleRead, SampleClass); - can(Action.SampleCreate, SampleClass); - can(Action.SampleUpdate, SampleClass); - can(Action.SampleAttachmentRead, SampleClass); - can(Action.SampleAttachmentCreate, SampleClass); - can(Action.SampleAttachmentUpdate, SampleClass); - can(Action.SampleAttachmentDelete, SampleClass); - can(Action.SampleDatasetRead, SampleClass); - - // ------------------------------------- - // data instance authorization can(Action.SampleCreateOwner, SampleClass, { ownerGroup: { $in: user.currentGroups }, }); @@ -1253,24 +1594,6 @@ export class CaslAbilityFactory { // users with no elevated permissions // ------------------------------------- - // ------------------------------------- - // endpoint authorization - can(Action.SampleRead, SampleClass); - cannot(Action.SampleCreate, SampleClass); - cannot(Action.SampleUpdate, SampleClass); - can(Action.SampleAttachmentRead, SampleClass); - cannot(Action.SampleAttachmentCreate, SampleClass); - cannot(Action.SampleAttachmentUpdate, SampleClass); - if ( - !user.currentGroups.some((g) => - configuration().deleteGroups.includes(g), - ) - ) { - cannot(Action.SampleAttachmentDelete, SampleClass); - } - - // ------------------------------------- - // data instance authorization can(Action.SampleReadManyAccess, SampleClass); can(Action.SampleReadOneAccess, SampleClass, { ownerGroup: { $in: user.currentGroups }, @@ -1293,121 +1616,6 @@ export class CaslAbilityFactory { } } - // ************************************ - // INSTRUMENT AUTHORIZATION - // ************************************ - - if (!user) { - cannot(Action.InstrumentRead, Instrument); - cannot(Action.InstrumentCreate, Instrument); - cannot(Action.InstrumentUpdate, Instrument); - cannot(Action.InstrumentDelete, Instrument); - } else { - if ( - user.currentGroups.some((g) => configuration().deleteGroups.includes(g)) - ) { - /* - * user that belongs to any of the group listed in DELETE_GROUPS - */ - - // ------------------------------------- - // endpoint authorization - can(Action.InstrumentDelete, Instrument); - } else { - cannot(Action.InstrumentDelete, Instrument); - } - - if ( - user.currentGroups.some((g) => configuration().adminGroups.includes(g)) - ) { - /** - * authenticated users belonging to any of the group listed in ADMIN_GROUPS - */ - - // ------------------------------------- - // endpoint authorization - can(Action.InstrumentRead, Instrument); - can(Action.InstrumentCreate, Instrument); - can(Action.InstrumentUpdate, Instrument); - } else { - can(Action.InstrumentRead, Instrument); - cannot(Action.InstrumentCreate, Instrument); - cannot(Action.InstrumentUpdate, Instrument); - } - } - - // Instrument permissions - //can(Action.Read, Instrument); - //if (user.currentGroups.some((g) => adminGroups.includes(g))) { - // can(Action.Manage, Instrument); - //} - - //can(Action.Manage, JobClass); - - can(Action.Read, Logbook); - - can(Action.Read, PublishedData); - can(Action.Update, PublishedData); - can(Action.Create, PublishedData); - - // can(Action.Manage, Attachment, { - // ownerGroup: { $in: user.currentGroups }, - // }); - // can(Action.Manage, Datablock, { - // ownerGroup: { $in: user.currentGroups }, - // }); - // can(Action.Manage, OrigDatablock, { - // ownerGroup: { $in: user.currentGroups }, - // }); - - // if (user.currentGroups.includes(Role.Admin)) { - // can(Action.Manage, "all"); - // } - // if (user.currentGroups.includes(Role.ArchiveManager)) { - // //cannot(Action.Create, DatasetClass); - // //cannot(Action.Update, DatasetClass); - // //can(Action.Delete, DatasetClass); - // cannot(Action.Manage, OrigDatablock); - // cannot(Action.Create, OrigDatablock); - // cannot(Action.Update, OrigDatablock); - // can(Action.Delete, OrigDatablock); - // cannot(Action.Manage, Datablock); - // cannot(Action.Create, Datablock); - // cannot(Action.Update, Datablock); - // can(Action.Delete, Datablock); - // can(Action.Delete, PublishedData); - // //-------------------------------- - // // instrument - // cannot(Action.InstrumentRead, Instrument); - // cannot(Action.InstrumentCreate, Instrument); - // cannot(Action.InstrumentUpdate, Instrument); - // can(Action.InstrumentDelete, Instrument); - // } - //if (user.currentGroups.includes(Role.GlobalAccess)) { - // can(Action.Read, "all"); - //} - // if (user.currentGroups.includes(Role.Ingestor)) { - // can(Action.Create, Attachment); - - // //cannot(Action.Delete, DatasetClass); - // //can(Action.Create, DatasetClass); - // //can(Action.Update, DatasetClass); - - // can(Action.Create, Instrument); - // can(Action.Update, Instrument); - // } - // if (user.currentGroups.includes(Role.ProposalIngestor)) { - // cannot(Action.Delete, ProposalClass); - // can(Action.Create, ProposalClass); - // can(Action.Update, ProposalClass); - // can(Action.Read, ProposalClass); - // can(Action.ListAll, ProposalClass); - // } - - //can(Action.Create, UserSettings, { userId: user._id }); - //can(Action.Read, UserSettings, { userId: user._id }); - //can(Action.Update, UserSettings, { userId: user._id }); - return build({ detectSubjectType: (item) => item.constructor as ExtractSubjectType, diff --git a/src/casl/decorators/check-policies.decorator.ts b/src/casl/decorators/check-policies.decorator.ts index 6ac4f3550..ec4af992a 100644 --- a/src/casl/decorators/check-policies.decorator.ts +++ b/src/casl/decorators/check-policies.decorator.ts @@ -3,5 +3,5 @@ import { PolicyHandler } from "../interfaces/policy-handler.interface"; export const CHECK_POLICIES_KEY = "check_policy"; -export const CheckPolicies = (...handlers: PolicyHandler[]) => - SetMetadata(CHECK_POLICIES_KEY, handlers); +export const CheckPolicies = (endpoint: string, ...handlers: PolicyHandler[]) => + SetMetadata(CHECK_POLICIES_KEY, { endpoint, handlers }); diff --git a/src/casl/guards/policies.guard.ts b/src/casl/guards/policies.guard.ts index 17ff148ce..0aa563ac0 100644 --- a/src/casl/guards/policies.guard.ts +++ b/src/casl/guards/policies.guard.ts @@ -12,22 +12,27 @@ export class PoliciesGuard implements CanActivate { ) {} async canActivate(context: ExecutionContext): Promise { - const policyHandlers = - this.reflector.get( - CHECK_POLICIES_KEY, - context.getHandler(), - ) || []; + const policyData = this.reflector.get<{ + endpoint: string; + handlers: PolicyHandler[]; + }>(CHECK_POLICIES_KEY, context.getHandler()); + if (!policyData) { + return false; + } + + const policyHandlers = policyData["handlers"]; + const endpoint = policyData["endpoint"]; const req = context.switchToHttp().getRequest(); const user = req.user; - const ability = this.caslAbilityFactory.createForUser(user); + + const ability = this.caslAbilityFactory.endpointAccess(endpoint, user); return policyHandlers.every((handler) => this.execPolicyHandler(handler, ability), ); } private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) { - //console.log('PoliciesGuard:execPolicyHandler ', handler, ability) if (typeof handler === "function") { const res = handler(ability); //console.log("PoliciesGuard:execPolicyHandler ", res); diff --git a/src/common/scientific-relation.enum.ts b/src/common/scientific-relation.enum.ts index 1eb550bf0..e9acf37b9 100644 --- a/src/common/scientific-relation.enum.ts +++ b/src/common/scientific-relation.enum.ts @@ -3,4 +3,5 @@ export enum ScientificRelation { EQUAL_TO_NUMERIC = "EQUAL_TO_NUMERIC", GREATER_THAN = "GREATER_THAN", LESS_THAN = "LESS_THAN", + CONTAINS_STRING = "CONTAINS_STRING", } diff --git a/src/common/utils.ts b/src/common/utils.ts index 7e16add43..f30ec2734 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -86,11 +86,26 @@ export const convertToRequestedUnit = ( }; }; +const buildCondition = ( + key: string, + value: string | number, + operator: string, +): Record => { + const conditions: Record = { $or: [] }; + conditions["$or"] = ["", ".v", ".value"].map((suffix) => { + return { + [`${key}${suffix}`]: { [`${operator}`]: value }, + }; + }); + return conditions; +}; + export const mapScientificQuery = ( key: string, scientific: IScientificFilter[] = [], ): Record => { const scientificFilterQuery: Record = {}; + const scientificFilterQueryOr: Record[] = []; const keyToFieldMapping: Record = { scientific: "scientificMetadata", @@ -112,7 +127,9 @@ export const mapScientificQuery = ( switch (relation) { case ScientificRelation.EQUAL_TO_STRING: { - scientificFilterQuery[`${matchKeyGeneric}.value`] = { $eq: rhs }; + scientificFilterQueryOr.push( + buildCondition(matchKeyGeneric, rhs, "$eq"), + ); break; } case ScientificRelation.EQUAL_TO_NUMERIC: { @@ -131,7 +148,9 @@ export const mapScientificQuery = ( scientificFilterQuery[matchKeyMeasurement] = { $gt: valueSI }; scientificFilterQuery[matchUnit] = { $eq: unitSI }; } else { - scientificFilterQuery[`${matchKeyGeneric}.value`] = { $gt: rhs }; + scientificFilterQueryOr.push( + buildCondition(matchKeyGeneric, rhs, "$gt"), + ); } break; } @@ -141,12 +160,26 @@ export const mapScientificQuery = ( scientificFilterQuery[matchKeyMeasurement] = { $lt: valueSI }; scientificFilterQuery[matchUnit] = { $eq: unitSI }; } else { - scientificFilterQuery[`${matchKeyGeneric}.value`] = { $lt: rhs }; + scientificFilterQueryOr.push( + buildCondition(matchKeyGeneric, rhs, "$lt"), + ); } break; } + case ScientificRelation.CONTAINS_STRING: { + scientificFilterQueryOr.push( + buildCondition(matchKeyGeneric, rhs, `/${rhs}/`), + ); + break; + } } }); + if (scientificFilterQueryOr.length == 1) { + scientificFilterQuery["$or"] = scientificFilterQueryOr[0]["$or"]; + } else if (scientificFilterQueryOr.length > 1) { + scientificFilterQuery["$and"] = scientificFilterQueryOr; + } + return scientificFilterQuery; }; @@ -892,6 +925,30 @@ export const samplesFullQueryDescriptionFields = }\n \ '; +export const filterUserIdentityExample = + '{ "profile.email": "this_email@your.site" }'; + +export const filterUserIdentityDescription = + '
\n \
+  this_email@some.site\n \
+or \n \
+  {\n \
+    "email": "this_email@some.site"\n \
+  }\n \
+or \n \
+  {\n \
+    "profile.email": "this_email@some.site"\n \
+  }\n \
+or \n \
+  {\n \
+    "where?": {\n \
+      "profile.email": "this_email@some.site"\n \
+    }\n \
+  }\n \
+This last version is deprecated and will be discontinued as soon as the FE is updated.\n \
+It has been maintanined for backward compatibility.\n \
+
'; + export const parseBoolean = (v: unknown): boolean => { switch (v) { case true: diff --git a/src/config/configuration.ts b/src/config/configuration.ts index e73d42a72..94a190d89 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -58,6 +58,10 @@ const configuration = () => { }); const config = { + versions: { + api: "3", + }, + swaggerPath: process.env.SWAGGER_PATH || "explorer", loggerConfigs: jsonConfigMap.loggers || [defaultLogger], adminGroups: adminGroups.split(",").map((v) => v.trim()) ?? [], deleteGroups: deleteGroups.split(",").map((v) => v.trim()) ?? [], diff --git a/src/config/frontend.config.json b/src/config/frontend.config.json index 8c5833abf..213c69a5e 100644 --- a/src/config/frontend.config.json +++ b/src/config/frontend.config.json @@ -2,21 +2,21 @@ "accessTokenPrefix": "Bearer ", "addDatasetEnabled": false, "archiveWorkflowEnabled": false, - "datasetJsonScientificMetadata": true, "datasetReduceEnabled": true, + "datasetJsonScientificMetadata": true, "editDatasetSampleEnabled": true, "editMetadataEnabled": true, "editPublishedData": false, "addSampleEnabled": false, "externalAuthEndpoint": "/api/v3/auth/msad", - "facility": "ESS", - "loginFacilityLabel": "ESS", + "facility": "SciCat Vanilla", + "siteIcon": "site-header-logo.png", + "loginFacilityLabel": "SciCat Vanilla", "loginLdapLabel": "Ldap", "loginLocalLabel": "Local", "loginFacilityEnabled": true, "loginLdapEnabled": true, "loginLocalEnabled": true, - "facilityLoginLabel": "ESS", "localLoginLabel": "Local", "fileColorEnabled": true, "fileDownloadEnabled": true, @@ -24,95 +24,15 @@ "ingestManual": null, "jobsEnabled": true, "jsonMetadataEnabled": true, - "jupyterHubUrl": "https://jupyterhub.esss.lu.se/", + "jupyterHubUrl": "", "landingPage": "doi.ess.eu/detail/", - "lbBaseURL": "", - "localColumns": [ - { - "name": "select", - "order": 0, - "type": "standard", - "enabled": true - }, - { - "name": "pid", - "order": 1, - "type": "standard", - "enabled": true - }, - { - "name": "datasetName", - "order": 2, - "type": "standard", - "enabled": true - }, - { - "name": "runNumber", - "order": 3, - "type": "standard", - "enabled": true - }, - { - "name": "sourceFolder", - "order": 4, - "type": "standard", - "enabled": true - }, - { - "name": "size", - "order": 5, - "type": "standard", - "enabled": true - }, - { - "name": "creationTime", - "order": 6, - "type": "standard", - "enabled": true - }, - { - "name": "type", - "order": 7, - "type": "standard", - "enabled": true - }, - { - "name": "image", - "order": 8, - "type": "standard", - "enabled": true - }, - { - "name": "metadata", - "order": 9, - "type": "standard", - "enabled": false - }, - { - "name": "proposalId", - "order": 10, - "type": "standard", - "enabled": true - }, - { - "name": "ownerGroup", - "order": 11, - "type": "standard", - "enabled": false - }, - { - "name": "dataStatus", - "order": 12, - "type": "standard", - "enabled": false - } - ], + "lbBaseURL": "http://localhost:3000", "logbookEnabled": true, "loginFormEnabled": true, "maxDirectDownloadSize": 5000000000, "metadataPreviewEnabled": true, "metadataStructure": "", - "multipleDownloadAction": "https://scicatfileserver.esss.dk/zip", + "multipleDownloadAction": "http:/127.0.0.1:3012/zip", "multipleDownloadEnabled": true, "oAuth2Endpoints": [ { @@ -134,5 +54,154 @@ "tableSciDataEnabled": true, "datasetDetailsShowMissingProposalId": false, "notificationInterceptorEnabled": true, - "metadataEditingUnitListDisabled": true + "metadataEditingUnitListDisabled": true, + "datafilesActionsEnabled": true, + "datafilesActions": [ + { + "id": "eed8efec-4354-11ef-a3b5-d75573a5d37f", + "order": 4, + "label": "Download All", + "files": "all", + "mat_icon": "download", + "url": "https://www.scicat.info/download/all", + "target": "_blank", + "enabled": "#SizeLimit", + "authorization": ["#datasetAccess", "#datasetPublic"] + }, + { + "id": "3072fafc-4363-11ef-b9f9-ebf568222d26", + "order": 3, + "label": "Download Selected", + "files": "selected", + "mat_icon": "download", + "url": "https://www.scicat.info/download/selected", + "target": "_blank", + "enabled": "#Selected && #SizeLimit", + "authorization": ["#datasetAccess", "#datasetPublic"] + }, + { + "id": "4f974f0e-4364-11ef-9c63-03d19f813f4e", + "order": 2, + "label": "Notebook All", + "files": "all", + "icon": "/assets/icons/jupyter_logo.png", + "url": "https://www.scicat.info/notebook/all", + "target": "_blank", + "authorization": ["#datasetAccess", "#datasetPublic"] + }, + { + "id": "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", + "order": 1, + "label": "Notebook Selected", + "files": "selected", + "icon": "/assets/icons/jupyter_logo.png", + "url": "https://www.scicat.info/notebook/all", + "target": "_blank", + "enabled": "#Selected", + "authorization": ["#datasetAccess", "#datasetPublic"] + } + ], + "labelMaps": { + "filters": { + "LocationFilter": "Location", + "PidFilter": "Pid", + "GroupFilter": "Group", + "TypeFilter": "Type", + "KeywordFilter": "Keyword", + "DateRangeFilter": "Start Date - End Date", + "TextFilter": "Text" + } + }, + "defaultDatasetsListSettings": { + "columns": [ + { + "name": "select", + "order": 0, + "type": "standard", + "enabled": true + }, + { + "name": "pid", + "order": 1, + "type": "standard", + "enabled": true + }, + { + "name": "datasetName", + "order": 2, + "type": "standard", + "enabled": true + }, + { + "name": "runNumber", + "order": 3, + "type": "standard", + "enabled": true + }, + { + "name": "sourceFolder", + "order": 4, + "type": "standard", + "enabled": true + }, + { + "name": "size", + "order": 5, + "type": "standard", + "enabled": true + }, + { + "name": "creationTime", + "order": 6, + "type": "standard", + "enabled": true + }, + { + "name": "type", + "order": 7, + "type": "standard", + "enabled": true + }, + { + "name": "image", + "order": 8, + "type": "standard", + "enabled": true + }, + { + "name": "metadata", + "order": 9, + "type": "standard", + "enabled": false + }, + { + "name": "proposalId", + "order": 10, + "type": "standard", + "enabled": true + }, + { + "name": "ownerGroup", + "order": 11, + "type": "standard", + "enabled": false + }, + { + "name": "dataStatus", + "order": 12, + "type": "standard", + "enabled": false + } + ], + "filters": [ + { "LocationFilter": true }, + { "PidFilter": true }, + { "GroupFilter": true }, + { "TypeFilter": true }, + { "KeywordFilter": true }, + { "DateRangeFilter": true }, + { "TextFilter": true } + ], + "conditions": [] + } } diff --git a/src/datasets/datasets.controller.ts b/src/datasets/datasets.controller.ts index 6655d3b5f..2f9491244 100644 --- a/src/datasets/datasets.controller.ts +++ b/src/datasets/datasets.controller.ts @@ -36,10 +36,10 @@ import { } from "@nestjs/swagger"; import { Request } from "express"; import { DatasetsService } from "./datasets.service"; -import { PartialUpdateDatasetDto } from "./dto/update-dataset.dto"; +import { PartialUpdateDatasetObsoleteDto } from "./dto/update-dataset-obsolete.dto"; import { DatasetClass, DatasetDocument } from "./schemas/dataset.schema"; -import { CreateRawDatasetDto } from "./dto/create-raw-dataset.dto"; -import { CreateDerivedDatasetDto } from "./dto/create-derived-dataset.dto"; +import { CreateRawDatasetObsoleteDto } from "./dto/create-raw-dataset-obsolete.dto"; +import { CreateDerivedDatasetObsoleteDto } from "./dto/create-derived-dataset-obsolete.dto"; import { PoliciesGuard } from "src/casl/guards/policies.guard"; import { CheckPolicies } from "src/casl/decorators/check-policies.decorator"; import { AppAbility, CaslAbilityFactory } from "src/casl/casl-ability.factory"; @@ -74,13 +74,13 @@ import { validate, ValidationError, ValidatorOptions } from "class-validator"; import { HistoryInterceptor } from "src/common/interceptors/history.interceptor"; import { CreateDatasetOrigDatablockDto } from "src/origdatablocks/dto/create-dataset-origdatablock"; import { - PartialUpdateRawDatasetDto, - UpdateRawDatasetDto, -} from "./dto/update-raw-dataset.dto"; + PartialUpdateRawDatasetObsoleteDto, + UpdateRawDatasetObsoleteDto, +} from "./dto/update-raw-dataset-obsolete.dto"; import { - PartialUpdateDerivedDatasetDto, - UpdateDerivedDatasetDto, -} from "./dto/update-derived-dataset.dto"; + PartialUpdateDerivedDatasetObsoleteDto, + UpdateDerivedDatasetObsoleteDto, +} from "./dto/update-derived-dataset-obsolete.dto"; import { CreateDatasetDatablockDto } from "src/datablocks/dto/create-dataset-datablock"; import { filterDescription, @@ -98,12 +98,18 @@ import { JWTUser } from "src/auth/interfaces/jwt-user.interface"; import { LogbooksService } from "src/logbooks/logbooks.service"; import configuration from "src/config/configuration"; import { DatasetType } from "./dataset-type.enum"; +import { OutputDatasetObsoleteDto } from "./dto/output-dataset-obsolete.dto"; +import { CreateDatasetDto } from "./dto/create-dataset.dto"; +import { + PartialUpdateDatasetDto, + UpdateDatasetDto, +} from "./dto/update-dataset.dto"; @ApiBearerAuth() @ApiExtraModels( CreateAttachmentDto, - CreateDerivedDatasetDto, - CreateRawDatasetDto, + CreateDerivedDatasetObsoleteDto, + CreateRawDatasetObsoleteDto, HistoryClass, TechniqueClass, RelationshipClass, @@ -160,7 +166,7 @@ export class DatasetsController { ): IFilters { const user: JWTUser = request.user as JWTUser; - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); const canViewAny = ability.can(Action.DatasetReadAny, DatasetClass); const canViewOwner = ability.can(Action.DatasetReadManyOwner, DatasetClass); const canViewAccess = ability.can( @@ -205,7 +211,7 @@ export class DatasetsController { const datasetInstance = await this.generateDatasetInstanceForPermissions(dataset); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); let canDoAction = false; @@ -282,7 +288,7 @@ export class DatasetsController { return dataset; } - async checkPermissionsForDataset(request: Request, id: string) { + async checkPermissionsForDatasetObsolete(request: Request, id: string) { const dataset = await this.datasetsService.findOne({ where: { pid: id } }); const user: JWTUser = request.user as JWTUser; @@ -290,7 +296,7 @@ export class DatasetsController { const datasetInstance = await this.generateDatasetInstanceForPermissions(dataset); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); const canView = ability.can(Action.DatasetReadAny, DatasetClass) || ability.can(Action.DatasetReadOneOwner, datasetInstance) || @@ -331,7 +337,10 @@ export class DatasetsController { } async generateDatasetInstanceForPermissions( - dataset: CreateRawDatasetDto | CreateDerivedDatasetDto | DatasetClass, + dataset: + | CreateRawDatasetObsoleteDto + | CreateDerivedDatasetObsoleteDto + | DatasetClass, ): Promise { const datasetInstance = new DatasetClass(); datasetInstance._id = ""; @@ -344,9 +353,9 @@ export class DatasetsController { return datasetInstance; } - async checkPermissionsForDatasetCreate( + async checkPermissionsForObsoleteDatasetCreate( request: Request, - dataset: CreateRawDatasetDto | CreateDerivedDatasetDto, + dataset: CreateRawDatasetObsoleteDto | CreateDerivedDatasetObsoleteDto, ) { const user: JWTUser = request.user as JWTUser; @@ -355,7 +364,7 @@ export class DatasetsController { const datasetInstance = await this.generateDatasetInstanceForPermissions(dataset); // instantiate the casl matrix for the user - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); // check if he/she can create this dataset const canCreate = ability.can(Action.DatasetCreateAny, DatasetClass) || @@ -387,9 +396,112 @@ export class DatasetsController { return dataset; } - // POST /datasets + convertObsoleteToCurrentSchema( + inputObsoleteDataset: + | CreateRawDatasetObsoleteDto + | CreateDerivedDatasetObsoleteDto + | UpdateRawDatasetObsoleteDto + | UpdateDerivedDatasetObsoleteDto + | PartialUpdateRawDatasetObsoleteDto + | PartialUpdateDerivedDatasetObsoleteDto, + ): CreateDatasetDto | UpdateDatasetDto | PartialUpdateDatasetDto { + const propertiesModifier: Record = { + version: "v3", + }; + + if ("proposalId" in inputObsoleteDataset) { + propertiesModifier.proposalIds = [ + (inputObsoleteDataset as CreateRawDatasetObsoleteDto).proposalId, + ]; + } + if ( + inputObsoleteDataset instanceof CreateRawDatasetObsoleteDto || + inputObsoleteDataset instanceof UpdateRawDatasetObsoleteDto || + inputObsoleteDataset instanceof PartialUpdateRawDatasetObsoleteDto + ) { + if ("sampleId" in inputObsoleteDataset) { + propertiesModifier.sampleIds = [ + (inputObsoleteDataset as CreateRawDatasetObsoleteDto).sampleId, + ]; + } + if ("instrumentId" in inputObsoleteDataset) { + propertiesModifier.instrumentIds = [ + (inputObsoleteDataset as CreateRawDatasetObsoleteDto).instrumentId, + ]; + } + } else { + if ("investigator" in inputObsoleteDataset) { + propertiesModifier.principalInvestigator = ( + inputObsoleteDataset as CreateDerivedDatasetObsoleteDto + ).investigator; + } + } + + let outputDataset: + | CreateDatasetDto + | UpdateDatasetDto + | PartialUpdateDatasetDto = {}; + if ( + inputObsoleteDataset instanceof CreateRawDatasetObsoleteDto || + inputObsoleteDataset instanceof CreateDerivedDatasetObsoleteDto + ) { + outputDataset = { + ...(inputObsoleteDataset as CreateDatasetDto), + ...propertiesModifier, + } as CreateDatasetDto; + } else if ( + inputObsoleteDataset instanceof UpdateRawDatasetObsoleteDto || + inputObsoleteDataset instanceof UpdateDerivedDatasetObsoleteDto + ) { + outputDataset = { + ...(inputObsoleteDataset as UpdateDatasetDto), + ...propertiesModifier, + } as UpdateDatasetDto; + } else if ( + inputObsoleteDataset instanceof PartialUpdateRawDatasetObsoleteDto || + inputObsoleteDataset instanceof PartialUpdateDerivedDatasetObsoleteDto + ) { + outputDataset = { + ...(inputObsoleteDataset as PartialUpdateDatasetDto), + ...propertiesModifier, + } as PartialUpdateDatasetDto; + } + + return outputDataset; + } + + convertCurrentToObsoleteSchema( + inputDataset: DatasetClass | null, + ): OutputDatasetObsoleteDto { + const propertiesModifier: Record = {}; + if (inputDataset) { + if ("proposalIds" in inputDataset) { + propertiesModifier.proposalId = inputDataset.proposalIds![0]; + } + if ("sampleIds" in inputDataset) { + propertiesModifier.sampleId = inputDataset.sampleIds![0]; + } + if ("instrumentIds" in inputDataset) { + propertiesModifier.instrumentId = inputDataset.instrumentIds![0]; + } + if (inputDataset.type == "derived") { + if ("investigator" in inputDataset) { + propertiesModifier.investigator = inputDataset.principalInvestigator; + } + } + } + + const outputDataset: OutputDatasetObsoleteDto = { + ...(inputDataset as DatasetDocument).toObject(), + ...propertiesModifier, + }; + + return outputDataset; + } + + // POST https://scicat.ess.eu/api/v3/datasets @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetCreate, DatasetClass), ) @UseInterceptors( @@ -403,14 +515,14 @@ export class DatasetsController { description: "It creates a new dataset and returns it completed with systems fields.", }) - @ApiExtraModels(CreateRawDatasetDto, CreateDerivedDatasetDto) + @ApiExtraModels(CreateRawDatasetObsoleteDto, CreateDerivedDatasetObsoleteDto) @ApiBody({ description: "Input fields for the dataset to be created", required: true, schema: { oneOf: [ - { $ref: getSchemaPath(CreateRawDatasetDto) }, - { $ref: getSchemaPath(CreateDerivedDatasetDto) }, + { $ref: getSchemaPath(CreateRawDatasetObsoleteDto) }, + { $ref: getSchemaPath(CreateDerivedDatasetObsoleteDto) }, ], }, }) @@ -421,25 +533,34 @@ export class DatasetsController { }) async create( @Req() request: Request, - @Body() createDatasetDto: CreateRawDatasetDto | CreateDerivedDatasetDto, - ): Promise { + @Body() + createDatasetObsoleteDto: + | CreateRawDatasetObsoleteDto + | CreateDerivedDatasetObsoleteDto, + ): Promise { // validate dataset - await this.validateDataset( - createDatasetDto, - createDatasetDto.type === "raw" - ? CreateRawDatasetDto - : CreateDerivedDatasetDto, - ); - - const datasetDTO = await this.checkPermissionsForDatasetCreate( - request, - createDatasetDto, - ); + const validatedDatasetObsoleteDto = (await this.validateDatasetObsolete( + createDatasetObsoleteDto, + createDatasetObsoleteDto.type === "raw" + ? CreateRawDatasetObsoleteDto + : CreateDerivedDatasetObsoleteDto, + )) as CreateRawDatasetObsoleteDto | CreateDerivedDatasetObsoleteDto; + + const obsoleteDatasetDto = + await this.checkPermissionsForObsoleteDatasetCreate( + request, + validatedDatasetObsoleteDto, + ); try { - const createdDataset = await this.datasetsService.create(datasetDTO); - - return createdDataset; + const datasetDto = this.convertObsoleteToCurrentSchema( + obsoleteDatasetDto, + ) as CreateDatasetDto; + const createdDataset = await this.datasetsService.create(datasetDto); + const outputObsoleteDatasetDto = + this.convertCurrentToObsoleteSchema(createdDataset); + + return outputObsoleteDatasetDto; } catch (error) { if ((error as MongoError).code === 11000) { throw new ConflictException( @@ -451,21 +572,21 @@ export class DatasetsController { } } - async validateDataset( + async validateDatasetObsolete( inputDatasetDto: - | CreateRawDatasetDto - | CreateDerivedDatasetDto - | PartialUpdateRawDatasetDto - | PartialUpdateDerivedDatasetDto - | UpdateRawDatasetDto - | UpdateDerivedDatasetDto, + | CreateRawDatasetObsoleteDto + | CreateDerivedDatasetObsoleteDto + | PartialUpdateRawDatasetObsoleteDto + | PartialUpdateDerivedDatasetObsoleteDto + | UpdateRawDatasetObsoleteDto + | UpdateDerivedDatasetObsoleteDto, dto: ClassConstructor< - | CreateRawDatasetDto - | CreateDerivedDatasetDto - | PartialUpdateRawDatasetDto - | PartialUpdateDerivedDatasetDto - | UpdateRawDatasetDto - | UpdateDerivedDatasetDto + | CreateRawDatasetObsoleteDto + | CreateDerivedDatasetObsoleteDto + | PartialUpdateRawDatasetObsoleteDto + | PartialUpdateDerivedDatasetObsoleteDto + | UpdateRawDatasetObsoleteDto + | UpdateDerivedDatasetObsoleteDto >, ) { const validateOptions: ValidatorOptions = { @@ -478,11 +599,18 @@ export class DatasetsController { }, }; + // first we convert input object to the correct class + const outputDatasetDto = plainToInstance(dto, inputDatasetDto); + if ( - inputDatasetDto instanceof - (CreateRawDatasetDto || CreateDerivedDatasetDto) + outputDatasetDto instanceof + (CreateRawDatasetObsoleteDto || CreateDerivedDatasetObsoleteDto) ) { - if (!(inputDatasetDto.type in DatasetType)) { + if ( + !(Object.values(DatasetType) as string[]).includes( + outputDatasetDto.type, + ) + ) { throw new HttpException( { status: HttpStatus.BAD_REQUEST, @@ -493,7 +621,6 @@ export class DatasetsController { } } - const outputDatasetDto = plainToInstance(dto, inputDatasetDto); const errors = await validate(outputDatasetDto, validateOptions); if (errors.length > 0) { @@ -510,7 +637,7 @@ export class DatasetsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetCreate, DatasetClass), ) @UseInterceptors( @@ -525,14 +652,14 @@ export class DatasetsController { description: "It validates the dataset provided as input, and returns true if the information is a valid dataset", }) - @ApiExtraModels(CreateRawDatasetDto, CreateDerivedDatasetDto) + @ApiExtraModels(CreateRawDatasetObsoleteDto, CreateDerivedDatasetObsoleteDto) @ApiBody({ description: "Input fields for the dataset that needs to be validated", required: true, schema: { oneOf: [ - { $ref: getSchemaPath(CreateRawDatasetDto) }, - { $ref: getSchemaPath(CreateDerivedDatasetDto) }, + { $ref: getSchemaPath(CreateRawDatasetObsoleteDto) }, + { $ref: getSchemaPath(CreateDerivedDatasetObsoleteDto) }, ], }, }) @@ -544,19 +671,25 @@ export class DatasetsController { }) async isValid( @Req() request: Request, - @Body() createDatasetDto: CreateRawDatasetDto | CreateDerivedDatasetDto, + @Body() + createDatasetObsoleteDto: + | CreateRawDatasetObsoleteDto + | CreateDerivedDatasetObsoleteDto, ): Promise<{ valid: boolean }> { - await this.checkPermissionsForDatasetCreate(request, createDatasetDto); + await this.checkPermissionsForObsoleteDatasetCreate( + request, + createDatasetObsoleteDto, + ); const dtoTestRawCorrect = plainToInstance( - CreateRawDatasetDto, - createDatasetDto, + CreateRawDatasetObsoleteDto, + createDatasetObsoleteDto, ); const errorsTestRawCorrect = await validate(dtoTestRawCorrect); const dtoTestDerivedCorrect = plainToInstance( - CreateDerivedDatasetDto, - createDatasetDto, + CreateDerivedDatasetObsoleteDto, + createDatasetObsoleteDto, ); const errorsTestDerivedCorrect = await validate(dtoTestDerivedCorrect); @@ -568,7 +701,7 @@ export class DatasetsController { // GET /datasets @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetRead, DatasetClass), ) @UseInterceptors(MainDatasetsPublicInterceptor) @@ -597,7 +730,7 @@ export class DatasetsController { @Req() request: Request, @Headers() headers: Record, @Query(new FilterPipe()) queryFilter: { filter?: string }, - ): Promise { + ): Promise { const mergedFilters = replaceLikeOperator( this.updateMergedFiltersForList( request, @@ -607,10 +740,14 @@ export class DatasetsController { // this should be implemented at database level const datasets = await this.datasetsService.findAll(mergedFilters); + let outputDatasets: OutputDatasetObsoleteDto[] = []; if (datasets && datasets.length > 0) { const includeFilters = mergedFilters.include ?? []; + outputDatasets = datasets.map((dataset) => + this.convertCurrentToObsoleteSchema(dataset), + ); await Promise.all( - datasets.map(async (dataset) => { + outputDatasets.map(async (dataset) => { if (includeFilters) { await Promise.all( includeFilters.map(async ({ relation }) => { @@ -645,12 +782,12 @@ export class DatasetsController { }), ); } - return datasets; + return outputDatasets; } // GET /datasets/fullquery @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetRead, DatasetClass), ) @UseInterceptors(SubDatasetsPublicInterceptor, FullQueryInterceptor) @@ -687,11 +824,11 @@ export class DatasetsController { async fullquery( @Req() request: Request, @Query() filters: { fields?: string; limits?: string }, - ): Promise { + ): Promise { const user: JWTUser = request.user as JWTUser; const fields: IDatasetFields = JSON.parse(filters.fields ?? "{}"); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); const canViewAny = ability.can(Action.DatasetReadAny, DatasetClass); if (!canViewAny && !fields.isPublished) { @@ -724,12 +861,14 @@ export class DatasetsController { limits: JSON.parse(filters.limits ?? "{}"), }; - return this.datasetsService.fullquery(parsedFilters); + const results = await this.datasetsService.fullquery(parsedFilters); + + return results as OutputDatasetObsoleteDto[]; } // GET /fullfacets @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetRead, DatasetClass), ) @UseInterceptors(SubDatasetsPublicInterceptor) @@ -769,7 +908,7 @@ export class DatasetsController { const user: JWTUser = request.user as JWTUser; const fields: IDatasetFields = JSON.parse(filters.fields ?? "{}"); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); const canViewAny = ability.can(Action.DatasetReadAny, DatasetClass); if (!canViewAny && !fields.isPublished) { @@ -811,7 +950,7 @@ export class DatasetsController { // GET /datasets/metadataKeys @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetRead, DatasetClass), ) @UseInterceptors(SubDatasetsPublicInterceptor) @@ -850,7 +989,7 @@ export class DatasetsController { const user: JWTUser = request.user as JWTUser; const fields: IDatasetFields = JSON.parse(filters.fields ?? "{}"); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); const canViewAny = ability.can(Action.DatasetReadAny, DatasetClass); if (!canViewAny && !fields.isPublished) { @@ -890,7 +1029,7 @@ export class DatasetsController { // GET /datasets/findOne @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetRead, DatasetClass), ) @Get("/findOne") @@ -917,7 +1056,7 @@ export class DatasetsController { @Req() request: Request, @Headers() headers: Record, @Query(new FilterPipe()) queryFilter: { filter?: string }, - ): Promise { + ): Promise { const mergedFilters = replaceLikeOperator( this.updateMergedFiltersForList( request, @@ -925,30 +1064,34 @@ export class DatasetsController { ) as Record, ) as IFilters; - const dataset = (await this.datasetsService.findOne( - mergedFilters, - )) as DatasetClass; + const databaseDataset = await this.datasetsService.findOne(mergedFilters); - if (dataset) { + const outputDataset = + await this.convertCurrentToObsoleteSchema(databaseDataset); + + if (outputDataset) { const includeFilters = mergedFilters.include ?? []; await Promise.all( includeFilters.map(async ({ relation }) => { switch (relation) { case "attachments": { - dataset.attachments = await this.attachmentsService.findAll({ - datasetId: dataset.pid, - }); + outputDataset.attachments = await this.attachmentsService.findAll( + { + datasetId: outputDataset.pid, + }, + ); break; } case "origdatablocks": { - dataset.origdatablocks = await this.origDatablocksService.findAll( - { where: { datasetId: dataset.pid } }, - ); + outputDataset.origdatablocks = + await this.origDatablocksService.findAll({ + where: { datasetId: outputDataset.pid }, + }); break; } case "datablocks": { - dataset.datablocks = await this.datablocksService.findAll({ - datasetId: dataset.pid, + outputDataset.datablocks = await this.datablocksService.findAll({ + datasetId: outputDataset.pid, }); break; } @@ -956,12 +1099,12 @@ export class DatasetsController { }), ); } - return dataset; + return outputDataset; } // GET /datasets/count @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetRead, DatasetClass), ) @Get("/count") @@ -1001,7 +1144,7 @@ export class DatasetsController { // GET /datasets/:id //@UseGuards(PoliciesGuard) @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetRead, DatasetClass), ) @Get("/:pid") @@ -1023,16 +1166,18 @@ export class DatasetsController { async findById( @Req() request: Request, @Param("pid") id: string, - ): Promise { - const dataset = await this.checkPermissionsForDataset(request, id); + ): Promise { + const dataset = this.convertCurrentToObsoleteSchema( + await this.checkPermissionsForDatasetObsolete(request, id), + ); - return dataset; + return dataset as OutputDatasetObsoleteDto; } // PATCH /datasets/:id // body: modified fields @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetUpdate, DatasetClass), ) @UseInterceptors( @@ -1052,15 +1197,18 @@ export class DatasetsController { description: "Id of the dataset to modify", type: String, }) - @ApiExtraModels(PartialUpdateRawDatasetDto, PartialUpdateDerivedDatasetDto) + @ApiExtraModels( + PartialUpdateRawDatasetObsoleteDto, + PartialUpdateDerivedDatasetObsoleteDto, + ) @ApiBody({ description: "Fields that needs to be updated in the dataset. Only the fields that needs to be updated have to be passed in.", required: true, schema: { oneOf: [ - { $ref: getSchemaPath(PartialUpdateRawDatasetDto) }, - { $ref: getSchemaPath(PartialUpdateDerivedDatasetDto) }, + { $ref: getSchemaPath(PartialUpdateRawDatasetObsoleteDto) }, + { $ref: getSchemaPath(PartialUpdateDerivedDatasetObsoleteDto) }, ], }, }) @@ -1075,9 +1223,9 @@ export class DatasetsController { @Param("pid") pid: string, @Body() updateDatasetDto: - | PartialUpdateRawDatasetDto - | PartialUpdateDerivedDatasetDto, - ): Promise { + | PartialUpdateRawDatasetObsoleteDto + | PartialUpdateDerivedDatasetObsoleteDto, + ): Promise { const foundDataset = await this.datasetsService.findOne({ where: { pid } }); if (!foundDataset) { @@ -1085,11 +1233,11 @@ export class DatasetsController { } // NOTE: Default validation pipe does not validate union types. So we need custom validation. - await this.validateDataset( + await this.validateDatasetObsolete( updateDatasetDto, foundDataset.type === "raw" - ? PartialUpdateRawDatasetDto - : PartialUpdateDerivedDatasetDto, + ? PartialUpdateRawDatasetObsoleteDto + : PartialUpdateDerivedDatasetObsoleteDto, ); // NOTE: We need DatasetClass instance because casl module can not recognize the type from dataset mongo database model. If other fields are needed can be added later. @@ -1098,7 +1246,7 @@ export class DatasetsController { // instantiate the casl matrix for the user const user: JWTUser = request.user as JWTUser; - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); // check if he/she can create this dataset const canUpdate = ability.can(Action.DatasetUpdateAny, DatasetClass) || @@ -1108,12 +1256,14 @@ export class DatasetsController { throw new ForbiddenException("Unauthorized to update this dataset"); } - return this.datasetsService.findByIdAndUpdate(pid, updateDatasetDto); + return this.convertCurrentToObsoleteSchema( + await this.datasetsService.findByIdAndUpdate(pid, updateDatasetDto), + ); } // PUT /datasets/:id @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetUpdate, DatasetClass), ) @UseInterceptors( @@ -1135,15 +1285,15 @@ export class DatasetsController { description: "Id of the dataset to modify", type: String, }) - @ApiExtraModels(UpdateRawDatasetDto, UpdateDerivedDatasetDto) + @ApiExtraModels(UpdateRawDatasetObsoleteDto, UpdateDerivedDatasetObsoleteDto) @ApiBody({ description: "Dataset object that needs to be updated. The whole dataset object with updated fields have to be passed in.", required: true, schema: { oneOf: [ - { $ref: getSchemaPath(UpdateRawDatasetDto) }, - { $ref: getSchemaPath(UpdateDerivedDatasetDto) }, + { $ref: getSchemaPath(UpdateRawDatasetObsoleteDto) }, + { $ref: getSchemaPath(UpdateDerivedDatasetObsoleteDto) }, ], }, }) @@ -1156,8 +1306,11 @@ export class DatasetsController { async findByIdAndReplace( @Req() request: Request, @Param("pid") pid: string, - @Body() updateDatasetDto: UpdateRawDatasetDto | UpdateDerivedDatasetDto, - ): Promise { + @Body() + updateDatasetObsoleteDto: + | UpdateRawDatasetObsoleteDto + | UpdateDerivedDatasetObsoleteDto, + ): Promise { const foundDataset = await this.datasetsService.findOne({ where: { pid } }); if (!foundDataset) { @@ -1165,11 +1318,11 @@ export class DatasetsController { } // NOTE: Default validation pipe does not validate union types. So we need custom validation. - const outputDto = await this.validateDataset( - updateDatasetDto, + const updateValidatedDto = await this.validateDatasetObsolete( + updateDatasetObsoleteDto, foundDataset.type === "raw" - ? UpdateRawDatasetDto - : UpdateDerivedDatasetDto, + ? UpdateRawDatasetObsoleteDto + : UpdateDerivedDatasetObsoleteDto, ); const datasetInstance = @@ -1177,7 +1330,7 @@ export class DatasetsController { // instantiate the casl matrix for the user const user: JWTUser = request.user as JWTUser; - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); // check if he/she can create this dataset const canUpdate = ability.can(Action.DatasetUpdateAny, DatasetClass) || @@ -1187,15 +1340,20 @@ export class DatasetsController { throw new ForbiddenException("Unauthorized to update this dataset"); } - return this.datasetsService.findByIdAndReplace( + const updateDatasetDto = + await this.convertObsoleteToCurrentSchema(updateValidatedDto); + + const outputDatasetDto = await this.datasetsService.findByIdAndReplace( pid, - outputDto as UpdateRawDatasetDto | UpdateDerivedDatasetDto, + updateDatasetDto as UpdateDatasetDto, ); + + return await this.convertCurrentToObsoleteSchema(outputDatasetDto); } // DELETE /datasets/:id @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetDelete, DatasetClass), ) @Delete("/:pid") @@ -1227,7 +1385,7 @@ export class DatasetsController { // instantiate the casl matrix for the user const user: JWTUser = request.user as JWTUser; - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); // check if he/she can create this dataset const canUpdate = ability.can(Action.DatasetDeleteAny, DatasetClass) || @@ -1241,7 +1399,7 @@ export class DatasetsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetUpdate, DatasetClass), ) @Post("/:pid/appendToArrayField") @@ -1278,7 +1436,7 @@ export class DatasetsController { @Query("data") data: string, ): Promise { const user: JWTUser = request.user as JWTUser; - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.datasetInstanceAccess(user); const datasetToUpdate = await this.datasetsService.findOne({ where: { pid: pid }, }); @@ -1312,7 +1470,7 @@ export class DatasetsController { // GET /datasets/:id/thumbnail @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetRead, DatasetClass), ) // @UseGuards(PoliciesGuard) @@ -1356,7 +1514,7 @@ export class DatasetsController { // POST /datasets/:id/attachments @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetAttachmentCreate, DatasetClass), ) @HttpCode(HttpStatus.CREATED) @@ -1406,7 +1564,7 @@ export class DatasetsController { // GET /datasets/:id/attachments @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetAttachmentRead, DatasetClass), ) @Get("/:pid/attachments") @@ -1443,7 +1601,7 @@ export class DatasetsController { // PATCH /datasets/:id/attachments/:fk @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetAttachmentUpdate, DatasetClass), ) @Put("/:pid/attachments/:aid") @@ -1490,7 +1648,7 @@ export class DatasetsController { // DELETE /datasets/:pid/attachments/:aid @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetAttachmentDelete, DatasetClass), ) @Delete("/:pid/attachments/:aid") @@ -1534,7 +1692,7 @@ export class DatasetsController { // POST /datasets/:id/origdatablocks @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => { + @CheckPolicies("datasets", (ability: AppAbility) => { return ability.can(Action.DatasetOrigdatablockCreate, DatasetClass); }) @UseInterceptors( @@ -1585,7 +1743,7 @@ export class DatasetsController { const datablock = await this.origDatablocksService.create(createOrigDatablock); - const updateDatasetDto: PartialUpdateDatasetDto = { + const updateDatasetDto: PartialUpdateDatasetObsoleteDto = { size: dataset.size + datablock.size, numberOfFiles: dataset.numberOfFiles + datablock.dataFileList.length, }; @@ -1600,7 +1758,7 @@ export class DatasetsController { // POST /datasets/:id/origdatablocks/isValid @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => { + @CheckPolicies("datasets", (ability: AppAbility) => { return ability.can(Action.DatasetOrigdatablockCreate, DatasetClass); }) @HttpCode(HttpStatus.OK) @@ -1649,7 +1807,7 @@ export class DatasetsController { // GET /datasets/:id/origdatablocks @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => { + @CheckPolicies("datasets", (ability: AppAbility) => { return ability.can(Action.DatasetOrigdatablockRead, DatasetClass); }) @Get("/:pid/origdatablocks") @@ -1686,7 +1844,7 @@ export class DatasetsController { // PATCH /datasets/:id/origdatablocks/:fk @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => { + @CheckPolicies("datasets", (ability: AppAbility) => { return ability.can(Action.DatasetOrigdatablockUpdate, DatasetClass); }) @UseInterceptors( @@ -1757,7 +1915,7 @@ export class DatasetsController { // DELETE /datasets/:id/origdatablocks/:fk @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetOrigdatablockDelete, DatasetClass), ) @Delete("/:pid/origdatablocks/:oid") @@ -1804,7 +1962,7 @@ export class DatasetsController { where: { datasetId: pid }, }); // update dataset size and files number - const updateDatasetDto: PartialUpdateDatasetDto = { + const updateDatasetDto: PartialUpdateDatasetObsoleteDto = { size: odb.reduce((a, b) => a + b.size, 0), numberOfFiles: odb.reduce((a, b) => a + b.dataFileList.length, 0), }; @@ -1819,7 +1977,7 @@ export class DatasetsController { // POST /datasets/:id/datablocks @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetDatablockCreate, DatasetClass), ) @UseInterceptors( @@ -1880,7 +2038,7 @@ export class DatasetsController { // GET /datasets/:id/datablocks @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetDatablockRead, DatasetClass), ) @Get("/:pid/datablocks") @@ -1917,7 +2075,7 @@ export class DatasetsController { // PATCH /datasets/:id/datablocks/:fk @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetDatablockUpdate, DatasetClass), ) @UseInterceptors( @@ -1987,7 +2145,7 @@ export class DatasetsController { // DELETE /datasets/:id/datablocks/:fk @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetDatablockDelete, DatasetClass), ) @Delete("/:pid/datablocks/:did") @@ -2034,7 +2192,7 @@ export class DatasetsController { datasetId: pid, }); // update dataset size and files number - const updateDatasetDto: PartialUpdateDatasetDto = { + const updateDatasetDto: PartialUpdateDatasetObsoleteDto = { packedSize: remainingDatablocks.reduce((a, b) => a + b.packedSize, 0), numberOfFilesArchived: remainingDatablocks.reduce( (a, b) => a + b.dataFileList.length, @@ -2056,7 +2214,7 @@ export class DatasetsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("datasets", (ability: AppAbility) => ability.can(Action.DatasetLogbookRead, DatasetClass), ) @Get("/:pid/logbook") @@ -2087,7 +2245,7 @@ export class DatasetsController { Action.DatasetLogbookRead, ); - const proposalId = dataset?.proposalId; + const proposalId = (dataset?.proposalIds || [])[0]; if (!proposalId) return null; diff --git a/src/datasets/datasets.service.ts b/src/datasets/datasets.service.ts index c78974c1e..b43385a3f 100644 --- a/src/datasets/datasets.service.ts +++ b/src/datasets/datasets.service.ts @@ -23,22 +23,14 @@ import { import { ElasticSearchService } from "src/elastic-search/elastic-search.service"; import { InitialDatasetsService } from "src/initial-datasets/initial-datasets.service"; import { LogbooksService } from "src/logbooks/logbooks.service"; -import { DatasetType } from "./dataset-type.enum"; import { CreateDatasetDto } from "./dto/create-dataset.dto"; +import { IDatasetFields } from "./interfaces/dataset-filters.interface"; +import { DatasetClass, DatasetDocument } from "./schemas/dataset.schema"; import { PartialUpdateDatasetDto, + PartialUpdateDatasetWithHistoryDto, UpdateDatasetDto, } from "./dto/update-dataset.dto"; -import { - PartialUpdateDerivedDatasetDto, - UpdateDerivedDatasetDto, -} from "./dto/update-derived-dataset.dto"; -import { - PartialUpdateRawDatasetDto, - UpdateRawDatasetDto, -} from "./dto/update-raw-dataset.dto"; -import { IDatasetFields } from "./interfaces/dataset-filters.interface"; -import { DatasetClass, DatasetDocument } from "./schemas/dataset.schema"; @Injectable({ scope: Scope.REQUEST }) export class DatasetsService { @@ -205,10 +197,7 @@ export class DatasetsService { // we update the full dataset if exist or create a new one if it does not async findByIdAndReplace( id: string, - updateDatasetDto: - | UpdateDatasetDto - | UpdateRawDatasetDto - | UpdateDerivedDatasetDto, + updateDatasetDto: UpdateDatasetDto, ): Promise { const username = (this.request.user as JWTUser).username; const existingDataset = await this.datasetModel.findOne({ pid: id }).exec(); @@ -253,9 +242,7 @@ export class DatasetsService { id: string, updateDatasetDto: | PartialUpdateDatasetDto - | PartialUpdateRawDatasetDto - | PartialUpdateDerivedDatasetDto - | UpdateQuery, + | PartialUpdateDatasetWithHistoryDto, ): Promise { const existingDataset = await this.datasetModel.findOne({ pid: id }).exec(); // check if we were able to find the dataset @@ -434,20 +421,28 @@ export class DatasetsService { async updateHistory( req: Request, dataset: DatasetClass, - data: UpdateDatasetDto, + data: PartialUpdateDatasetDto, ) { if (req.body.history) { delete req.body.history; } if (!req.body.size && !req.body.packedSize) { - const updatedFields: Omit = - data; + const updatedFields: Omit< + PartialUpdateDatasetDto, + "updatedAt" | "updatedBy" + > = data; const historyItem: Record = {}; Object.keys(updatedFields).forEach((updatedField) => { historyItem[updatedField as keyof UpdateDatasetDto] = { currentValue: data[updatedField as keyof UpdateDatasetDto], - previousValue: dataset[updatedField as keyof UpdateDatasetDto], + previousValue: + dataset[ + updatedField as keyof Omit< + UpdateDatasetDto, + "attachments" | "origdatablocks" | "datablocks" + > + ], }; }); dataset.history = dataset.history ?? []; @@ -460,18 +455,15 @@ export class DatasetsService { if (logbookEnabled) { const user = (req.user as JWTUser).username.replace("ldap.", ""); const datasetPid = dataset.pid; - const proposalId = - dataset.type === DatasetType.Raw - ? (dataset as unknown as DatasetClass).proposalId - : undefined; - if (proposalId) { + const proposalIds = dataset.proposalIds || []; + (proposalIds as Array).forEach(async (proposalId) => { await Promise.all( Object.keys(updatedFields).map(async (updatedField) => { const message = `${user} updated "${updatedField}" of dataset with PID ${datasetPid}`; await this.logbooksService.sendMessage(proposalId, { message }); }), ); - } + }); } } } diff --git a/src/datasets/dto/create-dataset-obsolete.dto.ts b/src/datasets/dto/create-dataset-obsolete.dto.ts new file mode 100644 index 000000000..a75fe5dfe --- /dev/null +++ b/src/datasets/dto/create-dataset-obsolete.dto.ts @@ -0,0 +1,34 @@ +import { IsEnum, IsOptional, IsString } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; +import { DatasetType } from "../dataset-type.enum"; +import { UpdateDatasetObsoleteDto } from "./update-dataset-obsolete.dto"; + +export class CreateDatasetObsoleteDto extends UpdateDatasetObsoleteDto { + @ApiProperty({ + type: String, + required: false, + description: "Persistent identifier of the dataset.", + }) + @IsOptional() + @IsString() + pid?: string; + + @ApiProperty({ + type: String, + required: true, + enum: [DatasetType.Raw, DatasetType.Derived], + description: + "Characterize type of dataset, either 'raw' or 'derived'. Autofilled when choosing the proper inherited models.", + }) + @IsEnum(DatasetType) + readonly type: string; + + @ApiProperty({ + type: String, + required: false, + description: "Version of the API used in creation of the dataset.", + }) + @IsOptional() + @IsString() + readonly version?: string; +} diff --git a/src/datasets/dto/create-dataset.dto.ts b/src/datasets/dto/create-dataset.dto.ts index c9b5b7ad5..8ee18d733 100644 --- a/src/datasets/dto/create-dataset.dto.ts +++ b/src/datasets/dto/create-dataset.dto.ts @@ -22,13 +22,4 @@ export class CreateDatasetDto extends UpdateDatasetDto { }) @IsEnum(DatasetType) readonly type: string; - - @ApiProperty({ - type: String, - required: false, - description: "Version of the API used in creation of the dataset.", - }) - @IsOptional() - @IsString() - readonly version?: string; } diff --git a/src/datasets/dto/create-derived-dataset.dto.ts b/src/datasets/dto/create-derived-dataset-obsolete.dto.ts similarity index 77% rename from src/datasets/dto/create-derived-dataset.dto.ts rename to src/datasets/dto/create-derived-dataset-obsolete.dto.ts index f95240c82..f6fcc180f 100644 --- a/src/datasets/dto/create-derived-dataset.dto.ts +++ b/src/datasets/dto/create-derived-dataset-obsolete.dto.ts @@ -1,9 +1,9 @@ -import { UpdateDerivedDatasetDto } from "./update-derived-dataset.dto"; +import { UpdateDerivedDatasetObsoleteDto } from "./update-derived-dataset-obsolete.dto"; import { ApiProperty } from "@nestjs/swagger"; import { IsEnum, IsOptional, IsString } from "class-validator"; import { DatasetType } from "../dataset-type.enum"; -export class CreateDerivedDatasetDto extends UpdateDerivedDatasetDto { +export class CreateDerivedDatasetObsoleteDto extends UpdateDerivedDatasetObsoleteDto { @ApiProperty({ type: String, required: false, diff --git a/src/datasets/dto/create-raw-dataset.dto.ts b/src/datasets/dto/create-raw-dataset-obsolete.dto.ts similarity index 78% rename from src/datasets/dto/create-raw-dataset.dto.ts rename to src/datasets/dto/create-raw-dataset-obsolete.dto.ts index fafa70b78..57f7d6ba9 100644 --- a/src/datasets/dto/create-raw-dataset.dto.ts +++ b/src/datasets/dto/create-raw-dataset-obsolete.dto.ts @@ -1,9 +1,9 @@ -import { UpdateRawDatasetDto } from "./update-raw-dataset.dto"; +import { UpdateRawDatasetObsoleteDto } from "./update-raw-dataset-obsolete.dto"; import { ApiProperty } from "@nestjs/swagger"; import { IsEnum, IsOptional, IsString } from "class-validator"; import { DatasetType } from "../dataset-type.enum"; -export class CreateRawDatasetDto extends UpdateRawDatasetDto { +export class CreateRawDatasetObsoleteDto extends UpdateRawDatasetObsoleteDto { @ApiProperty({ type: String, required: false, diff --git a/src/datasets/dto/output-dataset-obsolete.dto.ts b/src/datasets/dto/output-dataset-obsolete.dto.ts new file mode 100644 index 000000000..f99a7fe94 --- /dev/null +++ b/src/datasets/dto/output-dataset-obsolete.dto.ts @@ -0,0 +1,228 @@ +import { + IsArray, + IsDateString, + IsEnum, + IsObject, + IsOptional, + IsString, +} from "class-validator"; +import { ApiProperty, getSchemaPath } from "@nestjs/swagger"; +import { DatasetType } from "../dataset-type.enum"; +import { UpdateDatasetObsoleteDto } from "./update-dataset-obsolete.dto"; +import { Attachment } from "src/attachments/schemas/attachment.schema"; +import { Type } from "class-transformer"; +import { OrigDatablock } from "src/origdatablocks/schemas/origdatablock.schema"; +import { Datablock } from "src/datablocks/schemas/datablock.schema"; + +export class OutputDatasetObsoleteDto extends UpdateDatasetObsoleteDto { + @ApiProperty({ + type: String, + required: false, + description: "Persistent identifier of the dataset.", + }) + @IsString() + readonly pid: string; + + @ApiProperty({ + type: String, + required: true, + enum: [DatasetType.Raw, DatasetType.Derived], + description: + "Characterize type of dataset, either 'raw' or 'derived'. Autofilled when choosing the proper inherited models.", + }) + @IsEnum(DatasetType) + readonly type: string; + + @ApiProperty({ + type: String, + required: false, + description: "Version of the API used in creation of the dataset.", + }) + @IsOptional() + @IsString() + readonly version?: string; + + @ApiProperty({ + type: String, + required: true, + description: + "First name and last name of principal investigator(s). If multiple PIs are present, use a semicolon separated list. This field is required if the dataset is a Raw dataset.", + }) + @IsString() + @IsOptional() + readonly principalInvestigator?: string; + + @ApiProperty({ + type: Date, + required: false, + description: + "Start time of data acquisition for the current dataset.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + }) + @IsOptional() + @IsDateString() + readonly startTime?: Date; + + @ApiProperty({ + type: Date, + required: false, + description: + "End time of data acquisition for the current dataset.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + }) + @IsOptional() + @IsDateString() + readonly endTime?: Date; + + @ApiProperty({ + type: String, + required: true, + description: + "Unique location identifier where data was taken, usually in the form /Site-name/facility-name/instrumentOrBeamline-name. This field is required if the dataset is a Raw dataset.", + }) + @IsString() + @IsOptional() + readonly creationLocation?: string; + + @ApiProperty({ + type: String, + required: false, + description: + "Defines the format of the data files in this dataset, e.g Nexus Version x.y.", + }) + @IsOptional() + @IsString() + readonly dataFormat?: string; + + @ApiProperty({ + type: String, + required: false, + description: "ID of the sample used when collecting the data.", + }) + @IsOptional() + @IsString() + readonly sampleId?: string; + + @ApiProperty({ + type: String, + required: false, + description: "ID of the instrument where the data was created.", + }) + @IsOptional() + @IsString() + readonly instrumentId?: string; + + @ApiProperty({ + type: String, + required: true, + description: + "First name and last name of the person or people pursuing the data analysis. The string may contain a list of names, which should then be separated by semicolons.", + }) + @IsString() + @IsOptional() + readonly investigator?: string; + + @ApiProperty({ + type: [String], + required: true, + description: + "Array of input dataset identifiers used in producing the derived dataset. Ideally these are the global identifier to existing datasets inside this or federated data catalogs.", + }) + @IsString({ + each: true, + }) + @IsOptional() + readonly inputDatasets?: string[]; + + @ApiProperty({ + type: [String], + required: true, + description: + "A list of links to software repositories which uniquely identifies the pieces of software, including versions, used for yielding the derived data.", + }) + @IsString({ + each: true, + }) + @IsOptional() + readonly usedSoftware?: string[]; + + @ApiProperty({ + type: Object, + required: false, + description: + "The creation process of the derived data will usually depend on input job parameters. The full structure of these input parameters are stored here.", + }) + @IsOptional() + @IsObject() + readonly jobParameters?: Record; + + @ApiProperty({ + type: String, + required: false, + description: + "The output job logfile. Keep the size of this log data well below 15 MB.", + }) + @IsOptional() + @IsString() + readonly jobLogData?: string; + + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(Attachment) }, + required: false, + description: + "Small, less than 16 MB attachments, envisaged for png/jpeg previews.", + }) + @IsOptional() + @IsArray() + @Type(() => Attachment) + attachments?: Attachment[]; + + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(OrigDatablock) }, + required: false, + description: + "Containers that list all files and their attributes which make up a dataset. Usually filled at the time the dataset's metadata is created in the data catalog. Can be used by subsequent archiving processes to create the archived datasets.", + }) + origdatablocks?: OrigDatablock[]; + + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(Datablock) }, + required: false, + description: + "When archiving a dataset, all files contained in the dataset are listed here together with their checksum information. Several datablocks can be created if the file listing is too long for a single datablock. This partitioning decision is done by the archiving system to allow for chunks of datablocks with manageable sizes. E.g a datasets consisting of 10 TB of data could be split into 10 datablocks of about 1 TB each. The upper limit set by the data catalog system itself is given by the fact that documents must be smaller than 16 MB, which typically allows for datasets of about 100000 files.", + }) + datablocks?: Datablock[]; + + @ApiProperty({ + type: String, + required: true, + description: + "Indicate the user who created this record. This property is added and maintained by the system.", + }) + createdBy: string; + + @ApiProperty({ + type: String, + required: true, + description: + "Indicate the user who updated this record last. This property is added and maintained by the system.", + }) + updatedBy: string; + + @ApiProperty({ + type: Date, + required: true, + description: + "Date and time when this record was created. This field is managed by mongoose with through the timestamp settings. The field should be a string containing a date in ISO 8601 format (2024-02-27T12:26:57.313Z)", + }) + createdAt: Date; + + @ApiProperty({ + type: Date, + required: true, + description: + "Date and time when this record was updated last. This field is managed by mongoose with through the timestamp settings. The field should be a string containing a date in ISO 8601 format (2024-02-27T12:26:57.313Z)", + }) + updatedAt: Date; +} diff --git a/src/datasets/dto/output-dataset.dto.ts b/src/datasets/dto/output-dataset.dto.ts new file mode 100644 index 000000000..6c070a73c --- /dev/null +++ b/src/datasets/dto/output-dataset.dto.ts @@ -0,0 +1,41 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { CreateDatasetDto } from "./create-dataset.dto"; +import { IsDateString, IsString } from "class-validator"; + +export class OutputDatasetDto extends CreateDatasetDto { + @ApiProperty({ + type: String, + required: true, + description: + "Indicate the user who created this record. This property is added and maintained by the system.", + }) + @IsString() + createdBy: string; + + @ApiProperty({ + type: String, + required: true, + description: + "Indicate the user who updated this record last. This property is added and maintained by the system.", + }) + @IsString() + updatedBy: string; + + @ApiProperty({ + type: Date, + required: true, + description: + "Date and time when this record was created. This field is managed by mongoose with through the timestamp settings. The field should be a string containing a date in ISO 8601 format (2024-02-27T12:26:57.313Z)", + }) + @IsDateString() + createdAt: Date; + + @ApiProperty({ + type: Date, + required: true, + description: + "Date and time when this record was updated last. This field is managed by mongoose with through the timestamp settings. The field should be a string containing a date in ISO 8601 format (2024-02-27T12:26:57.313Z)", + }) + @IsDateString() + updatedAt: Date; +} diff --git a/src/datasets/dto/update-dataset-obsolete.dto.ts b/src/datasets/dto/update-dataset-obsolete.dto.ts new file mode 100644 index 000000000..73f4b34bd --- /dev/null +++ b/src/datasets/dto/update-dataset-obsolete.dto.ts @@ -0,0 +1,304 @@ +import { + ApiProperty, + ApiTags, + getSchemaPath, + PartialType, +} from "@nestjs/swagger"; +import { OwnableDto } from "../../common/dto/ownable.dto"; +import { + IsArray, + IsBoolean, + IsDateString, + IsEmail, + IsFQDN, + IsInt, + IsNumber, + IsObject, + IsOptional, + IsString, + ValidateNested, +} from "class-validator"; +import { TechniqueClass } from "../schemas/technique.schema"; +import { Type } from "class-transformer"; +import { CreateTechniqueDto } from "./create-technique.dto"; +import { RelationshipClass } from "../schemas/relationship.schema"; +import { CreateRelationshipDto } from "./create-relationship.dto"; +import { LifecycleClass } from "../schemas/lifecycle.schema"; + +@ApiTags("datasets") +export class UpdateDatasetObsoleteDto extends OwnableDto { + @ApiProperty({ + type: String, + required: true, + description: + "Owner or custodian of the dataset, usually first name + last name. The string may contain a list of persons, which should then be separated by semicolons.", + }) + @IsString() + readonly owner: string; + + @ApiProperty({ + type: String, + required: false, + description: + "Email of the owner or custodian of the dataset. The string may contain a list of emails, which should then be separated by semicolons.", + }) + @IsOptional() + @IsEmail() + readonly ownerEmail?: string; + + @ApiProperty({ + type: String, + required: false, + description: + "ORCID of the owner or custodian. The string may contain a list of ORCIDs, which should then be separated by semicolons.", + }) + @IsOptional() + @IsString() + readonly orcidOfOwner?: string; + + @ApiProperty({ + type: String, + required: true, + description: + "Email of the contact person for this dataset. The string may contain a list of emails, which should then be separated by semicolons.", + }) + @IsEmail() + readonly contactEmail: string; + + @ApiProperty({ + type: String, + required: true, + description: + "Absolute file path on file server containing the files of this dataset, e.g. /some/path/to/sourcefolder. In case of a single file dataset, e.g. HDF5 data, it contains the path up to, but excluding the filename. Trailing slashes are removed.", + }) + @IsString() + readonly sourceFolder: string; + + @ApiProperty({ + type: String, + required: false, + description: + "DNS host name of file server hosting sourceFolder, optionally including a protocol e.g. [protocol://]fileserver1.example.com", + }) + @IsOptional() + @IsFQDN() + readonly sourceFolderHost?: string; + + /* + * size and number of files fields should be managed by the system + */ + @ApiProperty({ + type: Number, + default: 0, + required: false, + description: + "Total size of all source files contained in source folder on disk when unpacked.", + }) + @IsOptional() + @IsInt() + readonly size?: number = 0; + + @ApiProperty({ + type: Number, + default: 0, + required: false, + description: + "Total size of all datablock package files created for this dataset.", + }) + @IsOptional() + @IsInt() + readonly packedSize?: number = 0; + + @ApiProperty({ + type: Number, + default: 0, + required: false, + description: + "Total number of files in all OrigDatablocks for this dataset.", + }) + @IsOptional() + @IsInt() + readonly numberOfFiles?: number = 0; + + @ApiProperty({ + type: Number, + default: 0, + required: true, + description: "Total number of files in all Datablocks for this dataset.", + }) + @IsOptional() + @IsInt() + readonly numberOfFilesArchived?: number; + + @ApiProperty({ + type: Date, + required: true, + description: + "Time when dataset became fully available on disk, i.e. all containing files have been written, or the dataset was created in SciCat.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + }) + @IsDateString() + readonly creationTime: Date; + + @ApiProperty({ + type: String, + required: false, + description: + "Defines a level of trust, e.g. a measure of how much data was verified or used by other persons.", + }) + @IsOptional() + @IsString() + readonly validationStatus?: string; + + @ApiProperty({ + type: [String], + required: false, + description: + "Array of tags associated with the meaning or contents of this dataset. Values should ideally come from defined vocabularies, taxonomies, ontologies or knowledge graphs.", + }) + @IsOptional() + @IsString({ + each: true, + }) + readonly keywords?: string[]; + + @ApiProperty({ + type: String, + required: false, + description: "Free text explanation of contents of dataset.", + }) + @IsOptional() + @IsString() + readonly description?: string; + + @ApiProperty({ + type: String, + required: false, + description: + "A name for the dataset, given by the creator to carry some semantic meaning. Useful for display purposes e.g. instead of displaying the pid. Will be autofilled if missing using info from sourceFolder.", + }) + @IsOptional() + @IsString() + readonly datasetName?: string; + + @ApiProperty({ + type: String, + required: false, + description: + "ACIA information about AUthenticity,COnfidentiality,INtegrity and AVailability requirements of dataset. E.g. AV(ailabilty)=medium could trigger the creation of a two tape copies. Format 'AV=medium,CO=low'", + }) + @IsOptional() + @IsString() + readonly classification?: string; + + @ApiProperty({ + type: String, + required: false, + description: "Name of the license under which the data can be used.", + }) + @IsOptional() + @IsString() + readonly license?: string; + + // it needs to be discussed if this fields is managed by the user or by the system + @ApiProperty({ + type: Boolean, + required: false, + default: false, + description: "Flag is true when data are made publicly available.", + }) + @IsOptional() + @IsBoolean() + readonly isPublished?: boolean; + + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(TechniqueClass) }, + required: false, + default: [], + description: "Stores the metadata information for techniques.", + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateTechniqueDto) + readonly techniques?: TechniqueClass[]; + + // it needs to be discussed if this fields is managed by the user or by the system + @ApiProperty({ + type: [String], + required: false, + default: [], + description: "List of users that the dataset has been shared with.", + }) + @IsOptional() + @IsString({ + each: true, + }) + readonly sharedWith?: string[]; + + // it needs to be discussed if this fields is managed by the user or by the system + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(RelationshipClass) }, + required: false, + default: [], + description: "Stores the relationships with other datasets.", + }) + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => CreateRelationshipDto) + readonly relationships?: RelationshipClass[]; + + @ApiProperty({ + type: LifecycleClass, + required: false, + default: {}, + description: + "Describes the current status of the dataset during its lifetime with respect to the storage handling systems.", + }) + @IsOptional() + readonly datasetlifecycle: LifecycleClass; + + @ApiProperty({ + type: Object, + required: false, + default: {}, + description: "JSON object containing the scientific metadata.", + }) + @IsOptional() + @IsObject() + readonly scientificMetadata?: Record; + + @ApiProperty({ + type: String, + required: false, + description: "Comment the user has about a given dataset.", + }) + @IsOptional() + @IsString() + readonly comment?: string; + + @ApiProperty({ + type: Number, + required: false, + description: + "Data Quality Metrics is a number given by the user to rate the dataset.", + }) + @IsOptional() + @IsNumber() + readonly dataQualityMetrics?: number; + + @ApiProperty({ + type: String, + required: false, + description: "The ID of the proposal to which the dataset belongs.", + }) + @IsOptional() + @IsString() + readonly proposalId?: string; +} + +export class PartialUpdateDatasetObsoleteDto extends PartialType( + UpdateDatasetObsoleteDto, +) {} diff --git a/src/datasets/dto/update-dataset.dto.ts b/src/datasets/dto/update-dataset.dto.ts index aa681784b..99c6c3eaf 100644 --- a/src/datasets/dto/update-dataset.dto.ts +++ b/src/datasets/dto/update-dataset.dto.ts @@ -24,9 +24,7 @@ import { CreateTechniqueDto } from "./create-technique.dto"; import { RelationshipClass } from "../schemas/relationship.schema"; import { CreateRelationshipDto } from "./create-relationship.dto"; import { LifecycleClass } from "../schemas/lifecycle.schema"; -import { Attachment } from "../../attachments/schemas/attachment.schema"; -import { OrigDatablock } from "../../origdatablocks/schemas/origdatablock.schema"; -import { Datablock } from "../../datablocks/schemas/datablock.schema"; +import { HistoryClass } from "../schemas/history.schema"; @ApiTags("datasets") export class UpdateDatasetDto extends OwnableDto { @@ -37,7 +35,8 @@ export class UpdateDatasetDto extends OwnableDto { "Owner or custodian of the dataset, usually first name + last name. The string may contain a list of persons, which should then be separated by semicolons.", }) @IsString() - readonly owner: string; + @IsOptional() + readonly owner?: string; @ApiProperty({ type: String, @@ -137,7 +136,7 @@ export class UpdateDatasetDto extends OwnableDto { type: Date, required: true, description: - "Time when dataset became fully available on disk, i.e. all containing files have been written. Format according to chapter 5.6 internet date/time format in RFC 3339. Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + "Time when dataset became fully available on disk, i.e. all containing files have been written, or the dataset was created in SciCat.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", }) @IsDateString() readonly creationTime: Date; @@ -175,13 +174,12 @@ export class UpdateDatasetDto extends OwnableDto { @ApiProperty({ type: String, - required: false, + required: true, description: "A name for the dataset, given by the creator to carry some semantic meaning. Useful for display purposes e.g. instead of displaying the pid. Will be autofilled if missing using info from sourceFolder.", }) - @IsOptional() @IsString() - readonly datasetName?: string; + readonly datasetName: string; @ApiProperty({ type: String, @@ -261,7 +259,7 @@ export class UpdateDatasetDto extends OwnableDto { "Describes the current status of the dataset during its lifetime with respect to the storage handling systems.", }) @IsOptional() - readonly datasetlifecycle: LifecycleClass; + readonly datasetlifecycle?: LifecycleClass; @ApiProperty({ type: Object, @@ -292,14 +290,153 @@ export class UpdateDatasetDto extends OwnableDto { @IsNumber() readonly dataQualityMetrics?: number; + @ApiProperty({ + type: String, + required: true, + description: + "First name and last name of principal investigator(s). If multiple PIs are present, use a semicolon separated list. This field is required if the dataset is a Raw dataset.", + }) + @IsString() + readonly principalInvestigator: string; + + @ApiProperty({ + type: Date, + required: false, + description: + "Start time of data acquisition for the current dataset.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + }) + @IsOptional() + @IsDateString() + readonly startTime?: Date; + + @ApiProperty({ + type: Date, + required: false, + description: + "End time of data acquisition for the current dataset.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + }) + @IsOptional() + @IsDateString() + readonly endTime?: Date; + + @ApiProperty({ + type: String, + required: false, + description: + "Unique location identifier where data was taken, usually in the form /Site-name/facility-name/instrumentOrBeamline-name. This field is required if the dataset is a Raw dataset.", + }) + @IsOptional() + @IsString() + readonly creationLocation?: string; + + @ApiProperty({ + type: String, + required: false, + description: + "Defines the format of the data files in this dataset, e.g Nexus Version x.y.", + }) + @IsOptional() + @IsString() + readonly dataFormat?: string; + + @ApiProperty({ + type: [String], + required: false, + description: + "ID of the proposal or proposals which the dataset belongs to.
This dataset might have been acquired under the listed proposals or is derived from datasets acquired from datasets belonging to the listed datasets.", + }) + @IsOptional() + @IsString({ + each: true, + }) + readonly proposalIds?: string[]; + + @ApiProperty({ + type: [String], + required: false, + description: + "ID of the sample or samples used when collecting the data included or used in this dataset.", + }) + @IsOptional() + @IsString({ + each: true, + }) + readonly sampleIds?: string[]; + + @ApiProperty({ + type: String, + required: false, + description: + "ID of the instrument or instruments where the data included or used in this datasets was collected on.", + }) @IsOptional() - attachments?: Attachment[]; + @IsString({ + each: true, + }) + readonly instrumentIds?: string[]; + @ApiProperty({ + type: [String], + required: true, + description: + "Array of input dataset identifiers used in producing the derived dataset. Ideally these are the global identifier to existing datasets inside this or federated data catalogs.", + }) @IsOptional() - origdatablocks?: OrigDatablock[]; + @IsString({ + each: true, + }) + readonly inputDatasets?: string[]; + @ApiProperty({ + type: [String], + required: false, + description: + "A list of links to software repositories which uniquely identifies the pieces of software, including versions, used for yielding the derived data.", + }) @IsOptional() - datablocks?: Datablock[]; + @IsString({ + each: true, + }) + readonly usedSoftware?: string[]; + + @ApiProperty({ + type: Object, + required: false, + description: + "The creation process of the derived data will usually depend on input job parameters. The full structure of these input parameters are stored here.", + }) + @IsOptional() + @IsObject() + readonly jobParameters?: Record; + + @ApiProperty({ + type: String, + required: false, + description: + "The output job logfile. Keep the size of this log data well below 15 MB.", + }) + @IsOptional() + @IsString() + readonly jobLogData?: string; } export class PartialUpdateDatasetDto extends PartialType(UpdateDatasetDto) {} + +export class UpdateDatasetWithHistoryDto extends UpdateDatasetDto { + @ApiProperty({ + type: "array", + items: { $ref: getSchemaPath(HistoryClass) }, + required: false, + default: [], + description: "List of history objects containing old and new values.", + }) + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => HistoryClass) + readonly history?: HistoryClass[]; +} + +export class PartialUpdateDatasetWithHistoryDto extends PartialType( + UpdateDatasetWithHistoryDto, +) {} diff --git a/src/datasets/dto/update-derived-dataset.dto.ts b/src/datasets/dto/update-derived-dataset-obsolete.dto.ts similarity index 86% rename from src/datasets/dto/update-derived-dataset.dto.ts rename to src/datasets/dto/update-derived-dataset-obsolete.dto.ts index 5f4d83a1d..b6ec0bf4c 100644 --- a/src/datasets/dto/update-derived-dataset.dto.ts +++ b/src/datasets/dto/update-derived-dataset-obsolete.dto.ts @@ -1,8 +1,8 @@ -import { UpdateDatasetDto } from "./update-dataset.dto"; +import { UpdateDatasetObsoleteDto } from "./update-dataset-obsolete.dto"; import { ApiProperty, PartialType } from "@nestjs/swagger"; import { IsObject, IsOptional, IsString } from "class-validator"; -export class UpdateDerivedDatasetDto extends UpdateDatasetDto { +export class UpdateDerivedDatasetObsoleteDto extends UpdateDatasetObsoleteDto { @ApiProperty({ type: String, required: true, @@ -55,6 +55,6 @@ export class UpdateDerivedDatasetDto extends UpdateDatasetDto { readonly jobLogData?: string; } -export class PartialUpdateDerivedDatasetDto extends PartialType( - UpdateDerivedDatasetDto, +export class PartialUpdateDerivedDatasetObsoleteDto extends PartialType( + UpdateDerivedDatasetObsoleteDto, ) {} diff --git a/src/datasets/dto/update-raw-dataset.dto.ts b/src/datasets/dto/update-raw-dataset-obsolete.dto.ts similarity index 58% rename from src/datasets/dto/update-raw-dataset.dto.ts rename to src/datasets/dto/update-raw-dataset-obsolete.dto.ts index 241e124b8..075406908 100644 --- a/src/datasets/dto/update-raw-dataset.dto.ts +++ b/src/datasets/dto/update-raw-dataset-obsolete.dto.ts @@ -1,8 +1,8 @@ import { IsDateString, IsOptional, IsString } from "class-validator"; -import { UpdateDatasetDto } from "./update-dataset.dto"; +import { UpdateDatasetObsoleteDto } from "./update-dataset-obsolete.dto"; import { ApiProperty, PartialType } from "@nestjs/swagger"; -export class UpdateRawDatasetDto extends UpdateDatasetDto { +export class UpdateRawDatasetObsoleteDto extends UpdateDatasetObsoleteDto { /* we need to discuss if the naming is adequate. */ @ApiProperty({ type: String, @@ -17,7 +17,17 @@ export class UpdateRawDatasetDto extends UpdateDatasetDto { type: Date, required: false, description: - "End time of data acquisition for this dataset, format according to chapter 5.6 internet date/time format in RFC 3339. Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + "Start time of data acquisition for the current dataset.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + }) + @IsOptional() + @IsDateString() + readonly startTime?: Date; + + @ApiProperty({ + type: Date, + required: false, + description: + "End time of data acquisition for the current dataset.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", }) @IsOptional() @IsDateString() @@ -42,14 +52,14 @@ export class UpdateRawDatasetDto extends UpdateDatasetDto { @IsString() readonly dataFormat?: string; - @ApiProperty({ - type: String, - required: false, - description: "The ID of the proposal to which the dataset belongs.", - }) - @IsOptional() - @IsString() - readonly proposalId?: string; + // @ApiProperty({ + // type: String, + // required: false, + // description: "The ID of the proposal to which the dataset belongs.", + // }) + // @IsOptional() + // @IsString() + // readonly proposalId?: string; @ApiProperty({ type: String, @@ -67,7 +77,7 @@ export class UpdateRawDatasetDto extends UpdateDatasetDto { }) @IsOptional() @IsString() - readonly instrumentId: string; + readonly instrumentId?: string; @IsOptional() investigator?: string; @@ -85,6 +95,6 @@ export class UpdateRawDatasetDto extends UpdateDatasetDto { jobLogData?: string; } -export class PartialUpdateRawDatasetDto extends PartialType( - UpdateRawDatasetDto, +export class PartialUpdateRawDatasetObsoleteDto extends PartialType( + UpdateRawDatasetObsoleteDto, ) {} diff --git a/src/datasets/schemas/dataset.schema.ts b/src/datasets/schemas/dataset.schema.ts index 239987347..a3fb81086 100644 --- a/src/datasets/schemas/dataset.schema.ts +++ b/src/datasets/schemas/dataset.schema.ts @@ -1,19 +1,7 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { ApiProperty, getSchemaPath } from "@nestjs/swagger"; import { Document } from "mongoose"; -import { - Attachment, - AttachmentSchema, -} from "src/attachments/schemas/attachment.schema"; import { OwnableClass } from "src/common/schemas/ownable.schema"; -import { - Datablock, - DatablockSchema, -} from "src/datablocks/schemas/datablock.schema"; -import { - OrigDatablock, - OrigDatablockSchema, -} from "src/origdatablocks/schemas/origdatablock.schema"; import { v4 as uuidv4 } from "uuid"; import { DatasetType } from "../dataset-type.enum"; import { HistoryClass, HistorySchema } from "./history.schema"; @@ -196,7 +184,7 @@ export class DatasetClass extends OwnableClass { type: Date, required: true, description: - "Time when dataset became fully available on disk, i.e. all containing files have been written. Format according to chapter 5.6 internet date/time format in RFC 3339. Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + "Time when dataset became fully available on disk, i.e. all containing files have been written, or the dataset was created in SciCat.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", }) @Prop({ type: Date, required: true, index: true }) creationTime: Date; @@ -244,28 +232,21 @@ export class DatasetClass extends OwnableClass { @ApiProperty({ type: String, - required: false, + required: true, description: - "A name for the dataset, given by the creator to carry some semantic meaning. Useful for display purposes e.g. instead of displaying the pid. Will be autofilled if missing using info from sourceFolder.", + "A name for the dataset, given by the creator to carry some semantic meaning. Useful for display purposes e.g. instead of displaying the pid.", }) @Prop({ type: String, - required: false, - default: function datasetName() { - const sourceFolder = (this as DatasetDocument).sourceFolder; - if (!sourceFolder) return ""; - const arr = sourceFolder.split("/"); - if (arr.length == 1) return arr[0]; - else return arr[arr.length - 2] + "/" + arr[arr.length - 1]; - }, + required: true, }) - datasetName?: string; + datasetName: string; @ApiProperty({ type: String, required: false, description: - "ACIA information about AUthenticity,COnfidentiality,INtegrity and AVailability requirements of dataset. E.g. AV(ailabilty)=medium could trigger the creation of a two tape copies. Format 'AV=medium,CO=low'", + "ACIA information about AUthenticity,COnfidentiality,INtegrity and AVailability requirements of dataset. E.g. AV(ailabilty)=medium could trigger the creation of a two tape copies. Format 'AV=medium,CO=low'. Please check the following post for more info: https://en.wikipedia.org/wiki/Parkerian_Hexad", }) @Prop({ type: String, required: false }) classification?: string; @@ -280,11 +261,12 @@ export class DatasetClass extends OwnableClass { @ApiProperty({ type: String, - required: false, - description: "Version of the API used in creation of the dataset.", + required: true, + description: + "Version of the API used when the dataset was created or last updated. API version is defined in code for each release. Managed by the system.", }) - @Prop({ type: String, required: false }) - version?: string; + @Prop({ type: String, required: true }) + version: string; @ApiProperty({ type: "array", @@ -298,12 +280,12 @@ export class DatasetClass extends OwnableClass { @ApiProperty({ type: LifecycleClass, - required: false, + required: true, default: {}, description: "Describes the current status of the dataset during its lifetime with respect to the storage handling systems.", }) - @Prop({ type: LifecycleSchema, default: {}, required: false }) + @Prop({ type: LifecycleSchema, default: {}, required: true }) datasetlifecycle?: LifecycleClass; @ApiProperty({ @@ -311,7 +293,8 @@ export class DatasetClass extends OwnableClass { items: { $ref: getSchemaPath(TechniqueClass) }, required: false, default: [], - description: "Stores the metadata information for techniques.", + description: + "Array of techniques information, with technique name and pid.", }) @Prop({ type: [TechniqueSchema], required: false, default: [] }) techniques?: TechniqueClass[]; @@ -321,7 +304,8 @@ export class DatasetClass extends OwnableClass { items: { $ref: getSchemaPath(RelationshipClass) }, required: false, default: [], - description: "Stores the relationships with other datasets.", + description: + "Array of relationships with other datasets. It contains relationship type and destination dataset", }) @Prop({ type: [RelationshipSchema], required: false, default: [] }) relationships?: RelationshipClass[]; @@ -330,7 +314,8 @@ export class DatasetClass extends OwnableClass { type: [String], required: false, default: [], - description: "List of users that the dataset has been shared with.", + description: + "List of additional users that the dataset has been shared with.", }) @Prop({ type: [String], @@ -339,37 +324,37 @@ export class DatasetClass extends OwnableClass { }) sharedWith?: string[]; - @ApiProperty({ - type: "array", - items: { $ref: getSchemaPath(Attachment) }, - required: false, - description: - "Small, less than 16 MB attachments, envisaged for png/jpeg previews.", - }) - @Prop({ type: [AttachmentSchema], default: [] }) - attachments?: Attachment[]; - - @ApiProperty({ - isArray: true, - type: OrigDatablock, - items: { $ref: getSchemaPath(OrigDatablock) }, - required: false, - description: - "Containers that list all files and their attributes which make up a dataset. Usually filled at the time the dataset's metadata is created in the data catalog. Can be used by subsequent archiving processes to create the archived datasets.", - }) - @Prop({ type: [OrigDatablockSchema], default: [] }) - origdatablocks: OrigDatablock[]; - - @ApiProperty({ - isArray: true, - type: Datablock, - items: { $ref: getSchemaPath(Datablock) }, - required: false, - description: - "When archiving a dataset, all files contained in the dataset are listed here together with their checksum information. Several datablocks can be created if the file listing is too long for a single datablock. This partitioning decision is done by the archiving system to allow for chunks of datablocks with manageable sizes. E.g a datasets consisting of 10 TB of data could be split into 10 datablocks of about 1 TB each. The upper limit set by the data catalog system itself is given by the fact that documents must be smaller than 16 MB, which typically allows for datasets of about 100000 files.", - }) - @Prop({ type: [DatablockSchema], default: [] }) - datablocks: Datablock[]; + // @ApiProperty({ + // type: "array", + // items: { $ref: getSchemaPath(Attachment) }, + // required: false, + // description: + // "Small, less than 16 MB attachments, envisaged for png/jpeg previews.", + // }) + // @Prop({ type: [AttachmentSchema], default: [] }) + // attachments?: Attachment[]; + + // @ApiProperty({ + // isArray: true, + // type: OrigDatablock, + // items: { $ref: getSchemaPath(OrigDatablock) }, + // required: false, + // description: + // "Containers that list all files and their attributes which make up a dataset. Usually filled at the time the dataset's metadata is created in the data catalog. Can be used by subsequent archiving processes to create the archived datasets.", + // }) + // @Prop({ type: [OrigDatablockSchema], default: [] }) + // origdatablocks: OrigDatablock[]; + + // @ApiProperty({ + // isArray: true, + // type: Datablock, + // items: { $ref: getSchemaPath(Datablock) }, + // required: false, + // description: + // "When archiving a dataset, all files contained in the dataset are listed here together with their checksum information. Several datablocks can be created if the file listing is too long for a single datablock. This partitioning decision is done by the archiving system to allow for chunks of datablocks with manageable sizes. E.g a datasets consisting of 10 TB of data could be split into 10 datablocks of about 1 TB each. The upper limit set by the data catalog system itself is given by the fact that documents must be smaller than 16 MB, which typically allows for datasets of about 100000 files.", + // }) + // @Prop({ type: [DatablockSchema], default: [] }) + // datablocks: Datablock[]; @ApiProperty({ type: Object, @@ -383,7 +368,8 @@ export class DatasetClass extends OwnableClass { @ApiProperty({ type: String, required: false, - description: "Comment the user has about a given dataset.", + description: + "Short comment provided by the user about a given dataset. This is additional to the description field.", }) @Prop({ type: String, @@ -400,7 +386,7 @@ export class DatasetClass extends OwnableClass { type: Number, required: false, }) - dataQualityMetrics: number; + dataQualityMetrics?: number; /* * fields related to Raw Datasets @@ -418,7 +404,16 @@ export class DatasetClass extends OwnableClass { type: Date, required: false, description: - "End time of data acquisition for this dataset, format according to chapter 5.6 internet date/time format in RFC 3339. Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + "Start time of data acquisition for the current dataset.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", + }) + @Prop({ type: Date, required: false }) + startTime?: Date; + + @ApiProperty({ + type: Date, + required: false, + description: + "End time of data acquisition for the current dataset.
It is expected to be in ISO8601 format according to specifications for internet date/time format in RFC 3339, chapter 5.6 (https://www.rfc-editor.org/rfc/rfc3339#section-5).
Local times without timezone/offset info are automatically transformed to UTC using the timezone of the API server.", }) @Prop({ type: Date, required: false }) endTime?: Date; @@ -427,7 +422,7 @@ export class DatasetClass extends OwnableClass { type: String, required: true, description: - "Unique location identifier where data was taken, usually in the form /Site-name/facility-name/instrumentOrBeamline-name. This field is required if the dataset is a Raw dataset.", + "Unique location identifier where data was acquired. Usually in the form /Site-name/facility-name/instrumentOrBeamline-name.", }) @Prop({ type: String, required: false, index: true }) creationLocation?: string; @@ -442,46 +437,49 @@ export class DatasetClass extends OwnableClass { dataFormat?: string; @ApiProperty({ - type: String, + type: [String], required: false, - description: "The ID of the proposal to which the dataset belongs.", + description: + "The ID of the proposal to which the dataset belongs to and it has been acquired under.", }) - @Prop({ type: String, ref: "Proposal", required: false }) - proposalId?: string; + @Prop({ type: [String], ref: "Proposal", required: false }) + proposalIds?: string[]; @ApiProperty({ - type: String, + type: [String], required: false, - description: "ID of the sample used when collecting the data.", + description: + "Single ID or array of IDS of the samples used when collecting the data.", }) - @Prop({ type: String, ref: "Sample", required: false }) - sampleId?: string; + @Prop({ type: [String], ref: "Sample", required: false }) + sampleIds?: string[]; @ApiProperty({ - type: String, + type: [String], required: false, - description: "ID of the instrument where the data was created.", + description: + "Id of the instrument or array of IDS of the instruments where the data contained in this dataset was created/acquired.", }) - @Prop({ type: String, ref: "Instrument", required: false }) - instrumentId?: string; + @Prop({ type: [String], ref: "Instrument", required: false }) + instrumentIds?: string[]; /* * Derived Dataset */ - @ApiProperty({ - type: String, - required: false, - description: - "First name and last name of the person or people pursuing the data analysis. The string may contain a list of names, which should then be separated by semicolons.", - }) - @Prop({ type: String, required: false, index: true }) - investigator?: string; + // @ApiProperty({ + // type: String, + // required: false, + // description: + // "First name and last name of the person or people pursuing the data analysis. The string may contain a list of names, which should then be separated by semicolons.", + // }) + // @Prop({ type: String, required: false, index: true }) + // investigator?: string; @ApiProperty({ type: [String], required: false, description: - "Array of input dataset identifiers used in producing the derived dataset. Ideally these are the global identifier to existing datasets inside this or federated data catalogs. This field is required if the dataset is a Derived dataset.", + "Array of input dataset identifiers used in producing the derived dataset. Ideally these are the global identifier to existing datasets inside this or federated data catalogs.", }) @Prop({ type: [String], required: false }) inputDatasets?: string[]; @@ -490,7 +488,7 @@ export class DatasetClass extends OwnableClass { type: [String], required: false, description: - "A list of links to software repositories which uniquely identifies the pieces of software, including versions, used for yielding the derived data. This field is required if the dataset is a Derived dataset.", + "A list of links to software repositories which uniquely identifies the pieces of software, including versions, used for yielding the derived data.", }) @Prop({ type: [String], required: false }) usedSoftware?: string[]; diff --git a/src/elastic-search/configuration/datasetFieldMapping.ts b/src/elastic-search/configuration/datasetFieldMapping.ts index 988bb5b7e..629d5f80f 100644 --- a/src/elastic-search/configuration/datasetFieldMapping.ts +++ b/src/elastic-search/configuration/datasetFieldMapping.ts @@ -30,28 +30,27 @@ export const datasetMappings: MappingObject = { creationTime: { type: "date", }, + endTime: { + type: "date", + }, scientificMetadata: { type: "nested", dynamic: true, - properties: { - runNumber: { - properties: { - value: { - type: "long", - }, - }, - }, - }, + properties: {}, }, history: { type: "nested", dynamic: false, }, - proposalId: { + proposalIds: { + type: "keyword", + ignore_above: 256, + }, + sampleIds: { type: "keyword", ignore_above: 256, }, - sampleId: { + instrumentIds: { type: "keyword", ignore_above: 256, }, diff --git a/src/elastic-search/configuration/indexSetting.ts b/src/elastic-search/configuration/indexSetting.ts index bbb60a9f2..7fc473aae 100644 --- a/src/elastic-search/configuration/indexSetting.ts +++ b/src/elastic-search/configuration/indexSetting.ts @@ -22,13 +22,25 @@ export const special_character_filter: AnalysisPatternReplaceCharFilter = { //Dynamic templates export const dynamic_template: Record[] = [ + // NOTE: date as keyword is temporary solution for date format inconsistency issue in the scientificMetadata field { - string_as_keyword: { + date_as_keyword: { path_match: "scientificMetadata.*.*", - match_mapping_type: "string", + match_mapping_type: "date", + mapping: { + type: "keyword", + }, + }, + }, + // NOTE: This is a workaround for the issue where the start_time field is not being + // parsed correctly. This is a temporary solution until + // we can find a better way to handle date format. + { + start_time_as_keyword: { + path_match: "scientificMetadata.start_time.*", + match_mapping_type: "long", mapping: { type: "keyword", - ignore_above: 256, }, }, }, @@ -44,9 +56,20 @@ export const dynamic_template: Record[] = [ }, }, { - date_as_keyword: { + double_as_double: { path_match: "scientificMetadata.*.*", - match_mapping_type: "date", + match_mapping_type: "double", + mapping: { + type: "double", + coerce: true, + ignore_malformed: true, + }, + }, + }, + { + string_as_keyword: { + path_match: "scientificMetadata.*.*", + match_mapping_type: "string", mapping: { type: "keyword", ignore_above: 256, diff --git a/src/elastic-search/elastic-search.controller.ts b/src/elastic-search/elastic-search.controller.ts index ebc875cbd..de2498da6 100644 --- a/src/elastic-search/elastic-search.controller.ts +++ b/src/elastic-search/elastic-search.controller.ts @@ -37,7 +37,7 @@ export class ElasticSearchServiceController { ) {} @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("elastic-search", (ability: AppAbility) => ability.can(Action.Manage, ElasticSearchActions), ) @HttpCode(HttpStatus.CREATED) @@ -53,7 +53,7 @@ export class ElasticSearchServiceController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("elastic-search", (ability: AppAbility) => ability.can(Action.Manage, ElasticSearchActions), ) @HttpCode(HttpStatus.OK) @@ -70,7 +70,7 @@ export class ElasticSearchServiceController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("elastic-search", (ability: AppAbility) => ability.can(Action.Manage, ElasticSearchActions), ) @HttpCode(HttpStatus.OK) @@ -88,7 +88,7 @@ export class ElasticSearchServiceController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("elastic-search", (ability: AppAbility) => ability.can(Action.Manage, ElasticSearchActions), ) @HttpCode(HttpStatus.OK) @@ -104,7 +104,7 @@ export class ElasticSearchServiceController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("elastic-search", (ability: AppAbility) => ability.can(Action.Manage, ElasticSearchActions), ) @HttpCode(HttpStatus.OK) @@ -120,7 +120,7 @@ export class ElasticSearchServiceController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("elastic-search", (ability: AppAbility) => ability.can(Action.Manage, ElasticSearchActions), ) @HttpCode(HttpStatus.OK) diff --git a/src/elastic-search/elastic-search.service.ts b/src/elastic-search/elastic-search.service.ts index 69782558b..36d6bb44b 100644 --- a/src/elastic-search/elastic-search.service.ts +++ b/src/elastic-search/elastic-search.service.ts @@ -26,9 +26,9 @@ import { import { ConfigService } from "@nestjs/config"; import { sleep } from "src/common/utils"; import { - transformKeysInObject, initialSyncTransform, transformFacets, + addValueType, } from "./helpers/utils"; import { SortFields } from "./providers/fields.enum"; @@ -86,6 +86,10 @@ export class ElasticSearchService implements OnModuleInit { const isIndexExists = await this.isIndexExists(this.defaultIndex); if (!isIndexExists) { await this.createIndex(this.defaultIndex); + Logger.log( + `New index ${this.defaultIndex}is created `, + "ElasticSearch", + ); } this.connected = true; Logger.log("Elasticsearch Connected", "ElasticSearch"); @@ -141,26 +145,18 @@ export class ElasticSearchService implements OnModuleInit { index, body: { settings: defaultElasticSettings, + mappings: { + dynamic: true, + dynamic_templates: dynamic_template, + numeric_detection: true, + date_detection: true, + dynamic_date_formats: [ + "yyyy-MM-dd'T'HH:mm:ss|| yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSSZ||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||yyyy-MM-dd'T'HH:mm:ss.SSS", + ], + properties: datasetMappings, + }, }, }); - await this.esService.indices.close({ index }); - await this.esService.indices.putSettings({ - index, - body: { - settings: defaultElasticSettings, - }, - }); - await this.esService.indices.putMapping({ - index, - dynamic: true, - body: { - dynamic_templates: dynamic_template, - properties: datasetMappings, - }, - }); - await this.esService.indices.open({ - index, - }); Logger.log( `Elasticsearch Index Created-> Index: ${index}`, "Elasticsearch", @@ -266,7 +262,7 @@ export class ElasticSearchService implements OnModuleInit { limit = 20, skip = 0, sort?: Record, - ): Promise<{ totalCount: number; data: string[] }> { + ): Promise<{ totalCount: number; data: (string | undefined)[] }> { const defaultMinScore = searchParam.text ? 1 : 0; try { @@ -362,7 +358,7 @@ export class ElasticSearchService implements OnModuleInit { async updateInsertDocument(data: Partial) { //NOTE: Replace all keys with lower case, also replace spaces and dot with underscore delete data._id; - const transformedScientificMetadata = transformKeysInObject( + const transformedScientificMetadata = addValueType( data.scientificMetadata as Record, ); diff --git a/src/elastic-search/helpers/utils.ts b/src/elastic-search/helpers/utils.ts index 0a0198b4b..9c8d28242 100644 --- a/src/elastic-search/helpers/utils.ts +++ b/src/elastic-search/helpers/utils.ts @@ -3,12 +3,17 @@ import { AggregationsFrequentItemSetsBucketKeys, } from "@elastic/elasticsearch/lib/api/types"; import { DatasetClass } from "src/datasets/schemas/dataset.schema"; -import { IFilter, ITransformedFullFacets } from "../interfaces/es-common.type"; +import { + IFilter, + ITransformedFullFacets, + nestedQueryObject, + ScientificQuery, +} from "../interfaces/es-common.type"; export const transformKey = (key: string): string => { return key.trim().replace(/[.]/g, "\\.").replace(/ /g, "_").toLowerCase(); }; -export const transformKeysInObject = (obj: Record) => { +export const addValueType = (obj: Record) => { const newObj: Record = {}; for (const [key, value] of Object.entries(obj)) { @@ -16,6 +21,7 @@ export const transformKeysInObject = (obj: Record) => { const isNumberValueType = typeof (value as Record)?.value === "number"; + if (isNumberValueType) { (value as Record)["value_type"] = "number"; } else { @@ -66,51 +72,103 @@ export const initialSyncTransform = (obj: DatasetClass) => { return modifiedDocInObject; }; +const extractNestedQueryOperationValue = (query: nestedQueryObject) => { + const field = Object.keys(query)[0]; + const operationWithPrefix = Object.keys(query[field])[0]; + + const value = + typeof query[field][operationWithPrefix] === "string" + ? (query[field][operationWithPrefix] as string).trim() + : query[field][operationWithPrefix]; + + const operation = operationWithPrefix.replace("$", ""); + + return { operation, value, field }; +}; + export const convertToElasticSearchQuery = ( - scientificQuery: Record, -) => { + scientificQuery: ScientificQuery, +): IFilter[] => { const filters: IFilter[] = []; for (const field in scientificQuery) { - const query = scientificQuery[field] as Record; - const operation = Object.keys(query)[0]; - const value = - typeof query[operation] === "string" - ? (query[operation] as string).trim() - : query[operation]; - - const esOperation = operation.replace("$", ""); - - // NOTE-EXAMPLE: - // trasnformedKey = "scientificMetadata.someKey.value" - // firstPart = "scientificMetadata", - // middlePart = "someKey" - const { transformedKey, firstPart, middlePart } = transformMiddleKey(field); - - let filter = {}; - - const fieldType = field.split(".").pop(); - - if (fieldType === "valueSI" || fieldType === "value") { - const numberFilter = { - term: { - [`${firstPart}.${middlePart}.value_type`]: - typeof value === "number" ? "number" : "string", + const query = scientificQuery[field]; + + if (field === "$and" && Array.isArray(query)) { + query.forEach((query: { $or: nestedQueryObject[] }) => { + const shouldQueries = query.$or.map((orQuery: nestedQueryObject) => { + const { operation, value, field } = + extractNestedQueryOperationValue(orQuery); + const filterType = operation === "eq" ? "term" : "range"; + return { + [filterType]: { + [field]: operation === "eq" ? value : { [operation]: value }, + }, + }; + }); + filters.push({ + bool: { + should: shouldQueries, + minimum_should_match: 1, + }, + }); + }); + } else if (field === "$or" && Array.isArray(query)) { + const shouldQueries = query.map((query: nestedQueryObject) => { + const { operation, value, field } = + extractNestedQueryOperationValue(query); + const filterType = operation === "eq" ? "term" : "range"; + return { + [filterType]: { + [field]: operation === "eq" ? value : { [operation]: value }, + }, + }; + }); + filters.push({ + bool: { + should: shouldQueries, + minimum_should_match: 1, }, - }; - filters.push(numberFilter); + }); + } else { + const operation = Object.keys(query)[0]; + + const value = + typeof (query as Record)[operation] === "string" + ? (query as Record)[operation].trim() + : (query as Record)[operation]; + const esOperation = operation.replace("$", ""); + + // NOTE: + // trasnformedKey = "scientificMetadata.someKey.value" + // firstPart = "scientificMetadata", + // middlePart = "someKey" + // lastPart = "value" + const { transformedKey, firstPart, middlePart, lastPart } = + transformMiddleKey(field); + + if (lastPart === "valueSI" || lastPart === "value") { + const numberFilter = { + term: { + [`${firstPart}.${middlePart}.value_type`]: + typeof value === "number" ? "number" : "string", + }, + }; + filters.push(numberFilter); + } + + const filter = + esOperation === "eq" + ? { + term: { [`${transformedKey}`]: value }, + } + : { + range: { [`${transformedKey}`]: { [esOperation]: value } }, + }; + + filters.push(filter); } - - filter = - esOperation === "eq" - ? { - term: { [`${transformedKey}`]: value }, - } - : { range: { [`${transformedKey}`]: { [esOperation]: value } } }; - - filters.push(filter); } - return filters; }; diff --git a/src/elastic-search/interfaces/es-common.type.ts b/src/elastic-search/interfaces/es-common.type.ts index 8cc66f227..8b49f5e01 100644 --- a/src/elastic-search/interfaces/es-common.type.ts +++ b/src/elastic-search/interfaces/es-common.type.ts @@ -20,6 +20,12 @@ export interface IShould { term?: { [key: string]: string | undefined; }; + range?: { + [key: string]: { + gte?: string | number; + lte?: string | number; + }; + }; } export interface IBoolShould { @@ -56,6 +62,10 @@ export interface IFilter { }; }; }; + bool?: { + should: IShould[]; + minimum_should_match?: number; + }; } export interface IFullFacets { @@ -81,3 +91,11 @@ export interface ITransformedFullFacets { } | { totalSets: number }; } + +export interface ScientificQuery { + [key: string]: Record | "$and" | "$or"; +} + +export interface nestedQueryObject { + [key: string]: Record; +} diff --git a/src/elastic-search/providers/query-builder.service.ts b/src/elastic-search/providers/query-builder.service.ts index 930f877f9..8d11fb6aa 100644 --- a/src/elastic-search/providers/query-builder.service.ts +++ b/src/elastic-search/providers/query-builder.service.ts @@ -7,6 +7,7 @@ import { IFullFacets, IShould, ObjectType, + ScientificQuery, } from "../interfaces/es-common.type"; import { FilterFields, @@ -125,7 +126,7 @@ export class SearchQueryService { ); const esScientificFilterQuery = convertToElasticSearchQuery( - scientificFilterQuery, + scientificFilterQuery as ScientificQuery, ); filterArray.push({ nested: { diff --git a/src/instruments/instruments.controller.ts b/src/instruments/instruments.controller.ts index b09fc7876..7ee9a6216 100644 --- a/src/instruments/instruments.controller.ts +++ b/src/instruments/instruments.controller.ts @@ -42,7 +42,7 @@ export class InstrumentsController { constructor(private readonly instrumentsService: InstrumentsService) {} @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("instruments", (ability: AppAbility) => ability.can(Action.InstrumentCreate, Instrument), ) @UseInterceptors( @@ -69,7 +69,7 @@ export class InstrumentsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("instruments", (ability: AppAbility) => ability.can(Action.InstrumentRead, Instrument), ) @Get() @@ -87,7 +87,7 @@ export class InstrumentsController { // GET /instrument/findOne @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("instruments", (ability: AppAbility) => ability.can(Action.InstrumentRead, Instrument), ) @Get("/findOne") @@ -117,7 +117,7 @@ export class InstrumentsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("instruments", (ability: AppAbility) => ability.can(Action.InstrumentRead, Instrument), ) @Get(":id") @@ -126,7 +126,7 @@ export class InstrumentsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("instruments", (ability: AppAbility) => ability.can(Action.InstrumentUpdate, Instrument), ) @UseInterceptors( @@ -152,7 +152,7 @@ export class InstrumentsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("instruments", (ability: AppAbility) => ability.can(Action.InstrumentDelete, Instrument), ) @Delete(":id") diff --git a/src/jobs/jobs.controller.ts b/src/jobs/jobs.controller.ts index 6430ca9ff..838fc724c 100644 --- a/src/jobs/jobs.controller.ts +++ b/src/jobs/jobs.controller.ts @@ -179,25 +179,26 @@ export class JobsController { // Indexing originDataBlock with pid and create set of files for each dataset const datasets = await this.datasetsService.findAll(filter); // Include origdatablocks - await Promise.all( + const aggregatedData = await Promise.all( datasets.map(async (dataset) => { - dataset.origdatablocks = await this.origDatablocksService.findAll( - { + return { + dataset: dataset, + origdatablocks: await this.origDatablocksService.findAll({ datasetId: dataset.pid, - }, - ); + }), + }; }), ); - const result: Record> = datasets.reduce( - (acc: Record>, dataset) => { + const result: Record> = aggregatedData.reduce( + (acc: Record>, data) => { // Using Set make searching more efficient - const files = dataset.origdatablocks.reduce((acc, block) => { + const files = data.origdatablocks.reduce((acc, block) => { block.dataFileList.forEach((file) => { acc.add(file.path); }); return acc; }, new Set()); - acc[dataset.pid] = files; + acc[data.dataset.pid] = files; return acc; }, {}, @@ -288,7 +289,9 @@ export class JobsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, JobClass)) + @CheckPolicies("jobs", (ability: AppAbility) => + ability.can(Action.Read, JobClass), + ) @Get() @ApiQuery({ name: "filter", @@ -304,7 +307,9 @@ export class JobsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, JobClass)) + @CheckPolicies("jobs", (ability: AppAbility) => + ability.can(Action.Read, JobClass), + ) @Get("/fullquery") async fullquery( @Query() filters: { fields?: string; limits?: string }, @@ -317,7 +322,9 @@ export class JobsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, JobClass)) + @CheckPolicies("jobs", (ability: AppAbility) => + ability.can(Action.Read, JobClass), + ) @Get("/fullfacet") async fullfacet( @Query() filters: { fields?: string; facets?: string }, @@ -330,14 +337,18 @@ export class JobsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, JobClass)) + @CheckPolicies("jobs", (ability: AppAbility) => + ability.can(Action.Read, JobClass), + ) @Get(":id") async findOne(@Param("id") id: string): Promise { return this.jobsService.findOne({ _id: id }); } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Update, JobClass)) + @CheckPolicies("jobs", (ability: AppAbility) => + ability.can(Action.Update, JobClass), + ) @Patch(":id") async update( @Param("id") id: string, @@ -356,7 +367,9 @@ export class JobsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Delete, JobClass)) + @CheckPolicies("jobs", (ability: AppAbility) => + ability.can(Action.Delete, JobClass), + ) @Delete(":id") async remove(@Param("id") id: string): Promise { return this.jobsService.remove({ _id: id }); diff --git a/src/logbooks/logbooks.controller.ts b/src/logbooks/logbooks.controller.ts index 58df20cda..628997c7c 100644 --- a/src/logbooks/logbooks.controller.ts +++ b/src/logbooks/logbooks.controller.ts @@ -22,7 +22,9 @@ export class LogbooksController { constructor(private readonly logbooksService: LogbooksService) {} @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Logbook)) + @CheckPolicies("logbooks", (ability: AppAbility) => + ability.can(Action.Read, Logbook), + ) @UseInterceptors(UsersLogbooksInterceptor) @Get() findAll() { @@ -30,7 +32,9 @@ export class LogbooksController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Logbook)) + @CheckPolicies("logbooks", (ability: AppAbility) => + ability.can(Action.Read, Logbook), + ) @UseInterceptors(UsersLogbooksInterceptor) @Get("/:name") async findByName( diff --git a/src/logbooks/logbooks.service.ts b/src/logbooks/logbooks.service.ts index 133c0a784..344596c5a 100644 --- a/src/logbooks/logbooks.service.ts +++ b/src/logbooks/logbooks.service.ts @@ -66,6 +66,11 @@ export class LogbooksService { ), ); + if (!res.data) { + Logger.log("Logbook not found", { name }); + return null; + } + Logger.log("Found logbook " + name, "LogbooksService.findByName"); const { skip, limit, sortField } = JSON.parse(filters); Logger.log( diff --git a/src/main.ts b/src/main.ts index a8c8aaec8..3af41b802 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,7 @@ import { SwaggerModule, } from "@nestjs/swagger"; import { AppModule } from "./app.module"; -import { Logger, ValidationPipe } from "@nestjs/common"; +import { Logger, ValidationPipe, VersioningType } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { AllExceptionsFilter, ScicatLogger } from "./loggers/logger.service"; @@ -15,6 +15,11 @@ async function bootstrap() { const app = await NestFactory.create(AppModule, { bufferLogs: true, }); + const configService: ConfigService, false> = app.get( + ConfigService, + ); + const apiVersion = configService.get("versions.api"); + const swaggerPath = `${configService.get("swaggerPath")}`; const scicatLogger = app.get(ScicatLogger); @@ -23,13 +28,24 @@ async function bootstrap() { app.useGlobalFilters(new AllExceptionsFilter(scicatLogger)); app.enableCors(); - app.setGlobalPrefix("api/v3"); + + app.setGlobalPrefix("api"); + + // NOTE: This is a workaround to enable versioning for individual routes + // Version decorator can be used to specify the version for a route + // Read more on https://docs.nestjs.com/techniques/versioning + app.enableVersioning({ + type: VersioningType.URI, + defaultVersion: apiVersion, + }); + const config = new DocumentBuilder() .setTitle("SciCat backend API") .setDescription("This is the API for the SciCat Backend") - .setVersion("" + process.env.npm_package_version) + .setVersion(`api/v${apiVersion}`) .addBearerAuth() .build(); + const document = SwaggerModule.createDocument(app, config); const swaggerOptions: SwaggerCustomOptions = { swaggerOptions: { @@ -37,7 +53,7 @@ async function bootstrap() { }, }; - SwaggerModule.setup("explorer", app, document, swaggerOptions); + SwaggerModule.setup(swaggerPath, app, document, swaggerOptions); app.useGlobalPipes( /** @@ -70,10 +86,6 @@ async function bootstrap() { app.use(json({ limit: "16mb" })); - const configService: ConfigService, false> = app.get( - ConfigService, - ); - const expressSessionSecret = configService.get( "expressSessionSecret", ); diff --git a/src/origdatablocks/origdatablocks.controller.ts b/src/origdatablocks/origdatablocks.controller.ts index ce68d9f22..97e21d50b 100644 --- a/src/origdatablocks/origdatablocks.controller.ts +++ b/src/origdatablocks/origdatablocks.controller.ts @@ -45,8 +45,8 @@ import { PartialUpdateDatasetDto } from "src/datasets/dto/update-dataset.dto"; import { filterDescription, filterExample } from "src/common/utils"; import { JWTUser } from "src/auth/interfaces/jwt-user.interface"; import { DatasetClass } from "src/datasets/schemas/dataset.schema"; -import { CreateRawDatasetDto } from "src/datasets/dto/create-raw-dataset.dto"; -import { CreateDerivedDatasetDto } from "src/datasets/dto/create-derived-dataset.dto"; +import { CreateRawDatasetObsoleteDto } from "src/datasets/dto/create-raw-dataset-obsolete.dto"; +import { CreateDerivedDatasetObsoleteDto } from "src/datasets/dto/create-derived-dataset-obsolete.dto"; import { logger } from "@user-office-software/duo-logger"; @ApiBearerAuth() @@ -90,7 +90,7 @@ export class OrigDatablocksController { // newDatasetClass.ownerGroup = dataset.ownerGroup; // if (user) { - // const ability = this.caslAbilityFactory.createForUser(user); + // const ability = this.caslAbilityFactory.createOrigDatablockForUser(user); // const canUpdate = ability.can(Action.Update, newDatasetClass); // if (!canUpdate) { // throw new ForbiddenException("Unauthorized access"); @@ -103,7 +103,10 @@ export class OrigDatablocksController { // } async generateOrigDatablockInstanceInstanceForPermissions( - dataset: CreateRawDatasetDto | CreateDerivedDatasetDto | DatasetClass, + dataset: + | CreateRawDatasetObsoleteDto + | CreateDerivedDatasetObsoleteDto + | DatasetClass, ): Promise { const origDatablockInstance = new OrigDatablock(); origDatablockInstance.datasetId = dataset.pid || ""; @@ -128,7 +131,7 @@ export class OrigDatablocksController { const origDatablockInstance = await this.generateOrigDatablockInstanceInstanceForPermissions(dataset); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.origDatablockInstanceAccess(user); let canDoAction = false; @@ -157,7 +160,7 @@ export class OrigDatablocksController { // POST /origdatablocks @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("origdatablocks", (ability: AppAbility) => ability.can(Action.OrigdatablockCreate, OrigDatablock), ) @HttpCode(HttpStatus.CREATED) @@ -226,7 +229,7 @@ export class OrigDatablocksController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("origdatablocks", (ability: AppAbility) => ability.can(Action.OrigdatablockCreate, OrigDatablock), ) @HttpCode(HttpStatus.OK) @@ -271,7 +274,7 @@ export class OrigDatablocksController { // GET /origdatablock @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("origdatablocks", (ability: AppAbility) => ability.can(Action.OrigdatablockRead, OrigDatablock), ) @Get() @@ -302,7 +305,7 @@ export class OrigDatablocksController { const user: JWTUser = request.user as JWTUser; const parsedFilters: IFilters = JSON.parse(filter ?? "{}"); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.origDatablockInstanceAccess(user); const canViewAny = ability.can(Action.OrigdatablockReadAny, OrigDatablock); if (!canViewAny) { parsedFilters.where = parsedFilters.where ?? {}; @@ -337,7 +340,7 @@ export class OrigDatablocksController { // GET /origdatablocks/fullquery @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("origdatablocks", (ability: AppAbility) => ability.can(Action.OrigdatablockRead, OrigDatablock), ) @Get("/fullquery") @@ -363,7 +366,7 @@ export class OrigDatablocksController { const user: JWTUser = request.user as JWTUser; const fields: IOrigDatablockFields = JSON.parse(filters.fields ?? "{}"); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.origDatablockInstanceAccess(user); const canViewAny = ability.can(Action.OrigdatablockReadAny, OrigDatablock); if (!canViewAny) { @@ -401,7 +404,7 @@ export class OrigDatablocksController { // GET /origdatablocks/fullquery/files @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("origdatablocks", (ability: AppAbility) => ability.can(Action.OrigdatablockRead, OrigDatablock), ) @Get("/fullquery/files") @@ -426,7 +429,7 @@ export class OrigDatablocksController { ): Promise { const user: JWTUser = request.user as JWTUser; const fields: IOrigDatablockFields = JSON.parse(filters.fields ?? "{}"); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.origDatablockInstanceAccess(user); const canViewAny = ability.can(Action.OrigdatablockReadAny, OrigDatablock); if (!canViewAny) { const canViewAccess = ability.can( @@ -456,7 +459,7 @@ export class OrigDatablocksController { // GET /origdatablocks/fullfacet @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("origdatablocks", (ability: AppAbility) => ability.can(Action.OrigdatablockRead, OrigDatablock), ) @Get("/fullfacet") @@ -467,7 +470,7 @@ export class OrigDatablocksController { const user: JWTUser = request.user as JWTUser; const fields: IOrigDatablockFields = JSON.parse(filters.fields ?? "{}"); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.origDatablockInstanceAccess(user); const canViewAny = ability.can(Action.OrigdatablockReadAny, OrigDatablock); if (!canViewAny) { const canViewAccess = ability.can( @@ -497,7 +500,7 @@ export class OrigDatablocksController { // GET /origdatablocks/fullfacet/files @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("origdatablocks", (ability: AppAbility) => ability.can(Action.OrigdatablockRead, OrigDatablock), ) @Get("/fullfacet/files") @@ -508,7 +511,7 @@ export class OrigDatablocksController { const user: JWTUser = request.user as JWTUser; const fields: IOrigDatablockFields = JSON.parse(filters.fields ?? "{}"); - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.origDatablockInstanceAccess(user); const canViewAny = ability.can(Action.OrigdatablockReadAny, OrigDatablock); if (!canViewAny) { const canViewAccess = ability.can( @@ -542,7 +545,7 @@ export class OrigDatablocksController { // GET /origdatablocks/:id @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("origdatablocks", (ability: AppAbility) => ability.can(Action.OrigdatablockRead, OrigDatablock), ) @Get("/:id") @@ -576,7 +579,7 @@ export class OrigDatablocksController { // PATCH /origdatablocks/:id @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("origdatablocks", (ability: AppAbility) => ability.can(Action.OrigdatablockUpdate, OrigDatablock), ) @Patch("/:id") @@ -623,7 +626,7 @@ export class OrigDatablocksController { // DELETE /origdatablocks/:id @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("origdatablocks", (ability: AppAbility) => ability.can(Action.OrigdatablockDelete, OrigDatablock), ) @Delete("/:id") diff --git a/src/policies/policies.controller.ts b/src/policies/policies.controller.ts index 0ee2ea98f..bbc9353dd 100644 --- a/src/policies/policies.controller.ts +++ b/src/policies/policies.controller.ts @@ -84,7 +84,8 @@ export class PoliciesController { const user: JWTUser = request.user as JWTUser; if (user) { - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.policyEndpointAccess(user); + // these actions are not defined in casl const canViewAll = ability.can(Action.ListAll, Policy); const canViewTheirOwn = ability.can(Action.ListOwn, Policy); if (!canViewAll && canViewTheirOwn) { @@ -102,14 +103,18 @@ export class PoliciesController { return mergedFilters; } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Create, Policy)) + @CheckPolicies("policies", (ability: AppAbility) => + ability.can(Action.Create, Policy), + ) @Post() async create(@Body() createPolicyDto: CreatePolicyDto): Promise { return this.policiesService.create(createPolicyDto); } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Policy)) + @CheckPolicies("policies", (ability: AppAbility) => + ability.can(Action.Read, Policy), + ) @Get() @ApiQuery({ name: "filter", @@ -133,7 +138,9 @@ export class PoliciesController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Policy)) + @CheckPolicies("policies", (ability: AppAbility) => + ability.can(Action.Read, Policy), + ) @Get("/count") async count(@Query("where") where?: string): Promise<{ count: number }> { const parsedWhere: FilterQuery = JSON.parse(where ?? "{}"); @@ -141,7 +148,9 @@ export class PoliciesController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Update, Policy)) + @CheckPolicies("policies", (ability: AppAbility) => + ability.can(Action.Update, Policy), + ) @UseInterceptors(HistoryInterceptor) @HttpCode(HttpStatus.OK) @Post("/updateWhere") @@ -153,14 +162,18 @@ export class PoliciesController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Policy)) + @CheckPolicies("policies", (ability: AppAbility) => + ability.can(Action.Read, Policy), + ) @Get(":id") async findOne(@Param("id") id: string): Promise { return this.policiesService.findOne({ _id: id }); } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Update, Policy)) + @CheckPolicies("policies", (ability: AppAbility) => + ability.can(Action.Update, Policy), + ) @Patch(":id") async update( @Param("id") id: string, @@ -170,7 +183,9 @@ export class PoliciesController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Delete, Policy)) + @CheckPolicies("policies", (ability: AppAbility) => + ability.can(Action.Delete, Policy), + ) @Delete(":id") async remove(@Param("id") id: string): Promise { return this.policiesService.remove({ _id: id }); diff --git a/src/proposals/proposals.controller.ts b/src/proposals/proposals.controller.ts index 324bd0b2f..ff509c0fc 100644 --- a/src/proposals/proposals.controller.ts +++ b/src/proposals/proposals.controller.ts @@ -33,7 +33,6 @@ import { ApiTags, } from "@nestjs/swagger"; import { PoliciesGuard } from "src/casl/guards/policies.guard"; -import { AuthenticatedPoliciesGuard } from "../casl/guards/auth-check.guard"; import { CheckPolicies } from "src/casl/decorators/check-policies.decorator"; import { AppAbility, CaslAbilityFactory } from "src/casl/casl-ability.factory"; import { Action } from "src/casl/action.enum"; @@ -100,7 +99,7 @@ export class ProposalsController { ); const user: JWTUser = request.user as JWTUser; - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.proposalsInstanceAccess(user); try { switch (group) { @@ -181,7 +180,7 @@ export class ProposalsController { ); if (!canDoAction) { - throw new ForbiddenException("Unauthorized access"); + throw new ForbiddenException("Unauthorized to this proposal"); } } return proposal; @@ -209,7 +208,7 @@ export class ProposalsController { mergedFilters.where = mergedFilters.where || {}; if (user) { - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.proposalsInstanceAccess(user); const canViewAll = ability.can(Action.ProposalsReadAny, ProposalClass); if (!canViewAll) { const canViewAccess = ability.can( @@ -244,7 +243,7 @@ export class ProposalsController { // POST /proposals @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsCreate, ProposalClass), ) @UseInterceptors( @@ -292,7 +291,7 @@ export class ProposalsController { } @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsCreate, ProposalClass), ) @HttpCode(HttpStatus.OK) @@ -331,7 +330,7 @@ export class ProposalsController { // GET /proposals @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsRead, ProposalClass), ) @Get() @@ -367,7 +366,7 @@ export class ProposalsController { // GET /proposals/fullquery @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsRead, ProposalClass), ) @Get("/fullquery") @@ -408,7 +407,7 @@ export class ProposalsController { const fields: IProposalFields = JSON.parse(filters.fields ?? "{}"); const limits: ILimitsFilter = JSON.parse(filters.limits ?? "{}"); if (user) { - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.proposalsInstanceAccess(user); const canViewAll = ability.can(Action.ProposalsReadAny, ProposalClass); if (!canViewAll) { @@ -446,7 +445,7 @@ export class ProposalsController { // GET /proposals/fullfacet @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsRead, ProposalClass), ) @Get("/fullfacet") @@ -479,7 +478,7 @@ export class ProposalsController { const fields: IProposalFields = JSON.parse(filters.fields ?? "{}"); const facets = JSON.parse(filters.facets ?? "[]"); if (user) { - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.proposalsInstanceAccess(user); const canViewAll = ability.can(Action.ProposalsReadAny, ProposalClass); if (!canViewAll) { @@ -516,9 +515,9 @@ export class ProposalsController { return this.proposalsService.fullfacet(parsedFilters); } - // GET /proposals/:id - @UseGuards(AuthenticatedPoliciesGuard) - @CheckPolicies((ability: AppAbility) => + // GET /proposals/:pid + @UseGuards(PoliciesGuard) + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsRead, ProposalClass), ) @Get("/:pid") @@ -532,7 +531,7 @@ export class ProposalsController { type: String, }) @ApiResponse({ - status: 200, + status: HttpStatus.OK, type: ProposalClass, isArray: false, description: "Return proposal with pid specified", @@ -546,12 +545,52 @@ export class ProposalsController { proposalId, Action.ProposalsRead, ); + return proposal; } + // GET /proposals/:pid/authorization + @UseGuards(PoliciesGuard) + @CheckPolicies("proposals", (ability: AppAbility) => + ability.can(Action.ProposalsRead, ProposalClass), + ) + @Get("/:pid/authorization") + @ApiOperation({ + summary: "Check user access to a specific proposal.", + description: + "Returns a boolean indicating whether the user has access to the proposal with the specified ID.", + }) + @ApiParam({ + name: "pid", + description: "ID of the proposal to check access for", + type: String, + }) + @ApiResponse({ + status: HttpStatus.OK, + type: Boolean, + description: + "Returns true if the user has access to the specified proposal, otherwise false.", + }) + async findByIdAccess( + @Req() request: Request, + @Param("pid") proposalId: string, + ): Promise<{ canAccess: boolean }> { + const proposal = await this.proposalsService.findOne({ + proposalId, + }); + if (!proposal) return { canAccess: false }; + + const canAccess = await this.permissionChecker( + Action.ProposalsRead, + proposal, + request, + ); + return { canAccess }; + } + // PATCH /proposals/:pid @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsUpdate, ProposalClass), ) @UseInterceptors( @@ -599,7 +638,7 @@ export class ProposalsController { // DELETE /proposals/:id @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsDelete, ProposalClass), ) @Delete("/:pid") @@ -630,7 +669,7 @@ export class ProposalsController { // POST /proposals/:id/attachments @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsAttachmentCreate, ProposalClass), ) @Post("/:pid/attachments") @@ -675,7 +714,7 @@ export class ProposalsController { // GET /proposals/:pid/attachments @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsAttachmentRead, ProposalClass), ) @Get("/:pid/attachments") @@ -711,7 +750,7 @@ export class ProposalsController { // PATCH /proposals/:pid/attachments/:aid @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsAttachmentUpdate, ProposalClass), ) @Patch("/:pid/attachments/:aid") @@ -758,7 +797,7 @@ export class ProposalsController { // DELETE /proposals/:pid/attachments/:aid @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsAttachmentDelete, ProposalClass), ) @Delete("/:pid/attachments/:aid") @@ -802,7 +841,7 @@ export class ProposalsController { // GET /proposals/:id/datasets @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("proposals", (ability: AppAbility) => ability.can(Action.ProposalsDatasetRead, ProposalClass), ) @Get("/:pid/datasets") @@ -830,7 +869,7 @@ export class ProposalsController { @Param("pid") proposalId: string, ): Promise { const user: JWTUser = request.user as JWTUser; - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.proposalsInstanceAccess(user); const canViewAny = ability.can(Action.DatasetReadAny, DatasetClass); const fields: IDatasetFields = JSON.parse("{}"); diff --git a/src/published-data/published-data.controller.ts b/src/published-data/published-data.controller.ts index c8030b6ca..0786e9a53 100644 --- a/src/published-data/published-data.controller.ts +++ b/src/published-data/published-data.controller.ts @@ -69,7 +69,7 @@ export class PublishedDataController { // POST /publisheddata @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("publisheddata", (ability: AppAbility) => ability.can(Action.Create, PublishedData), ) @Post() @@ -155,7 +155,7 @@ export class PublishedDataController { // GET /publisheddata/formpopulate @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("publisheddata", (ability: AppAbility) => ability.can(Action.Read, PublishedData), ) @Get("/formpopulate") @@ -166,13 +166,17 @@ export class PublishedDataController { }) async formPopulate(@Query("pid") pid: string) { const formData: IFormPopulateData = {}; - const dataset = await this.datasetsService.findOne({ where: { pid } }); + const dataset = (await this.datasetsService.findOne({ + where: { pid }, + })) as unknown as DatasetClass; let proposalId; if (dataset) { formData.resourceType = dataset.type; formData.description = dataset.description; - proposalId = (dataset as unknown as DatasetClass).proposalId; + if ("proposalIds" in dataset) { + proposalId = dataset.proposalIds![0]; + } } let proposal; @@ -221,7 +225,7 @@ export class PublishedDataController { // PATCH /publisheddata/:id @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("publisheddata", (ability: AppAbility) => ability.can(Action.Update, PublishedData), ) @Patch("/:id") @@ -237,7 +241,7 @@ export class PublishedDataController { // DELETE /publisheddata/:id @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("publisheddata", (ability: AppAbility) => ability.can(Action.Delete, PublishedData), ) @Delete("/:id") @@ -247,7 +251,7 @@ export class PublishedDataController { // POST /publisheddata/:id/register @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("publisheddata", (ability: AppAbility) => ability.can(Action.Update, PublishedData), ) @Post("/:id/register") @@ -428,7 +432,7 @@ export class PublishedDataController { // POST /publisheddata/:id/resync @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("publisheddata", (ability: AppAbility) => ability.can(Action.Update, PublishedData), ) @ApiOperation({ diff --git a/src/samples/samples.controller.ts b/src/samples/samples.controller.ts index 4f49a3dac..e5c5feaf7 100644 --- a/src/samples/samples.controller.ts +++ b/src/samples/samples.controller.ts @@ -63,6 +63,7 @@ import { Request } from "express"; import { JWTUser } from "src/auth/interfaces/jwt-user.interface"; import { IDatasetFields } from "src/datasets/interfaces/dataset-filters.interface"; import { CreateSubAttachmentDto } from "src/attachments/dto/create-sub-attachment.dto"; +import { AuthenticatedPoliciesGuard } from "src/casl/guards/auth-check.guard"; @ApiBearerAuth() @ApiTags("samples") @@ -96,7 +97,7 @@ export class SamplesController { ); const user: JWTUser = request.user as JWTUser; - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.samplesInstanceAccess(user); try { switch (group) { @@ -165,9 +166,8 @@ export class SamplesController { if (sample) { const canDoAction = await this.permissionChecker(group, sample, request); - if (!canDoAction) { - throw new ForbiddenException("Unauthorized to update this sample"); + throw new ForbiddenException("Unauthorized to this sample"); } } @@ -198,7 +198,7 @@ export class SamplesController { /* eslint-disable @typescript-eslint/no-explicit-any */ const authorizationFilter: Record = { where: {} }; if (user) { - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.samplesInstanceAccess(user); const canViewAll = ability.can(Action.SampleReadAny, SampleClass); if (!canViewAll) { const canViewAccess = ability.can( @@ -242,8 +242,8 @@ export class SamplesController { return mergedFilters; } // POST /samples - @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleCreate, SampleClass), ) @UseInterceptors( @@ -281,7 +281,7 @@ export class SamplesController { // GET /samples @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleRead, SampleClass), ) @Get() @@ -314,8 +314,8 @@ export class SamplesController { } // GET /samples/fullquery - @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleRead, SampleClass), ) @Get("/fullquery") @@ -356,7 +356,7 @@ export class SamplesController { const fields: ISampleFields = JSON.parse(filters.fields ?? "{}"); const limits: ILimitsFilter = JSON.parse(filters.limits ?? "{}"); if (user) { - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.samplesInstanceAccess(user); const canViewAll = ability.can(Action.SampleReadAny, SampleClass); if (!canViewAll) { @@ -391,8 +391,8 @@ export class SamplesController { } // GET /samples/metadataKeys - @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleRead, SampleClass), ) @Get("/metadataKeys") @@ -427,7 +427,7 @@ export class SamplesController { const fields: ISampleFields = JSON.parse(filters.fields ?? "{}"); const limits: ILimitsFilter = JSON.parse(filters.limits ?? "{}"); if (user) { - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.samplesInstanceAccess(user); const canViewAll = ability.can(Action.SampleReadAny, SampleClass); if (!canViewAll) { @@ -463,8 +463,8 @@ export class SamplesController { } // GET /samples/findOne - @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleRead, SampleClass), ) @Get("/findOne") @@ -523,7 +523,7 @@ export class SamplesController { // GET /samples/:id @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleRead, SampleClass), ) @Get("/:id") @@ -546,13 +546,55 @@ export class SamplesController { @Req() request: Request, @Param("id") id: string, ): Promise { - await this.checkPermissionsForSample(request, id, Action.SampleRead); - return this.samplesService.findOne({ sampleId: id }); + const sample = await this.checkPermissionsForSample( + request, + id, + Action.SampleRead, + ); + return sample; } - // PATCH /samples/:id + // GET /samples/:id/authorization @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("samples", (ability: AppAbility) => + ability.can(Action.SampleRead, SampleClass), + ) + @Get("/:id/authorization") + @ApiOperation({ + summary: "Check user access to a specific sample.", + description: + "Returns a boolean indicating whether the user has access to the sample with the specified ID.", + }) + @ApiParam({ + name: "id", + description: "ID of the sample to check access for", + type: String, + }) + @ApiResponse({ + status: HttpStatus.OK, + type: Boolean, + description: + "Returns true if the user has access to the specified sample, otherwise false.", + }) + async findByIdAccess( + @Req() request: Request, + @Param("id") id: string, + ): Promise<{ canAccess: boolean }> { + const sample = await this.samplesService.findOne({ + sampleId: id, + }); + if (!sample) return { canAccess: false }; + const canAccess = await this.permissionChecker( + Action.SampleRead, + sample, + request, + ); + return { canAccess }; + } + + // PATCH /samples/:id + @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleUpdate, SampleClass), ) @UseInterceptors( @@ -592,7 +634,7 @@ export class SamplesController { // DELETE /samples/:id @UseGuards() - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleDelete, SampleClass), ) @Delete("/:id") @@ -618,8 +660,8 @@ export class SamplesController { } // POST /samples/:id/attachments - @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleAttachmentDelete, SampleClass), ) @Post("/:id/attachments") @@ -671,7 +713,7 @@ export class SamplesController { // GET /samples/:id/attachments @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleAttachmentRead, SampleClass), ) @Get("/:id/attachments") @@ -704,8 +746,8 @@ export class SamplesController { } // GET /samples/:id/attachments/:fk - @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleAttachmentRead, SampleClass), ) @Get("/:id/attachments/:fk") @@ -732,23 +774,23 @@ export class SamplesController { }) async findOneAttachment( @Req() request: Request, - @Param("id") sampleId: string, + @Param("id") id: string, @Param("fk") attachmentId: string, ): Promise { await this.checkPermissionsForSample( request, - sampleId, + id, Action.SampleAttachmentRead, ); return this.attachmentsService.findOne({ id: attachmentId, - sampleId: sampleId, + sampleId: id, }); } // DELETE /samples/:id/attachments/:fk - @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleAttachmentDelete, SampleClass), ) @Delete("/:id/attachments/:fk") @@ -774,23 +816,23 @@ export class SamplesController { }) async findOneAttachmentAndRemove( @Req() request: Request, - @Param("id") sampleId: string, + @Param("id") id: string, @Param("fk") attachmentId: string, ): Promise { await this.checkPermissionsForSample( request, - sampleId, + id, Action.SampleAttachmentDelete, ); return this.attachmentsService.findOneAndDelete({ _id: attachmentId, - sampleId: sampleId, + sampleId: id, }); } // POST /samples/:id/datasets - /* @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Create, Dataset)) + /* @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.Create, Dataset)) @Post("/:id/datasets") async createDataset( @Param("id") id: string, @@ -802,8 +844,8 @@ export class SamplesController { */ // GET /samples/:id/datasets - @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.SampleDatasetRead, SampleClass), ) @Get("/:id/datasets") @@ -825,10 +867,10 @@ export class SamplesController { }) async findAllDatasets( @Req() request: Request, - @Param("id") sampleId: string, + @Param("id") id: string, ): Promise { const user: JWTUser = request.user as JWTUser; - const ability = this.caslAbilityFactory.createForUser(user); + const ability = this.caslAbilityFactory.samplesInstanceAccess(user); const canViewAny = ability.can(Action.DatasetReadAny, DatasetClass); const fields: IDatasetFields = JSON.parse("{}"); @@ -858,15 +900,15 @@ export class SamplesController { } const dataset = await this.datasetsService.fullquery({ - where: { sampleId }, + where: { sampleId: id }, fields: fields, }); return dataset; } // PATCH /samples/:id/datasets/:fk - /* @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Update, Dataset)) + /* @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.Update, Dataset)) @Patch("/:id/datasets/:fk") async findOneDatasetAndUpdate( @Param("id") sampleId: string, @@ -880,8 +922,8 @@ export class SamplesController { } */ // DELETE /samples/:id/datasets/:fk - /* @UseGuards(PoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.Delete, Dataset)) + /* @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("samples", (ability: AppAbility) => ability.can(Action.Delete, Dataset)) @Delete("/:id/datasets/:fk") async findOneDatasetAndRemove( @Param("id") sampleId: string, diff --git a/src/samples/samples.service.spec.ts b/src/samples/samples.service.spec.ts index 9425bec06..1933161b4 100644 --- a/src/samples/samples.service.spec.ts +++ b/src/samples/samples.service.spec.ts @@ -91,9 +91,23 @@ describe("SamplesService", () => { $text: { $search: "test", }, - "sampleCharacteristics.test.value": { - $eq: "test", - }, + $or: [ + { + "sampleCharacteristics.test": { + $eq: "test", + }, + }, + { + "sampleCharacteristics.test.v": { + $eq: "test", + }, + }, + { + "sampleCharacteristics.test.value": { + $eq: "test", + }, + }, + ], }; await service.fullquery(filter); diff --git a/src/users/dto/create-user-settings.dto.ts b/src/users/dto/create-user-settings.dto.ts index 923d616bf..63095f1c2 100644 --- a/src/users/dto/create-user-settings.dto.ts +++ b/src/users/dto/create-user-settings.dto.ts @@ -1,7 +1,9 @@ import { ApiProperty } from "@nestjs/swagger"; import { UpdateUserSettingsDto } from "./update-user-settings.dto"; +import { IsString } from "class-validator"; export class CreateUserSettingsDto extends UpdateUserSettingsDto { @ApiProperty({ type: String, required: true }) + @IsString() readonly userId: string; } diff --git a/src/users/dto/update-user-settings.dto.ts b/src/users/dto/update-user-settings.dto.ts index 9c6741b9e..63794c6ea 100644 --- a/src/users/dto/update-user-settings.dto.ts +++ b/src/users/dto/update-user-settings.dto.ts @@ -1,14 +1,23 @@ import { ApiProperty, PartialType } from "@nestjs/swagger"; +import { IsNumber, IsObject, IsOptional } from "class-validator"; export class UpdateUserSettingsDto { - @ApiProperty() - readonly columns: Record[]; - @ApiProperty({ type: Number, required: false, default: 25 }) + @IsNumber() readonly datasetCount?: number; @ApiProperty({ type: Number, required: false, default: 25 }) + @IsNumber() readonly jobCount?: number; + + @ApiProperty({ + type: Object, + required: false, + default: {}, + }) + @IsOptional() + @IsObject() + readonly externalSettings?: Record; } export class PartialUpdateUserSettingsDto extends PartialType( diff --git a/src/users/interceptors/create-user-settings.interceptor.ts b/src/users/interceptors/create-user-settings.interceptor.ts deleted file mode 100644 index d059602db..000000000 --- a/src/users/interceptors/create-user-settings.interceptor.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - CallHandler, - ExecutionContext, - Injectable, - Logger, - NestInterceptor, -} from "@nestjs/common"; -import { Observable, tap } from "rxjs"; -import { CreateUserSettingsDto } from "../dto/create-user-settings.dto"; -import { UsersService } from "../users.service"; - -@Injectable() -export class CreateUserSettingsInterceptor implements NestInterceptor { - constructor(private usersService: UsersService) {} - async intercept( - context: ExecutionContext, - next: CallHandler, - ): Promise> { - return next.handle().pipe( - tap(async () => { - const res = context.switchToHttp().getResponse(); - const user = res.req.user; - if (!user) { - return; - } - const userId = user._id; - const userSettings = - await this.usersService.findByIdUserSettings(userId); - if (!userSettings) { - Logger.log( - `Adding default settings to user ${user.username}`, - "CreateUserSettingsInterceptor", - ); - const createUserSettingsDto: CreateUserSettingsDto = { - userId, - columns: [], - }; - return this.usersService.createUserSettings( - userId, - createUserSettingsDto, - ); - } - return; - }), - ); - } -} diff --git a/src/users/schemas/user-settings.schema.ts b/src/users/schemas/user-settings.schema.ts index d0682b629..e5d524936 100644 --- a/src/users/schemas/user-settings.schema.ts +++ b/src/users/schemas/user-settings.schema.ts @@ -5,6 +5,13 @@ import { Document } from "mongoose"; export type UserSettingsDocument = UserSettings & Document; +// Define the Condition interface +export interface ScientificCondition { + field: string; + value: string; + operator: string; +} + @Schema({ collection: "UserSetting", toJSON: { @@ -16,14 +23,6 @@ export class UserSettings { id?: string; - @ApiProperty({ - type: [Object], - default: [], - description: "Array of the users preferred columns in dataset table", - }) - @Prop({ type: [Object], default: [] }) - columns: Record[]; - @ApiProperty({ type: Number, default: 25, @@ -43,6 +42,15 @@ export class UserSettings { @ApiProperty({ type: String, required: true }) @Prop({ type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }) userId: string; + + @ApiProperty({ + type: "object", + default: {}, + description: + "A customizable object for storing the user's external settings, which can contain various nested properties and configurations.", + }) + @Prop({ type: Object, default: {}, required: false }) + externalSettings: Record; } export const UserSettingsSchema = SchemaFactory.createForClass(UserSettings); diff --git a/src/users/user-identities.controller.ts b/src/users/user-identities.controller.ts index a20b421b3..9a4105710 100644 --- a/src/users/user-identities.controller.ts +++ b/src/users/user-identities.controller.ts @@ -8,7 +8,13 @@ import { Req, UseGuards, } from "@nestjs/common"; -import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { + ApiBearerAuth, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, +} from "@nestjs/swagger"; import { Action } from "src/casl/action.enum"; import { AppAbility, CaslAbilityFactory } from "src/casl/casl-ability.factory"; import { CheckPolicies } from "src/casl/decorators/check-policies.decorator"; @@ -18,6 +24,11 @@ import { Request } from "express"; import { JWTUser } from "src/auth/interfaces/jwt-user.interface"; import { User } from "./schemas/user.schema"; import { AuthenticatedPoliciesGuard } from "../casl/guards/auth-check.guard"; +import { boolean } from "mathjs"; +import { + filterUserIdentityDescription, + filterUserIdentityExample, +} from "src/common/utils"; @ApiBearerAuth() @ApiTags("user identities") @@ -30,11 +41,33 @@ export class UserIdentitiesController { @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies( + "users", (ability: AppAbility) => ability.can(Action.UserReadOwn, User) || ability.can(Action.UserReadAny, User), ) @Get("/findOne") + @ApiOperation({ + summary: + "It returns the user identity profile of the first user matching the query", + description: + "This endpoint returns the user identity profile of the first user matching teh condition", + }) + @ApiQuery({ + name: "filter", + description: + "Full database filters to apply when checking for the email. The filter can be just the where clause or the full filter syntax\n" + + filterUserIdentityDescription, + required: false, + type: String, + example: filterUserIdentityExample, + }) + @ApiResponse({ + status: 201, + type: boolean, + description: + "Results is true if a registered user exists that have the emailed provided listed as main email", + }) async findOne( // NOTE: This now supports both headers filter and query filter. // There is a loopback config file where we have this as a setting on the frontend. @@ -56,7 +89,7 @@ export class UserIdentitiesController { const authenticatedUser: JWTUser = request.user as JWTUser; const ability = - await this.caslAbilityFactory.createForUser(authenticatedUser); + await this.caslAbilityFactory.userEndpointAccess(authenticatedUser); if ( !ability.can(Action.UserReadAny, User) && @@ -88,7 +121,31 @@ export class UserIdentitiesController { } @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies("users", (ability: AppAbility) => + ability.can(Action.UserReadAny, User), + ) @Get("/isValidEmail") + @ApiOperation({ + summary: + "It returns true if the emailed passed in is linked to any registered users", + description: + "This endpoint check if the email passed in as parameter is a valid email connected to a known user that has a record in this instance of SciCat", + }) + @ApiQuery({ + name: "filter", + description: + "Email to be checked or full filter format query to apply when checking for the email\n" + + filterUserIdentityDescription, + required: false, + type: String, + example: filterUserIdentityExample, + }) + @ApiResponse({ + status: 201, + type: boolean, + description: + "Results is true if a registered user exists that have the emailed provided listed as main email", + }) async isValidEmail( // NOTE: This now supports both headers filter and query filter. // There is a loopback config file where we have this as a setting on the frontend. @@ -96,7 +153,17 @@ export class UserIdentitiesController { @Headers() headers: Record, @Query("filter") queryFilters?: string, ): Promise { - const parsedQueryFilters = JSON.parse(queryFilters ?? "{}"); + let parsedQueryFilters; + try { + parsedQueryFilters = JSON.parse(queryFilters ?? "{}"); + } catch { + parsedQueryFilters = { + where: { + "profile.email": queryFilters, + }, + }; + } + let filter = {}; if (headers.filter) { const parsedFilter = JSON.parse(headers.filter); @@ -111,10 +178,6 @@ export class UserIdentitiesController { filter, )) as UserIdentity; - if (!identity) { - return false; - } - - return true; + return identity ? true : false; } } diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index 1b22d0350..5c111e716 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -3,17 +3,44 @@ import { AuthService } from "src/auth/auth.service"; import { CaslModule } from "src/casl/casl.module"; import { UsersController } from "./users.controller"; import { UsersService } from "./users.service"; +import { UpdateUserSettingsDto } from "./dto/update-user-settings.dto"; +import { Request } from "express"; +import { UserSettings } from "./schemas/user-settings.schema"; class UsersServiceMock { findByIdUserIdentity(id: string) { return { id }; } + + async findByIdUserSettings(userId: string) { + return mockUserSettings; + } + + async findOneAndUpdateUserSettings( + userId: string, + updateUserSettingsDto: UpdateUserSettingsDto, + ) { + return { ...updateUserSettingsDto, _id: userId }; + } } +const mockUserSettings = { + _id: "user1", + userId: "user1", + datasetCount: 25, + jobCount: 25, + externalSettings: { + filters: [{ LocationFilter: true }, { PidFilter: true }], + conditions: [{ field: "status", value: "active", operator: "equals" }], + columns: [], + }, +}; + class AuthServiceMock {} describe("UsersController", () => { let controller: UsersController; + let usersService: UsersService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -26,9 +53,86 @@ describe("UsersController", () => { }).compile(); controller = module.get(UsersController); + usersService = module.get(UsersService); + + jest + .spyOn(controller as UsersController, "checkUserAuthorization") + .mockImplementation(() => Promise.resolve()); }); it("should be defined", () => { expect(controller).toBeDefined(); }); + + it("should return user settings with filters and conditions", async () => { + const userId = "user1"; + mockUserSettings._id = userId; + + const mockRequest: Partial = { + user: { _id: userId }, + }; + + const result = await controller.getSettings(mockRequest as Request, userId); + + // Assert + expect(result).toEqual(mockUserSettings); + expect(result?.externalSettings?.filters).toBeDefined(); + expect( + (result?.externalSettings?.filters as Record).length, + ).toBeGreaterThan(0); + expect(result?.externalSettings?.conditions).toBeDefined(); + expect( + (result?.externalSettings?.conditions as Record).length, + ).toBeGreaterThan(0); + }); + + it("should update user settings with filters and conditions", async () => { + const userId = "user-id"; + mockUserSettings._id = userId; + + const updatedSettings = { + ...mockUserSettings, + externalSettings: { + filters: [{ PidFilter: true }], + conditions: [ + { field: "status", value: "inactive", operator: "equals" }, + ], + columns: [], + }, + }; + + const mockRequest: Partial = { + user: { _id: userId }, + }; + + const expectedResponse: UserSettings = { + ...updatedSettings, + _id: userId, + userId: userId, + datasetCount: updatedSettings.datasetCount, + jobCount: updatedSettings.jobCount, + externalSettings: updatedSettings.externalSettings, + }; + + jest + .spyOn(usersService, "findOneAndUpdateUserSettings") + .mockResolvedValue(expectedResponse); + + const result = await controller.updateSettings( + mockRequest as Request, + userId, + updatedSettings, + ); + + expect(result).toEqual(expectedResponse); + expect(result?.externalSettings?.filters).toBeDefined(); + expect( + (result?.externalSettings?.filters as Record).length, + ).toBe(1); + expect(result?.externalSettings?.conditions).toBeDefined(); + expect( + (result?.externalSettings?.conditions as Record) + .length, + ).toBe(1); + }); }); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index f5438de8b..0747b450c 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -7,7 +7,6 @@ import { Req, Patch, Delete, - UseInterceptors, Put, Body, ForbiddenException, @@ -33,7 +32,6 @@ import { UserSettings } from "./schemas/user-settings.schema"; import { CreateUserSettingsDto } from "./dto/create-user-settings.dto"; import { PartialUpdateUserSettingsDto } from "./dto/update-user-settings.dto"; import { User } from "./schemas/user.schema"; -import { CreateUserSettingsInterceptor } from "./interceptors/create-user-settings.interceptor"; import { AuthService } from "src/auth/auth.service"; import { CredentialsDto } from "src/auth/dto/credentials.dto"; import { LocalAuthGuard } from "src/auth/guards/local-auth.guard"; @@ -65,7 +63,8 @@ export class UsersController { viewedUserSchema._id = viewedUserId; viewedUserSchema.id = viewedUserId; - const ability = this.caslAbilityFactory.createForUser(authenticatedUser); + const ability = + this.caslAbilityFactory.userEndpointAccess(authenticatedUser); // const authorized = actions.map( action => // ability.can(action, viewedUserSchema) // ) as Array; @@ -96,24 +95,25 @@ export class UsersController { @UseGuards(LocalAuthGuard) @Post("login") @ApiOperation({ - summary: "Functional accounts login.", - description: "It allows to login with functional (local) accounts.", + summary: + "This endpoint is deprecated and will be removed soon. Use /auth/login instead", + description: + "This endpoint is deprecated and will be removed soon. Use /auth/login instead", }) @ApiResponse({ status: 201, type: ReturnedAuthLoginDto, description: - "Create a new JWT token for anonymous or the user that is currently logged in", + "This endpoint is deprecated and will be removed soon. Use /auth/login instead", }) - async login( - @Req() req: Record, - ): Promise { - return await this.authService.login(req.user as Omit); + async login(@Req() req: Record): Promise { + return null; } @UseGuards(AuthenticatedPoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.UserReadOwn, User)) - @UseInterceptors(CreateUserSettingsInterceptor) + @CheckPolicies("users", (ability: AppAbility) => + ability.can(Action.UserReadOwn, User), + ) @Get("/my/self") @ApiOperation({ summary: "Returns the information of the user currently logged in.", @@ -137,7 +137,9 @@ export class UsersController { } @UseGuards(AuthenticatedPoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.UserReadOwn, User)) + @CheckPolicies("users", (ability: AppAbility) => + ability.can(Action.UserReadOwn, User), + ) @Get("/my/identity") async getMyUserIdentity( @Req() request: Request, @@ -152,7 +154,9 @@ export class UsersController { } @UseGuards(AuthenticatedPoliciesGuard) - @CheckPolicies((ability: AppAbility) => ability.can(Action.UserReadOwn, User)) + @CheckPolicies("users", (ability: AppAbility) => + ability.can(Action.UserReadOwn, User), + ) @Get("/my/settings") async getMySettings(@Req() request: Request): Promise { const authenticatedUserId: string = (request.user as JWTUser)._id; @@ -166,11 +170,11 @@ export class UsersController { @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies( + "users", (ability: AppAbility) => ability.can(Action.UserReadOwn, User) || ability.can(Action.UserReadAny, User), ) - @UseInterceptors(CreateUserSettingsInterceptor) @Get("/:id") async findById( @Req() request: Request, @@ -186,6 +190,7 @@ export class UsersController { @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies( + "users", (ability: AppAbility) => ability.can(Action.UserReadOwn, User) || ability.can(Action.UserReadAny, User), @@ -205,6 +210,7 @@ export class UsersController { @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies( + "users", (ability: AppAbility) => ability.can(Action.UserCreateOwn, User) || ability.can(Action.UserCreateAny, User), @@ -225,6 +231,7 @@ export class UsersController { @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies( + "users", (ability: AppAbility) => ability.can(Action.UserReadOwn, User) || ability.can(Action.UserReadAny, User), @@ -244,6 +251,7 @@ export class UsersController { @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies( + "users", (ability: AppAbility) => ability.can(Action.UserUpdateOwn, User) || ability.can(Action.UserUpdateAny, User), @@ -267,6 +275,7 @@ export class UsersController { @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies( + "users", (ability: AppAbility) => ability.can(Action.UserUpdateOwn, User) || ability.can(Action.UserUpdateAny, User), @@ -275,14 +284,14 @@ export class UsersController { async patchSettings( @Req() request: Request, @Param("id") id: string, - updateUserSettingsDto: PartialUpdateUserSettingsDto, + @Body() updateUserSettingsDto: PartialUpdateUserSettingsDto, ): Promise { await this.checkUserAuthorization( request, [Action.UserUpdateAny, Action.UserUpdateOwn], id, ); - return this.usersService.findOneAndUpdateUserSettings( + return this.usersService.findOneAndPatchUserSettings( id, updateUserSettingsDto, ); @@ -290,6 +299,31 @@ export class UsersController { @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies( + "users", + (ability: AppAbility) => + ability.can(Action.UserUpdateOwn, User) || + ability.can(Action.UserUpdateAny, User), + ) + @Patch("/:id/settings/external") + async patchExternalSettings( + @Req() request: Request, + @Param("id") id: string, + @Body() externalSettings: Record, + ): Promise { + await this.checkUserAuthorization( + request, + [Action.UserUpdateAny, Action.UserUpdateOwn], + id, + ); + return this.usersService.findOneAndPatchUserExternalSettings( + id, + externalSettings, + ); + } + + @UseGuards(AuthenticatedPoliciesGuard) + @CheckPolicies( + "users", (ability: AppAbility) => ability.can(Action.UserDeleteOwn, User) || ability.can(Action.UserDeleteAny, User), @@ -308,7 +342,7 @@ export class UsersController { } @UseGuards(AuthenticatedPoliciesGuard) - @CheckPolicies((ability: AppAbility) => { + @CheckPolicies("users", (ability: AppAbility) => { return ( ability.can(Action.UserReadOwn, User) || ability.can(Action.UserReadAny, User) @@ -328,7 +362,7 @@ export class UsersController { const viewedUser = (await this.usersService.findById2JWTUser( id, )) as JWTUser; - const ability = this.caslAbilityFactory.createForUser(viewedUser); + const ability = this.caslAbilityFactory.datasetEndpointAccess(viewedUser); const canCreateDataset = ability.can(Action.DatasetCreate, DatasetClass); @@ -353,7 +387,7 @@ export class UsersController { } @UseGuards(AuthenticatedPoliciesGuard) - @CheckPolicies((ability: AppAbility) => + @CheckPolicies("users", (ability: AppAbility) => ability.can(Action.UserCreateJwt, User), ) @Post("/:id/jwt") diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts index 24a65a86d..d69ca6012 100644 --- a/src/users/users.service.spec.ts +++ b/src/users/users.service.spec.ts @@ -29,6 +29,18 @@ const mockUser: User = { datasetCount: 25, jobCount: 25, userId: "testUserId", + filters: [ + { type: "LocationFilterComponent", visible: true }, + { type: "PidFilterComponent", visible: true }, + { type: "PidFilterContainsComponent", visible: false }, + { type: "PidFilterStartsWithComponent", visible: false }, + { type: "GroupFilterComponent", visible: true }, + { type: "TypeFilterComponent", visible: true }, + { type: "KeywordFilterComponent", visible: true }, + { type: "DateRangeFilterComponent", visible: true }, + { type: "TextFilterComponent", visible: true }, + ], + conditions: [], }, }; @@ -58,6 +70,18 @@ const mockUserSettings: UserSettings = { datasetCount: 25, jobCount: 25, userId: "testUserId", + filters: [ + { type: "LocationFilterComponent", visible: true }, + { type: "PidFilterComponent", visible: true }, + { type: "PidFilterContainsComponent", visible: false }, + { type: "PidFilterStartsWithComponent", visible: false }, + { type: "GroupFilterComponent", visible: true }, + { type: "TypeFilterComponent", visible: true }, + { type: "KeywordFilterComponent", visible: true }, + { type: "DateRangeFilterComponent", visible: true }, + { type: "TextFilterComponent", visible: true }, + ], + conditions: [], }; describe("UsersService", () => { diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 7369730d7..c94394aa0 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -273,18 +273,57 @@ export class UsersService implements OnModuleInit { } async findByIdUserSettings(userId: string): Promise { - return this.userSettingsModel.findOne({ userId }).exec(); + const result = await this.userSettingsModel + .findOne({ userId }) + .lean() + .exec(); + + return result; } async findOneAndUpdateUserSettings( userId: string, updateUserSettingsDto: UpdateUserSettingsDto | PartialUpdateUserSettingsDto, ): Promise { - return this.userSettingsModel - .findOneAndUpdate({ userId }, updateUserSettingsDto, { new: true }) + const result = await this.userSettingsModel + .findOneAndUpdate({ userId }, updateUserSettingsDto, { + new: true, + upsert: true, + setDefaultsOnInsert: true, + }) .exec(); + + return result; } + async findOneAndPatchUserSettings( + userId: string, + updateUserSettingsDto: UpdateUserSettingsDto | PartialUpdateUserSettingsDto, + ): Promise { + const result = await this.userSettingsModel + .findOneAndUpdate( + { userId }, + { $set: updateUserSettingsDto }, + { new: true }, + ) + .exec(); + return result; + } + + async findOneAndPatchUserExternalSettings( + userId: string, + externalSettings: Record, + ): Promise { + const updateQuery: Record = {}; + + for (const [key, value] of Object.entries(externalSettings)) { + updateQuery[`externalSettings.${key}`] = value; + } + const result = await this.userSettingsModel + .findOneAndUpdate({ userId }, { $set: updateQuery }, { new: true }) + .exec(); + return result; + } async findOneAndDeleteUserSettings(userId: string): Promise { return this.userSettingsModel.findOneAndDelete({ userId }).exec(); } diff --git a/test/DatasetAuthorization.js b/test/DatasetAuthorization.js index bcecc2b03..3aeb7f749 100644 --- a/test/DatasetAuthorization.js +++ b/test/DatasetAuthorization.js @@ -45,69 +45,36 @@ describe("0300: DatasetAuthorization: Test access to dataset", () => { before(() => { db.collection("Dataset").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "user1", - password: TestData.Accounts["user1"]["password"], - }, - (tokenVal) => { - accessTokenUser1 = tokenVal; - utils.getToken( - appUrl, - { - username: "user2", - password: TestData.Accounts["user2"]["password"], - }, - (tokenVal) => { - accessTokenUser2 = tokenVal; - utils.getToken( - appUrl, - { - username: "user3", - password: TestData.Accounts["user3"]["password"], - }, - (tokenVal) => { - accessTokenUser3 = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: - TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - utils.getToken( - appUrl, - { - username: "admin", - password: TestData.Accounts["admin"]["password"], - }, - (tokenVal) => { - accessTokenAdmin = tokenVal; - done(); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); + beforeEach(async () => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenUser1 = await utils.getToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + + accessTokenUser2 = await utils.getToken(appUrl, { + username: "user2", + password: TestData.Accounts["user2"]["password"], + }); + + accessTokenUser3 = await utils.getToken(appUrl, { + username: "user3", + password: TestData.Accounts["user3"]["password"], + }); + + accessTokenAdmin = await utils.getToken(appUrl, { + username: "admin", + password: TestData.Accounts["admin"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); afterEach((done) => { @@ -695,7 +662,6 @@ describe("0300: DatasetAuthorization: Test access to dataset", () => { pid: TestData.PidPrefix + "/" + uuidv4(), ownerGroup: "admin", }; - console.log("0502: pid : " + datasetWithPid["pid"]); return request(appUrl) .post("/api/v3/Datasets") diff --git a/test/DatasetFilter.js b/test/DatasetFilter.js index 37b71bf79..4fedab6f9 100644 --- a/test/DatasetFilter.js +++ b/test/DatasetFilter.js @@ -62,6 +62,10 @@ const RawCorrect3 = { value: 6, unit: "", }, + test_field_string: { + value: "test_string_value", + unit: "", + }, }, datasetName: "This is the third correct test raw dataset", description: @@ -92,59 +96,31 @@ describe("0400: DatasetFilter: Test retrieving datasets using filtering capabili before(() => { db.collection("Dataset").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "user1", - password: TestData.Accounts["user1"]["password"], - }, - (tokenVal) => { - accessTokenUser1 = tokenVal; - utils.getToken( - appUrl, - { - username: "user2", - password: TestData.Accounts["user2"]["password"], - }, - (tokenVal) => { - accessTokenUser2 = tokenVal; - utils.getToken( - appUrl, - { - username: "user3", - password: TestData.Accounts["user2"]["password"], - }, - (tokenVal) => { - accessTokenUser3 = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: - TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenUser1 = await utils.getToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + + accessTokenUser2 = await utils.getToken(appUrl, { + username: "user2", + password: TestData.Accounts["user2"]["password"], + }); + + accessTokenUser3 = await utils.getToken(appUrl, { + username: "user3", + password: TestData.Accounts["user3"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); afterEach((done) => { @@ -790,9 +766,9 @@ describe("0400: DatasetFilter: Test retrieving datasets using filtering capabili mode: {}, scientific: [ { - lhs: "test_field_1", + lhs: "test_field_string", relation: "EQUAL_TO_STRING", - rhs: "6", + rhs: "test_string_value", unit: "", }, ], @@ -808,7 +784,7 @@ describe("0400: DatasetFilter: Test retrieving datasets using filtering capabili .expect(TestData.SuccessfulGetStatusCode) .expect("Content-Type", /json/) .then((res) => { - res.body.length.should.be.equal(0); + res.body.length.should.be.equal(1); }); }); diff --git a/test/DatasetLifecycle.js b/test/DatasetLifecycle.js index 2f3fed3bc..0611be077 100644 --- a/test/DatasetLifecycle.js +++ b/test/DatasetLifecycle.js @@ -15,31 +15,21 @@ const raw2 = { ...TestData.RawCorrect }; describe("0500: DatasetLifecycle: Test facet and filter queries", () => { before(() => { db.collection("Dataset").deleteMany({}); + db.collection("Policy").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); + beforeEach(async () => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); + it("0010: adds a new raw dataset", async () => { return request(appUrl) .post("/api/v3/Datasets") diff --git a/test/DatasetSimple.js b/test/DatasetSimple.js index efaf246e0..897b79e9f 100644 --- a/test/DatasetSimple.js +++ b/test/DatasetSimple.js @@ -16,28 +16,16 @@ describe("0200: Dataset Simple: Check different dataset types and their inherita db.collection("Dataset").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); function deleteDataset(item) { diff --git a/test/DerivedDataset.js b/test/DerivedDataset.js index 61b960799..7e4c2e6ce 100644 --- a/test/DerivedDataset.js +++ b/test/DerivedDataset.js @@ -17,48 +17,26 @@ describe("0700: DerivedDataset: Derived Datasets", () => { before(() => { db.collection("Dataset").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "user1", - password: TestData.Accounts["user1"]["password"], - }, - (tokenVal) => { - accessTokenUser1 = tokenVal; - utils.getToken( - appUrl, - { - username: "user2", - password: TestData.Accounts["user2"]["password"], - }, - (tokenVal) => { - accessTokenUser2 = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); - }, - ); - }, - ); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenUser1 = await utils.getToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + + accessTokenUser2 = await utils.getToken(appUrl, { + username: "user2", + password: TestData.Accounts["user2"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); async function deleteDataset(item) { @@ -121,6 +99,9 @@ describe("0700: DerivedDataset: Derived Datasets", () => { .and.be.equal(TestData.DerivedCorrect.owner); res.body.should.have.property("type").and.be.equal("derived"); res.body.should.have.property("pid").and.be.string; + res.body.should.have.property("proposalId").and.be.string; + //res.body.should.have.property("sampleId").and.be.string; + //res.body.should.have.property("instrumentId").and.be.string; pid = res.body["pid"]; }); }); diff --git a/test/DerivedDatasetDatablock.js b/test/DerivedDatasetDatablock.js index ca62c7fc8..6c223246c 100644 --- a/test/DerivedDatasetDatablock.js +++ b/test/DerivedDatasetDatablock.js @@ -10,32 +10,19 @@ describe("0750: DerivedDatasetDatablock: Test Datablocks and their relation to d let datablockId1 = null; let datablockId2 = null; + before(() => { + db.collection("Dataset").deleteMany({}); + }); + beforeEach(async () => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); - beforeEach((done) => { - before(() => { - db.collection("Dataset").deleteMany({}); + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], }); - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); }); it("0100:adds a new derived dataset", async () => { diff --git a/test/DerivedDatasetOrigDatablock.js b/test/DerivedDatasetOrigDatablock.js index a8566b816..8dcc066e3 100644 --- a/test/DerivedDatasetOrigDatablock.js +++ b/test/DerivedDatasetOrigDatablock.js @@ -11,32 +11,20 @@ describe("0800: DerivedDatasetOrigDatablock: Test OrigDatablocks and their relat let origDatablockId1 = null; let origDatablockId2 = null; - beforeEach((done) => { - before(() => { - db.collection("Dataset").deleteMany({}); - db.collection("OrigDatablock").deleteMany({}); + before(() => { + db.collection("Dataset").deleteMany({}); + db.collection("OrigDatablock").deleteMany({}); + }); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], }); - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); }); it("0010: adds a new derived dataset", async () => { diff --git a/test/ElasticSearch.js b/test/ElasticSearch.js index 221a57043..a027564cb 100644 --- a/test/ElasticSearch.js +++ b/test/ElasticSearch.js @@ -45,28 +45,16 @@ const scientificMetadata = (values) => { before(() => { db.collection("Dataset").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); + beforeEach(async () => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); it("0010: adds a new raw dataset with scientificMetadata", async () => { @@ -190,11 +178,11 @@ const scientificMetadata = (values) => { }); }); - it("0030: should fetching dataset with correct proposalId and size", async () => { + it("0030: should fetching dataset with correct proposalIds and size", async () => { return request(appUrl) .post("/api/v3/elastic-search/search") .send({ - proposalId: TestData.ScientificMetadataForElasticSearch.proposalId, + proposalIds: TestData.ScientificMetadataForElasticSearch.proposalId, size: TestData.ScientificMetadataForElasticSearch.size, }) .set("Accept", "application/json") @@ -206,11 +194,11 @@ const scientificMetadata = (values) => { }); }); - it("0031: should fail fetching dataset with correct proposalId but wrong size", async () => { + it("0031: should fail fetching dataset with correct proposalIds but wrong size", async () => { return request(appUrl) .post("/api/v3/elastic-search/search") .send({ - proposalId: TestData.ScientificMetadataForElasticSearch.proposalId, + proposalIds: [TestData.ScientificMetadataForElasticSearch.proposalId], size: faker.number.int({ min: 100000001, max: 100400000 }), }) .set("Accept", "application/json") @@ -221,11 +209,11 @@ const scientificMetadata = (values) => { res.body.data.should.be.length(0); }); }); - it("0032: should fail fetching dataset with wrong proposalId but correct size", async () => { + it("0032: should fail fetching dataset with wrong proposalIds but correct size", async () => { return request(appUrl) .post("/api/v3/elastic-search/search") .send({ - proposalId: "wrongProposalId", + proposalIds: ["wrongProposalId"], size: TestData.ScientificMetadataForElasticSearch.size, }) .set("Accept", "application/json") @@ -237,11 +225,11 @@ const scientificMetadata = (values) => { }); }); - it("0033: should fail fetching dataset with incorrect proposalId and size", async () => { + it("0033: should fail fetching dataset with incorrect proposalIds and size", async () => { return request(appUrl) .post("/api/v3/elastic-search/search") .send({ - proposalId: "wrongProposalId", + proposalIds: ["wrongProposalId"], size: faker.number.int({ min: 100000001, max: 100400000 }), }) .set("Accept", "application/json") diff --git a/test/Instrument.js b/test/Instrument.js index 613f5f621..385f5ebe9 100644 --- a/test/Instrument.js +++ b/test/Instrument.js @@ -20,38 +20,21 @@ describe("0900: Instrument: instrument management, creation, update, deletion an before(() => { db.collection("Instrument").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - utils.getToken( - appUrl, - { - username: "user1", - password: TestData.Accounts["user1"]["password"], - }, - (tokenVal) => { - accessTokenUser1 = tokenVal; - done(); - }, - ); - }, - ); - }, - ); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenUser1 = await utils.getToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); it("0010: adds new instrument #1 as ingestor", async () => { diff --git a/test/InstrumentsFilter.js b/test/InstrumentsFilter.js index a3958f90d..83981f3c1 100644 --- a/test/InstrumentsFilter.js +++ b/test/InstrumentsFilter.js @@ -45,59 +45,31 @@ describe("1000: InstrumentFilter: Test retrieving instruments using filtering ca before(() => { db.collection("Instrument").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "user1", - password: TestData.Accounts["user1"]["password"], - }, - (tokenVal) => { - accessTokenUser1 = tokenVal; - utils.getToken( - appUrl, - { - username: "user2", - password: TestData.Accounts["user2"]["password"], - }, - (tokenVal) => { - accessTokenUser2 = tokenVal; - utils.getToken( - appUrl, - { - username: "user3", - password: TestData.Accounts["user3"]["password"], - }, - (tokenVal) => { - accessTokenUser3 = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: - TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenUser1 = await utils.getToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + + accessTokenUser2 = await utils.getToken(appUrl, { + username: "user2", + password: TestData.Accounts["user2"]["password"], + }); + + accessTokenUser3 = await utils.getToken(appUrl, { + username: "user3", + password: TestData.Accounts["user3"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); afterEach((done) => { diff --git a/test/Jobs.js b/test/Jobs.js index 9fa9375c9..b36393792 100644 --- a/test/Jobs.js +++ b/test/Jobs.js @@ -21,38 +21,46 @@ var publicJobIds = []; var origDatablockId = null; describe.skip("1100: Jobs: Test New Job Model", () => { - before((done) => { + before(() => { db.collection("Dataset").deleteMany({}); db.collection("Job").deleteMany({}); + }); - archiveJob = { ...TestData.ArchiveJob }; - retrieveJob = { ...TestData.RetrieveJob }; - publicJob = { ...TestData.PublicJob }; - done(); - }); - - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); + beforeEach(async () => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenUser1 = await utils.getToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + + accessTokenUser3 = await utils.getToken(appUrl, { + username: "user3", + password: TestData.Accounts["user3"]["password"], + }); + + accessTokenUser51 = await utils.getToken(appUrl, { + username: "user5.1", + password: TestData.Accounts["user5.1"]["password"], + }); + + accessTokenUser52 = await utils.getToken(appUrl, { + username: "user5.2", + password: TestData.Accounts["user5.2"]["password"], + }); + + accessTokenAdmin = await utils.getToken(appUrl, { + username: "admin", + password: TestData.Accounts["admin"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); it("0010: adds a new raw dataset", async () => { diff --git a/test/LoginUtils.js b/test/LoginUtils.js index b54f82ea3..d5694d7ff 100644 --- a/test/LoginUtils.js +++ b/test/LoginUtils.js @@ -1,32 +1,36 @@ /* eslint-disable @typescript-eslint/no-var-requires */ var request = require("supertest"); -exports.getToken = function (appUrl, user, cb) { - request(appUrl) - .post("/api/v3/Users/Login?include=user") - .send(user) - .set("Accept", "application/json") - .end((err, res) => { - if (err) { - cb(err); - } else { - cb(res.body.id); - } - }); +exports.getToken = function (appUrl, user) { + return new Promise((resolve, reject) => { + request(appUrl) + .post("/api/v3/auth/Login?include=user") + .send(user) + .set("Accept", "application/json") + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res.body.id); + } + }); + }); }; -exports.getIdAndToken = function (appUrl, user, cb) { - request(appUrl) - .post("/api/v3/Users/Login?include=user") - .send(user) - .set("Accept", "application/json") - .end((err, res) => { - if (err) { - cb(err); - } else { - cb(res.body.userId, res.body.id); - } - }); +exports.getIdAndToken = function (appUrl, user) { + return new Promise((resolve, reject) => { + request(appUrl) + .post("/api/v3/auth/Login?include=user") + .send(user) + .set("Accept", "application/json") + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve({ userId: res.body.userId, token: res.body.id }); + } + }); + }); }; exports.getTokenAD = function (appUrl, user, cb) { diff --git a/test/OrigDatablockForRawDataset.js b/test/OrigDatablockForRawDataset.js index e7d226d62..d329d5867 100644 --- a/test/OrigDatablockForRawDataset.js +++ b/test/OrigDatablockForRawDataset.js @@ -23,28 +23,16 @@ describe("1200: OrigDatablockForRawDataset: Test OrigDatablocks and their relati db.collection("Dataset").deleteMany({}); db.collection("OrigDatablock").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); + beforeEach(async () => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); origDatablockData1 = { ...TestData.OrigDataBlockCorrect1, diff --git a/test/Policy.js b/test/Policy.js index e6b3b9fa4..7339542fb 100644 --- a/test/Policy.js +++ b/test/Policy.js @@ -25,30 +25,18 @@ describe("1300: Policy: Simple Policy tests", () => { before(() => { db.collection("Policy").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); - }); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); + }); + it("0010: adds a new policy", async () => { return request(appUrl) .post("/api/v3/Policies") diff --git a/test/Proposal.js b/test/Proposal.js index 411aa220f..cb0ae6661 100644 --- a/test/Proposal.js +++ b/test/Proposal.js @@ -15,38 +15,21 @@ describe("1500: Proposal: Simple Proposal", () => { before(() => { db.collection("Proposal").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "proposalIngestor", - password: TestData.Accounts["proposalIngestor"]["password"], - }, - (tokenVal) => { - accessTokenProposalIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); - }, - ); + beforeEach(async() => { + accessTokenProposalIngestor = await utils.getToken(appUrl, { + username: "proposalIngestor", + password: TestData.Accounts["proposalIngestor"]["password"], + }); + + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); // the following two function definition prepare for diff --git a/test/ProposalAuthorization.js b/test/ProposalAuthorization.js index 4e4a9a200..8294895f3 100644 --- a/test/ProposalAuthorization.js +++ b/test/ProposalAuthorization.js @@ -9,7 +9,7 @@ let accessTokenProposalIngestor = null, accessTokenArchiveManager = null, accessTokenAdminIngestor = null, accessTokenUser1 = null, - accessTokenUser2 = null; + accessTokenUser3 = null; let proposalPid1 = null, encodedProposalPid1 = null, @@ -17,6 +17,8 @@ let proposalPid1 = null, encodedProposalPid2 = null, proposalPid3 = null, encodedProposalPid3 = null; +// proposalPid10 = null, +// encodedProposalPid10 = null; const proposal1 = { ...TestData.ProposalCorrectMin, @@ -39,64 +41,43 @@ const proposal3 = { accessGroups: ["group3"], }; +// const proposal10 = { +// ...TestData.ProposalCorrectMin, +// proposalId: "20170271", +// ownerGroup: "admin", +// accessGroups: ["admin"], +// isPublished: true, +// }; + describe("1400: ProposalAuthorization: Test access to proposal", () => { before(() => { db.collection("Proposal").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - - utils.getToken( - appUrl, - { - username: "proposalIngestor", - password: TestData.Accounts["proposalIngestor"]["password"], - }, - (tokenVal) => { - accessTokenProposalIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "user1", - password: TestData.Accounts["user1"]["password"], - }, - (tokenVal) => { - accessTokenUser1 = tokenVal; - utils.getToken( - appUrl, - { - username: "user3", - password: TestData.Accounts["user3"]["password"], - }, - (tokenVal) => { - accessTokenUser2 = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: - TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenProposalIngestor = await utils.getToken(appUrl, { + username: "proposalIngestor", + password: TestData.Accounts["proposalIngestor"]["password"], + }); + + accessTokenUser1 = await utils.getToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + + accessTokenUser3 = await utils.getToken(appUrl, { + username: "user3", + password: TestData.Accounts["user3"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); afterEach((done) => { @@ -152,14 +133,38 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { }); }); + // it("0035: adds proposal 10", async () => { + // return request(appUrl) + // .post("/api/v3/proposals") + // .send(proposal10) + // .set("Accept", "application/json") + // .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + // .expect(TestData.EntryCreatedStatusCode) + // .expect("Content-Type", /json/) + // .then((res) => { + // res.body.should.have.property("ownerGroup").and.equal("admin"); + // res.body.should.have.property("proposalId").and.be.string; + // proposalPid10 = res.body["proposalId"]; + // encodedProposalPid10 = encodeURIComponent(proposalPid10); + // }); + // }); + it("0040: cannot access proposal as unauthenticated user", async () => { return request(appUrl) .get("/api/v3/proposals/" + encodedProposalPid2) .set("Accept", "application/json") .expect("Content-Type", /json/) - .expect(TestData.UnauthorizedStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); + // it("0045: can access public proposal as unauthenticated user", async () => { + // return request(appUrl) + // .get("/api/v3/proposals/" + encodedProposalPid10) + // .set("Accept", "application/json") + // .expect("Content-Type", /json/) + // .expect(TestData.SuccessfulGetStatusCode); + // }); + it("0050: admin can list all proposals", async () => { return request(appUrl) .get("/api/v3/proposals") @@ -184,6 +189,18 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { }); }); + it("0061: check admin access to proposal 1 should return true", async () => { + return request(appUrl) + .get("/api/v3/proposals/" + encodedProposalPid1 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(true); + }); + }); + it("0070: full query for proposals for admin", async () => { return request(appUrl) .get("/api/v3/proposals/fullquery") @@ -208,6 +225,18 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { }); }); + it("0081: check admin access to proposal 2 should return true", async () => { + return request(appUrl) + .get("/api/v3/proposals/" + encodedProposalPid2 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(true); + }); + }); + it("0090: access proposal 3 as admin", async () => { return request(appUrl) .get("/api/v3/proposals/" + encodedProposalPid3) @@ -220,6 +249,18 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { }); }); + it("0091: check admin access to proposal 3 should return true", async () => { + return request(appUrl) + .get("/api/v3/proposals/" + encodedProposalPid3 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(true); + }); + }); + it("0100: list of proposals for user 1", async () => { return request(appUrl) .get("/api/v3/proposals") @@ -233,15 +274,27 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { }); }); - it("0110: access proposal 1 as user 1", async () => { + it("0110: access proposal 1 as user 1 should fail", async () => { return request(appUrl) - .get("/api/v3/proposals/" + 20170268) + .get("/api/v3/proposals/" + encodedProposalPid1) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenUser1}` }) .expect("Content-Type", /json/) .expect(TestData.AccessForbiddenStatusCode); }); + it("0111: check user 1 access to proposal 1 should return false", async () => { + return request(appUrl) + .get("/api/v3/proposals/" + encodedProposalPid1 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser1}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(false); + }); + }); + it("0120: access proposal 2 as user 1", async () => { return request(appUrl) .get("/api/v3/proposals/" + encodedProposalPid2) @@ -254,7 +307,19 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { }); }); - it("0130: access proposal 3 as user 1", async () => { + it("0121: check user 1 access to proposal 2 should return true", async () => { + return request(appUrl) + .get("/api/v3/proposals/" + encodedProposalPid2 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser1}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(true); + }); + }); + + it("0130: access proposal 3 as user 1 should fail", async () => { return request(appUrl) .get("/api/v3/proposals/" + encodedProposalPid3) .set("Accept", "application/json") @@ -263,6 +328,18 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { .expect(TestData.AccessForbiddenStatusCode); }); + it("0131: check user 1 access to proposal 3 should return false", async () => { + return request(appUrl) + .get("/api/v3/proposals/" + encodedProposalPid3 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser1}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(false); + }); + }); + it("0140: full query for proposals for user 1", async () => { return request(appUrl) .get("/api/v3/proposals/fullquery") @@ -280,7 +357,7 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { return request(appUrl) .get("/api/v3/proposals") .set("Accept", "application/json") - .set({ Authorization: `Bearer ${accessTokenUser2}` }) + .set({ Authorization: `Bearer ${accessTokenUser3}` }) .expect(TestData.SuccessfulGetStatusCode) .expect("Content-Type", /json/) .then((res) => { @@ -289,20 +366,32 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { }); }); - it("0160: access proposal 1 as user 2", async () => { + it("0160: access proposal 1 as user 2 should fail", async () => { return request(appUrl) .get("/api/v3/proposals/" + encodedProposalPid1) .set("Accept", "application/json") - .set({ Authorization: `Bearer ${accessTokenUser2}` }) + .set({ Authorization: `Bearer ${accessTokenUser3}` }) .expect("Content-Type", /json/) .expect(TestData.AccessForbiddenStatusCode); }); - it("0160: access proposal 2 as user 2", async () => { + it("0161: check user 2 access to proposal 1 should return false", async () => { + return request(appUrl) + .get("/api/v3/proposals/" + encodedProposalPid1 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser3}` }) + .expect("Content-Type", /json/) + .expect(TestData.SuccessfulGetStatusCode) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(false); + }); + }); + + it("0165: access proposal 2 as user 2", async () => { return request(appUrl) .get("/api/v3/proposals/" + encodedProposalPid2) .set("Accept", "application/json") - .set({ Authorization: `Bearer ${accessTokenUser2}` }) + .set({ Authorization: `Bearer ${accessTokenUser3}` }) .expect("Content-Type", /json/) .expect(TestData.SuccessfulGetStatusCode) .then((res) => { @@ -310,11 +399,23 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { }); }); + it("0166: check user 2 access to proposal 2 should return true", async () => { + return request(appUrl) + .get("/api/v3/proposals/" + encodedProposalPid2 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser3}` }) + .expect("Content-Type", /json/) + .expect(TestData.SuccessfulGetStatusCode) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(true); + }); + }); + it("0170: access proposal 3 as user 2", async () => { return request(appUrl) .get("/api/v3/proposals/" + encodedProposalPid3) .set("Accept", "application/json") - .set({ Authorization: `Bearer ${accessTokenUser2}` }) + .set({ Authorization: `Bearer ${accessTokenUser3}` }) .expect("Content-Type", /json/) .expect(TestData.SuccessfulGetStatusCode) .then((res) => { @@ -322,11 +423,23 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { }); }); + it("0171: check user 2 access to proposal 3 should return true", async () => { + return request(appUrl) + .get("/api/v3/proposals/" + encodedProposalPid3 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser3}` }) + .expect("Content-Type", /json/) + .expect(TestData.SuccessfulGetStatusCode) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(true); + }); + }); + it("0180: full query for proposals for user 2", async () => { return request(appUrl) .get("/api/v3/proposals/fullquery") .set("Accept", "application/json") - .set({ Authorization: `Bearer ${accessTokenUser2}` }) + .set({ Authorization: `Bearer ${accessTokenUser3}` }) .expect(TestData.SuccessfulGetStatusCode) .expect("Content-Type", /json/) .then((res) => { @@ -340,7 +453,7 @@ describe("1400: ProposalAuthorization: Test access to proposal", () => { return request(appUrl) .get(`/api/v3/proposals/fullfacet`) .set("Accept", "application/json") - .set({ Authorization: `Bearer ${accessTokenUser2}` }) + .set({ Authorization: `Bearer ${accessTokenUser3}` }) .expect(TestData.SuccessfulGetStatusCode) .expect("Content-Type", /json/) .then((res) => { diff --git a/test/PublishedData.js b/test/PublishedData.js index 69531a188..dccfbfcb1 100644 --- a/test/PublishedData.js +++ b/test/PublishedData.js @@ -38,28 +38,16 @@ describe("1600: PublishedData: Test of access to published data", () => { db.collection("Dataset").deleteMany({}); db.collection("PublishedData").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); afterEach((done) => { diff --git a/test/RandomizedDatasetPermissions.js b/test/RandomizedDatasetPermissions.js index 9a31f8eb2..c69781853 100644 --- a/test/RandomizedDatasetPermissions.js +++ b/test/RandomizedDatasetPermissions.js @@ -188,61 +188,33 @@ describe("1700: Randomized Datasets: permission test with bigger amount of data" before(() => { db.collection("Dataset").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "user1", - password: TestData.Accounts["user1"]["password"], - }, - (tokenVal) => { - accessTokenUser1 = tokenVal; - utils.getToken( - appUrl, - { - username: "user2", - password: TestData.Accounts["user2"]["password"], - }, - (tokenVal) => { - accessTokenUser2 = tokenVal; - utils.getToken( - appUrl, - { - username: "user3", - password: TestData.Accounts["user3"]["password"], - }, - (tokenVal) => { - accessTokenUser3 = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: - TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); - }); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenUser1 = await utils.getToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + + accessTokenUser2 = await utils.getToken(appUrl, { + username: "user2", + password: TestData.Accounts["user2"]["password"], + }); + accessTokenUser3 = await utils.getToken(appUrl, { + username: "user3", + password: TestData.Accounts["user3"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); + }); + it("0010: access private dataset as unauthenticated user", async () => { await addAllDatasets(); const randomGroup = randomIntFromInterval(1, 4); diff --git a/test/RawDataset.js b/test/RawDataset.js index 89a04f3dd..6e9fcd748 100644 --- a/test/RawDataset.js +++ b/test/RawDataset.js @@ -18,38 +18,21 @@ describe("1900: RawDataset: Raw Datasets", () => { db.collection("Dataset").deleteMany({}); db.collection("Proposals").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "proposalIngestor", - password: TestData.Accounts["proposalIngestor"]["password"], - }, - (tokenVal) => { - accessProposalToken = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); - }, - ); + beforeEach(async() => { + accessProposalToken = await utils.getToken(appUrl, { + username: "proposalIngestor", + password: TestData.Accounts["proposalIngestor"]["password"], + }); + + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); it("0010: adds a new proposal", async () => { @@ -110,6 +93,9 @@ describe("1900: RawDataset: Raw Datasets", () => { res.body.should.have.property("owner").and.be.string; res.body.should.have.property("type").and.equal("raw"); res.body.should.have.property("pid").and.be.string; + res.body.should.have.property("instrumentId").and.be.string; + res.body.should.have.property("proposalId").and.be.string; + res.body.should.have.property("sampleId").and.be.string; pid = encodeURIComponent(res.body["pid"]); }); }); diff --git a/test/RawDatasetDatablock.js b/test/RawDatasetDatablock.js index fae50686b..952230220 100644 --- a/test/RawDatasetDatablock.js +++ b/test/RawDatasetDatablock.js @@ -9,32 +9,20 @@ describe("1800: RawDatasetDatablock: Test Datablocks and their relation to raw D var datasetPid = null; var datablockId = null; var datablockId2 = null; + + before(() => { + db.collection("Dataset").deleteMany({}); + }); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); - beforeEach((done) => { - before(() => { - db.collection("Dataset").deleteMany({}); + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], }); - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); }); it("0010: adds a new raw dataset", async () => { diff --git a/test/RawDatasetOrigDatablock.js b/test/RawDatasetOrigDatablock.js index fc46197a1..553d5bbb0 100644 --- a/test/RawDatasetOrigDatablock.js +++ b/test/RawDatasetOrigDatablock.js @@ -14,32 +14,20 @@ describe("2000: RawDatasetOrigDatablock: Test OrigDatablocks and their relation origDatablockWithEmptyChkAlg = null, origDatablockWithValidChkAlg = null; - beforeEach((done) => { - before(() => { - db.collection("Dataset").deleteMany({}); - db.collection("OrigDatablock").deleteMany({}); + before(() => { + db.collection("Dataset").deleteMany({}); + db.collection("OrigDatablock").deleteMany({}); + }); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], }); - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); origDatablockData1 = { ...TestData.OrigDataBlockCorrect1 }; origDatablockData2 = { ...TestData.OrigDataBlockCorrect2 }; diff --git a/test/ResetDataset.js b/test/ResetDataset.js index a737bbd3e..66f97ead0 100644 --- a/test/ResetDataset.js +++ b/test/ResetDataset.js @@ -150,28 +150,16 @@ var foundId1 = null; var foundId2 = null; describe("2100: ResetDataset: Create Dataset and its Datablocks, then reset Datablocks and embedded Datasetlifecycle status", () => { - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); // first get existing datasets with the test archieId to allow to delete them diff --git a/test/Sample.js b/test/Sample.js index 3baccdb69..3e05d007c 100644 --- a/test/Sample.js +++ b/test/Sample.js @@ -14,29 +14,17 @@ describe("2200: Sample: Simple Sample", () => { before(() => { db.collection("Sample").deleteMany({}); db.collection("Dataset").deleteMany({}); - }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - done(); - }, - ); - }, - ); + }); + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); }); it("0010: adds a new sample", async () => { diff --git a/test/SampleAuthorization.js b/test/SampleAuthorization.js index a8fcfcb25..d9f78c39b 100644 --- a/test/SampleAuthorization.js +++ b/test/SampleAuthorization.js @@ -39,93 +39,47 @@ describe("2250: Sample Authorization", () => { before(() => { db.collection("Sample").deleteMany({}); }); - beforeEach((done) => { - utils.getToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (tokenVal) => { - accessTokenAdminIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "sampleIngestor", - password: TestData.Accounts["sampleIngestor"]["password"], - }, - (tokenVal) => { - accessTokenSampleIngestor = tokenVal; - utils.getToken( - appUrl, - { - username: "user1", - password: TestData.Accounts["user1"]["password"], - }, - (tokenVal) => { - accessTokenUser1 = tokenVal; - utils.getToken( - appUrl, - { - username: "user2", - password: TestData.Accounts["user2"]["password"], - }, - (tokenVal) => { - accessTokenUser2 = tokenVal; - utils.getToken( - appUrl, - { - username: "archiveManager", - password: - TestData.Accounts["archiveManager"]["password"], - }, - (tokenVal) => { - accessTokenArchiveManager = tokenVal; - utils.getToken( - appUrl, - { - username: "user3", - password: TestData.Accounts["user3"]["password"], - }, - (tokenVal) => { - accessTokenUser3 = tokenVal; - utils.getToken( - appUrl, - { - username: "user4", - password: - TestData.Accounts["user4"]["password"], - }, - (tokenVal) => { - accessTokenUser4 = tokenVal; - utils.getToken( - appUrl, - { - username: "user5.1", - password: - TestData.Accounts["user5.1"]["password"], - }, - (tokenVal) => { - accessTokenUser5 = tokenVal; - done(); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); - }); - + beforeEach(async() => { + accessTokenAdminIngestor = await utils.getToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + + accessTokenSampleIngestor = await utils.getToken(appUrl, { + username: "sampleIngestor", + password: TestData.Accounts["sampleIngestor"]["password"], + }); + accessTokenUser1 = await utils.getToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + + accessTokenUser2 = await utils.getToken(appUrl, { + username: "user2", + password: TestData.Accounts["user2"]["password"], + }); + + accessTokenUser3 = await utils.getToken(appUrl, { + username: "user3", + password: TestData.Accounts["user3"]["password"], + }); + + accessTokenUser4 = await utils.getToken(appUrl, { + username: "user4", + password: TestData.Accounts["user4"]["password"], + }); + + accessTokenUser5 = await utils.getToken(appUrl, { + username: "user5.1", + password: TestData.Accounts["user5.1"]["password"], + }); + + accessTokenArchiveManager = await utils.getToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); + }); + it("0010: adds sample 1 as Admin Ingestor with owner group its own group", async () => { let sample = { ...TestData.SampleCorrect, @@ -485,7 +439,7 @@ describe("2250: Sample Authorization", () => { .post("/api/v3/Samples") .send(sample) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("0180: adds a new sample as an unauthenticated user with owner group as sampleingestor, which should fail", async () => { @@ -498,7 +452,7 @@ describe("2250: Sample Authorization", () => { .post("/api/v3/Samples") .send(sample) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("0190: adds a new sample as an unauthenticated user with owner group as user1, which should fail", async () => { @@ -511,7 +465,7 @@ describe("2250: Sample Authorization", () => { .post("/api/v3/Samples") .send(sample) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("0200: adds a new sample as an unauthenticated user with its owner group, which should fail", async () => { @@ -524,7 +478,7 @@ describe("2250: Sample Authorization", () => { .post("/api/v3/Samples") .send(sample) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("0210: adds a new sample as Archive Manager with owner group as adminingestor, which should fail", async () => { @@ -977,7 +931,7 @@ describe("2250: Sample Authorization", () => { .post("/api/v3/Samples/" + sampleId1 + "/attachments") .send(TestData.AttachmentCorrect) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("0420: adds an attachment as an unauthenticated user to sample 2, which should fail", async () => { @@ -985,7 +939,7 @@ describe("2250: Sample Authorization", () => { .post("/api/v3/Samples/" + sampleId2 + "/attachments") .send(TestData.AttachmentCorrect) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("0430: adds an attachment as an unauthenticated user to sample 3, which should fail", async () => { @@ -993,7 +947,7 @@ describe("2250: Sample Authorization", () => { .post("/api/v3/Samples/" + sampleId3 + "/attachments") .send(TestData.AttachmentCorrect) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("0440: adds an attachment as an unauthenticated user to sample 4, which should fail", async () => { @@ -1001,7 +955,7 @@ describe("2250: Sample Authorization", () => { .post("/api/v3/Samples/" + sampleId4 + "/attachments") .send(TestData.AttachmentCorrect) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("0450: adds an attachment as Archive Manager to sample 1, which should fail", async () => { @@ -1349,6 +1303,18 @@ describe("2250: Sample Authorization", () => { }); }); + it("0641: check Admin Ingestor access to public sample 1 should return true", async () => { + return request(appUrl) + .get("/api/v3/Samples/" + sampleId1 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdminIngestor}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(true); + }); + }); + it("0645: fetch all attachments for sample 1 as Admin Ingestor", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId1 + "/attachments") @@ -1585,6 +1551,18 @@ describe("2250: Sample Authorization", () => { .expect(TestData.CreationForbiddenStatusCode); }); + it("0731: check Sample Ingestor access to sample 1 should return false", async () => { + return request(appUrl) + .get("/api/v3/Samples/" + sampleId1 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenSampleIngestor}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(false); + }); + }); + it("0735: fetch all attachments for sample 1 as Sample Ingestor, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId1 + "/attachments") @@ -1605,6 +1583,18 @@ describe("2250: Sample Authorization", () => { }); }); + it("0741: check Sample Ingestor access to sample 2 should return true", async () => { + return request(appUrl) + .get("/api/v3/Samples/" + sampleId2 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenSampleIngestor}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(true); + }); + }); + it("0745: fetch all attachments for sample 2 as Sample Ingestor", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId2 + "/attachments") @@ -1769,6 +1759,18 @@ describe("2250: Sample Authorization", () => { .expect(TestData.CreationForbiddenStatusCode); }); + it("0831: check User 1 access to sample 2 should return false", async () => { + return request(appUrl) + .get("/api/v3/Samples/" + sampleId1 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser1}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(false); + }); + }); + it("0835: fetch all attachments for sample 1 as User 1, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId1 + "/attachments") @@ -1805,6 +1807,18 @@ describe("2250: Sample Authorization", () => { }); }); + it("0851: check User 1 access to sample 3 should return true", async () => { + return request(appUrl) + .get("/api/v3/Samples/" + sampleId3 + "/authorization") + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser1}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(true); + }); + }); + it("0855: fetch all attachments for sample 3 as User 1", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId3 + "/attachments") @@ -2733,126 +2747,137 @@ describe("2250: Sample Authorization", () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId1) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); + }); + + it("1331: check unauthenticated user access to sample 1 should return false", async () => { + return request(appUrl) + .get("/api/v3/Samples/" + sampleId1 + "/authorization") + .set("Accept", "application/json") + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(false); + }); }); it("1335: fetch all attachments for sample 1 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId1 + "/attachments") .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1340: access sample 2 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId2) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); - it("1345: fetch all attachments for sample 1 as Unauthenticated User, which should fail", async () => { + it("1345: fetch all attachments for sample 2 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId2 + "/attachments") .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1350: access sample 3 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId3) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1355: fetch all attachments for sample 5 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId3 + "/attachments") .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1360: access sample 4 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId4) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1365: fetch all attachments for sample 4 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId4 + "/attachments") .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1370: access sample 5 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId5) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1375: fetch all attachments for sample 5 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId5 + "/attachments") .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1380: access sample 6 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId6) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1385: fetch all attachments for sample 6 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId6 + "/attachments") .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1390: access sample 7 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId7) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1395: fetch all attachments for sample 7 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId7 + "/attachments") .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1400: access sample 8 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId8) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1405: fetch all attachments for sample 8 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId8 + "/attachments") .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1410: access sample 9 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId9) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1415: fetch all attachments for sample 9 as Unauthenticated User, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId9 + "/attachments") .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1420: access public sample 10 as Unauthenticated User", async () => { @@ -2866,6 +2891,17 @@ describe("2250: Sample Authorization", () => { }); }); + it("1421: check unauthenticated user access to public sample 10 should return true", async () => { + return request(appUrl) + .get("/api/v3/Samples/" + sampleId10 + "/authorization") + .set("Accept", "application/json") + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("canAccess").and.be.equal(true); + }); + }); + it("1425: fetch all attachments for sample 10 as Unauthenticated User", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId10 + "/attachments") @@ -2882,7 +2918,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId1) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1435: fetch all attachments for sample 5 as Archive Manager, which should fail", async () => { @@ -2890,7 +2926,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId1 + "/attachments") .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1440: access sample 2 as Archive Manager, which should fail", async () => { @@ -2898,7 +2934,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId2) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1445: fetch all attachments for sample 2 as Archive Manager, which should fail", async () => { @@ -2906,7 +2942,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId2 + "/attachments") .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1450: access sample 3 as Archive Manager, which should fail", async () => { @@ -2914,7 +2950,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId3) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1455: fetch all attachments for sample 5 as Archive Manager, which should fail", async () => { @@ -2922,7 +2958,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId3 + "/attachments") .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1460: access sample 4 as Archive Manager, which should fail", async () => { @@ -2930,7 +2966,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId4) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1465: fetch all attachments for sample 4 as Archive Manager, which should fail", async () => { @@ -2938,7 +2974,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId4 + "/attachments") .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1470: access sample 5 as Archive Manager, which should fail", async () => { @@ -2946,7 +2982,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId5) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1475: fetch all attachments for sample 5 as Archive Manager, which should fail", async () => { @@ -2954,7 +2990,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId5 + "/attachments") .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1480: access sample 6 as Archive Manager, which should fail", async () => { @@ -2962,14 +2998,14 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId6) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1485: fetch all attachments for sample 6 as Archive Manager, which should fail", async () => { return request(appUrl) .get("/api/v3/Samples/" + sampleId6 + "/attachments") .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1490: access sample 7 as Archive Manager, which should fail", async () => { @@ -2977,7 +3013,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId7) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1495: fetch all attachments for sample 7 as Archive Manager, which should fail", async () => { @@ -2985,7 +3021,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId7 + "/attachments") .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1500: access sample 8 as Archive Manager, which should fail", async () => { @@ -2993,7 +3029,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId8) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1505: fetch all attachments for sample 8 as Archive Manager, which should fail", async () => { @@ -3001,7 +3037,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId8 + "/attachments") .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1510: access sample 9 as Archive Manager, which should fail", async () => { @@ -3009,7 +3045,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId9) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1515: fetch all attachments for sample 9 as Archive Manager, which should fail", async () => { @@ -3017,7 +3053,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId5 + "/attachments") .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); it("1520: access public sample 10 as Archive Manager", async () => { @@ -3037,7 +3073,7 @@ describe("2250: Sample Authorization", () => { .get("/api/v3/Samples/" + sampleId7 + "/attachments") .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenArchiveManager}` }) - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.AccessForbiddenStatusCode); }); // modify sample @@ -3576,7 +3612,7 @@ describe("2250: Sample Authorization", () => { .patch("/api/v3/Samples/" + sampleId1) .send({ sampleCharacteristics: characteristics }) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("2280: update sample characteristic for public sample 10 as Unauthenticated User, which should fail", async () => { @@ -3592,7 +3628,7 @@ describe("2250: Sample Authorization", () => { .patch("/api/v3/Samples/" + sampleId10) .send({ sampleCharacteristics: characteristics }) .set("Accept", "application/json") - .expect(TestData.CreationForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); // delete sample attachment @@ -3600,7 +3636,7 @@ describe("2250: Sample Authorization", () => { return request(appUrl) .delete("/api/v3/samples/" + sampleId1 + "/attachments/" + attachmentId1) .set("Accept", "application/json") - .expect(TestData.DeleteForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("4010: delete attachment 10 from sample 10 as Unauthenticated User, which should fail", async () => { @@ -3609,7 +3645,7 @@ describe("2250: Sample Authorization", () => { "/api/v3/samples/" + sampleId10 + "/attachments/" + attachmentId10, ) .set("Accept", "application/json") - .expect(TestData.DeleteForbiddenStatusCode); + .expect(TestData.CreationUnauthorizedStatusCode); }); it("4020: delete attachment 1 from sample 1 as User 5, which should fail", async () => { diff --git a/test/TestData.js b/test/TestData.js index f5ca82fbd..801a45768 100644 --- a/test/TestData.js +++ b/test/TestData.js @@ -6,6 +6,12 @@ const TestAccounts = Object.fromEntries( RawTestAccounts.map((account) => [account.username, account]), ); +const DatasetDates = faker.date.betweens({ + from: faker.date.recent({ days: 15 }).toISOString(), + to: faker.date.soon({ days: 10 }).toISOString(), + count: 2, +}); + const TestData = { EntryCreatedStatusCode: 201, EntryValidStatusCode: 200, @@ -18,6 +24,7 @@ const TestData = { BadRequestStatusCode: 400, AccessForbiddenStatusCode: 403, UnauthorizedStatusCode: 401, + CreationUnauthorizedStatusCode: 401, ConflictStatusCode: 409, ApplicationErrorStatusCode: 500, LoginSuccessfulStatusCode: 201, @@ -36,6 +43,37 @@ const TestData = { accessGroups: [], }, + userSettingsCorrect: { + datasetCount: 10, + jobCount: 25, + externalSettings: { + columns: [ + { + name: "select", + order: 0, + type: "standard", + enabled: true, + }, + ], + filters: [ + { + LocationFilter: true, + }, + ], + conditions: [ + { + condition: { + lhs: "test", + relation: "GREATER_THAN", + rhs: 1, + unit: "", + }, + enabled: true, + }, + ], + }, + }, + ProposalCorrectComplete: { proposalId: "20170267", pi_email: "pi@uni.edu", @@ -112,10 +150,12 @@ const TestData = { sourceFolder: faker.system.directoryPath(), owner: faker.internet.userName(), contactEmail: faker.internet.email(), + datasetName: faker.string.sample(), }, RawCorrect: { principalInvestigator: "scicatingestor@your.site", + startTime: "2011-09-14T05:29:11.000Z", endTime: "2011-09-14T06:31:25.000Z", creationLocation: "/SU/XQX/RAMJET", dataFormat: "Upchuck pre 2017", @@ -192,13 +232,16 @@ const TestData = { ownerGroup: "p13388", accessGroups: [], proposalId: "10.540.16635/20110123", + instrumentId: "1f016ec4-7a73-11ef-ae3e-439013069377", + sampleId: "20c32b4e-7a73-11ef-9aec-5b9688aa3791i", type: "raw", keywords: ["sls", "protein"], }, RawCorrectRandom: { principalInvestigator: faker.internet.email(), - endTime: faker.date.past().toISOString(), + startTime: DatasetDates[0], + endTime: DatasetDates[1], creationLocation: faker.system.directoryPath(), dataFormat: faker.lorem.words(3), scientificMetadata: { @@ -298,6 +341,7 @@ const TestData = { RawWrong_2: { principalInvestigator: "bertram.astor@grumble.com", + startTime: "2011-09-15T02:13:52.000Z", endTime: "2011-09-14T06:31:25.000Z", creationLocation: "/SU/XQX/RAMJET", dataFormat: "Upchuck pre 2017", @@ -374,13 +418,14 @@ const TestData = { DerivedCorrectMin: { investigator: faker.internet.email(), - inputDatasets: [faker.system.filePath()], + inputDatasets: [faker.string.uuid()], usedSoftware: [faker.internet.url()], owner: faker.internet.userName(), contactEmail: faker.internet.email(), sourceFolder: faker.system.directoryPath(), creationTime: faker.date.past(), ownerGroup: faker.string.alphanumeric(6), + datasetName: faker.string.sample(), type: "derived", }, @@ -408,6 +453,9 @@ const TestData = { ownerGroup: "p34123", accessGroups: [], type: "derived", + proposalId: "10.540.16635/20110123", + //instrumentId: "1f016ec4-7a73-11ef-ae3e-439013069377", + //sampleId: "20c32b4e-7a73-11ef-9aec-5b9688aa3791i", }, DerivedWrong: { @@ -806,6 +854,7 @@ const TestData = { creationLocation: faker.location.city(), principalInvestigator: faker.internet.userName(), type: "raw", + datasetName: faker.string.sample(), creationTime: faker.date.past(), sourceFolder: faker.system.directoryPath(), owner: faker.internet.userName(), diff --git a/test/UserAuthorization.js b/test/UserAuthorization.js index 205b2ca34..012b09d1f 100644 --- a/test/UserAuthorization.js +++ b/test/UserAuthorization.js @@ -21,85 +21,55 @@ let accessTokenAdminIngestor = null, userIdArchiveManager = null; describe("2300: User Authorization: test that user authorization are correct", () => { - beforeEach((done) => { - utils.getIdAndToken( - appUrl, - { - username: "adminIngestor", - password: TestData.Accounts["adminIngestor"]["password"], - }, - (idVal, tokenVal) => { - accessTokenAdminIngestor = tokenVal; - userIdIngestor = idVal; - utils.getIdAndToken( - appUrl, - { - username: "user1", - password: TestData.Accounts["user1"]["password"], - }, - (idVal, tokenVal) => { - accessTokenUser1 = tokenVal; - userIdUser1 = idVal; - utils.getIdAndToken( - appUrl, - { - username: "user2", - password: TestData.Accounts["user2"]["password"], - }, - (idVal, tokenVal) => { - accessTokenUser2 = tokenVal; - userIdUser2 = idVal; - utils.getIdAndToken( - appUrl, - { - username: "user3", - password: TestData.Accounts["user3"]["password"], - }, - (idVal, tokenVal) => { - accessTokenUser3 = tokenVal; - userIdUser3 = idVal; - utils.getIdAndToken( - appUrl, - { - username: "user4", - password: TestData.Accounts["user4"]["password"], - }, - (idVal, tokenVal) => { - accessTokenUser4 = tokenVal; - userIdUser4 = idVal; - utils.getIdAndToken( - appUrl, - { - username: "archiveManager", - password: TestData.Accounts["archiveManager"]["password"], - }, - (idVal, tokenVal) => { - accessTokenArchiveManager = tokenVal; - userIdArchiveManager = idVal; - utils.getIdAndToken( - appUrl, - { - username: "admin", - password: TestData.Accounts["admin"]["password"], - }, - (idVal, tokenVal) => { - accessTokenAdmin = tokenVal; - userIdAdmin = idVal; - done(); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); - }, - ); + beforeEach(async () => { + const loginResponseIngestor = await utils.getIdAndToken(appUrl, { + username: "adminIngestor", + password: TestData.Accounts["adminIngestor"]["password"], + }); + userIdIngestor = loginResponseIngestor.userId; + accessTokenAdminIngestor = loginResponseIngestor.token; + + const loginResponseUser1 = await utils.getIdAndToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + userIdUser1 = loginResponseUser1.userId; + accessTokenUser1 = loginResponseUser1.token; + + const loginResponseUser2 = await utils.getIdAndToken(appUrl, { + username: "user2", + password: TestData.Accounts["user2"]["password"], + }); + userIdUser2 = loginResponseUser2.userId; + accessTokenUser2 = loginResponseUser2.token; + + const loginResponseUser3 = await utils.getIdAndToken(appUrl, { + username: "user3", + password: TestData.Accounts["user3"]["password"], + }); + userIdUser3 = loginResponseUser3.userId; + accessTokenUser3 = loginResponseUser3.token; + + const loginResponseUser4 = await utils.getIdAndToken(appUrl, { + username: "user4", + password: TestData.Accounts["user4"]["password"], + }); + userIdUser4 = loginResponseUser4.userId; + accessTokenUser4 = loginResponseUser4.token; + + const loginResponseAdmin = await utils.getIdAndToken(appUrl, { + username: "admin", + password: TestData.Accounts["admin"]["password"], + }); + userIdAdmin = loginResponseAdmin.userId; + accessTokenAdmin = loginResponseAdmin.token; + + const loginResponseArchiveManager = await utils.getIdAndToken(appUrl, { + username: "archiveManager", + password: TestData.Accounts["archiveManager"]["password"], + }); + userIdArchiveManager = loginResponseArchiveManager.userId; + accessTokenArchiveManager = loginResponseArchiveManager.token; }); afterEach((done) => { diff --git a/test/Users.js b/test/Users.js index 12c8c9691..a6e81ec91 100644 --- a/test/Users.js +++ b/test/Users.js @@ -10,7 +10,7 @@ let userIdUser1 = null, describe("2350: Users: Login with functional accounts", () => { it("0010: Admin ingestor login fails with incorrect credentials", async () => { return request(appUrl) - .post("/api/v3/Users/Login?include=user") + .post("/api/v3/auth/Login?include=user") .send({ username: "adminIngestor", password: TestData.Accounts["user1"]["password"], @@ -23,7 +23,7 @@ describe("2350: Users: Login with functional accounts", () => { it("0020: Login should succeed with correct credentials", async () => { return request(appUrl) - .post("/api/v3/Users/Login?include=user") + .post("/api/v3/auth/Login?include=user") .send({ username: "adminIngestor", password: TestData.Accounts["adminIngestor"]["password"], @@ -38,24 +38,51 @@ describe("2350: Users: Login with functional accounts", () => { }); describe("2360: Users settings", () => { - beforeEach((done) => { - utils.getIdAndToken( - appUrl, - { - username: "user1", - password: TestData.Accounts["user1"]["password"], - }, - (idVal, tokenVal) => { - accessTokenUser1 = tokenVal; - userIdUser1 = idVal; - done(); - }, - ); + beforeEach(async () => { + const loginResponseUser1 = await utils.getIdAndToken(appUrl, { + username: "user1", + password: TestData.Accounts["user1"]["password"], + }); + userIdUser1 = loginResponseUser1.userId; + accessTokenUser1 = loginResponseUser1.token; }); - it("0010: Update users settings with valid value should sucess ", async () => { + it("0020: Update users settings with valid value should success ", async () => { return request(appUrl) .put(`/api/v3/Users/${userIdUser1}/settings`) + .send(TestData.userSettingsCorrect) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser1}` }) + .expect(TestData.SuccessfulPatchStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("userId", userIdUser1); + res.body.should.have.property("datasetCount"); + res.body.should.have.property("jobCount"); + res.body.should.have.property("externalSettings"); + }); + }); + + it("0030: Patch users settings with valid value should success ", async () => { + return request(appUrl) + .patch(`/api/v3/Users/${userIdUser1}/settings`) + .send(TestData.userSettingsCorrect) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser1}` }) + .expect(TestData.SuccessfulPatchStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("userId", userIdUser1); + res.body.should.have.property("datasetCount"); + res.body.should.have.property("jobCount"); + res.body.should.have.property("externalSettings"); + }); + }); + + it("0040: Patch users external settings with valid value should success ", async () => { + return request(appUrl) + .patch(`/api/v3/Users/${userIdUser1}/settings/external`) + .send(TestData.userSettingsCorrect.externalSettings) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenUser1}` }) .expect(TestData.SuccessfulPatchStatusCode) @@ -64,6 +91,7 @@ describe("2360: Users settings", () => { res.body.should.have.property("userId", userIdUser1); res.body.should.have.property("datasetCount"); res.body.should.have.property("jobCount"); + res.body.should.have.property("externalSettings"); }); }); }); diff --git a/test/config/jest-e2e.json b/test/config/jest-e2e.json index cf83246b2..0fb0cd924 100644 --- a/test/config/jest-e2e.json +++ b/test/config/jest-e2e.json @@ -1,4 +1,5 @@ { + "setupFilesAfterEnv": ["./test/config/jest.setup.js"], "moduleFileExtensions": ["js", "json", "ts"], "rootDir": "../../", "testTimeout": 30000, @@ -16,5 +17,4 @@ "isolatedModules": true } } - } diff --git a/test/config/jest.setup.js b/test/config/jest.setup.js new file mode 100644 index 000000000..a861b3a8c --- /dev/null +++ b/test/config/jest.setup.js @@ -0,0 +1,2 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +global.ReadableStream = require("node:stream/web").ReadableStream; diff --git a/test/config/jobconfig.json b/test/config/jobconfig.json new file mode 100755 index 000000000..e69de29bb diff --git a/test/config/pretest.js b/test/config/pretest.js index e884f4e73..f935d2239 100644 --- a/test/config/pretest.js +++ b/test/config/pretest.js @@ -1,19 +1,19 @@ /* eslint-disable @typescript-eslint/no-var-requires */ //NOTE: Here we load and initialize some global variables that are used throughout the tests - +require("dotenv").config(); var chaiHttp = require("chai-http"); + const { MongoClient } = require("mongodb"); -const uri = "mongodb://localhost:27017/scicat"; -const client = new MongoClient(uri); +const client = new MongoClient(process.env.MONGODB_URI); async function loadChai() { const { chai } = import("chai"); chai.use(chaiHttp); await client.connect(); } -loadChai(); +loadChai(); global.appUrl = "http://localhost:3000"; global.request = require("supertest"); -global.db = client.db("scicat"); +global.db = client.db();